Repository navigation
fix(agent-runtime): keep context recoverable after a failed compaction - #249
Conversation
A retained-tail fallback checkpoint persisted an empty retainedTail on completed turns (the Codex-shaped tail only retains messages on an active turn), so after a runtime rebuild — model switch, restart — the session restored with nothing before the boundary: the new model saw no history and no usable summary, while the transcript kept all 500+ messages (vastsa#224). Two further paths cemented the failure instead of recovering it: pi's prepareCompaction copies the previous checkpoint's summary as the next run's previousSummary, and the fallback rebuild carried the terminal checkpoint's summary forward — both fed the recovery notice text into the next summarization as if it were a real summary, so the model kept "updating" a summary that never existed. - createFallbackCheckpoint: when the shaped tail is empty, retain the newest user messages from the summarized range under the same budget, so the failure path still restores a bounded, non-empty context. - prepareCompactionInput / fallbackPreparation: strip a previousSummary that contains the fallback notice, so the next compaction regenerates a real summary from the transcript instead of updating the notice. Fixes vastsa#224
The drop-before/after hairline shipped with a raw `border-radius: 1px`, which trips the style-token gate (`check-style-tokens.mjs`) that guards lint on every PR. The established pattern for a 2px hairline is `--radius-full` (see `.subagent-topology-connector`). Pre-existing on main (introduced with the project drag-reorder feature); fixing it here so this PR's CI can be green against a red main.
`c944d808` shipped `.empty-hero .project-underline:focus-visible` with a raw `border-radius: 4px`, which trips the style-token gate — main's CI is red on it. 4px maps to `--radius-3xs`.
…ject-move The project drag-reorder refactor replaced the HTML5 drag handlers the source-assertion tests were anchored on (`handleProjectDragOver`, `handleProjectDrop`, `source.meta.archived` inline buckets), so both drag tests fail on current main. Point the assertions at the code that exists now: the drop-target handlers' payload-authoritative guard, the long-press arm threshold, and the bucket guard in sidebar-project-reorder.ts.
vastsa
left a comment
There was a problem hiding this comment.
Found one merge-blocking context regression in the self-healing path.
prepareCompactionInput clears the entire previousSummary whenever the latest checkpoint contains COMPACTION_FALLBACK_MARKER (runtime.ts:4859-4869). A fallback checkpoint is built by prefixing that marker to the carried-forward summary (runtime.ts:5185-5198), so this also removes any valid summary that existed before the failed compaction.
This is not rebuilt from the canonical transcript: pi's prepareCompaction starts after the latest compaction and only exposes its retained-tail virtual entries plus later messages. I reproduced this on the PR head with a fallback containing REAL SUMMARY, retained tail recent, and a later prompt; the next preparation returned previousSummary: undefined and summary input [recent, new prompt], so REAL SUMMARY was absent. After the next compaction, older task context is therefore permanently missing from model context.
Please preserve/extract the carried-forward real summary while removing only the recovery notice (and add a behavioral regression test for a fallback that already contains a valid summary). The same issue should be handled in the terminal-checkpoint branch at runtime.ts:5361-5366.
…s stripped pi reads `previousSummary` straight from the previous compaction entry. A retained-tail fallback stores its carried-forward summary ahead of the recovery notice, so dropping the whole `previousSummary` on seeing the marker also discarded the valid summary the failed compaction was carrying forward. After the next compaction that older context was gone for good. Strip only the notice — and the "no previous summary" placeholder — leaving any real summary ahead of the marker intact. Applies to both `prepareCompactionInput` and the terminal-checkpoint branch of `fallbackPreparation`. Adds behavioral regressions for a fallback that already carries a valid summary, and keeps the source-wiring assertion in the desktop compaction test in sync.
…ck-restore # Conflicts: # apps/desktop/src/styles/sidebar-threads.css # apps/desktop/test/session-project-move.test.mjs
|
Fixed in A fallback checkpoint is
The helper returns Behavioral regressions added in
Both fail against the previous logic (the terminal case degenerated to the placeholder) and pass now. Verification: |
|
Branch refreshed onto current E2E disposition (fork PR): the E2E-164 provider/UI journey is still Draft — no runnable E2E exists for this runtime path yet. The relevant local coverage is the two behavioral regressions added in @vastsa could you re-review the post-fix head? Both call sites you flagged ( |
|
All three merged — thank you, @vastsa! 🎉 Thanks especially for the sharp review earlier on this one: the carried-summary catch made the fix strictly better, and your runtime-era context around #169/#134 kept the scope honest. #202 and #235 also landed clean with the new architecture/format gates. We'll keep following main as it moves and stay responsive on the plugin side (session-import 0.4.8 is up next for review whenever you get to it). |
Fixes #224
Problem
After an automatic context compaction fails and the session writes a
retained_tailfallback checkpoint (CONTEXT_COMPACTION_FAILED/retainedTail: []), switching provider/model or restarting restores a runtime whose model context contains nothing before the compaction boundary — no history and no usable summary — even though the full transcript (500+ messages in the report) is intact in SQLite and the session JSONL. The new model reports it cannot read prior task content, and the condition never self-heals.Root cause
createFallbackCheckpoint's tail comes fromcodexShapedPreparation, which retains no user messages on acompleted_turn— correct for a real summary, fatal for a fallback whose "summary" is only the recovery notice. Empty tail + notice ⇒ the rebuilt context has nothing before the boundary.prepareCompactioncopies the previous checkpoint'ssummaryas the next run'spreviousSummary. With the notice aspreviousSummary, the next compaction runsUPDATE_SUMMARIZATION_PROMPTagainst text that was never a real summary — the failure is cemented.fallbackPreparationcarriesterminal.summary(possibly the notice) into the rebuild path, same pollution.Fix
Three minimal changes in
packages/agent-runtime/src/runtime.ts:createFallbackCheckpoint: when the shaped tail is empty, retain the newest user messages from the summarized range under the same budget (reusingselectRetainedUserMessages) — the failure path now always restores a bounded, non-empty context instead of an empty one.prepareCompactionInput: strippreviousSummarywhen it containsCOMPACTION_FALLBACK_MARKER, so the next summarization regenerates a real summary from the transcript (self-healing retry) instead of updating the recovery notice.fallbackPreparation: apply the same strip on the rebuild path.Against the issue's expectations: no checkpoint that restores an empty context is persisted anymore; the next compaction automatically rebuilds a real summary; a runtime rebuild restores from the canonical transcript plus a non-empty tail. A dedicated user-facing recovery affordance (the issue's last expectation) is left out of this PR — the existing
contextCompaction.recoveredtoast already covers the fallback notification.Testing
packages/agent-runtime: vitest 356/356 pass.apps/desktop/test/context-compaction.test.mjs: +2 source-assertion tests (non-empty tail on fallback; notice never used aspreviousSummary), 12/12 pass.tsc --noEmitclean.