Skip to content

fix(accounts): sync cursorByFamily in markSwitched (HI-02) - #421

Merged
ndycode merged 2 commits into
mainfrom
fix/marksswitched-cursor-sync
Apr 18, 2026
Merged

fix(accounts): sync cursorByFamily in markSwitched (HI-02)#421
ndycode merged 2 commits into
mainfrom
fix/marksswitched-cursor-sync

Conversation

@ndycode

@ndycode ndycode commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Addresses HI-02 from the deep accounts-rotation audit.

markSwitched updated currentAccountIndexByFamily but not cursorByFamily, breaking the rotation invariant that the cursor advances past the just-selected account. This could cause the next family selection to start from stale state.

This PR keeps both pointers in sync and adds a regression test.

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

fixes HI-02: markSwitched and markSwitchedLocked now advance cursorByFamily to (account.index + 1) % count after updating currentAccountIndexByFamily, keeping both pointers in lockstep with the round-robin convention used in getCurrentOrNextForFamily and getCurrentOrNextForFamilyHybrid.

  • the regression test for the markSwitchedLocked path (line 1969) passes a third constructor argument that AccountManager doesn't accept — routingMutexMode stays \"legacy\", the mutex path is never engaged, and npm run typecheck will fail with TS2554.

Confidence Score: 4/5

core fix is correct and safe; one P1 test bug (invalid constructor arg) needs fixing before merge

the production fix in lib/accounts.ts is clean and correct. the test for markSwitchedLocked will break typecheck and doesn't actually exercise the mutex mode it claims to cover — needs a one-line fix (manager.setRoutingMutexMode("enabled")) before CI passes cleanly

test/accounts.test.ts line 1969

Important Files Changed

Filename Overview
lib/accounts.ts cursor sync fix in both markSwitched and markSwitchedLocked is correct and consistent with the (account.index+1)%count pattern used across the rest of the rotation pipeline
test/accounts.test.ts HI-02 regression test for markSwitched is solid; markSwitchedLocked test passes an invalid 3rd constructor arg, leaving routingMutexMode as "legacy" and never exercising the mutex path it claims to cover — also a TS compile error

Sequence Diagram

sequenceDiagram
    participant Caller
    participant AccountManager
    participant RoutingMutex

    Note over AccountManager: markSwitched (legacy sync path)
    Caller->>AccountManager: markSwitched(account, reason, family)
    AccountManager->>AccountManager: currentAccountIndexByFamily[family] = account.index
    AccountManager->>AccountManager: cursorByFamily[family] = (account.index+1) % count ← HI-02 fix

    Note over AccountManager: markSwitchedLocked (mutex path)
    Caller->>AccountManager: markSwitchedLocked(account, reason, family)
    AccountManager->>RoutingMutex: withRoutingMutex(mode, fn)
    RoutingMutex->>AccountManager: fn() [exclusive]
    AccountManager->>AccountManager: currentAccountIndexByFamily[family] = account.index
    AccountManager->>AccountManager: cursorByFamily[family] = (account.index+1) % count ← HI-02 fix
    AccountManager-->>Caller: SelectionRecord

    Note over AccountManager: next rotation call
    Caller->>AccountManager: getCurrentOrNextForFamily(family)
    AccountManager->>AccountManager: cursor = cursorByFamily[family] → starts AFTER switched account
    AccountManager-->>Caller: next account in round-robin
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: test/accounts.test.ts
Line: 1969-1971

Comment:
**constructor options arg doesn't exist — mutex mode never set to "enabled"**

`AccountManager`'s constructor only accepts two parameters (`authFallback`, `stored`). the third argument `{ routingMutexMode: "enabled" }` is silently dropped at runtime and `routingMutexMode` stays `"legacy"`, meaning `markSwitchedLocked` runs the callback inline via `withRoutingMutex("legacy", ...)` — the mutex-serialized code path is never actually exercised. TypeScript will also reject this with `TS2554 Expected 0-2 arguments, but got 3`, breaking `npm run typecheck`.

fix: construct with 2 args and call `setRoutingMutexMode` explicitly:

```suggestion
			const manager = new AccountManager(undefined, stored);
			manager.setRoutingMutexMode("enabled");
```

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

Reviews (2): Last reviewed commit: "test(accounts): cover markSwitchedLocked..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

markSwitched updated currentAccountIndexByFamily but left cursorByFamily
pointing at the pre-switch position. Subsequent round-robin passes
(getCurrentOrNextForFamily / getNextForFamily) started from the stale
cursor and could either re-pick the same slot or skip the just-switched
account entirely, dropping the caller's explicit switch intent after a
rate-limit-triggered rotation.

Fix both markSwitched and its mutex-serialized sibling markSwitchedLocked
to advance cursorByFamily[family] to `(account.index + 1) % count`,
matching the convention already used in getCurrentOrNextForFamilyHybrid
and the inner loop of getCurrentOrNextForFamily. No-op when the pool is
empty.

Adds a regression test that walks the cursor to a non-zero position,
marks a different account as switched, and asserts the next rotation
resumes AFTER the marked slot rather than from the stale cursor.

Not covered by PR #399 (which normalized pointers on setAccountEnabled
and getActiveIndexForFamily only).
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

fixed cursor desynchronization in account switching by updating cursorByFamily alongside currentAccountIndexByFamily in both direct and mutex-locked paths. also adjusted typescript typing for removeAccount variables. ensures round-robin selection resumes at correct position after account switch.

Changes

Cohort / File(s) Summary
Cursor Synchronization
lib/accounts.ts:markSwitched, lib/accounts.ts:markSwitchedLocked
Added cursor advancement logic setting cursorByFamily[family] to (account.index + 1) % this.accounts.length when marking account as switched, keeping round-robin state aligned with active index.
Type Annotation
lib/accounts.ts:removeAccount
Explicit Record<ModelFamily, number> multi-line type for priorCursor and priorActive without runtime behavior change.
Regression Test
test/accounts.test.ts:markSwitched
New test verifying cursor advances to post-switch position after markSwitched, confirming next rotation starts from correct index.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Review notes

  • concurrency risk: markSwitchedLocked path uses mutex correctly, but verify no other code paths mutate cursorByFamily[family] without updating the active index (lib/accounts.ts: check all assignments to cursor tracking).
  • modulo arithmetic: (account.index + 1) % this.accounts.length assumes this.accounts.length > 0; verify guard against empty pools (lib/accounts.ts:markSwitched, lib/accounts.ts:markSwitchedLocked).
  • test coverage: regression test correctly validates both active index and next rotation behavior (test/accounts.test.ts), but missing: test switching to same index twice (cursor idempotence), test with single-account pool (edge case where index wraps immediately).
  • windows edge case: none identified; modulo behavior consistent across platforms.

Suggested labels

bug

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR description is detailed and references the audit issue, but validation checklist is missing entirely and risk/rollback sections are unfilled. Complete the validation checklist (lint, typecheck, test, build), specify risk level, and document rollback plan. also address greptile's P1 issue: test/accounts.test.ts:1969 has invalid constructor arg that breaks typecheck.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format (type: fix, scope: accounts, summary under 72 chars in lowercase imperative), clearly summarizes the main change of syncing cursorByFamily in markSwitched.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/marksswitched-cursor-sync
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/marksswitched-cursor-sync

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/accounts.ts`:
- Around line 913-919: The test suite misses asserting the mutex-serialized
rotation path: add the same HI-02 assertion used for markSwitched into the
locked-path test by invoking markSwitchedLocked with routingMutexMode enabled
and verifying cursorByFamily[family] equals (account.index + 1) % count;
specifically, update the test at the same spot that exercises markSwitched
(test/accounts.test.ts) to also exercise markSwitchedLocked, enable
routingMutexMode for that run, and assert the cursorByFamily update matches the
legacy rotation invariant so the mutex path cannot drift from markSwitched.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74d35594-e5c1-4f39-bf7e-cd0b10f0f5aa

📥 Commits

Reviewing files that changed from the base of the PR and between 3f1c1fe and 3226352.

📒 Files selected for processing (2)
  • lib/accounts.ts
  • test/accounts.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/accounts.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/accounts.test.ts
🔇 Additional comments (3)
test/accounts.test.ts (1)

1915-1955: good direct regression for hi-02.

test/accounts.test.ts:1915 deterministically proves the stale cursor case for the legacy markSwitched path and asserts both the active pointer and next rotation result. no real secrets, no skipped assertions.

lib/accounts.ts (2)

869-878: direct cursor sync looks correct.

lib/accounts.ts:869 now advances the family cursor past the switched account, matching getCurrentOrNextForFamily and the regression at test/accounts.test.ts:1915.


1336-1343: typing-only change looks safe.

lib/accounts.ts:1336 keeps the prior pointer snapshots explicit, and both records are fully populated from MODEL_FAMILIES before any read.

Comment thread lib/accounts.ts
Responds to PR #421 review feedback.

The HI-02 regression test already covered the legacy markSwitched path.
This adds the same assertion through the mutex-serialized
markSwitchedLocked path with routingMutexMode enabled so the concurrency
variant cannot drift from the sync behavior.
Comment thread test/accounts.test.ts
Comment on lines +1969 to +1971
const manager = new AccountManager(undefined, stored, {
routingMutexMode: "enabled",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 constructor options arg doesn't exist — mutex mode never set to "enabled"

AccountManager's constructor only accepts two parameters (authFallback, stored). the third argument { routingMutexMode: "enabled" } is silently dropped at runtime and routingMutexMode stays "legacy", meaning markSwitchedLocked runs the callback inline via withRoutingMutex("legacy", ...) — the mutex-serialized code path is never actually exercised. TypeScript will also reject this with TS2554 Expected 0-2 arguments, but got 3, breaking npm run typecheck.

fix: construct with 2 args and call setRoutingMutexMode explicitly:

Suggested change
const manager = new AccountManager(undefined, stored, {
routingMutexMode: "enabled",
});
const manager = new AccountManager(undefined, stored);
manager.setRoutingMutexMode("enabled");
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/accounts.test.ts
Line: 1969-1971

Comment:
**constructor options arg doesn't exist — mutex mode never set to "enabled"**

`AccountManager`'s constructor only accepts two parameters (`authFallback`, `stored`). the third argument `{ routingMutexMode: "enabled" }` is silently dropped at runtime and `routingMutexMode` stays `"legacy"`, meaning `markSwitchedLocked` runs the callback inline via `withRoutingMutex("legacy", ...)` — the mutex-serialized code path is never actually exercised. TypeScript will also reject this with `TS2554 Expected 0-2 arguments, but got 3`, breaking `npm run typecheck`.

fix: construct with 2 args and call `setRoutingMutexMode` explicitly:

```suggestion
			const manager = new AccountManager(undefined, stored);
			manager.setRoutingMutexMode("enabled");
```

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

Fix in Codex

@ndycode
ndycode merged commit 098a17f into main Apr 18, 2026
2 checks passed
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.

1 participant