feat(routing): sticky-balanced placement, session reset, and cachekeep sustain - #82
feat(routing): sticky-balanced placement, session reset, and cachekeep sustain#82iceteaSA wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 25 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
4f8e0e4 to
9ce70e0
Compare
|
Pushed The bug: a degraded config read silently deleted routing state.
Fixed at the type level rather than per call site: the roster now expresses unknown ( Found while live-testing, and worth stating plainly: I couldn't tell what had happened. A pin count dropped to zero and nothing in the log explained it. That gap is now closed on both sides — pruning logs what it removed and why, placement and migration log at debug with hashed session keys, and requests record which account served them. Under The sidebar was displaying routing that contradicted reality. Its resolver never consulted pins, so a session whose display entry was missing fell through to the mode default and reported On verification. An independent reviewer ran per-hunk mutation proofs and found that one section — the debug logs above — reverted with the suite still green. Unverified code, and pointedly so: that logging exists because silent routing state cost real debugging time, and it shipped with nothing gating it. Proving the retention-silence contract needed a different mechanic than the rest. Removing a log cannot fail a test asserting that no log occurs — it deepens the silence — so the guard was temporarily broadened until a retained pin logged, which reddened the test, then restored. Positive assertions used reverse-removal. Six sections, all now individually proven; the reviewer re-ran every mutation independently rather than accepting the reported evidence. Live-verified on a real account set before this push: placement picked the predicted account (0.406 sustainable weight vs 0.117, with a 0%-remaining account correctly excluded rather than scored), the pin held across turns with input bytes growing monotonically, and a subagent received its own independent pin. One known gap, deliberately deferred: |
|
Reviewed commit One blocker before merge. The killswitch is not enforced on the sticky-balanced path. Under Two concrete consequences with killswitch on:
Note the fallback roster does not cover this either: Requested change: treat a killswitch-failing account the same as exhaustion on the sticky path — exclude it from weighted placement and from the fail-open branch, migrate a retained pin off it, and when nothing passes, return the same blocked 429 the ordered modes produce ( Two smaller notes, non-blocking:
Please also rebase once #57 lands so the diff collapses to the single commit. |
9ce70e0 to
d4c7a4f
Compare
|
Blocker fixed in Killswitch on the sticky path. A killswitch-failing account is now excluded from weighted placement, a retained pin migrates off one that drops below its floor, and when nothing passes the request gets the shared The fail-open exclusion is the half worth calling out. Excluding killed accounts from placement is the obvious part; excluding them from the mode-fallback branch is what stops that branch becoming a way to spend on a killed account precisely when every account is killed — the failure only appears in the state where all weights are zero, which is exactly when the escape hatch fires. Tested both directions: all-below-floor returns a 429 without reaching the wire, and a separate test asserts killswitch-disabled placement and retention are unchanged. The second is what proves the default path was not quietly altered; its mutation is an inverse (apply the filter unconditionally), since reverse-removal cannot fail a test asserting nothing changed. Your README request surfaced a real bug, and I want to flag it rather than let it ride. You asked me to document that the fail-open branch orders by Its unit test set Fixed the key and added coverage through the real path (sidebar file → roster builder → sort), where This is a behaviour change and you should decide whether you want it here. The fail-open pick now genuinely orders by credit priority. Blast radius is narrow — only when every quota is stale or unknown (weighted set empty) and two or more candidates carry non-zero credits. No existing expectation shifted; I checked every suite reference to My reasoning for keeping it: shipping your README line without the fix would put a false claim in the docs in the PR where you asked for accuracy, and the fix is one key on one extractor with no signature or consumer changes. But it is your call — happy to split it into its own PR (extractor + test + README sentence, nothing else) if you would rather be able to revert one without the other. Both smaller notes done: routing command blurb now lists Will rebase once #57 lands so the diff collapses to a single commit. |
|
Verified the killswitch fix at 875 tests pass locally on the branch. The remaining item is the stack. This still carries #57's commit as its base, and #57 is now ready to merge on my side. Once #57 lands, please rebase so this collapses to the single commit — I would rather not merge the same commit through two paths. |
|
Understood on the stack — I would not want the same commit merged through two paths either. Watching #57. The moment it lands I will rebase this onto the new No other outstanding work on my side: the killswitch fix and the reset-credit extractor are both in at Still open for you: whether the reset-credit key fix stays here or splits into its own PR. It rides along because your README request is what made it load-bearing — documenting an ordering that never happened would have been a false claim — but it is a behaviour change in a PR you are reviewing for the killswitch, so the choice should be yours. If you want it split I will pull it out in the same pass as the rebase; otherwise I will leave it. |
…p sustain Routing re-derived the same answer on every request, so a session's account could change between turns. That is cheap with a cold prompt cache and expensive with a warm one - moving a large session re-uploads its whole context at write price, measured on the sibling plugin at 450-560K cache-write tokens per move. sticky-balanced decides once, at cold start, then stops deciding. It picks the account with the least projected pressure - sustainable spend rate, not raw remaining percent, so an account resetting in twenty minutes outranks one with more headroom but days to go. Capacity is the minimum across present windows, since an account is only as usable as its tightest binding one. A stale or missing snapshot is excluded rather than assumed healthy; if that leaves nothing, selection falls open to the configured mode order and the wire stays authoritative. The killswitch binds here as it does everywhere else. An account below its threshold is excluded from weighted placement AND from the fail-open branch, a retained pin migrates off one that drops below it, and when no account passes the request gets the same 429 the ordered modes return rather than a bespoke error. Without that last exclusion the fail-open branch would have become a way to spend on a killed account precisely when every account was killed. The whole path is inert when the killswitch is off. Concurrent processes converge without new coordination: assignments resolve under the existing sidebar lock, and each account carries the pending bytes of sessions already placed on it, so a second process sees the first one's load. Ordering is fully deterministic, which is what makes the in-lock recheck idempotent. A placed session moves only on confirmed exhaustion or permanent auth failure, never for load. There is no hold-for-reset branch: that would depend on the host honouring Retry-After, and @ai-sdk/provider-utils ignores it entirely - a hold would surface as a visible failure after about six seconds. Migration after a response is limited to statuses that did not serve, because the request that exhausts an account returns 200 with headers showing 100% used; treating that as a migration cancelled a successful response and billed the turn twice. The fail-open branch's credit priority also needed repair to be worth documenting: it read resetCreditsApplicable from a shape that carries resetCreditsAvailable, so the sort silently never fired. Its unit test set the field directly on the candidate and passed while the wiring did nothing, so the behaviour is now covered through the real path. The roster a pin is pruned against can be unknown. A degraded config read produced an empty account list, indistinguishable from a roster that genuinely has no fallbacks, so a transient failure silently deleted every fallback-pinned placement. Unknown now skips the account-membership test; an authoritatively empty roster still prunes, since main-only is a real state. Age expiry and explicit removal are unaffected either way. Routing state also changed silently. Pruning, placement and migration now log at debug with hashed session keys - retaining a healthy pin stays silent, since that is the common path - and requests record which account served them. Under the older modes that was inferable from mode plus quota; with per-session pins it is not. The sidebar showed routing that contradicted reality: its resolver never consulted pins, so a session whose display entry was missing reported main while pinned elsewhere. It now prefers the observed serving account, then the session's own pin, then the previous mode-driven answer, and shows which account a session is pinned to. /openai-routing reset clears the calling session's pin and nothing else. Quota, backoff and killswitch state are account-health facts; asking to re-route a session asserts nothing about them. /openai-cachekeep sustain keeps main-agent sessions warm past the idle cap. Subagents keep every existing bound, since warming a short-lived session forever buys a cache nothing returns to. It is named sustain rather than always because the sibling plugin already ships /claude-cachekeep always for the clock-window axis. Session ids are stored as SHA-256 hashes with a seven-day TTL, capped with least-recently-seen eviction, and the state file and directory are private. main-first and fallback-first are unchanged.
d4c7a4f to
132c0b9
Compare
feat(routing): sticky-balanced placement, session reset, and cachekeep sustain Rebased onto the revised #57 so the admission identity rule and the replayability gate survive; freshestQuotaSnapshot keeps both windows' timestamps as the freshness key.
|
Rebased onto The rebase was not clean, and the conflict was worth the care: this branch was cut before your #57 fixes, and it had refactored the same code into a Resolved as sticky structure with main's semantics:
Also confirmed present after the rebase rather than assuming: Nothing else outstanding from me. The reset-credit key fix is still in this commit — say the word if you would rather have it split and I will pull it into its own PR. |
|
Integrated into main as af400b2 (merge b628540). Rebased onto the revised #57 locally so the admission identity rule and the replayability gate survive: the stacked base carried the pre-revision versions of both, and freshestQuotaSnapshot now keeps both windows' timestamps as its freshness key. All 875 branch tests plus the integrated suite pass. Thanks — the pre-resolved killswitchPasses threading was a better shape than what I asked for. |
Why
Routing today is mode-driven: every request re-derives the same answer, so a session's account can change between turns. That is cheap when the prompt cache is cold and expensive when it is not — moving a large session to a different account re-uploads its whole context at write price. The sibling
anthropic-authplugin measured that at 450–560K tokens of cache-write per mid-session move, which is the number this design is built around.What
sticky-balancedrouting mode. Decides once, at cold start, then stops deciding.spendable / hoursUntilReset), not raw remaining percent. An account resetting in twenty minutes outranks one with more headroom but days to go.0600/0700./openai-routing resetclears the current session's pin and nothing else. Quota, backoff, killswitch and cachekeep state are account-health facts — asking to re-route a session asserts nothing about them. It does not force a different account; the next request may legitimately pick the same one./openai-cachekeep sustain on|offkeeps main-agent sessions warm past the idle cap. Subagents keep every existing bound (30-min idle, 75-min gpt-5.6, 2-warm cap) — warming a short-lived session forever buys a cache nothing returns to. Sustain bypasses idle reclamation only; ceilings, eviction and any clock window still apply. Defaults off.Two decisions worth flagging for review
No
hold/Retry-After branch. The sibling plugin holds a session for ≤15 min when a short window is about to reset, rather than paying a cold start. That depends on the host honouringRetry-After. Checked here:retry-afterappears zero times across every@ai-sdk/provider-utilsversion on this box (4.0.21→5.0.12), and the retry policy ismaxRetries: 2, initialDelayInMs: 2000, backoffFactor: 2with response headers never consulted. A 15-minute hold would surface as a visible failure after ~6s. So exhaustion migrates immediately. Worth revisiting if Codex reintroduces a short (5h-class) window.Migration after a response is limited to statuses that did not serve. The request that exhausts an account returns 200 OK with headers showing 100% used. An earlier revision classified that as
migrateand re-sent the turn, cancelling the successful response — billing the same turn twice. Caught in review; the quota push from that 200 is what steers the next request elsewhere.Subagent pins are independent. opencode sets
x-session-affinityto each request's own session id, so subagents get their own pins and may land on different accounts than their parent — which is what keeps a parent's warm cache from being depleted by its children. Their serving account is therefore no longer mirrored into the parent's sidebar entry under this mode (it still is under the other two).Naming
sustain, notalways— the sibling plugin already ships/claude-cachekeep alwaysfor the clock-window axis. Ours is the idle-cap axis. Both READMEs document the split so one word doesn't mean two things across sibling plugins.Testing
833 pass / 0 fail (from a 750 baseline — 83 new tests). Build, typecheck and biome clean; full suite run 3× with no transient failures.
Reviewed per task by cross-family reviewers (gemini-3.1-pro, MiniMax-M3, deepseek-v4-pro), plus a dual review on the behaviour-changing commit and a final adversarial whole-branch pass. Every new test was checked by per-hunk reverse-apply — that found three tests that looked like coverage but gated nothing (one passing via IEEE-754 semantics rather than the guard under test, one exercising pre-existing transport plumbing, one whose idle bound pruned before eviction could run); all three were rewritten to gate.
main-firstandfallback-firstare unchanged.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Adds sticky-balanced routing to
opencodeto pin sessions to the least‑pressured account and cut cache churn, plus a per-session pin reset and a main-only cachekeep sustain toggle. Also threadssessionIdthrough RPC/TUI and records the serving account in request dumps.New Features
/openai-routing resetclears only this session’s pin./openai-cachekeep sustain on|offbypasses the main agent’s idle warm cap and toggles at runtime. TUI shows a compact pin row when usable. RPC apply carriessessionIdso actions target the correct session. Request dumps record the internal serving account id (ChatGPT account id stays redacted).Bug Fixes
resetCreditsAvailablecorrectly.Written for commit 132c0b9. Summary will update on new commits.