Skip to content

fix(accounts): invalidate runtime tracker key on removeAccount (HI-01)#419

Merged
ndycode merged 2 commits into
mainfrom
fix/stale-runtime-tracker-key
Apr 18, 2026
Merged

fix(accounts): invalidate runtime tracker key on removeAccount (HI-01)#419
ndycode merged 2 commits into
mainfrom
fix/stale-runtime-tracker-key

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented Apr 17, 2026

Summary

  • Fixes HI-01 from .sisyphus/notepads/deep-audit/reports/accounts-rotation.json: _runtimeTrackerKey cached to a numeric index goes stale after removeAccount reindex.
  • Invalidate the cached tracker key on each surviving account whose cached key was numeric, right after the splice + reindex loop in removeAccount.
  • Identity-based string keys are left alone because accountId/email are not affected by array-position changes.

Why this matters

getRuntimeTrackerKey caches the resolved key on account._runtimeTrackerKey. When an account has no accountId/email, getRuntimeAccountIdentityKey falls back to account.index. After removeAccount splices the pool and reindexes survivors with acc.index = index, the cached numeric key is never invalidated — subsequent rotation, health, and token-bucket lookups all consult the stale (pre-reindex) position.

Test

Added invalidates numeric runtime tracker key after removeAccount reindex (HI-01) to test/accounts.test.ts under the active-account pointer dangle describe. The test:

  • Builds a four-account pool mixing refresh-only (numeric-keyed) and identity-bearing (string-keyed) accounts.
  • Primes _runtimeTrackerKey on both a refresh-only account (index 2) and an identity account (index 3).
  • Records a recordFailure degradation under the pre-reindex numeric key and captures the health score.
  • Removes the identity-bearing account at index 1, pulling the refresh-only account down to index 1.
  • Asserts the refresh-only account reports its new numeric key (1), not the stale cached 2.
  • Asserts no health-score bleed from the old numeric key into the new one.
  • Asserts the identity-bearing string key survives the reindex unchanged.

Validation

  • npm test -- accounts rotation → 276/276 passing (9 files).
  • npm test (full suite) → 3530/3530 passing (232 files). Test count delta in accounts.test.ts: 144 → 145 (+1).
  • npm run typecheck → clean.
  • npm run lint → clean.

Constraints honoured

  • No as any, @ts-ignore, @ts-expect-error.
  • No force-push, no amend.
  • Branched off origin/main at 3f1c1fe (current origin/main; audit was authored against 1f6da97 but the _runtimeTrackerKey caching logic is identical at both points).

Audit reference: .sisyphus/notepads/deep-audit/reports/accounts-rotation.json HI-01.

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 fixes HI-01 by invalidating the cached _runtimeTrackerKey on numeric-keyed accounts after removeAccount reindexes the pool, and adds a follow-up commit that also purges stale numeric tracker entries via clearNumericKeysAtOrAbove. the _runtimeTrackerKey invalidation in the forEach loop is correct, but the follow-up commit calls getHealthTracker().clearNumericKeysAtOrAbove(idx) and getTokenTracker().clearNumericKeysAtOrAbove(idx) — a method that does not exist on either HealthScoreTracker or TokenBucketTracker in lib/rotation.ts.

  • P0: clearNumericKeysAtOrAbove is missing from both tracker classes; every removeAccount call will throw TypeError at runtime and fail TypeScript strict compilation, making the fix net-broken despite the correct intent.

Confidence Score: 2/5

not safe to merge — every account removal throws TypeError at runtime due to missing method definition

the P0 missing-method bug makes the entire removeAccount path throw unconditionally; the correct part of the fix (numeric key invalidation in forEach) is sound but unreachable until clearNumericKeysAtOrAbove is added to both tracker classes in rotation.ts

lib/rotation.ts needs clearNumericKeysAtOrAbove(threshold: number): void added to both HealthScoreTracker and TokenBucketTracker

Important Files Changed

Filename Overview
lib/accounts.ts adds clearNumericKeysAtOrAbove + _runtimeTrackerKey invalidation in removeAccount, but clearNumericKeysAtOrAbove is not defined on either tracker class — runtime TypeError on every account removal
test/accounts.test.ts adds HI-01 regression test covering numeric key invalidation and tracker state bleed; logic is correct but the token-d fixture block has inconsistent indentation and the test cannot currently pass due to the missing method in rotation.ts

Sequence Diagram

sequenceDiagram
    participant Caller
    participant AccountManager
    participant HealthScoreTracker
    participant TokenBucketTracker
    participant accounts as accounts[]

    Caller->>AccountManager: removeAccount(account)
    AccountManager->>accounts: splice(idx, 1)
    AccountManager->>HealthScoreTracker: clearNumericKeysAtOrAbove(idx) ❌ undefined
    Note over HealthScoreTracker: TypeError — method missing in rotation.ts
    AccountManager->>TokenBucketTracker: clearNumericKeysAtOrAbove(idx) ❌ undefined
    AccountManager->>accounts: forEach → acc.index = newIndex
    Note over accounts: if _runtimeTrackerKey is numeric → delete (✅ correct)
    AccountManager-->>Caller: true (never reached)
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/accounts.ts
Line: 1336-1337

Comment:
**missing method definition — `clearNumericKeysAtOrAbove` does not exist**

`clearNumericKeysAtOrAbove` is called on both `HealthScoreTracker` and `TokenBucketTracker` here, but neither class defines this method in `lib/rotation.ts`. `HealthScoreTracker` (line 77) and `TokenBucketTracker` (line 198) expose only `getScore/recordSuccess/recordRateLimit/recordFailure/reset/clear` and `getTokens/tryConsume/refundToken/drain/reset/clear` respectively — no `clearNumericKeysAtOrAbove` on either.

This means:
- TypeScript strict-mode compilation rejects both calls (contradicting the "typecheck clean" claim).
- At runtime, every call to `removeAccount` throws `TypeError: getHealthTracker(...).clearNumericKeysAtOrAbove is not a function`.
- The regression test added in this PR cannot pass as written.

The method needs to be added to both classes in `rotation.ts`. Sketch of the required implementation — the internal key format is `JSON.stringify([numericStringOrId, quotaKey ?? null])`, so numeric-slot entries can be identified and pruned by threshold:

```typescript
// HealthScoreTracker — add inside the class body
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.entries.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.entries.delete(key);
        }
    }
}

// TokenBucketTracker — identical shape, targets this.buckets
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.buckets.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.buckets.delete(key);
        }
    }
}
```

Without this, the HI-01 fix is incomplete and breaks all account removal paths.

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

---

This is a comment left during a code review.
Path: test/accounts.test.ts
Line: 2914-2918

Comment:
**inconsistent indentation in test fixture**

the `token-d` account object uses 5-tab indentation while the other three accounts in the same array use 6-tab. minor but inconsistent with the surrounding fixture.

```suggestion
					{
						refreshToken: "token-d",
						addedAt: now,
						lastUsed: now,
					},
```

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

Reviews (2): Last reviewed commit: "fix(accounts): clear shifted numeric tra..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

getRuntimeTrackerKey caches the resolved tracker key on the account via
_runtimeTrackerKey. For accounts without accountId/email, the key falls
back to the numeric account.index. When removeAccount splices the pool
and reindexes survivors (acc.index = index), the cached numeric key is
not invalidated, so later lookups keep consulting the pre-reindex
position. Rotation, health, and token-bucket state for that account all
mismatch the current pool order.

Clear _runtimeTrackerKey on each survivor whose cached key was numeric,
right after the reindex loop. Identity-based string keys remain stable
because accountId/email are not affected by array-position changes, so
they do not need invalidation.

Regression test exercises the remove-then-select path: a pool of four
accounts (mixed refresh-only and identity-bearing) primes the runtime
tracker, records a health failure under the pre-reindex key, removes
the identity-bearing account at index 1, and asserts the surviving
refresh-only account reports its new numeric index (1) instead of the
stale cached value (2). The identity account's string key is verified
to survive the reindex unchanged.

Audit reference: .sisyphus/notepads/deep-audit/reports/accounts-rotation.json HI-01
@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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

modified AccountManager.removeAccount to invalidate cached numeric runtime tracker keys after reindexing accounts. when an account is removed and remaining accounts are reindexed, any runtime tracker key derived from array position (numeric) gets reset to undefined to force recomputation, while identity-based string keys remain unchanged. added regression test validating this cache invalidation behavior.

Changes

Cohort / File(s) Summary
Core tracker key invalidation logic
lib/accounts.ts
Expanded type assertions and added logic to set acc._runtimeTrackerKey to undefined when the cached value is numeric (array-position-derived), forcing recomputation on next access. string identity-based keys are preserved. see lib/accounts.ts:line for the invalidation loop.
Regression test for tracker key correctness
test/accounts.test.ts
new test validates that numeric runtime tracker keys are recomputed post-reindex after removeAccount splice, stale cached indices don't propagate to health tracker state, and string identity keys persist unchanged across reindex.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

bug


notes for review

  • missing concurrency guardrails: the runtime tracker key invalidation happens synchronously during removeAccount. if tracker key access is happening concurrently from another task, there's a window where the cache is cleared but hasn't been recomputed yet. are there any locks protecting _runtimeTrackerKey mutations? flagging this explicitly because multi-auth scenarios often involve async account operations.

  • regression test is solid: the test properly primes both numeric (refresh-token-only) and string (identity-bearing) cached states, then validates the differentiated behavior post-reindex. however, verify the test actually runs against real account reordering scenarios, not just the happy path.

  • windows edge case check: confirm Record<ModelFamily, number> type expansion works correctly on windows builds. the type assertion widening shouldn't cause issues but worth a quick sanity check on your ci matrix.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR description is detailed and complete, but the greptile review embedded within the description reveals critical blockers: clearNumericKeysAtOrAbove is not defined on either HealthScoreTracker or TokenBucketTracker, causing runtime TypeError on every removeAccount call and breaking typecheck. add clearNumericKeysAtOrAbove(threshold: number): void to both HealthScoreTracker (lib/rotation.ts:77+) and TokenBucketTracker (lib/rotation.ts:198+) as sketched in the greptile feedback; verify typecheck and full test suite pass after implementation.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format with type 'fix', scope 'accounts', and summary under 72 chars in lowercase imperative.
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/stale-runtime-tracker-key
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/stale-runtime-tracker-key

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
Copy Markdown
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: 2

🤖 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 1335-1345: The numeric runtime-tracker cache
(acc._runtimeTrackerKey) is cleared on splice but lib/rotation.ts still keeps
health/token entries keyed by the old numeric indices, allowing a later
refresh-only survivor to inherit another account’s state; update the rotation
trackers (e.g., the health and token maps used in rotation.ts—referenced where
entries are read/removed around the functions handling rotation and health
checks) to also clear any entries whose numeric keys fall in the shifted index
range when you remove an account, or switch refresh-only accounts to a stable
per-account runtime id returned by getRuntimeAccountIdentityKey and use that
stable id as the tracker key; also add a vitest regression that removes one slot
with two refresh-only survivors after it and asserts no state bleed, update any
affected queues to handle EBUSY/429 retries, and ensure logging changes do not
leak tokens/emails.

In `@test/accounts.test.ts`:
- Around line 2914-2919: Update the test in test/accounts.test.ts to cover
numeric key reuse: make the post-removal survivor account (the one initialized
with refreshToken "token-d") a refresh-only account by removing/omitting its
email so it can shift into numeric slot 2, and keep a separate identity account
(with an email) for the existing string-key assertions; after the removal/shift
assert that the new numeric key 2 has pristine state (no stale health/token
entries) to catch the bleed case referenced in lib/accounts.ts:1335, and ensure
assertions remain deterministic under vitest (no real secret mocks or skipped
asserts).
🪄 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: b0850a31-0480-40d0-a6ef-9e218505ba63

📥 Commits

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

📒 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 (1)
lib/accounts.ts (1)

1319-1326: this type-only cleanup looks safe.

lib/accounts.ts:1319 keeps the family pointer snapshots explicit without changing removeAccount behavior. no auth rotation or logging concern here.

Comment thread lib/accounts.ts
Comment thread test/accounts.test.ts Outdated
Follow-up for PR #419 review comments.

Invalidate cached numeric tracker keys was not sufficient on its own: the
rotation health/token tracker maps still retained entries keyed by the old
numeric slots, so a later refresh-only survivor could inherit another
account's state after reindex.

Fix: clear numeric-keyed tracker entries at or above the removed index for
both HealthScoreTracker and TokenBucketTracker, then keep the regression test
fully numeric by making the shifted survivor refresh-only and asserting both
health and token state are pristine after reindex.
Comment thread lib/accounts.ts
Comment on lines +1336 to +1337
getHealthTracker().clearNumericKeysAtOrAbove(idx);
getTokenTracker().clearNumericKeysAtOrAbove(idx);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 missing method definition — clearNumericKeysAtOrAbove does not exist

clearNumericKeysAtOrAbove is called on both HealthScoreTracker and TokenBucketTracker here, but neither class defines this method in lib/rotation.ts. HealthScoreTracker (line 77) and TokenBucketTracker (line 198) expose only getScore/recordSuccess/recordRateLimit/recordFailure/reset/clear and getTokens/tryConsume/refundToken/drain/reset/clear respectively — no clearNumericKeysAtOrAbove on either.

This means:

  • TypeScript strict-mode compilation rejects both calls (contradicting the "typecheck clean" claim).
  • At runtime, every call to removeAccount throws TypeError: getHealthTracker(...).clearNumericKeysAtOrAbove is not a function.
  • The regression test added in this PR cannot pass as written.

The method needs to be added to both classes in rotation.ts. Sketch of the required implementation — the internal key format is JSON.stringify([numericStringOrId, quotaKey ?? null]), so numeric-slot entries can be identified and pruned by threshold:

// HealthScoreTracker — add inside the class body
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.entries.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.entries.delete(key);
        }
    }
}

// TokenBucketTracker — identical shape, targets this.buckets
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.buckets.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.buckets.delete(key);
        }
    }
}

Without this, the HI-01 fix is incomplete and breaks all account removal paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/accounts.ts
Line: 1336-1337

Comment:
**missing method definition — `clearNumericKeysAtOrAbove` does not exist**

`clearNumericKeysAtOrAbove` is called on both `HealthScoreTracker` and `TokenBucketTracker` here, but neither class defines this method in `lib/rotation.ts`. `HealthScoreTracker` (line 77) and `TokenBucketTracker` (line 198) expose only `getScore/recordSuccess/recordRateLimit/recordFailure/reset/clear` and `getTokens/tryConsume/refundToken/drain/reset/clear` respectively — no `clearNumericKeysAtOrAbove` on either.

This means:
- TypeScript strict-mode compilation rejects both calls (contradicting the "typecheck clean" claim).
- At runtime, every call to `removeAccount` throws `TypeError: getHealthTracker(...).clearNumericKeysAtOrAbove is not a function`.
- The regression test added in this PR cannot pass as written.

The method needs to be added to both classes in `rotation.ts`. Sketch of the required implementation — the internal key format is `JSON.stringify([numericStringOrId, quotaKey ?? null])`, so numeric-slot entries can be identified and pruned by threshold:

```typescript
// HealthScoreTracker — add inside the class body
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.entries.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.entries.delete(key);
        }
    }
}

// TokenBucketTracker — identical shape, targets this.buckets
clearNumericKeysAtOrAbove(threshold: number): void {
    for (const key of this.buckets.keys()) {
        const parsed = JSON.parse(key) as [string, string | null];
        const n = Number(parsed[0]);
        if (Number.isInteger(n) && String(n) === parsed[0] && n >= threshold) {
            this.buckets.delete(key);
        }
    }
}
```

Without this, the HI-01 fix is incomplete and breaks all account removal paths.

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

Fix in Codex

ndycode added a commit that referenced this pull request Apr 18, 2026
The rollup integration exposed that PR #419's branch updates accounts.ts to call
HealthScoreTracker.clearNumericKeysAtOrAbove() and
TokenBucketTracker.clearNumericKeysAtOrAbove(), but the helper methods never
landed in lib/rotation.ts on that branch.

Add the two tiny helper methods directly on the rollup branch so the full
integration set typechecks and the rollup PR is self-contained. PR #419 itself
should receive the same patch before final merge.
@ndycode ndycode merged commit de1e179 into main Apr 18, 2026
2 checks passed
ndycode added a commit that referenced this pull request Apr 18, 2026
Merge the reviewed rollup fix branch after deep audit, full verification battery, and real pack/install smoke test.

This lands the integrated fix set from PRs #414-#429 and #431 via one reviewed branch, including the two rollup-only integration commits:
- 7f50455 fix(rollup): add missing numeric tracker cleanup helpers from PR #419
- 056ad18 test(rollup): restore auth-list empty-state expectation to current behavior

Rationale: the rollup branch was the only branch deep-audited end-to-end and pack/install smoke-tested as a complete artifact. Baseline failures in codex-bin-wrapper / benchmark-runtime-path-script already exist on main and are not regressions from this merge.
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