Repository navigation
perf(claude-queue): keep a bounded window of queue decisions (#676) - #1364
Merged
Merged
Conversation
Refs #676 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the rest The decision log was append-only for a session's lifetime and every departure copied the whole array, so a long-lived session paid memory and O(n) per departure for history nothing reads (no UI, no debug bundle). One helper now appends, keeps the newest 200 decisions and counts the evicted ones; conservation is restated exactly as decisions.length + droppedDecisions === nextSeq. Fail-first: 1,000 queue/remove cycles kept 1,000 decisions on origin/main. Refs #676 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l invariant (review) Round 1 of #1364 (a, b, c): - `decisions.length + droppedDecisions === nextSeq` was false for valid states (pending items, stale marks). droppedDecisions now counts evicted decision RECORDS; the equality is asserted only for drained, never-stale sessions, and says why. - A recent-only window evicted every decision around rows still stranded in the queue. While any item is pending the log may grow to QUEUE_DECISION_CEILING (2000); with the queue empty it trims to the window, raised to 500 (the corpus already has a 175-decision session). - Boundary tests for a single large batch and the popAll path. - markStaleWhenIdle's "anything departed" guard counts evicted records. Mutations killed: batch counted as one, popAll bypassing the bound, no pending ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
Review disposition (round 1 at
|
| # | Finding | Verdict | Disposition |
|---|---|---|---|
| a1 / b1 / c2 | decisions.length + droppedDecisions === nextSeq is false for valid states (pending items, stale marks) |
valid | Fixed: droppedDecisions is restated as evicted decision records. The equality is asserted only for drained, never-stale sessions, and the test says why. |
| c1 | A recent-only window evicted every decision around rows still stranded in the queue | valid | Fixed: while any item is pending, the log may grow to a 2 000 ceiling; with the queue empty, it trims to the window, raised to 500. A fixture-replay test and a forever-stranded bound test pin it; the no-ceiling mutation is red. Residual: evidence older than 2 000 later decisions. |
| a2 | A batch trim is untested (+1 mutant survived) |
valid | Fixed: a single 600-item batch test; the mutant is red. |
| b2 | The popAll path could bypass the bound |
valid | Fixed: a popAll boundary test; the bypass mutant is red. |
Also fixed: markStaleWhenIdle's "anything departed" guard now counts evicted records. Verification pass: see Round 2 below.
Round 2 (verification at 5313a7b8: a and b MERGE-READY, c FIX-BEFORE-MERGE):
- c major — the ceiling only postponed the loss (after 2 200 departures, 0 of the 164 episode decisions were kept). Valid. Fixed in
94d4d5ca: while items are pending, the episode's first 200 decisions are kept inepisodeHeadwithin the ceiling. - c's two surviving idle-path mutations: pinned.
Round 2 is the cap: any further finding is the manager's call.
…ling (review round 2) Round 2 of #1364 (c): the 2000 ceiling only postponed the loss; after 2,200 unrelated departures none of the 164 decisions around two stranded rows remained. An episode opens when an item enters an empty queue and closes when it is empty again; while open, its first decisions move into episodeHead (up to 200) instead of being evicted. Head and log share the ceiling; the head is released and counted when the episode closes. Also pins the idle-marking path (stale marks bounded, and by the pending queue). Mutations killed: no episode head, stale marks bounded by the empty window, stale marks unbounded, head outside the ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #676.
Problem
ClaudeQueueState.decisionswas append-only for the life of a Claude session. Every time a prompt left the queue (delivered, consumed, popped, stale), aQueueDecisionwas appended with a content preview and evidence. Each append copied the whole array ([...state.decisions, …], at 7 sites), so a long session paid growing memory and O(n) per departure.Nothing reads the old decisions:
useIpcSubscriptions' module-level map, not inSessionRuntime, so debug bundles don't carry it;QueueStriprenderspendingandstale, not decisions.Change
QUEUE_DECISION_WINDOW = 500most recent decisions. The corpus already has a 175-decision session, so 200 was too thin.QUEUE_DECISION_CEILING = 2 000. Evidence about a row that is still stranded then survives ordinary churn (review c).episodeHead(up to 200) instead of being evicted. The head shares the ceiling, and is released and counted when the episode closes.droppedDecisionscounts evicted decision records, so kept + evicted is every decision ever recorded.nextSeq): a pending item has no decision yet, and astale-unattributedmark is an extra decision (reviews a, b, c).withDecisions(state, added, pendingAfter), owns appending and trimming, and replaces the 7 hand-written spreads. Each append copies at most the bound.markStaleWhenIdle's "has anything departed" guard counts evicted records too.docs/plans/2026-09-26-claude-queue-decision-window.md(with a review section).Tests
src/renderer/src/session-runtime/claudeQueue/reconcile.test.ts:origin/mainthe session keeps all 1 000.divergence-stranded-background-commandsfixture plus 600 churn cycles still holds every original decision while its rows are pending.popAlldeparture is bounded like any other.episodeHead. Stale marks at the ceiling are bounded, and by the pending queue.popAllbypassing the bound (b);npx tsc -bprints 0 lines, and the branch is merged with currentmain.🤖 Generated with Claude Code