fix(accounts): normalize active pointer when the active account is disabled#399
fix(accounts): normalize active pointer when the active account is disabled#399
Conversation
…sabled getActiveIndexForFamily() previously only bounds-clamped the stored index; it did not check whether the referenced account was actually routable. setAccountEnabled(idx, false) set the enabled flag but did not repair any active-index pointer that happened to reference the just-disabled slot. Together that meant UI / automation / routing could hold a stale pointer to a disabled account after a user toggled it off in the CLI dashboard, returning either a disabled ManagedAccount or forcing an implicit skip inside the fetch loop — neither of which is easy to observe or explain. This change: - getActiveIndexForFamily() walks forward from the clamped slot to the next enabled account when the primary resolution lands on a disabled slot; if all accounts are disabled, it preserves the clamped pointer so callers can surface the 'no routable account' condition through the existing unavailable-pool paths rather than getting -1 mid-rotation. - setAccountEnabled(idx, false) repairs every activeIndexByFamily pointer that referenced idx by walking forward to the next enabled slot. Adds a regression test exercising the disable-active-account flow. Closes AUDIT-H10 / D-05 (active-account pointer can dangle after disable, identified in master repository audit). Evidence: 225/225 test files, 3419/3419 tests pass (+1 regression). typecheck + lint exit 0. No new dependencies, no new public API.
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 18 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
…ByFamily Addresses oracle audit HIGH findings on PR #399 (skipping HIGH-3 which is pre-existing and out of scope): - F1: getActiveIndexForFamily now returns -1 when all accounts disabled (previously returned a disabled index that callers trusted) - F2: setAccountEnabled now also normalizes cursorByFamily, matching the normalization already applied to currentAccountIndexByFamily - F4: Added regression tests for all-disabled, disable-then-re-enable, and multi-family divergence scenarios
Addresses oracle audit HIGH-3 finding (flagged as pre-existing during PR #399 review). When removing the currently active account while other accounts remain in the pool, pointers now advance to the next enabled account instead of defaulting to -1. Normalizes activeIndex, currentAccountIndexByFamily, and cursorByFamily consistently via a shared findNextEnabled helper. The helper walks forward (with modulo wrap) from a search origin and only returns -1 when every remaining account is disabled or the pool is empty. Tests cover: remove-at-last, remove-at-middle, remove-with-all-others-disabled, remove-until-empty, and multi-family isolation. Source: .sisyphus/notepads/phase1-audit/reports/pr399.json HIGH-3
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).
Summary
Normalizes the active-account pointer so it always references a routable (enabled) account. Previously
getActiveIndexForFamily()only bounds-clamped the stored index, andsetAccountEnabled(idx, false)set theenabledflag without repairing any active-index pointer that referenced the just-disabled slot. UI / automation / routing could therefore hold a stale pointer to a disabled account after a user toggled it off in the CLI dashboard.Problem
Audit (
docs/audits/MASTER_AUDIT.md§5 HIGHAUDIT-H10/dim-D-routing.mdD-05) demonstrated the dangling-pointer case:[0, 1, 2], active pointer =1.1via the settings-hub orcodex authcommand.account.enabled = falsebutactiveIndexByFamily.codex = 1remains.getActiveIndexForFamily("codex")returns1— a disabled account.Consumers either returned a disabled
ManagedAccountor forced an implicit skip inside the fetch loop — neither is easy to observe or explain.Change
lib/accounts.tsgetActiveIndexForFamily()lib/accounts.tssetAccountEnabled(index, false)Walks forward from the just-disabled slot to the next enabled account for each family whose
activeIndexByFamilypointer matchedindex. No-op whenenabled = true.Test
Added a regression test in
test/accounts.test.tsthat exercises the disable-active-account flow and asserts the pointer advances to the next enabled slot:Verification
npm run typecheckexit 0npm run lintexit 0Audit reference
docs/audits/MASTER_AUDIT.md§5 HIGH (post-Oracle adjustment) →AUDIT-H10docs/audits/evidence/dim-D-routing.mdD-05docs/audits/evidence/oracle-verdicts.md§1.1 (Oracle demoted H10 to MEDIUM; this PR still closes it at the implementation level)Scope guarantees
activeIndex(the global pointer) — onlyactiveIndexByFamilyFollow-up
Phase 1 continues with PR-G (release hygiene —
AUDIT-H7 pack:check+AUDIT-H8 AGENTS.md regen+AUDIT-M31 tmp-file leakage). Tracked in.sisyphus/plans/phase1-implementation.md.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 the dangling active-pointer bug (AUDIT-H10 / D-05) by patching two sites:
getActiveIndexForFamilynow defensively walks forward past disabled slots on every read, andsetAccountEnabled(idx, false)eagerly normalizes bothcurrentAccountIndexByFamilyandcursorByFamilyin lockstep so the stored pointer is repaired immediately rather than deferred to the next read.the PR description's change snippet shows
return clamped;for the all-disabled case, but the actual code (and all four new regression tests) correctly return-1— the description is just a stale draft excerpt and doesn't affect correctness.Confidence Score: 5/5
safe to merge — fix is correct, all-disabled edge case returns -1 as expected, no P0/P1 findings
all remaining findings are P2 — the only gap is that cursorByFamily normalization isn't directly exercised by a getCurrentOrNextForFamily call post-disable, but the skip-disabled loop in rotation masks any incorrectness there, and the four core AUDIT-H10 scenarios are well covered
test/accounts.test.ts — add a getCurrentOrNextForFamily assertion to cover the cursorByFamily normalization path
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["setAccountEnabled(idx, false)"] --> B["account.enabled = false"] B --> C{"currentAccountIndexByFamily matches idx?"} C -- yes --> D["findNextEnabled(idx)"] D --> E{found?} E -- yes --> F["update currentAccountIndexByFamily"] E -- no --> G["leave stale - all disabled"] C -- no --> H["skip"] F --> I{"cursorByFamily matches idx?"} H --> I G --> I I -- yes --> J["findNextEnabled(idx) again"] J --> K{found?} K -- yes --> L["update cursorByFamily"] K -- no --> M["leave stale"] I -- no --> N["skip"] P["getActiveIndexForFamily(family)"] --> Q{accounts empty?} Q -- yes --> R["return -1"] Q -- no --> S["clamp index to valid range"] S --> T{"accounts-at-clamped enabled?"} T -- yes --> U["return clamped"] T -- no --> V["walk forward N-1 steps"] V --> W{enabled candidate found?} W -- yes --> X["return candidate"] W -- no --> Y["return -1 sentinel"]Prompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix(accounts): harden pointer normalizat..." | Re-trigger Greptile