mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* feat(memory): opt-in extraction cadence after a no-op run Managed auto-memory forks an extractor after every user turn, about 9-10k input tokens even when it saves nothing. Add an internal, default-off experiment (QWEN_CODE_MEMORY_EXTRACT_NOOP_SKIP_TURNS, 0-3) that skips up to N turns after a run that read memory and saved nothing. The extractor reads the last 40 curated entries captured after the turn, not the cursor's unprocessed slice, so a skip is allowed only while every unprocessed entry still fits half of that tail, and only below the compaction warning. Failed or MAX_TURNS runs never arm; the cursor and the #6311 hold-back are unchanged. Skipped turns are flushed before boundaries that would discard them: compaction (all paths, in the compression service), /clear, /resume, /branch, the OpenTUI session switch, and ACP session close. Cadence state is per session and in memory only. Process exit is unchanged. Refs #13004 * fix(cli): flush skipped memory turns before session usage is captured /resume, /branch and the OpenTUI switch flushed after opening the telemetry swap transaction, and /clear flushed after persisting session usage, so the flush's own model call could land outside the outgoing session's recorded usage or be dropped by a rollback. Flush first, while the outgoing session is still current and before either capture. * test(memory): pin a skipped turn surviving an older trailing run * ci(docs): format the daemon event-schema table for the Prettier gate docs/developers/daemon/09-event-schema.md reached main unformatted in8505479d87(#13281), whose docs-only CI profile skipped the Prettier lane. scripts/lint.js --prettier runs `prettier --check .` across the whole repo, so every branch with that commit as an ancestor now fails Lint & Static on a file it never touched. Normalize the emphasis markers and table padding so this branch's own gate can pass. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmuxqta15bw * fix(memory): make the pending-extract flush honest about what it saved `flushPendingExtract` deleted the cadence entry up front and resolved `true` for any run that did not throw, so every boundary certified success while the owed turns could be gone. Tighten the method around what it can prove: - resolve `false` and keep the pending turns when the bypass run extracted nothing (session mismatch, memory pressure, a queued slot) or threw, so a later boundary retries them instead of a no-op consuming them - share one run between overlapping boundaries for a session, each caller keeping its own timeout, so a second boundary waits instead of passing at once on an entry the first one already took - delete the entry again after the bypass run settles: `recordCadenceOutcome` re-armed it under the outgoing session id, which `/resume` restores, so a resumed session inherited a skip the design forbids - drop the snapshot when the live session moved on (`startNewSession()` mutates the Config in place, so both sides of the cache-safe mismatch guard move together and it never fires), and when a newer run for the session already covered it, which is what otherwise persists the extract cursor backwards and turns the next run into a full re-scan - correct the JSDoc: a run whose caller timed out keeps going but is not registered in `inFlight`, so `drain()` does not wait for it; log the timeout arm instead of resolving `false` silently Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuy53ol838 * refactor(memory): keep only the ACP-close flush for skipped extraction turns Shrink #13571 to the opt-in cadence plus the one boundary flush #13004 adopted for phase 1: - drop the /clear, /resume, /branch and OpenTUI switch flushes and the pre-compaction flush; a switch now drops both sessions' cadence state (Config.startNewSession -> discardExtractCadence) so a resumed id never inherits a skip - give the ACP close flush only what remains of the close budget, so the close stays under the bridge's outer wait - fail closed in isBelowCompactionWarn on a zero prompt count - state the skip-time-only window bound as a known limit, and record the experiment and its boundary losses in the design doc (EN and zh) * fix(memory): reset the #13004 cadence state and correct five stale comments `resetExtractStateForTests()` documented itself as clearing all extract scheduling state but skipped the two maps this PR added, so a shared-manager suite that armed a cadence skip leaked it into the next test and silently suppressed that test's extraction. Clear both. Also correct five comments that claimed guarantees the code does not give: the flush JSDoc said `drain()` never waits for a caller-timed-out flush, but the extraction it schedules is tracked; `EXTRACT_FLUSH_TIMEOUT_MS` is a no-budget fallback, not the ACP-close bound; the flush slot comment named a daemon-mode peer session that cannot appear in a per-manager map; the cadence skip window is measured at skip time only; and a skip armed below the warn tier can still be pending when compaction fires. Comment-only apart from the two `clear()` calls, which the new `resetExtractStateForTests()` case pins. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuyk3t08d5 * docs(memory): keep the flush slot comment true across a session switch Self-review of the previous commit: my rewrite of the slot comment claimed another session's extraction can never appear in these maps. It can — `startNewSession()` mutates one canonical Config in place (config.ts:6197 calls `discardExtractCadence(previousSessionId, nextSessionId)` on this same manager) and both maps are keyed by projectRoot/taskId, not by session, which is why the stale-session guard right below exists. Name both holders instead. Also drop "smaller" from the `EXTRACT_FLUSH_TIMEOUT_MS` doc: the ACP close budget is caller-supplied, so the residue is not provably under 60s. Corrects the previous commit's message as well: the new test pins `extractCadence.clear()`; `extractFlushRuns.clear()` is contract-only, since no caller reaches the helper with a live flush. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuyk3t08d5 * fix(acp): keep the #13004 close flush from sizing process exit disposeSessions() reached closeStoredSession with no drainTimeoutMs, so the close flush sized itself from SESSION_DRAIN_TIMEOUT_MS (30s). Both of its callers are process exit (SIGTERM/SIGINT and IDE disconnect): nothing bounds that drain from outside, and the generation aborts ahead of it make the close gate and drain return at once, so the flush received almost the whole 30s and held exit — and every session MCP subprocess — open for it. A forked extraction ran ahead of drainPoolBeforeExit, runExitCleanup and process.exit(0), which is what the comment above runExitCleanup exists to prevent. Process teardown now skips the flush. That is what the design record already promised ("process exit do not flush: up to N skipped turns are lost there"), so this narrows the code to the documented contract rather than adding a second timeout knob to tune. The interactive close and the daemon's sessionClose keep flushing inside their bridge-sized budget. Test: a disposal case whose flushPendingExtract never settles. Before the fix disposeSessions() does not reach the pool drain within 2s (red: "expected 'timeout' to be 'disposed'"); after it, the flush is never called and the close still runs to completion. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuyqjactdi * test(memory): pin the cadence flag and the warn tier; correct the close-flush claim R1-14 (pin 1 of the class): extract-cadence.test.ts covers getExtractNoopSkipTurns' digit-only parsing, trim and 0-3 clamp. It is the flag every other cadence site is downstream of, so it is the cheapest pin; the remaining mutations in the class are not claimed here and the thread stays open. Each mutant was run alone: dropping .trim() -> "expected +0 to be 2", dropping the /^\d+$/ check -> "abc: expected NaN to be +0", dropping the Math.min clamp -> "4: expected 4 to be 3". R2-2: the isBelowCompactionWarn fake reported exactly the values the implementation falls back to — DEFAULT_TOKEN_LIMIT is 200_000, the same window the fake returns, and computeThresholds already defaults an undefined pct to DEFAULT_PCT — so both of its cases stayed green under either fallback. Added a 32K-window case and a 0.5 autoCompactThreshold case that differ from both. Mutants run alone: window forced to DEFAULT_TOKEN_LIMIT and pct dropped each fail with "expected true to be false". R2-4: the design record stated the ACP close flush as an unqualified guarantee and left out the one boundary where it drops silently. The cache-safe slot the flush reads is process-global (forkedAgent.ts currentCacheSafeParams), so in a multi-session daemon another session's turn-completion capture makes getSessionMismatchResult fire and runPendingExtractFlush return false — a boolean the ACP caller discards without a log. Both the EN record and its zh-CN twin now call the close flush best-effort and list that boundary among the losses the paired runs must measure. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuyqjactdi * test(memory): drop the message argument vitest/valid-expect rejects * fix(memory): drop cadence outcomes of extractions in flight across a switch recordCadenceOutcome re-created the entry discardExtractCadence had just dropped when the extraction finished after the switch, and a restored id (resume, or a failed resume rolling back) then met an unearned arm. Every discard bumps an epoch; a run captured before it drops its outcome. * fix(memory): drop a queued extraction's cadence outcome across a switch Capture the cadence discard epoch where a trailing extract request is queued rather than where runExtract starts, so a switch landing while the request waits in extractQueued still drops its outcome instead of re-arming the departed session id (R3-4). Refresh the stored epoch when a newer turn supersedes the queued params. Bump the same counter in resetExtractStateForTests() so an already in-flight run cannot re-arm an id the helper just cleared, and correct that test's justification comment, which described a shared-manager shape no suite in this repo has (R2-5). Pin the DEFAULT_TOKEN_LIMIT fallback of isBelowCompactionWarn with a no-window config, and correct the comment that claimed the two existing cases cover both fallbacks (R2-2). Reword the design doc's compaction claim in EN and zh-CN: the warning-tier condition only makes compaction less likely, since the screenshot trigger and a forced /compress both compact below the warn tier (R3-6). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuzmop02ey * fix(memory): keep a carried skip's spent cadence budget A trailing run whose snapshot predates the pending skip carries that skip forward, but `recordCadenceOutcome` still reset `skips` to 0, so the session could skip up to 2N consecutive turns while the design record and the `cadence` telemetry certify "up to N skipped turns". Keep the spent budget on that branch and pin it with a test that goes red without the change. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmuzzjnorfk * fix(memory): distinguish a history shrink from a trailing cadence run The carry test in recordCadenceOutcome ordered two history lengths, but chat compaction shrinks history too, so after one compaction every later completed run re-stored the pre-compaction pending snapshot and reset lastExtractedLength to the short post-compaction length. A run shorter than what the session already extracted now means the snapshot predates a shrink and is dropped instead of carried or replayed by the close flush. The memory_tool branch also kept the pending snapshot it had just disarmed, so a close-time flush inspected the skipped turn while forking over the live tail that wrote the memory file; drop the snapshot with it. The shouldSkipForCadence doc comment claimed a skip can only overlap a trailing request, but the cadence is evaluated before the extractRunning branch, so a direct run overlaps it too. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmv0cemawgb * fix(memory): bind a flush's answer to its own snapshot `extractFlushRuns` was keyed by session id alone, so a boundary that flushed after a skip had replaced `pending` joined an older run's promise and was told those turns were extracted, while a run that had parked on the project slot before the skip deleted the newer snapshot's cadence entry unconditionally on success. Key the shared run by the snapshot it owns, and let the success path drop the entry only when that snapshot still owns it. Two cases in `manager.test.ts` cover each half: a retry issued while the older run is still parked, and a retry after it settled without extracting the newer snapshot. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmv0goxwkgj * fix(memory): key the extract cadence entry by project root `/cd` and the daemon's `session/cd` call `Config.relocateWorkingDirectory`, which moves a live session's project root without a session switch. `startNewSession()` is the only production caller of `discardExtractCadence`, so an armed skip survives the move and suppresses a turn in the new project on the strength of a no-op run that only ever read the old project's memory -- certifying "nothing to remember" about files it never opened. Neither route clears history either, so `pendingEntries` stays small and positive and the gate keeps firing. Record the project root on the cadence entry and refuse to skip when it no longer matches. Every other piece of extract scheduling state in this class (`extractRunning`, `extractCurrentTaskId`, `extractQueued`) is already keyed by project root; the cadence map was the one that was not. The parked snapshot is deliberately left in place rather than discarded on mismatch: `runPendingExtractFlush` still runs it against its own captured `projectRoot`, which is where those owed turns belong, and `extract.ts` resolves every path from `params.projectRoot`. A root guard on the flush side would destroy that owed extraction instead of protecting it. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmv0tjwjth4 * fix(memory): invalidate a parked extract snapshot when history shrinks under it The cadence carry and the close-time flush both used history length as a stand-in for temporal order, so a compaction that shrank the history to a length still above the recorded cursor read as a newer trailing request: - scheduleExtract carried the pre-shrink snapshot forward together with its spent skip budget, keeping the gate shut until the history regrew past it. It now drops the snapshot at admission when the incoming history is shorter, which within one session only a shrink can produce. - runPendingExtractFlush refused only on a forward cursor comparison, so a shrink landing while it was parked let it replay the abandoned snapshot and resolve true to a boundary that had already moved on. It now also refuses when the entry holds no pending snapshot at all. Each case is pinned by a test that goes red when its own fix is reverted. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmv22k9vj0e * fix(memory): drop a pending flush snapshot after the session relocated The close-time flush replayed a pending snapshot earned in project A after `/cd` moved the session to project B. The fork's conversation comes from the session's cache-safe capture, not from the snapshot's history, so the run read B's turns against A's memory context and wrote A's memory from them. `relocateWorkingDirectory` mutates the live Config in place, so the session-id guard cannot see the move. `runPendingExtractFlush` now checks the live project root the same way it checks the live session id, and drops the snapshot instead of forking it. Also records the `cadence` skip reason in the scheduleExtract flow doc that the PR body and telemetry unions already carry. * fix(memory): drop a cadence outcome for a root the session already left `/cd` relocates the live Config in place and fires no discard, so an extraction still in flight for the old root settles after the new root has re-keyed the entry and parked its own snapshot. The whole-entry `extractCadence.set` in `recordCadenceOutcome` was root-blind: it overwrote the live root, watermark and skip budget with the abandoned root's and dropped the pending snapshot, so the ACP close flush found nothing to extract and resolved `true` — certifying turns as extracted that never were. Guard the outcome on the live root, mirroring the flush relocation guard, and compare against `params.config.getProjectRoot()` rather than the carried entry's root so the first run after a `/cd` can still re-key the entry and re-arm the cadence in the new project. Placed after the disabled-experiment delete so turning the flag off still clears state. This is the only `extractCadence.set` call in the file, so guarding it closes the cadence write entrance for the relocation class. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmv2nzvmx19 * fix(core): keep the cadence in sync through relocation and shrink Three follow-ups from the review of the opt-in extraction cadence, all in the flush bookkeeping around a pending snapshot: - `recordCadenceOutcome` now records the coverage of a completed run before the root-change and discard-epoch guards drop its outcome. Both exits leave the session's entry — and any snapshot parked behind it — in place, so a later boundary replayed a snapshot the run had already extracted and persisted the cursor backwards, which the next run reads as a reason to re-scan everything after it. Only the snapshot this run's own root parked is covered: after a `/cd` the entry belongs to the live root, whose newer snapshot this run never extracted. - `runPendingExtractFlush` tests snapshot identity before length. A newer run that shrinks the history and then skips again leaves a *shorter* snapshot parked, which the forward-only length comparison cannot see, so the parked flush forked turns no live history had. - The relocation guard clears only the snapshot it refuses. A `/cd` keeps the session, so a skip in the new root can have parked a newer snapshot behind the parked flush; deleting the entry left that snapshot with no run at all. Each case is pinned by a test that goes red when its own fix is reverted. * fix(memory): abandon a parked extract flush once its budget expires * docs(memory): record the abandoned flush in the loss table * refactor(memory): trim extraction cadence API surface * fix(memory): preserve skipped turns and invalidate stale cadence * test(memory): match the skippedHistory arg in the extractAgent spy assertion runAutoMemoryExtract now forwards params.skippedHistory as the third argument to runAutoMemoryExtractionByAgent, so the spy receives three arguments. The existing assertion still expected two, which made Test (ubuntu-latest, Node 22.x) fail deterministically on this branch. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmv9bozsb73 * fix(memory): retain skipped history until extraction consumes it * fix(memory): keep cadence snapshots honest across compaction, close, and image-only tails (#13004) - Date cadence coverage/carry by request order instead of history length, so a compaction between schedule and settle can no longer drop a parked snapshot a run never extracted (R6-2, R13-1). - Abort the close-flush fork when its budget expires instead of letting it run against a torn-down Config; the aborted outcome stays pending for a later boundary (R13-2). - Treat parked skipped history as new input so an image-only unprocessed tail cannot swallow it through the no-new-message early return (R13-4). Each regression is pinned by a test built from the reproduced failure arm; findings were verified againsta458f81ebefore fixing. * style: apply prettier to the extract-cadence files and merged docs The Lint & Static lane short-circuited at the freshness gate on this stale branch, so step 31 Run Prettier never ran. After merging main the gate resolves and prettier flags four files: the two design docs brought in by the merge and this PR's memory manager sources. Formatted only those files, no whole-tree rewrite. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmvdo5pktf4 * fix(memory): measure cadence windows from the snapshot's own base (#13004) Park skippedFrom beside the skipped slice so a carried settle cannot move the supplement and skip-window gates off the slice's real base, name the 40-entry capture bound once, and splice no foreign-root snapshot into a re-keyed entry. Drop the part of the slice the captured tail still holds at the fork join instead of replaying it, and invalidate a parked snapshot where compaction shrinks the history, before a cancelled turn hides the shrink from every scheduleExtract. * test(memory): pin the cross-root carry guard at the supplement gate (#13004) The regression case for the carry root condition probed at history 43, measured from the entry's re-keyed base (43 - 2 = 41 > 40) — the same span both with and without a foreign snapshot carried, so removing previous.projectRoot === params.projectRoot from the carry left it green (verified by mutation). Probe at 45 instead: measured from the snapshot's own base (skippedFrom 4) the gate fires exactly when a foreign snapshot survives the settle, and the case now fails when the root condition is reverted. * fix(memory): dedup a parked slice the captured tail fully covers (#13004) uncoveredParkedPrefix only proved coverage when the captured tail began inside the parked slice. In the close-flush shape the tail is the whole short history, so it begins before the slice: the anchor matched nothing and the whole slice was prepended again — the duplication R14-1 asked to remove. Prove that shape separately with a contiguous whole-slice match, which one matching value cannot, since repeated content would otherwise be able to cut turns the tail never held. Probe: parked [turn 2, reply 2] inside the 6-entry tail produced 8 extraHistory entries before this change and 6 after; removing the new branch turns the regression case red. * test(memory): pin the supplement window boundary (#13004) The supplement gate is a strict past-window test measured from the parked slice's own base, but every existing case probed it one entry past the boundary, where a >= comparison would pass too. At exactly 42 - 2 = 40 the captured tail still holds the slice, so the turn extracts without prepending it; flipping the gate to >= turns this case red and leaves the rest green. * fix(memory): refuse cadence replays whose state no longer owns them A parked flush fell through both staleness guards when its cadence entry had been deleted whole (`state === undefined`), and forked a snapshot nothing owned, persisting its cursor over the live one. Refuse the replay when the entry is gone. A run whose early return advanced the cursor without forking (no new-user-text tail, session mismatch, memory pressure) was still taken as coverage for a parked snapshot by the coverage delete and the re-key. Only a run that forked settles the entry; the others retry the parked turns. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvfrj6szjg * fix(memory): prove parked-slice coverage by whole runs, invalidate on rewind `uncoveredParkedPrefix` accepted a single entry equal to the tail's first one as proof, so an unrelated later turn sharing only a tool name dropped every parked entry behind it — including the durable instruction it carried, which nothing re-scans once the cursor moves. Compare the whole overlap entry for entry, include `functionCall.args` in the key, and take the longest overlap so repeated content cannot cut unheld turns. `LlmClient.setHistory()`/`truncateHistory()` shrink the timeline a parked cadence snapshot was cut from without a session switch, so the close flush replayed it at `bypassCadence` over a history that no longer holds those turns. Invalidate the parked snapshot where the shift is decided, next to the existing file-read-cache and IDE-context invalidations. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvfrj6szjg * style(memory): apply prettier to extractionAgentPlanner test Lint & Static (run 39329627709) flagged packages/core/src/memory/extractionAgentPlanner.test.ts; formatting-only fix, locally reproduced with `npx prettier --check` before and after. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmvftbhkrjk * fix(memory): prove parked-slice coverage over the captured rendering The coverage proof compared the raw parked slice with the curated, slimmed tail, so media entries and curation-merged user turns never matched and the whole slice was prepended again: duplicated turns, inverted chronology and re-shipped media. Slim the slice as the capture does and compare flattened part sequences, which curation merges but never rewrites. * fix(memory): guard the #13004 cadence against stale supplements, cursor rewinds and cross-chat invalidation Three defects in the extract-cadence state this PR adds, each confirmed real by source read ateb955298eeand each now pinned by a regression test: - R18-1: the parked-slice supplement was spliced at schedule time only, so a request waiting in extractQueued replayed a slice a concurrent run had already consumed and had its snapshot deleted for. runExtract now re-checks the stamped snapshot identity and drops the orphaned slice. A flush replay carries no stamp, so the slice the flush exists to extract survives. - R18-2: a close-time flush replay wrote the project extract cursor backwards (to the snapshot length, or to 0 after the shrink clamp) after a non-forking run had already persisted a later offset. The replay's write is now floored at the offset it read; a genuine post-compaction run still moves it down. - R18-3: the compaction shrink hook was keyed on the Config's session id while compress() runs per chat, so a subagent's derived Config (same session id, same manager) or a forked chat (same Config instance) dropped the main session's parked snapshot. invalidatePendingExtract now takes the requesting Config and refuses a snapshot another Config owns, and the call site skips forked chats. Callers that pass no owner keep the unconditional drop. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvn9lerfu3 * fix(memory): retain replay debt until extraction makes progress * test(cli): mock memory invalidation in restore admission fixture * fix(memory): drop the parked extract snapshot on the fast-compression rewrite `tryCompressChatFast()` rewrites history through `LlmChat.compressFast()` -> `LlmChat.setHistory()`, so it bypasses both existing invalidation hooks: `LlmClient.setHistory()` and `ChatCompressionService.compress()`. `compressFast()` drops thought-only model turns via `stripThoughtPartsFromContent()`, so the live history really does shorten. A cadence snapshot parked by `QWEN_CODE_MEMORY_EXTRACT_NOOP_SKIP_TURNS` then survives the shrink. If the session closes before another turn reaches `scheduleExtract()` (whose shrink guard would have deleted it), `runPendingExtractFlush()` replays it with `bypassCadence: true` — the very flag that exempts the replay from that guard — writing durable memory derived from a timeline that no longer exists and resolving `true` to the boundary. Invalidate at the fourth rewrite site, alongside the sibling post-rewrite resets and after the COMPRESSED early return: a fast compression that shrinks nothing (`afterEstimate >= beforeEstimate`) never rewrites history and must not drop the snapshot. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvo5qtfnv2 * fix(memory): cut the cadence park from the base the fit test measured shouldSkipForCadence sized the skip fit from `pendingFrom = state.pending?.skippedFrom ?? state.lastExtractedLength` but cut and stamped the parked snapshot from `state.lastExtractedLength` alone. When an in-flight run scheduled before the park settles and takes recordCadenceOutcome's carry branch, it writes `lastExtractedLength` from its own longer, pre-compaction index space while leaving `pending.skippedFrom` older. The next skip then parks `history.slice(lastExtractedLength)` -- an offset counted in a different index space -- so `skippedHistory` comes out empty and the previously parked turns are silently discarded, and the recorded `skippedFrom` exceeds the snapshot's own `history.length` against the field contract. Cut and stamp the park from `pendingFrom` so the gate and the park share one base. With no snapshot parked, `pendingFrom` is `state.lastExtractedLength`, so that path is byte-identical to before, and the existing `pendingEntries` bound keeps the slice inside EXTRACT_CADENCE_MAX_PENDING_ENTRIES. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvoils4rvh * fix(memory): retain skipped turns across foreground memory writes * fix(memory): keep the cadence experiment opt-in and align its gates Four deferred findings on the #13004 extraction-cadence surface: - Both test setups now delete QWEN_CODE_MEMORY_EXTRACT_NOOP_SKIP_TURNS, the way they already drop other operator-owned inputs. The suites read the flag from the ambient environment before, so one exported value switched the experiment on for unrelated suites: 153/544 core client tests and 637/1292 cli Session tests failed with no code change. - The flag was absent from PROJECT_ENV_HARDCODED_EXCLUSIONS, so a project .env could switch a default-off experiment on for every session in that repository. It joins the hardcoded tier, pinned in shared-env-keys. - isBelowCompactionWarn compared the bare last prompt count while both compaction gates also measure the retained output. Callers now pass prompt + output, and the doc states the quantity and the WARN_BUFFER reserve instead of the false "read crossed routes" premise. - The cadence gate ran before the memory-pressure gate, so a pressured turn was recorded as a 'cadence' skip and spent the experiment's budget. The cadence gate now yields to pressure, and a memory_pressure result no longer disarms the entry. * test(memory): pin the cadence telemetry and the session-switch discard Two R1-14 sites on the #13004 surface shipped without a test that could fail on them: - The cadence skip's telemetry event: a case now passes a Config and asserts logMemoryExtract receives status 'skipped' with skipped_reason 'cadence'. Mutating the reason reddens it. - The switch-side discard in Config.startNewSession: a case spies on MemoryManager.discardExtractCadence for both a fresh id and a same-id resume. Removing the call reddens it. Re-measured R17-12 at this head and found nothing to fix: the persistent runAutoMemoryExtract implementation installed by the resetExtractStateForTests suite is undefined by the time the next suite runs even with no reset in place, so the leak the finding describes does not reproduce here. * test(memory): pin the epoch refresh when a request supersedes the queue R1-14: deleting `queued.epoch = this.cadenceDiscardEpoch` left the whole suite green. The refresh is what keeps a request that supersedes a queued one after a session switch judged by the new epoch, so its completed run still arms the cadence for the session it now describes — without it the superseded run's outcome is discarded as pre-switch and the experiment goes inert until the next completed run. * fix(memory): report cadence flushes a close boundary drops D6-4: the close flush was dropped silently when the close budget was already exhausted, and runPendingExtractFlush returned false silently when its own deadline expired while it waited for the project slot. At a close no later boundary retries, so those turns are lost. Both drops now emit one warning naming the reason and the owed turn count, through a new MemoryManager.pendingExtractTurns. No retry and no deadline extension. Pins: the ACP close harness asserts that a positive residue runs the flush with the remaining budget, and that an exhausted budget skips it and reports reason 'close_budget_exhausted' with owedTurns; the manager suite pins 'flush_deadline_expired'. Removing either report reddens its case. Also dedupes the resolver-capture pattern in the cadence suite onto the file's deferred<T>() — 17 sites through one holdNextExtract helper, net -110 lines with assertions untouched — and pins the skip-window negative half. --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: yiliang114 <[email protected]>