Skip to content

persist account footer state and remove redundant info toasts#80

Open
ndycode wants to merge 40 commits intomainfrom
feature/footer-toast-cleanup
Open

persist account footer state and remove redundant info toasts#80
ndycode wants to merge 40 commits intomainfrom
feature/footer-toast-cleanup

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 14, 2026

What Changed

  • Keeps the active account visible in chat footer metadata after the first successful response as the configured footer variant.
  • Reuses runtime-cached footer config during account.select refreshes, so switch events do not re-read config from disk or silently fall back when the config file is locked on Windows.
  • Preserves LRU eviction order during bulk footer refreshes and adds overflow regression coverage for the 200-session cap.
  • Suppresses transient Using ... and Switched to ... info toasts when the persistent footer is enabled while preserving warning/error terminal toasts.

Why

The footer should be the persistent source of truth for the active account instead of a temporary toast. The switch-event refresh path also needs to avoid new sync config reads so Windows file-lock stalls cannot silently downgrade footer updates back into toasts.

Validation

  • npm run typecheck
  • npm run lint
  • npm test -- test/index.test.ts
  • npm run build

Summary by CodeRabbit

  • New Features

    • Account footer persistence: display and remember account indicators with three style options (masked email label, full email, or label only).
    • New plugin hooks for chat message handling and experimental message transformation.
  • Behavior

    • Persisted indicators sync with runtime config, refresh across sessions/threads, and update after account rotations/logins.
    • Rate-limit/503 conditions now surface as terminal toasts instead of raw errors.
    • New config getters and defaults for enabling persistence and selecting footer style.
  • Tests

    • Expanded coverage for persistence flows, config validation, indicator styling, and email-masking in logs.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr ships the persisted account footer feature: after the first successful response, the active account is shown as a persistent footer indicator (masked email, full email, or label-only) in the chat ui instead of a transient toast. it introduces a 200-entry LRU session map, two new plugin hooks (chat.message and experimental.chat.messages.transform), a runtime config snapshot cache (runtimePluginConfigSnapshot) to avoid repeated disk reads, a fallback-marker symbol on DEFAULT_CONFIG so transient config-loader failures can be distinguished from explicit user config, and suppresses Using…/Switched to… info toasts when the persistent footer is active.

key points:

  • syncRuntimePluginConfig correctly guards against stale closure state by preferring the in-memory snapshot over a marked-fallback disk read — this is the right approach for windows antivirus file-lock resilience
  • LRU cap logic is correct: setPersistedAccountIndicator delete-then-reinsert promotes active sessions; refreshVisiblePersistedAccountIndicators intentionally skips re-promotion during bulk switches
  • account.select handler passes preReloadTargetAccount ?? account to avoid label flicker on label-only style when the manager hasn't reloaded yet — thoughtful detail
  • one new windows concurrency issue found (not in previous threads): the manual OAuth authorize path (~line 4057) calls syncRuntimePluginConfig(authorizePluginConfig) before refreshAuthorizeStoragePath, then discards the return value. the standard OAuth path correctly passes the refreshed config to syncRuntimePluginConfig after the refresh call. on windows, if the config is locked during the initial read and refreshAuthorizeStoragePath recovers on retry, the manual path will leave runtimePluginConfigSnapshot pointing at the fallback config — silently mis-routing storage paths and footer state for any subsequent event handlers or auth flows that call resolveRuntimePluginConfig() before the first fetch corrects it. no vitest regression covers this path.
  • test coverage is otherwise thorough: all three footer styles, CODEX_THREAD_ID session-key priority, session id mismatch, LRU overflow, account count hint fallback, transform hook edge cases, and email masking in logs are all exercised

Confidence Score: 2/5

  • not safe to merge as-is — manual OAuth path has a windows-specific config snapshot bug that can silently mis-route storage writes during no-browser logins
  • the standard OAuth authorize path correctly calls syncRuntimePluginConfig with the post-refresh config, but the manual OAuth path inverts the order and discards the refreshed return value. on windows (the primary at-risk OS for this plugin given antivirus file locks), a config-locked first auth will cache a fallback snapshot, which persists until the next fetch call overwrites it. the scope of damage is storage path mis-routing and stale persistAccountFooter state for users doing no-browser logins — exactly the windows concurrency class this PR claims to defend against. all other logic is sound and test coverage is notably strong.
  • index.ts lines 4057–4063 (manual oauth authorize path): syncRuntimePluginConfig called before refreshAuthorizeStoragePath, return value discarded

Important Files Changed

Filename Overview
index.ts major feature addition (persisted account footer, two new plugin hooks, LRU session map, runtime config cache). contains a concrete windows concurrency bug: the manual oauth authorize path calls syncRuntimePluginConfig before refreshAuthorizeStoragePath and discards the refreshed return value, leaving runtimePluginConfigSnapshot stale after a successful config retry.
lib/config.ts adds fallback marker symbol, isFallbackPluginConfig helper, and two new getters (getPersistAccountFooter, getPersistAccountFooterStyle). DEFAULT_CONFIG now exported and marked as fallback. createFallbackPluginConfig() correctly uses a spread to keep the default distinct from loader fallbacks. no issues found.
lib/persist-account-footer.ts new file: thin type-only module defining PersistAccountFooterStyle, PersistedAccountDetails, SessionModelRef, and PersistedAccountIndicatorEntry. no logic, no issues.
lib/schemas.ts adds persistAccountFooter (boolean optional) and persistAccountFooterStyle (enum optional) to PluginConfigSchema. straightforward zod schema extension, no issues.
test/index.test.ts extensive new coverage: all three footer styles, CODEX_THREAD_ID priority, session id mismatch, LRU eviction cap, account count hint fallback, transform hook edge cases. missing: manual oauth path (path 2) config snapshot regression when refreshAuthorizeStoragePath recovers from a windows fallback.
test/logger.test.ts updated to import actual maskEmail and adds email masking assertions. no issues.
test/plugin-config.test.ts adds coverage for getPersistAccountFooter and getPersistAccountFooterStyle including env-var override paths. no issues.
test/schemas.test.ts adds schema validation tests for persistAccountFooter boolean and persistAccountFooterStyle enum. rejects invalid style strings. no issues.

Sequence Diagram

sequenceDiagram
    participant Fetch as innerFetch
    participant SelectEvent as account.select event
    participant Indicators as persistedAccountIndicators (LRU Map)
    participant ChatMsg as chat.message hook
    participant Transform as experimental.chat.messages.transform hook

    Fetch->>Fetch: syncRuntimePluginConfig(pluginConfig)<br/>→ request-local persistAccountFooter copy
    Fetch->>Fetch: indicatorRevision = nextRevision()
    Fetch->>Fetch: threadIdCandidate = resolvePersistedIndicatorSessionID(promptCacheKey)
    Fetch-->>Fetch: ...await network...
    Fetch->>Indicators: setPersistedAccountIndicator(sessionID, account, index, count, style, revision)
    Note over Indicators: delete + re-insert (LRU promotion)<br/>trimPersistedAccountIndicators() if size > 200

    SelectEvent->>SelectEvent: await AccountManager.loadFromDisk()
    SelectEvent->>Indicators: refreshVisiblePersistedAccountIndicators(account, index, count, style)
    Note over Indicators: update-only, no LRU re-promotion<br/>revision check skips entries fresher than switch

    ChatMsg->>Indicators: getPersistedAccountIndicatorLabel(resolveSessionID(input.sessionID))
    Indicators-->>ChatMsg: label (or undefined)
    ChatMsg->>ChatMsg: applyPersistedAccountIndicator(message, label, input.model)<br/>writes message.variant + message.model.variant

    Transform->>Transform: find last user message in output.messages
    Transform->>Indicators: getPersistedAccountIndicatorLabel(resolveSessionID(info.sessionID))
    Indicators-->>Transform: label (or undefined)
    Transform->>Transform: applyPersistedAccountIndicator(lastUserMessage.info, label)<br/>no fallbackModel — model.variant written only if providerID/modelID already present
Loading

Comments Outside Diff (25)

  1. index.ts, line 1933-1993 (link)

    runtimePersistAccountFooterStyle still unguarded before first innerFetch

    runtimePersistAccountFooter and runtimePersistAccountFooterStyle are initialised to false / "label-masked-email" at closure level. syncRuntimePluginConfig is called inside innerFetch, so the first time the account.select event fires it uses the initialised defaults rather than the loaded config.

    in a long-running session where the user enables the footer via config or env var and then manually switches accounts before the first successful fetch completes (e.g. switching accounts during startup), runtimePersistAccountFooter is still false. the handler will fire the "Switched to account N" toast instead of refreshing the footer indicator — silently defeating the feature for that switch.

    syncRuntimePluginConfig(pluginConfig) is already called in the loader function; reading the returned values and storing them on runtimePersistAccountFooter/runtimePersistAccountFooterStyle there as well would close the window:

    // inside loader(), after syncRuntimePluginConfig(pluginConfig)
    const { persistAccountFooter, persistAccountFooterStyle } = syncRuntimePluginConfig(pluginConfig);
    // runtimePersistAccountFooter / runtimePersistAccountFooterStyle are now in sync at load time

    no test currently covers account.select triggered before the first successful fetch with persistAccountFooter: true.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: index.ts
    Line: 1933-1993
    
    Comment:
    **`runtimePersistAccountFooterStyle` still unguarded before first `innerFetch`**
    
    `runtimePersistAccountFooter` and `runtimePersistAccountFooterStyle` are initialised to `false` / `"label-masked-email"` at closure level. `syncRuntimePluginConfig` is called inside `innerFetch`, so the first time the `account.select` event fires it uses the initialised defaults rather than the loaded config.
    
    in a long-running session where the user enables the footer via config or env var and then manually switches accounts *before* the first successful fetch completes (e.g. switching accounts during startup), `runtimePersistAccountFooter` is still `false`. the handler will fire the "Switched to account N" toast instead of refreshing the footer indicator — silently defeating the feature for that switch.
    
    `syncRuntimePluginConfig(pluginConfig)` is already called in the `loader` function; reading the returned values and storing them on `runtimePersistAccountFooter`/`runtimePersistAccountFooterStyle` there as well would close the window:
    
    ```ts
    // inside loader(), after syncRuntimePluginConfig(pluginConfig)
    const { persistAccountFooter, persistAccountFooterStyle } = syncRuntimePluginConfig(pluginConfig);
    // runtimePersistAccountFooter / runtimePersistAccountFooterStyle are now in sync at load time
    ```
    
    no test currently covers `account.select` triggered before the first successful fetch with `persistAccountFooter: true`.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

  2. index.ts, line 1446-1448 (link)

    resolveUiRuntime now silently resets runtimePersistAccountFooter on Windows config-file lock

    resolveUiRuntime() was changed from calling applyUiRuntimeFromConfig(loadPluginConfig()) to syncRuntimePluginConfig(loadPluginConfig()).ui. the new call now also mutates the shared closure vars runtimePersistAccountFooter and runtimePersistAccountFooterStyle as a side effect.

    on windows, when antivirus holds the config file, loadPluginConfig() returns DEFAULT_CONFIG (which has persistAccountFooter: false). calling syncRuntimePluginConfig(DEFAULT_CONFIG) then silently resets runtimePersistAccountFooter = false. the next account.select event will fire the "Switched to account N" info toast instead of updating the footer indicator — defeating the feature this PR ships.

    before this PR, resolveUiRuntime() only touched UI glyphs/colors. the side-effect on footer state is new and unguarded. consider making syncRuntimePluginConfig only update the runtime flags when the caller explicitly opts in:

    // Proposed split: update footer state only on explicit calls (loader, fetch, authorize)
    const resolveUiRuntime = (): UiRuntimeOptions => {
        return applyUiRuntimeFromConfig(loadPluginConfig());
    };

    there is also no vitest test covering the case where resolveUiRuntime() triggers a stale-config reset of runtimePersistAccountFooter while a user has persistAccountFooter: true.

  3. index.ts, line 3003-3004 (link)

    authorize() fresh disk read can reset runtimePersistAccountFooter on Windows

    loadPluginConfig() is a synchronous readFileSync call. its own try/catch silently returns DEFAULT_CONFIG (which has persistAccountFooter: false) on any i/o failure — including an antivirus lock on Windows. syncRuntimePluginConfig(authPluginConfig) then writes runtimePersistAccountFooter = false to the shared closure, which the account.select handler reads at line 1911. the result: a user with persistAccountFooter: true who triggers an OAuth re-auth (e.g. token expiry) will silently lose the footer and start seeing "Switched to account N" toasts for all subsequent account switches — with no log or warning explaining why.

    the same risk exists in the manual OAuth authorize at line 3979.

    innerFetch already solved this for the hot path by passing the loader's already-read pluginConfig to syncRuntimePluginConfig (line 2210) instead of doing a fresh read. the same pattern should be applied here: pass the loader's cached pluginConfig rather than calling loadPluginConfig() again. no regression test currently covers this failure mode on the authorize() path.

    this also applies to:

    • index.ts:3978-3979 (manual OAuth authorize)
  4. index.ts, line 1942 (link)

    chat.message skips CODEX_THREAD_ID fallback unlike experimental.chat.messages.transform

    getPersistedAccountIndicatorLabel(input.sessionID) looks up the indicator directly. when a request has no promptCacheKey, resolvePersistedAccountSessionID(promptCacheKey) at line 2313 returns CODEX_THREAD_ID as the map key. if the chat system then calls chat.message with an input.sessionID that differs from the value of CODEX_THREAD_ID (e.g., it assigns its own internal id), the lookup returns undefined and the indicator is silently dropped.

    the experimental.chat.messages.transform hook correctly handles this case at line 1978 by calling resolvePersistedAccountSessionID(lastUserMessage.info.sessionID), which falls back to CODEX_THREAD_ID. there is no vitest coverage for: fetch with no promptCacheKey + CODEX_THREAD_ID set → chat.message called with sessionID = CODEX_THREAD_ID. the existing "falls back to CODEX_THREAD_ID in the transform hook" test covers experimental.chat.messages.transform only.

    consider wrapping the lookup with resolvePersistedAccountSessionID:

  5. index.ts, line 2318-2319 (link)

    nextPersistedAccountIndicatorRevision() called unconditionally when footer is disabled

    indicatorRevision is computed here on every request regardless of whether persistAccountFooter is true. the revision counter is only consumed inside the if (persistAccountFooter) block further below. when the feature is off, the counter still advances on every request, so once the feature is enabled the first indicator revision starts much higher than the account-switch revision. the existing.revision > revision guard in setPersistedAccountIndicator is intended to let a newer account switch win over a stale in-flight response, but the revision gap created by many disabled-state requests can make the first few switch events write with a lower number than outstanding indicator entries, silently failing to update footers right after the feature is turned on.

    guard the revision allocation with the flag:

    const indicatorRevision = persistAccountFooter
        ? nextPersistedAccountIndicatorRevision()
        : 0;
  6. index.ts, line 1917-1925 (link)

    silent no-feedback window on first switch with footer enabled

    when runtimePersistAccountFooter is true but no successful request has yet run (i.e. persistedAccountIndicators is empty), refreshVisiblePersistedAccountIndicators returns false immediately and does nothing. the else branch (the "Switched to account N" toast) is also skipped. the user switches accounts and receives zero visual confirmation.

    the test "uses the loader-synced footer setting before the first fetch completes" asserts the toast is absent, but asserts no alternative feedback. this is a regression for users who enable the footer and switch accounts before any request completes — common on startup.

    consider emitting a low-priority toast or a one-shot "no active sessions yet" indicator in this path, or only suppress the toast when the refresh actually updated at least one session:

    if (runtimePersistAccountFooter) {
        const refreshed = refreshVisiblePersistedAccountIndicators(
            previousManagedAccount ?? account,
            index,
            storage.accounts.length,
            runtimePersistAccountFooterStyle,
        );
        if (!refreshed) {
            // no active sessions yet — fall back to toast so the user isn't left in silence
            await showToast(`Switched to account ${index + 1}`, "info");
        }
    } else {
        await showToast(`Switched to account ${index + 1}`, "info");
    }
  7. index.ts, line 2321-2322 (link)

    session ID priority reversal breaks chat.message lookup

    the old code was process.env.CODEX_THREAD_ID ?? promptCacheKey, giving CODEX_THREAD_ID higher priority. the new resolvePersistedAccountSessionID(promptCacheKey) tries promptCacheKey first and only falls back to CODEX_THREAD_ID.

    the lookup paths (chat.message hook at line 1950 and experimental.chat.messages.transform at line 1987) both resolve their session ID with input.sessionID / lastUserMessage.info.sessionID → CODEX_THREAD_ID. when promptCacheKey is present in the request body but differs from the host-assigned CODEX_THREAD_ID, the indicator is stored under promptCacheKey but looked up under CODEX_THREAD_ID — miss, no footer renders.

    the tests don't catch this because they delete process.env.CODEX_THREAD_ID in beforeEach and pass the same value as both promptCacheKey and sessionID.

    restoring the original priority — env var first, then cache key — aligns storage and lookup:

    alternatively, change the resolvePersistedAccountSessionID variadic signature to accept an explicit env-var slot and document the intended priority order so it can't silently regress again.

  8. index.ts, line 1944-1963 (link)

    chat.message hook missing role guard

    the hook calls applyPersistedAccountIndicator on output.message for every message regardless of role. unlike the experimental.chat.messages.transform hook — which explicitly scans backward for the last user message — this path writes message.variant and message.model.variant to assistant, tool, and system messages too. any non-user turn that fires this hook would get the account indicator label injected into its metadata, which downstream ui code reading variant would then render as the footer for that assistant turn.

    the fix is a role check before the applyPersistedAccountIndicator call:

    there are no vitest tests exercising this hook on a non-user message; adding a case where output.message.role === "assistant" and asserting variant stays unset would catch any regression here.

  9. index.ts, line 1957-1959 (link)

    non-string role defaults to "user", misapplies indicator

    when message.role is absent, null, or any non-string value, messageRole silently defaults to "user" and applyPersistedAccountIndicator runs. a system-prompt message or tool-call message that arrives without a string role field would receive the account indicator — writing the raw email into its variant and model.variant fields when full-email style is configured. this is a pii write to a message type that should never carry account metadata.

    the experimental.chat.messages.transform hook avoids this with a strict === "user" check. the same guard should be used here:

    this brings the two hooks into parity and eliminates the accidental indicator injection on non-user roles.

  10. index.ts, line 1939-2002 (link)

    stale indicators persist after footer is disabled

    the chat.message and experimental.chat.messages.transform hooks call getPersistedAccountIndicatorLabel and unconditionally apply the result — they never check runtimePersistAccountFooter. if a user toggles the persist-footer env var off mid-session, any indicator already stored in persistedAccountIndicators will keep being applied to that session's messages indefinitely.

    concrete scenario:

    1. footer enabled → setPersistedAccountIndicator stores indicator for session-X
    2. user disables the feature via env var
    3. next innerFetchsyncRuntimePluginConfigpersistAccountFooter = false, no new indicator stored
    4. chat.message fires for session-X → indicator found in map → applyPersistedAccountIndicator applies it anyway

    the map is never cleared when footer transitions from enabled to disabled. adding a runtimePersistAccountFooter guard in the hook dispatch, or clearing the map inside syncRuntimePluginConfig when the flag transitions true → false, closes this gap. no vitest coverage exists for the toggle-off path.

  11. index.ts, line 287-291 (link)

    stale persistedAccountCountHint survives footer disable

    when syncRuntimePluginConfig detects the footer was just disabled it clears persistedAccountIndicators, but does not reset persistedAccountCountHint. if the footer is later re-enabled, the stale hint acts as the denominator fallback whenever liveAccountCount transiently drops to 0 (e.g. the manager hasn't loaded yet). the first successful request will correct it, but the window between re-enable and that first request can produce a wrong [N/M] ratio in the indicator.

  12. test/index.test.ts, line 963-968 (link)

    type-unsafe model?.variant access in test helper

    buildMessageTransformOutput infers info.model as { providerID: string; modelID: string } — no variant field. the ?.variant access on that type should be a compile-time error: Property 'variant' does not exist on type '{ providerID: string; modelID: string }'. at runtime the plugin mutates the object and adds variant, so the test reads correctly, but the type system never sees that mutation and the access silently slips past the npm run typecheck gate if the inferred type happens to be any in a looser check. the PluginType test definition for experimental.chat.messages.transform also omits variant from model, so any future refactor that renames the field won't be caught by the type checker.

    fix: extend the PluginType.experimental.chat.messages.transform info.model type to include variant?: string, and add the cast in readPersistedAccountIndicator:

  13. test/index.test.ts, line 829-836 (link)

    PluginType model missing variant field

    the model field in the experimental.chat.messages.transform output type is declared as { providerID: string; modelID: string }, which does not include variant. applyPersistedAccountIndicator always writes model.variant, so every test assertion on model.variant (e.g. via readPersistedAccountIndicator) reads a property that the TypeScript type does not know about. add variant?: string here so the type system catches regressions:

  14. index.ts, line 3025-3030 (link)

    authorize paths now use cached snapshot — stale perProjectAccounts risk

    resolveRuntimePluginConfig() returns runtimePluginConfigSnapshot (set once by the loader) until the next loader run. for account.select refreshes this is the explicit intent, but authorize is a write path: if the user edits perProjectAccounts (e.g. flips it from true to false) after the loader ran, the next authorize call passes the stale value to setStoragePath, potentially writing new accounts to the wrong location (per-project store vs global store). the same problem exists for the manualOAuth authorize path at the other resolveRuntimePluginConfig call below.

    // before this PR — guaranteed fresh read on every authorize invocation
    const authPluginConfig = loadPluginConfig();

    authorize is invoked infrequently and is not on the hot path, so the Windows file-lock protection is less critical here. consider keeping a direct loadPluginConfig() call for the non-footer settings that matter (e.g. perProjectAccounts) and only sourcing footer vars from the cached snapshot:

    const authPluginConfig = loadPluginConfig();
    syncRuntimePluginConfig(authPluginConfig);

    there is also no vitest case covering the scenario where perProjectAccounts changes between the loader run and a subsequent authorize call.

  15. index.ts, line 1895 (link)

    account.select handler doesn't update persistedAccountCountHint

    refreshVisiblePersistedAccountIndicators uses storage.accounts.length as accountCount for the bulk label refresh, but updatePersistedAccountCountHint is only called further down in the rotation sub-path. if the manual switch fires before the first successful request (so persistedAccountCountHint is still 0), subsequent fetches that hit the liveAccountCount > 0 branch are fine, but a transient zero from getAccountCount() immediately after a manual switch would fall back to a stale hint rather than the just-confirmed storage.accounts.length. calling updatePersistedAccountCountHint(storage.accounts.length) at the start of the account.select code path (alongside the existing call in the rotation path) would keep the hint consistent:

    const now = Date.now();
    account.lastUsed = now;
    account.lastSwitchReason = "rotation";
    storage.activeIndex = index;
    // ...
    await saveAccounts(storage);
    updatePersistedAccountCountHint(storage.accounts.length); // already present in rotation path

    this is also a windows safety concern: if getAccountCount() returns 0 during a windows file-lock race, the stale hint is the only fallback.

  16. index.ts, line 3045-3051 (link)

    double disk read on first cold-start authorize

    when runtimePluginConfigSnapshot is still undefined (no prior loader or innerFetch call), resolveRuntimePluginConfig() falls through to loadPluginConfig() — that's read 1. refreshAuthorizeStoragePath() then unconditionally calls loadPluginConfig() again — read 2. the original code had exactly one read per authorize invocation.

    on windows, two synchronous readFileSync calls against the same config file double the antivirus-lock exposure window. the first read is only needed to populate runtimePluginConfigSnapshot; if it already populated the snapshot, the second read in refreshAuthorizeStoragePath is redundant for the footer-config part.

    one fix: have refreshAuthorizeStoragePath accept an optional already-loaded config:

    syncRuntimePluginConfig(resolveRuntimePluginConfig());
    refreshAuthorizeStoragePath(runtimePluginConfigSnapshot);

    and inside refreshAuthorizeStoragePath, use the passed snapshot as the baseline before attempting a fresh read, mirroring the pattern already used in the function body.

  17. index.ts, line 1926-1960 (link)

    account.select skips updatePersistedAccountCountHint

    the rotation handler calls updatePersistedAccountCountHint(storage.accounts.length) after saving, but the account.select path does not. refreshVisiblePersistedAccountIndicators correctly passes storage.accounts.length for the immediate bulk refresh, so the visible label is right at switch time.

    the problem surfaces on the next request when accountManager.getAccountCount() transiently returns 0 (e.g., during the async manager reload that can happen on windows with a locked file). the fallback in setPersistedAccountIndicator is:

    const persistedAccountCount =
        liveAccountCount > 0
            ? liveAccountCount
            : persistedAccountCountHint > 0
                ? persistedAccountCountHint
                : 1;

    if the switch added or removed an account, persistedAccountCountHint still holds the pre-switch value, so the denominator in the indicator ([1/N]) will be wrong for that one request.

    adding updatePersistedAccountCountHint(storage.accounts.length) immediately after saveAccounts(storage) in the account.select path would keep the hint in sync, and no new test infrastructure is needed since the count-hint tests already cover the transient-zero scenario.

  18. index.ts, line 1421-1422 (link)

    resolveUiRuntime still reads config from disk directly

    resolveUiRuntime() calls loadPluginConfig() unconditionally — it wasn't migrated to use resolveRuntimePluginConfig(). on windows, every tool invocation (e.g. codex-list) that triggers a ui refresh hits a raw readFileSync. if antivirus holds the lock at that moment, the call silently falls back to DEFAULT_CONFIG and the ui reverts to defaults.

    every other hot path in this PR was hardened to use the runtime snapshot, but this one was missed:

    const resolveUiRuntime = (): UiRuntimeOptions => {
        return applyUiRuntimeFromConfig(loadPluginConfig());
    };

    should be:

  19. index.ts, line 1221-1240 (link)

    refreshVisiblePersistedAccountIndicators returns true even when no session was updated

    the function's return value drives toast suppression in the account.select handler: if the map is non-empty the function returns true and the "Switched to account N" toast is skipped. but the inner for loop can skip every session via the existing.revision > revision guard without writing anything — in that case no footer was refreshed yet the toast is still silenced.

    in practice the guard can never trigger today (the sync for loop has no await, so no concurrent write can increment beyond revision before the loop finishes). however the semantic contract is wrong: the return should express "at least one session label was written", not "there were sessions to iterate".

  20. index.ts, line 1995-1996 (link)

    session key priority diverges from fetch path in both hooks

    the fetch path explicitly prefers CODEX_THREAD_ID over prompt_cache_key:

    const runtimeThreadId = process.env.CODEX_THREAD_ID?.toString().trim();
    const threadIdCandidate = runtimeThreadId || resolvePersistedAccountSessionID(promptCacheKey);

    when CODEX_THREAD_ID is set, the indicator is always stored under it regardless of any prompt_cache_key in the request body. however, both hooks use the caller-supplied session id as the primary key:

    • chat.message (here): resolvePersistedAccountSessionID(input.sessionID) → returns input.sessionID immediately if non-empty, never reaching CODEX_THREAD_ID
    • experimental.chat.messages.transform (lines 2038-2042): same pattern via lastUserMessage.info.sessionID

    concrete failure: host sets CODEX_THREAD_ID="thread-123" but passes input.sessionID="session-456" (non-empty, different value). the fetch stored the indicator under "thread-123". both hooks look up "session-456" first → miss → footer silently not applied.

    no vitest coverage for this mismatch path. the existing tests only verify that an empty input.sessionID falls back to CODEX_THREAD_ID. to align with the fetch path:

    the same fix applies to the experimental.chat.messages.transform hook at lines 2038-2042.

  21. index.ts, line 342-348 (link)

    === DEFAULT_CONFIG sentinel relies on reference equality

    refreshedPluginConfig === DEFAULT_CONFIG only fires the fallback warning when loadPluginConfig() returns the exact same exported DEFAULT_CONFIG object reference. if loadPluginConfig() ever returns a new object shaped like DEFAULT_CONFIG (e.g. after a future refactor that always allocates a fresh default, or if a future try/catch creates { ...DEFAULT_CONFIG }), the Windows file-lock fallback silently stops working — the locked config is treated as a valid one and storagePluginConfig is set to a fresh-default-shaped object. the per-project storage path could then be set incorrectly with no warning emitted.

    a safer sentinel that doesn't depend on object identity:

    alternatively, loadPluginConfig could attach a __isDefault: true symbol property when it falls back, making the sentinel explicit and identity-independent. at minimum add a comment noting the reference-equality requirement so future refactors don't silently break Windows file-lock protection.

  22. index.ts, line 2389-2395 (link)

    footer indicator silently missed when thread env var is absent

    the fetch path stores the indicator under resolvePersistedIndicatorSessionID(promptCacheKey). the chat.message and experimental.chat.messages.transform hooks look up by resolvePersistedIndicatorSessionID(input.sessionID) and lastUserMessage.info.sessionID respectively.

    when the thread env var is not set both paths fall back to their respective candidate values. if the host passes a prompt_cache_key value in the request body that differs from the sessionID it supplies to the chat hooks, the stored indicator is never found and the footer is silently blank — no warning or log explains why.

    all tests feed the same string as both prompt_cache_key and sessionID so the mismatch path has zero coverage. adding a vitest case where the two keys differ would lock in the observable behavior and make clear whether the thread env var is a hard requirement for this feature.

  23. index.ts, line 1988-2019 (link)

    "chat.message" hook role check may silently never apply

    the transform hook checks message?.info.role === "user" (via the info sub-object), but this hook accesses message?.role at the top level of output.message. if the host passes the full message envelope ({ info: { role, ... }, parts: [] }) rather than the flattened info object, then message.role is always undefined and the indicator is never applied through this hook path — silently.

    all indicator tests exercise experimental.chat.messages.transform exclusively; there is zero vitest coverage for "chat.message". the missing tests mean neither the role-check level nor the indicator placement (message.variant vs message.info.variant) can be verified. adding at least one test that calls plugin["chat.message"] directly and asserts on the output shape would catch this regression.

    if output.message is already the info/content object (flat shape with a top-level role), add a comment making that explicit. if it is the full envelope, the role check needs to be:

  24. index.ts, line 1292-1305 (link)

    syncRuntimePluginConfig can wipe footer state and poison the snapshot on a windows file-lock hit

    syncRuntimePluginConfig is the single function that updates both runtimePluginConfigSnapshot and the footer-reset gate. the function is called in the authorize flow via:

    const authorizePluginConfig = resolveRuntimePluginConfig();
    syncRuntimePluginConfig(authorizePluginConfig);

    resolveRuntimePluginConfig() is runtimePluginConfigSnapshot ?? loadPluginConfig(). on a cold-start authorize (no prior snapshot) that hits a windows file lock, loadPluginConfig() returns a createFallbackPluginConfig() result where persistAccountFooter: false. syncRuntimePluginConfig then:

    1. resets footer state: runtimePersistAccountFooter could be true from a previous hot-reload path while the fallback config shows false, triggering resetPersistedAccountFooterState() and wiping all in-memory session indicators.
    2. poisons the snapshot: runtimePluginConfigSnapshot = fallbackConfig. future calls to resolveRuntimePluginConfig() return this fallback, silently preventing the user's persistAccountFooter: true from being re-read until the next auth.loader invocation.

    by contrast, refreshAuthorizeStoragePath already guards against this exact scenario:

    if (isFallbackPluginConfig(refreshedPluginConfig) && runtimePluginConfigSnapshot) {
        logWarn("Falling back to cached authorize storage config ...");
    } else {
        storagePluginConfig = refreshedPluginConfig;
    }

    applying the same guard in syncRuntimePluginConfig before updating runtimePluginConfigSnapshot and before calling resetPersistedAccountFooterState would close this inconsistency. no regression test exercises a cold-start authorize with a locked config file today.

  25. index.ts, line 4057-4063 (link)

    manual OAuth path discards refreshAuthorizeStoragePath return value, stale snapshot on Windows

    the manual OAuth authorize path calls syncRuntimePluginConfig(authorizePluginConfig) before refreshAuthorizeStoragePath, and then silently discards refreshAuthorizeStoragePath's return value. this is the opposite of what the standard OAuth path does (path 1 at ~line 3077, which calls syncRuntimePluginConfig(refreshedAuthorizePluginConfig) after the refresh).

    concrete failure path on windows:

    1. user does a no-browser (manual) login, config file is read-locked by antivirus
    2. resolveRuntimePluginConfig() falls through to loadPluginConfig() → returns a fallback config
    3. syncRuntimePluginConfig(fallbackConfig) caches the fallback in runtimePluginConfigSnapshot
    4. refreshAuthorizeStoragePath(fallbackConfig) retries, succeeds, sets storage path correctly — but its return value (the real config) is thrown away
    5. runtimePluginConfigSnapshot remains the fallback until the next innerFetch corrects it
    6. any event handler or auth flow that calls resolveRuntimePluginConfig() in that window reads stale defaults (e.g. perProjectAccounts: true vs the user's configured value — routing auth writes to the wrong storage path)

    the fix is to mirror path 1: call syncRuntimePluginConfig with the return value of refreshAuthorizeStoragePath, not the pre-refresh authorizePluginConfig.

    // path 2 should match path 1:
    const hadRuntimePluginConfig = runtimePluginConfigSnapshot !== undefined;
    const authorizePluginConfig = resolveRuntimePluginConfig();
    const refreshedAuthorizePluginConfig = refreshAuthorizeStoragePath(
        hadRuntimePluginConfig ? undefined : authorizePluginConfig,
    );
    syncRuntimePluginConfig(refreshedAuthorizePluginConfig);

    no vitest coverage for the manual oauth path verifying that a windows-fallback config is not persisted in runtimePluginConfigSnapshot when refreshAuthorizeStoragePath recovers successfully.

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: index.ts
Line: 4057-4063

Comment:
**manual OAuth path discards `refreshAuthorizeStoragePath` return value, stale snapshot on Windows**

the manual OAuth authorize path calls `syncRuntimePluginConfig(authorizePluginConfig)` *before* `refreshAuthorizeStoragePath`, and then silently discards `refreshAuthorizeStoragePath`'s return value. this is the opposite of what the standard OAuth path does (path 1 at ~line 3077, which calls `syncRuntimePluginConfig(refreshedAuthorizePluginConfig)` *after* the refresh).

concrete failure path on windows:
1. user does a no-browser (manual) login, config file is read-locked by antivirus
2. `resolveRuntimePluginConfig()` falls through to `loadPluginConfig()` → returns a fallback config
3. `syncRuntimePluginConfig(fallbackConfig)` caches the fallback in `runtimePluginConfigSnapshot`
4. `refreshAuthorizeStoragePath(fallbackConfig)` retries, succeeds, sets storage path correctly — **but its return value (the real config) is thrown away**
5. `runtimePluginConfigSnapshot` remains the fallback until the next `innerFetch` corrects it
6. any event handler or auth flow that calls `resolveRuntimePluginConfig()` in that window reads stale defaults (e.g. `perProjectAccounts: true` vs the user's configured value — routing auth writes to the wrong storage path)

the fix is to mirror path 1: call `syncRuntimePluginConfig` with the return value of `refreshAuthorizeStoragePath`, not the pre-refresh `authorizePluginConfig`.

```typescript
// path 2 should match path 1:
const hadRuntimePluginConfig = runtimePluginConfigSnapshot !== undefined;
const authorizePluginConfig = resolveRuntimePluginConfig();
const refreshedAuthorizePluginConfig = refreshAuthorizeStoragePath(
    hadRuntimePluginConfig ? undefined : authorizePluginConfig,
);
syncRuntimePluginConfig(refreshedAuthorizePluginConfig);
```

no vitest coverage for the manual oauth path verifying that a windows-fallback config is not persisted in `runtimePluginConfigSnapshot` when `refreshAuthorizeStoragePath` recovers successfully.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 9dd973a

Context used:

  • Rule used - What: Every code change must explain how it defend... (source)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted account indicator infrastructure, config/schema fields, runtime config synchronization, indicator lifecycle/formatting helpers, integration into account rotation/request flows, and two new plugin handlers for message and transform operations.

Changes

Cohort / File(s) Summary
Core Runtime
index.ts
Introduced MAX_PERSISTED_ACCOUNT_INDICATORS, new types (PersistedAccountDetails, SessionModelRef, PersistedAccountIndicatorEntry), persisted indicators map/revision/count hint, lifecycle helpers (set/trim/refresh/apply/format), resolvePersistedIndicatorSessionID, runtime config sync (syncRuntimePluginConfig, resolveRuntimePluginConfig), refreshAuthorizeStoragePath, and replaced ad‑hoc 503/429 handling with showTerminalToastResponse. Exposed "chat.message" and "experimental.chat.messages.transform" handlers.
Persisted Footer Definition
lib/persist-account-footer.ts
Added PERSIST_ACCOUNT_FOOTER_STYLES tuple and exported PersistAccountFooterStyle union type.
Configuration & Schemas
lib/config.ts, lib/schemas.ts
Made DEFAULT_CONFIG exported; added persistAccountFooter and persistAccountFooterStyle to default config and schema; added accessors getPersistAccountFooter and getPersistAccountFooterStyle that respect env overrides and constrain values to allowed styles.
Tests & Mocks
test/index.test.ts, test/plugin-config.test.ts, test/schemas.test.ts, test/logger.test.ts
Expanded test harness/mocks to exercise persisted-footer flows and new handlers: added mocks for getPersistAccountFooter, getPersistAccountFooterStyle, loadPluginConfig, retry mocks, and logger mask behavior. Added tests covering config defaults/env/validation, persisted indicators display/rotation, CODEX_THREAD_ID fallbacks, and logger masking of full-email indicators.
Package Manifest
package.json
Large update to package metadata (lines added/removed) accompanying the feature and tests.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Runtime
  participant PersistStore
  participant UI

  Client->>Runtime: Request / chat event (may include thread/session)
  Runtime->>PersistStore: resolvePersistedAccountSessionID(candidateIDs)
  PersistStore-->>Runtime: sessionID (or CODEX_THREAD_ID) + persisted indicators
  Runtime->>Runtime: nextPersistedAccountIndicatorRevision -> formatPersistedAccountIndicator
  Runtime->>PersistStore: setPersistedAccountIndicator / trimPersistedAccountIndicators
  Runtime->>UI: refreshVisiblePersistedAccountIndicators or showTerminalToastResponse (503/429)
  UI-->>Client: Render message with persistent footer indicators or show toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I hop through threads and stitch a sign,

Footers that linger, masked and fine,
I count and trim, revise and store,
Toasts that warn, labels that soar,
A tiny rabbit keeps states in line.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description is comprehensive and detailed, covering what changed (persisted footer, config caching, toast suppression), why it's needed (footer as source of truth, avoiding Windows file-lock fallbacks), and validation steps. However, the description does not follow the repository's required template structure with distinct sections. Restructure the description to match the template: use 'Summary' section for what/why, add explicit 'Testing' checklist with all required commands marked, and include 'Compliance Confirmation' and 'Notes' sections as specified in the template.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the two main changes: persisting account footer state and removing info toasts, matching the core feature additions in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/footer-toast-cleanup
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
index.ts (1)

2857-2859: Avoid storage reads in the hot success path.

Line 2857 introduces loadAccounts() on every successful request just to compute account count. This can add unnecessary I/O and latency under sustained traffic. Prefer accountManager.getAccountCount() here and refresh from disk only on account mutation paths.

Proposed fix
 if (persistAccountFooter) {
-	const persistedStorage = await loadAccounts();
-	const persistedAccountCount = persistedStorage?.accounts.length ??
-		accountManager.getAccountCount();
+	const persistedAccountCount = accountManager.getAccountCount();
 	setPersistedAccountIndicator(
 		threadIdCandidate,
 		account,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 2857 - 2859, The code is calling loadAccounts() in the
hot success path to compute persistedAccountCount, causing unnecessary storage
I/O; remove the loadAccounts() call and compute persistedAccountCount using
accountManager.getAccountCount() instead (i.e., replace the
persistedStorage/loadAccounts usage with accountManager.getAccountCount()), and
ensure any disk refresh of persistedStorage is performed only on account
mutation paths (create/update/delete) where you explicitly call loadAccounts()
or otherwise reload from disk; update references to
persistedStorage/persistedAccountCount accordingly in the surrounding logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.ts`:
- Around line 1157-1166: The helper resolvePersistedAccountSessionID currently
checks process.env.CODEX_THREAD_ID before the explicit ...candidates, causing
env-set session ID to override per-call candidates; change the iteration order
so explicit candidates are tested first and only fall back to
process.env.CODEX_THREAD_ID if none of the explicit candidates yield a non-empty
sessionID (i.e., iterate over [...candidates, process.env.CODEX_THREAD_ID] or
otherwise place process.env.CODEX_THREAD_ID after candidates), keeping the same
trimming and return-first-non-empty behavior in
resolvePersistedAccountSessionID.
- Around line 1919-1924: The code only reads lastUserMessage.info.sessionID
before calling getPersistedAccountIndicatorLabel, skipping the persisted
indicator when that exact field is missing; change this to use the shared
session-resolution logic used elsewhere (i.e., the same resolver that resolves a
fallback session ID across hooks) to compute the sessionID from lastUserMessage
(falling back to other available sources) and then pass that resolved sessionID
into getPersistedAccountIndicatorLabel so persisted footer decoration is applied
consistently.

In `@test/index.test.ts`:
- Around line 2119-2121: The test's expectedLabelOnlyIndicator is wrong; update
the test variable expectedLabelOnlyIndicator to match getPersistedAccountLabel's
behavior for accountId "acc-1" by changing its value to "Account 1 [id:acc-1]
[1/1]". Locate the test variable expectedLabelOnlyIndicator in
test/index.test.ts and replace the incorrect substring "ount-1" with "acc-1" so
it aligns with the getPersistedAccountLabel function's logic.

---

Nitpick comments:
In `@index.ts`:
- Around line 2857-2859: The code is calling loadAccounts() in the hot success
path to compute persistedAccountCount, causing unnecessary storage I/O; remove
the loadAccounts() call and compute persistedAccountCount using
accountManager.getAccountCount() instead (i.e., replace the
persistedStorage/loadAccounts usage with accountManager.getAccountCount()), and
ensure any disk refresh of persistedStorage is performed only on account
mutation paths (create/update/delete) where you explicitly call loadAccounts()
or otherwise reload from disk; update references to
persistedStorage/persistedAccountCount accordingly in the surrounding logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e65c410-c282-4d39-b9b5-9af190445605

📥 Commits

Reviewing files that changed from the base of the PR and between ce086c4 and 463696a.

📒 Files selected for processing (6)
  • index.ts
  • lib/config.ts
  • lib/schemas.ts
  • test/index.test.ts
  • test/plugin-config.test.ts
  • test/schemas.test.ts

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.ts">

<violation number="1" location="index.ts:2857">
P2: Avoid `loadAccounts()` on every successful response in this hot path; use `accountManager.getAccountCount()` directly to remove unnecessary disk I/O and reduce response-path blocking risk.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.ts">

<violation number="1" location="index.ts:2872">
P2: Use the live account count when setting the persisted footer indicator; the cached hint can become stale after runtime account removals and show an incorrect `[current/total]` value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copilot AI review requested due to automatic review settings March 14, 2026 17:06
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.ts">

<violation number="1" location="index.ts:2178">
P2: Avoid re-reading plugin config from disk on every fetch. A transient `loadPluginConfig()` failure can flip runtime footer flags back to defaults and cause inconsistent toast/footer behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes the active account indicator persistent in the chat footer (instead of transient info toasts), while avoiding redundant config reads during account-switch refreshes and adding regression coverage for indicator caching/eviction and terminal toast behavior.

Changes:

  • Add persistAccountFooter + persistAccountFooterStyle config options (schema + getters).
  • Implement per-session persisted account indicators (LRU-capped, revision-protected) and suppress “Using … / Switched …” info toasts when enabled.
  • Expand vitest coverage for styles, toast suppression, hot-path storage reads, revision race behavior, and eviction at the 200-session limit.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
index.ts Adds persisted footer indicator cache + revisioning, hooks to decorate messages, and consolidates terminal toast responses.
lib/config.ts Adds config getters + defaults for persistent footer behavior with env precedence.
lib/schemas.ts Extends config schema with persisted footer fields and style enum validation.
test/index.test.ts Adds extensive tests for footer decoration/toast suppression/race + eviction + terminal toast variants.
test/plugin-config.test.ts Adds tests for new config getters and env precedence/validation behavior.
test/schemas.test.ts Updates schema snapshot and adds rejection test for invalid footer style.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

index.ts Outdated
Comment on lines +228 to +232
type PersistAccountFooterStyle =
| "label-masked-email"
| "full-email"
| "label-only";

Comment on lines +705 to +710
it('should prioritize env var over config', () => {
process.env.CODEX_AUTH_PERSIST_ACCOUNT_FOOTER = '0';
expect(getPersistAccountFooter({ persistAccountFooter: true })).toBe(false);
process.env.CODEX_AUTH_PERSIST_ACCOUNT_FOOTER = '1';
expect(getPersistAccountFooter({ persistAccountFooter: false })).toBe(true);
});
Comment on lines +128 to +137
maskEmail: (email: string) => {
const atIndex = email.indexOf("@");
if (atIndex < 0) return "***@***";
const local = email.slice(0, atIndex);
const domain = email.slice(atIndex + 1);
const parts = domain.split(".");
const tld = parts.pop() || "";
const prefix = local.slice(0, Math.min(2, local.length));
return `${prefix}***@***.${tld}`;
},
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="test/index.test.ts">

<violation number="1" location="test/index.test.ts:2278">
P2: This mock sequence can pass without actually testing the `liveAccountCount === 0` fallback branch. Use a persistent `0` mock after priming the first call so the indicator computation is forced through the hint path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/index.test.ts (1)

2009-2035: ⚠️ Potential issue | 🟠 Major

Reset index.js between fetch-handler cases.

setupPlugin() keeps re-importing ../index.js, but this describe never clears the module cache. With the new footer/session caches in this PR, persisted indicators and the runtime footer snapshot can bleed across tests and make cases like the “first footer session” / overflow-cap assertions order-dependent.

💡 Suggested fix
 beforeEach(async () => {
+	vi.resetModules();
 	vi.clearAllMocks();
 	const configModule = await import("../lib/config.js");

You can verify the leak surface by checking for module-scoped footer caches and the lack of vi.resetModules() around the repeated ../index.js imports:

#!/bin/bash
set -euo pipefail

printf '\n== Module-scoped footer caches ==\n'
rg -n -C2 'persistedAccountIndicators|runtimePluginConfigSnapshot' --type ts

printf '\n== index.js imports vs module resets in test/index.test.ts ==\n'
rg -n -C1 'vi\.resetModules\(|import\("\.\./index\.js"\)' test/index.test.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 2009 - 2035, Tests are leaking module-scoped
footer/session caches because setupPlugin() repeatedly imports ../index.js
without resetting the module cache; add a vi.resetModules() call before each
import of "../index.js" (or at the start of the beforeEach in this describe) so
module-level state like persistedAccountIndicators and
runtimePluginConfigSnapshot in index.js is cleared between cases, ensuring the
first-footer/session behavior and overflow-cap assertions don't bleed across
tests.
🧹 Nitpick comments (1)
test/index.test.ts (1)

2823-2849: Keep prompt_cache_key alive in the delayed request.

This regression case bypasses sendPersistedAccountRequest() and falls back to the default transformRequestForCodex stub, which rewrites body to { model: "gpt-5.1" }. That makes the test ambiguous: if session lookup follows the transformed-body path used elsewhere in this suite, the late response is no longer tied to "session-stale", so the stale-write guard is never exercised.

💡 Suggested fix
+		const fetchHelpers = await import("../lib/request/fetch-helpers.js");
+		vi.mocked(fetchHelpers.transformRequestForCodex).mockImplementation(
+			async (init, _url, _config, _codexMode, parsedBody) => ({
+				updatedInit: init,
+				body: parsedBody,
+			}),
+		);
+
 		const pendingResponse = sdk.fetch!("https://api.openai.com/v1/chat", {
 			method: "POST",
 			body: JSON.stringify({ model: "gpt-5.1", prompt_cache_key: "session-stale" }),
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 2823 - 2849, The delayed fetch in the test
loses prompt_cache_key because the request falls back to
transformRequestForCodex which rewrites the body; update the test so the
outgoing request keeps the original body with prompt_cache_key (e.g., ensure
sdk.fetch's mocked implementation preserves the Request/RequestInit.body or
injects the original JSON including "prompt_cache_key") so the late response
remains tied to "session-stale" and the stale-write guard in
sendPersistedAccountRequest is exercised; reference the mocked globalThis.fetch,
the sdk.fetch call that creates pendingResponse, and the
transformRequestForCodex/sendPersistedAccountRequest behavior when making the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.ts`:
- Around line 1918-1930: The current branch still calls showToast(...) when
refreshVisiblePersistedAccountIndicators(...) returns false even if
runtimePersistAccountFooter is true; change the logic so the info toast is
emitted only when runtimePersistAccountFooter is false and
refreshVisiblePersistedAccountIndicators(...) returned false—i.e., ensure you
check runtimePersistAccountFooter before calling showToast; update the condition
around the refreshVisiblePersistedAccountIndicators(...) call (references:
runtimePersistAccountFooter, refreshVisiblePersistedAccountIndicators,
showToast, runtimePersistAccountFooterStyle, previousManagedAccount, account,
index, storage.accounts.length) so persistent footers never trigger the
transient "Switched to account ..." toast.

---

Outside diff comments:
In `@test/index.test.ts`:
- Around line 2009-2035: Tests are leaking module-scoped footer/session caches
because setupPlugin() repeatedly imports ../index.js without resetting the
module cache; add a vi.resetModules() call before each import of "../index.js"
(or at the start of the beforeEach in this describe) so module-level state like
persistedAccountIndicators and runtimePluginConfigSnapshot in index.js is
cleared between cases, ensuring the first-footer/session behavior and
overflow-cap assertions don't bleed across tests.

---

Nitpick comments:
In `@test/index.test.ts`:
- Around line 2823-2849: The delayed fetch in the test loses prompt_cache_key
because the request falls back to transformRequestForCodex which rewrites the
body; update the test so the outgoing request keeps the original body with
prompt_cache_key (e.g., ensure sdk.fetch's mocked implementation preserves the
Request/RequestInit.body or injects the original JSON including
"prompt_cache_key") so the late response remains tied to "session-stale" and the
stale-write guard in sendPersistedAccountRequest is exercised; reference the
mocked globalThis.fetch, the sdk.fetch call that creates pendingResponse, and
the transformRequestForCodex/sendPersistedAccountRequest behavior when making
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2c45f70-c032-469a-9eaf-4a451e0d8292

📥 Commits

Reviewing files that changed from the base of the PR and between 463696a and 3bc1d89.

📒 Files selected for processing (6)
  • index.ts
  • lib/config.ts
  • lib/persist-account-footer.ts
  • lib/schemas.ts
  • test/index.test.ts
  • test/logger.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/schemas.ts
  • lib/config.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
index.ts (2)

2330-2336: ⚠️ Potential issue | 🟠 Major

Avoid env-first session keying in fetch path to prevent key mismatch/bleed.

At Line 2335, runtimeThreadId || resolvePersistedAccountSessionID(promptCacheKey) forces env precedence whenever CODEX_THREAD_ID is set. That can diverge from explicit session IDs used by chat hooks and reintroduce cross-session indicator bleed/misses.

Suggested fix
-const runtimeThreadId = process.env.CODEX_THREAD_ID?.toString().trim();
-const threadIdCandidate =
-	runtimeThreadId || resolvePersistedAccountSessionID(promptCacheKey);
+const threadIdCandidate = resolvePersistedAccountSessionID(promptCacheKey);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 2330 - 2336, The fetch path currently prefers the
environment-provided session key by using runtimeThreadId ||
resolvePersistedAccountSessionID(promptCacheKey), which can cause mismatches
with chat hooks; change the selection logic so
resolvePersistedAccountSessionID(promptCacheKey) is used when present and only
fall back to process.env.CODEX_THREAD_ID (runtimeThreadId) if the persisted
session id is falsy. Update the assignment of threadIdCandidate to use the
persisted account session id first (resolvePersistedAccountSessionID) and only
use runtimeThreadId as a fallback, leaving promptCacheKey and indicatorRevision
logic unchanged.

1918-1930: ⚠️ Potential issue | 🟡 Minor

Persistent-footer mode still leaks a transient “Switched to …” info toast.

At Line 1928, info toast emission still depends on refreshedVisibleIndicator. This means persistent-footer mode can still show a transient switch toast when refresh returns false, which conflicts with the suppression goal.

Suggested fix
-const refreshedVisibleIndicator = runtimePersistAccountFooter
-	? refreshVisiblePersistedAccountIndicators(
-			preReloadTargetAccount ?? account,
-			index,
-			storage.accounts.length,
-			runtimePersistAccountFooterStyle,
-		)
-	: false;
-if (!runtimePersistAccountFooter || !refreshedVisibleIndicator) {
-	await showToast(`Switched to account ${index + 1}`, "info");
-}
+if (runtimePersistAccountFooter) {
+	refreshVisiblePersistedAccountIndicators(
+		preReloadTargetAccount ?? account,
+		index,
+		storage.accounts.length,
+		runtimePersistAccountFooterStyle,
+	);
+} else {
+	await showToast(`Switched to account ${index + 1}`, "info");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 1918 - 1930, The persistent-footer mode still shows a
transient "Switched to ..." toast when refreshVisiblePersistedAccountIndicators
returns false; change the logic so toast emission only depends on
runtimePersistAccountFooter and not on refreshedVisibleIndicator. In the block
that calls refreshVisiblePersistedAccountIndicators (referencing
runtimePersistAccountFooter, refreshedVisibleIndicator,
refreshVisiblePersistedAccountIndicators, and showToast), remove
refreshedVisibleIndicator from the toast condition and only call await
showToast(`Switched to account ${index + 1}`, "info") when
runtimePersistAccountFooter is false (i.e., wrap the showToast call in if
(!runtimePersistAccountFooter) {...}).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@index.ts`:
- Around line 2330-2336: The fetch path currently prefers the
environment-provided session key by using runtimeThreadId ||
resolvePersistedAccountSessionID(promptCacheKey), which can cause mismatches
with chat hooks; change the selection logic so
resolvePersistedAccountSessionID(promptCacheKey) is used when present and only
fall back to process.env.CODEX_THREAD_ID (runtimeThreadId) if the persisted
session id is falsy. Update the assignment of threadIdCandidate to use the
persisted account session id first (resolvePersistedAccountSessionID) and only
use runtimeThreadId as a fallback, leaving promptCacheKey and indicatorRevision
logic unchanged.
- Around line 1918-1930: The persistent-footer mode still shows a transient
"Switched to ..." toast when refreshVisiblePersistedAccountIndicators returns
false; change the logic so toast emission only depends on
runtimePersistAccountFooter and not on refreshedVisibleIndicator. In the block
that calls refreshVisiblePersistedAccountIndicators (referencing
runtimePersistAccountFooter, refreshedVisibleIndicator,
refreshVisiblePersistedAccountIndicators, and showToast), remove
refreshedVisibleIndicator from the toast condition and only call await
showToast(`Switched to account ${index + 1}`, "info") when
runtimePersistAccountFooter is false (i.e., wrap the showToast call in if
(!runtimePersistAccountFooter) {...}).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b6ba040a-e05b-4678-a503-6e8ee2be76f1

📥 Commits

Reviewing files that changed from the base of the PR and between 3bc1d89 and 16e6851.

📒 Files selected for processing (1)
  • index.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
index.ts (1)

1949-1951: ⚠️ Potential issue | 🟠 Major

Persistent-footer mode still leaks transient “Switched to …” info toasts.

At Line 1949, toast emission still occurs when footer mode is enabled but refresh returns false. PR objective says these info toasts should be suppressed whenever persistent footer is enabled.

Suggested change
-const refreshedVisibleIndicator = runtimePersistAccountFooter
-	? refreshVisiblePersistedAccountIndicators(
-			preReloadTargetAccount ?? account,
-			index,
-			storage.accounts.length,
-			runtimePersistAccountFooterStyle,
-		)
-	: false;
-if (!runtimePersistAccountFooter || !refreshedVisibleIndicator) {
-	await showToast(`Switched to account ${index + 1}`, "info");
-}
+if (runtimePersistAccountFooter) {
+	refreshVisiblePersistedAccountIndicators(
+		preReloadTargetAccount ?? account,
+		index,
+		storage.accounts.length,
+		runtimePersistAccountFooterStyle,
+	);
+} else {
+	await showToast(`Switched to account ${index + 1}`, "info");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 1949 - 1951, The toast is still emitted when
runtimePersistAccountFooter is true because the condition uses ||; change the
logic so showToast is only called when persistent-footer is disabled. Locate the
block using runtimePersistAccountFooter, refreshedVisibleIndicator and showToast
(the if (!runtimePersistAccountFooter || !refreshedVisibleIndicator) { await
showToast(...) }) and revise it so that showToast is guarded by a check for
runtimePersistAccountFooter being falsy (e.g. only call showToast when
!runtimePersistAccountFooter, and keep the refreshedVisibleIndicator check
inside that guard as needed) to ensure no info toasts appear when persistent
footer mode is enabled.
🧹 Nitpick comments (1)
index.ts (1)

1196-1202: Make cap enforcement invariant-based (while), not single-step.

Current logic trims at most one entry per call. A while keeps the map bounded even if future call sites grow it by more than one.

Suggested change
 const trimPersistedAccountIndicators = (): void => {
-	if (persistedAccountIndicators.size > MAX_PERSISTED_ACCOUNT_INDICATORS) {
-		const oldestKey = persistedAccountIndicators.keys().next().value;
-		if (oldestKey === undefined) return;
-		persistedAccountIndicators.delete(oldestKey);
-	}
+	while (persistedAccountIndicators.size > MAX_PERSISTED_ACCOUNT_INDICATORS) {
+		const oldestKey = persistedAccountIndicators.keys().next().value;
+		if (oldestKey === undefined) break;
+		persistedAccountIndicators.delete(oldestKey);
+	}
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 1196 - 1202, The trimming function
trimPersistedAccountIndicators currently deletes at most one entry per call;
change the single-step if to a while loop so it repeatedly removes the oldest
key until persistedAccountIndicators.size <= MAX_PERSISTED_ACCOUNT_INDICATORS.
Keep the guard that oldestKey may be undefined and return if so, and use
persistedAccountIndicators.keys().next().value to identify the oldest entry each
iteration; this makes the cap enforcement invariant-based rather than
single-step.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.ts`:
- Around line 1455-1471: refreshAuthorizeStoragePath currently trusts
loadPluginConfig's return value even when it silently returns DEFAULT_CONFIG on
errors, which can flip perProjectAccounts; modify refreshAuthorizeStoragePath to
detect when loadPluginConfig returned the DEFAULT_CONFIG sentinel (or otherwise
indicates a fallback) and in that case keep using runtimePluginConfigSnapshot
(and emit a warning) instead of using the DEFAULT_CONFIG result. Concretely,
call loadPluginConfig(), assign to a local (e.g., newConfig), and if newConfig
=== DEFAULT_CONFIG && runtimePluginConfigSnapshot is defined then treat that as
a failure and retain runtimePluginConfigSnapshot; continue to use
getPerProjectAccounts(newOrFallbackConfig) and setStoragePath as before, and
reference the symbols refreshAuthorizeStoragePath, loadPluginConfig,
runtimePluginConfigSnapshot, DEFAULT_CONFIG, getPerProjectAccounts,
setStoragePath, and logWarn when making the change.

---

Duplicate comments:
In `@index.ts`:
- Around line 1949-1951: The toast is still emitted when
runtimePersistAccountFooter is true because the condition uses ||; change the
logic so showToast is only called when persistent-footer is disabled. Locate the
block using runtimePersistAccountFooter, refreshedVisibleIndicator and showToast
(the if (!runtimePersistAccountFooter || !refreshedVisibleIndicator) { await
showToast(...) }) and revise it so that showToast is guarded by a check for
runtimePersistAccountFooter being falsy (e.g. only call showToast when
!runtimePersistAccountFooter, and keep the refreshedVisibleIndicator check
inside that guard as needed) to ensure no info toasts appear when persistent
footer mode is enabled.

---

Nitpick comments:
In `@index.ts`:
- Around line 1196-1202: The trimming function trimPersistedAccountIndicators
currently deletes at most one entry per call; change the single-step if to a
while loop so it repeatedly removes the oldest key until
persistedAccountIndicators.size <= MAX_PERSISTED_ACCOUNT_INDICATORS. Keep the
guard that oldestKey may be undefined and return if so, and use
persistedAccountIndicators.keys().next().value to identify the oldest entry each
iteration; this makes the cap enforcement invariant-based rather than
single-step.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dad1c363-b256-498e-86f4-32925022aaa7

📥 Commits

Reviewing files that changed from the base of the PR and between 16e6851 and 380c5af.

📒 Files selected for processing (2)
  • index.ts
  • test/index.test.ts

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.ts">

<violation number="1" location="index.ts:1463">
P2: Cold-start authorize still does two synchronous config reads; avoid the second `loadPluginConfig()` call to reduce Windows file-lock exposure and startup auth latency.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="index.ts">

<violation number="1" location="index.ts:1465">
P1: Cold-start authorize can silently use fallback defaults because refresh is skipped whenever `initialConfig` is present.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
index.ts (1)

1190-1203: ⚠️ Potential issue | 🟠 Major

Prioritize explicit session IDs before CODEX_THREAD_ID fallback.

Line 1193 currently prefers process.env.CODEX_THREAD_ID ahead of explicit candidates. That can collapse different chats into one persisted indicator key and show the wrong footer across sessions.

Suggested fix
 const resolvePersistedIndicatorSessionID = (
 	...candidates: Array<string | null | undefined>
 ): string | undefined => {
-	const runtimeThreadId = process.env.CODEX_THREAD_ID?.toString().trim();
-	if (runtimeThreadId) {
-		return runtimeThreadId;
-	}
-	for (const candidate of candidates) {
+	for (const candidate of [...candidates, process.env.CODEX_THREAD_ID]) {
 		const sessionID = candidate?.toString().trim();
 		if (sessionID) {
 			return sessionID;
 		}
 	}
 	return undefined;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 1190 - 1203, The function
resolvePersistedIndicatorSessionID currently returns process.env.CODEX_THREAD_ID
before checking explicit candidate IDs, which can merge distinct sessions;
change the logic in resolvePersistedIndicatorSessionID to iterate over the
supplied candidates first and return the first non-empty trimmed candidate, and
only if no candidate is present then read and return process.env.CODEX_THREAD_ID
(trimmed) as a fallback so explicit session IDs take priority over the
CODEX_THREAD_ID fallback.
🧹 Nitpick comments (2)
test/index.test.ts (2)

2216-2231: Make loader read-count comparison deterministic across runs.

Line 2216 compares baseline vs footer-enabled reads in a single test run. If module-level caches are warmed by the first call, the second call can be biased. Consider isolating each case (separate test or module reset + re-init per case) to avoid cache carryover affecting this assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 2216 - 2231, The comparison between baseline
and footer-enabled storage reads is non-deterministic because module-level
caches can be warmed by the first call; change the test so each case runs in
isolation by resetting modules and re-importing the plugin before each
measurement (e.g., call vi.resetModules() or an equivalent module-reset between
calls), then recreate mockClient and re-import OpenAIOAuthPlugin and
re-initialize plugin.auth.loader for each scenario; ensure you still clear
vi.mocked(storageModule.loadAccounts) before each run and use
disablePersistedFooter and enablePersistedFooter only in the isolated runs to
get deterministic read counts.

2821-2826: Harden toast-suppression assertions to avoid false negatives.

Using not.toHaveBeenCalledWith on an exact object can still pass if the same info toast is emitted with additional fields. Prefer scanning calls for any info switch toast message. (Same pattern appears in other suppression checks.)

🔧 Suggested assertion pattern
- expect(mockClient.tui.showToast).not.toHaveBeenCalledWith({
-   body: {
-     message: "Switched to account 2",
-     variant: "info",
-   },
- });
+ expect(
+   mockClient.tui.showToast.mock.calls.some(([payload]) => {
+     const body = (payload as { body?: { message?: string; variant?: string } })?.body;
+     return body?.variant === "info" && body?.message?.startsWith("Switched to account");
+   }),
+ ).toBe(false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 2821 - 2826, The current negative assertion
uses not.toHaveBeenCalledWith with an exact object which can miss toasts that
include extra fields; update the check for mockClient.tui.showToast to scan its
mock calls and assert that no call has an arg with body.message === "Switched to
account 2" and body.variant === "info" (e.g., use
mockClient.tui.showToast.mock.calls.some(...) and expect(...).toBe(false));
apply the same pattern to other toast-suppression checks to ensure any info
"Switched to account" toast — even with extra properties — is detected and fails
the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@index.ts`:
- Around line 1190-1203: The function resolvePersistedIndicatorSessionID
currently returns process.env.CODEX_THREAD_ID before checking explicit candidate
IDs, which can merge distinct sessions; change the logic in
resolvePersistedIndicatorSessionID to iterate over the supplied candidates first
and return the first non-empty trimmed candidate, and only if no candidate is
present then read and return process.env.CODEX_THREAD_ID (trimmed) as a fallback
so explicit session IDs take priority over the CODEX_THREAD_ID fallback.

---

Nitpick comments:
In `@test/index.test.ts`:
- Around line 2216-2231: The comparison between baseline and footer-enabled
storage reads is non-deterministic because module-level caches can be warmed by
the first call; change the test so each case runs in isolation by resetting
modules and re-importing the plugin before each measurement (e.g., call
vi.resetModules() or an equivalent module-reset between calls), then recreate
mockClient and re-import OpenAIOAuthPlugin and re-initialize plugin.auth.loader
for each scenario; ensure you still clear vi.mocked(storageModule.loadAccounts)
before each run and use disablePersistedFooter and enablePersistedFooter only in
the isolated runs to get deterministic read counts.
- Around line 2821-2826: The current negative assertion uses
not.toHaveBeenCalledWith with an exact object which can miss toasts that include
extra fields; update the check for mockClient.tui.showToast to scan its mock
calls and assert that no call has an arg with body.message === "Switched to
account 2" and body.variant === "info" (e.g., use
mockClient.tui.showToast.mock.calls.some(...) and expect(...).toBe(false));
apply the same pattern to other toast-suppression checks to ensure any info
"Switched to account" toast — even with extra properties — is detected and fails
the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f25e9f19-90de-4292-af94-7eca76917288

📥 Commits

Reviewing files that changed from the base of the PR and between 380c5af and 5adf448.

📒 Files selected for processing (3)
  • index.ts
  • lib/config.ts
  • test/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/config.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants