Live Stream: Add compression recovery action - #5538
35 commits merged into
Conversation
346658b to
dfc0686
Compare
|
CI triage update:
|
|
| Filename | Overview |
|---|---|
| api/compression_recovery.py | New module providing recovery payload construction, generic-continuation intent matching, and session stamp/clear helpers; logic is clean and well-scoped. |
| api/models.py | Adds four new Session fields and find_compression_recovery_session; memory scan defensively wraps _compression_recovery_child_matches in try/except but the disk scan loop does not, leaving an unhandled exception path. |
| api/routes.py | Adds _handle_session_compression_recovery_start with _COMPRESSION_RECOVERY_START_LOCK for idempotency, and modifies _handle_chat_start to clear/restore recovery around _start_run with correct 501/4xx/exception restore paths. |
| api/streaming.py | Stamps compression recovery metadata on both inline-error and exception-path error messages when _err_type/exc_type is compression_exhausted; consistent with the two existing error paths. |
| static/ui.js | Adds recovery card rendering, hasOwnProperty-guarded payload lookup, generic-continuation intercept, and startCompressionRecovery with malformed-response guard; all user-controlled values passed through esc(). |
| static/messages.js | Adds shouldInterceptCompressionRecoveryContinuation check in send() and persists _compressionRecovery from the SSE error payload onto the message object. |
| tests/test_compression_recovery_action.py | New test file with 9 tests covering intent matching, chat-start blocking/clearing/restoring, recovery-start idempotency, profile isolation, disk round-trip, and UI wiring assertions. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Browser
participant routes as api/routes.py
participant streaming as api/streaming.py
participant recovery as api/compression_recovery.py
participant models as api/models.py
Browser->>routes: POST /api/session/chat (long run)
routes->>streaming: _start_run(session)
streaming-->>recovery: stamp_compression_exhausted_recovery(session)
recovery-->>models: "session.compression_recovery = payload"
streaming-->>Browser: SSE: compression_exhausted error + _compressionRecovery
Note over Browser: Renders .compression-recovery-card
Browser->>routes: POST /api/session/chat (bare continue)
routes-->>Browser: 409 compression_recovery_required
Browser->>routes: POST /api/session/compression-recovery/start
routes->>models: find_compression_recovery_session(sid, action)
models-->>routes: None (first call)
routes->>models: "Session(..., context_messages=[])"
models-->>routes: saved child session
routes-->>Browser: "200 {session, source_session_id}"
Browser->>routes: POST /api/session/compression-recovery/start (retry)
routes->>models: find_compression_recovery_session(sid, action)
models-->>routes: existing child session
routes-->>Browser: "200 {session, message: Opened existing...}"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Browser
participant routes as api/routes.py
participant streaming as api/streaming.py
participant recovery as api/compression_recovery.py
participant models as api/models.py
Browser->>routes: POST /api/session/chat (long run)
routes->>streaming: _start_run(session)
streaming-->>recovery: stamp_compression_exhausted_recovery(session)
recovery-->>models: "session.compression_recovery = payload"
streaming-->>Browser: SSE: compression_exhausted error + _compressionRecovery
Note over Browser: Renders .compression-recovery-card
Browser->>routes: POST /api/session/chat (bare continue)
routes-->>Browser: 409 compression_recovery_required
Browser->>routes: POST /api/session/compression-recovery/start
routes->>models: find_compression_recovery_session(sid, action)
models-->>routes: None (first call)
routes->>models: "Session(..., context_messages=[])"
models-->>routes: saved child session
routes-->>Browser: "200 {session, source_session_id}"
Browser->>routes: POST /api/session/compression-recovery/start (retry)
routes->>models: find_compression_recovery_session(sid, action)
models-->>routes: existing child session
routes-->>Browser: "200 {session, message: Opened existing...}"
Reviews (26): Last reviewed commit: "Merge latest master into compression rec..." | Re-trigger Greptile
Make focused compression-recovery continuation creation idempotent by marking child sessions and reusing the existing child on repeated start requests. Also surface malformed successful recovery responses in the UI instead of silently re-enabling the action. Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Aftercare update for the latest Greptile findings:
Local verification:
GitHub checks are running on the new head. |
Restore the compact() source-order expectation used by the branch metadata contract test while keeping focused compression recovery markers in the compact payload. Co-Authored-By: Paperclip <noreply@paperclip.ing>
🎬 Cutter preview — PR #5538 |
|
CI follow-up for the shard-4 branch metadata failure:
Local verification:
GitHub checks restarted on |
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Aftercare base refresh update:
|
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Resolved the session SSE RFC anchor conflict by keeping the latest symbol-based wording from master. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Treat a cleared session-level compression_recovery payload as authoritative so old message metadata cannot intercept a later generic continuation after a successful new turn. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
🔬 Gate certification — RED ⛔ (recovery-child not lineage-isolated → shows parent transcript + wrong-session button 409; + cross-profile child reuse — 2 CORE) · concept → NathanCertified head: What I ran (rebased worktree
|
| Gate | Result |
|---|---|
| Rebase onto current master | ✅ git apply clean |
| Codex (reproduce) | SHIP-WITH-FIXES — 2 distinct CORE (lineage-leak + cross-profile reuse); Codex verified by direct invocation |
| Full pytest suite | ✅ rc=0 green — but the two edges are NOT test-covered (Codex's direct invocation caught them, the gate's value on a new subsystem) |
| PR's own test | ✅ 248-line test_compression_recovery_action.py passes (doesn't cover the lineage/profile edges) |
Findings
⛔ CORE (Codex-verified) — recovery child shows the exhausted PARENT's transcript + a wrong-session recovery button (routes.py:19877 → _merged_webui_lineage_messages_for_display() :11709/:8126): the focused-continuation child gets parent_session_id but NO lineage-isolation marker (not session_source=="fork", no relationship_type=="child_session"), so GET /api/session merges the parent's messages into the empty child (Codex: empty child + parent returns parent messages). This surfaces the parent's _compressionRecovery card in the NEW session, and ui.js:13499 renders the button with the CHILD session id → posting the wrong session → 409. Fix: mark recovery children isolated (session_source="fork" or a dedicated recovery-child guard in _merged_webui_lineage_messages_for_display() returning primary_messages when compression_recovery_source_session_id/_action is set); render recovery buttons with recovery.source_session_id, not the current session id. (Same lineage-isolation class as #5582/#5556/#5563 — a new child carrying parent_session_id MUST be marked isolated.)
⛔ CORE (Codex-verified) — recovery-child reuse is not session/profile-scoped (routes.py:19854, find_compression_recovery_session() at models.py:3323/3340): the start handler validates the SOURCE session's profile, but the child-reuse lookup scans all cached/persisted sessions matching only compression_recovery_source_session_id + action — no child-profile check. A foreign-profile/corrupt child with that marker can be returned + serialized. Fix: make the lookup take the source profile or validate _profiles_match(child.profile, source.profile) + active-profile visibility before reuse; ignore mismatches and create a fresh child.
✅ Otherwise: server payload build (build_compression_recovery_payload/stamp_...) is structured; the concept (recovery-from-exhaustion action) is a real improvement.
Recommendation to the next agent / author
RED — gate-fail/changes-requested (2 CORE): (1) mark the recovery-continuation child as lineage-isolated so it doesn't inherit the parent's transcript/recovery-card, and render the recovery button with recovery.source_session_id; (2) profile-scope find_compression_recovery_session() (require _profiles_match + active-profile visibility on the reused child). The concept is worth shipping — a compression-exhausted recovery action is a genuine UX win — but the child-session isolation + profile-scoping must be fixed (recurring lineage class). Then, as a substantial new subsystem + visible feature from a new contributor, it's Nathan's concept + visual sign-off. concept 4/5 (good idea, two implementation COREs). Author @franksong2702 (new). crit=3. (Gate value: a green suite didn't cover the lineage-merge or cross-profile-reuse — Codex's direct route invocation exposed both; a new child that carries parent_session_id without an isolation marker is the SAME recurring bug I've now seen on #5582/#5556/#5563 — always check a new-child feature marks isolation + scopes lookups by profile.)
_Gate-certifier layer (warm-up → gate → release). I do not merge/tag/deploy. Rebased onto current master; Codex verified (direct invocation) the recovery child inherits the parent transcript (no isolation marker → _merged_webui_lineage_messages_for_display merges parent) + renders the recovery button with the child sid (409), and find_compression_recovery_session reuses cross-profile (no profiles_match). Suite green (edges not test-covered). Concept good; 2 lineage/profile COREs. Cert valid for sha:7425dc35.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
🔬 Release-manager deep review — engineering GREEN, converged; held for maintainer concept + visual sign-offRan the full authoritative gate on the live head ( ✅ CORE-1 (lineage isolation) — verified by code read. ✅ CORE-2 (profile-scope) — verified by code read. Gate results
Fable non-blocking UX items (fold in on approval)
StatusEngineering-converged and gate-clean. Held for maintainer decision because this is a new subsystem ( |
Co-Authored-By: Paperclip <noreply@paperclip.ing>
1870f77
|
Shipped in v0.51.889 (deployed live — the recovery functions are in the served |
… on compression_exhausted (nesquena#5538) Folds in gate fixes: map the stale-card 409 to a neutral info toast + retire the card (finally-guard so it stays retired), + consumed-state CSS. Full gate: Codex SAFE, Fable SHIP-UX, suite 12140/0, browser-smoke clean. Co-authored-by: Frank Song <franksong2702@gmail.com>
… sessions (nesquena#5655) Follow-up to nesquena#5538: the recovery card now also renders from _activeCompressionRecoveryPayload() on the final assistant message when a rebuilt/legacy session lacks the per-message _compressionRecovery marker (guarded to last/turn-final assistant only; no double-render; cleared recovery still hidden). Gate: Codex SAFE, Fable SHIP-UX, suite 12179/0, browser clean. Co-authored-by: Frank Song <franksong2702@gmail.com>


Thinking Path
Long-running sessions can end in
compression_exhaustedwhen automatic compression cannot safely create enough model-facing room. A bare follow-up likecontinueusually replays the same exhausted context, so WebUI should make the terminal state durable and guide the user into a focused linked continuation instead of silently trying the same turn again.This PR implements the first #4685 recovery slice: persist terminal recovery metadata, expose one recommended action, make that action retry-safe, and block generic continuation attempts while preserving substantive user prompts.
Refs #4685.
What Changed
api/compression_recovery.pyfor durablecompression_exhaustedrecovery payloads and generic continuation-intent matching.compression_recoveryandrecommended_recovery_actionwhen streaming produces a compression-exhausted terminal error.POST /api/session/compression-recovery/startto create a linked focused continuation session with the same workspace/model/profile/project/toolset/worktree lane and an empty model-facing transcript.continue/go on/继续prompts on exhausted sessions with a 409 recovery response, while allowing substantive prompts and clearing session-level recovery only when a new turn actually starts.Start focused continuationaction and frontend interception for generic continuation attempts.Context compression exhaustedand updates the session-SSE RFC source anchors that shifted with the new route.Why It Matters
Users should not have to guess what to do after a long-running session exhausts compression. The exhausted session remains terminal and inspectable, while the recommended path creates a focused linked continuation that avoids replaying the oversized context tail.
Repeated clicks or multiple open tabs now converge on the same focused continuation instead of creating duplicate sibling sessions.
Contract Routing
compression_exhaustedis a specific terminal state, not normal completion, and automatic compression material should not become current user intent.messages/context_messages, keepsparent_session_idfor lineage, and has a persisted child marker so the recovery action is idempotent.UI Evidence
.compression-recovery-cardwithStart focused continuation; generic continuation input scrolls/focuses that card and server-side fallback returnscompression_recovery_required.Compression recovery failed: Compression recovery did not return a session.instead of silently doing nothing.tests/test_compression_recovery_action.pyverifies the card/action/intercept hooks and malformed-response guard are present, andnode --checkverifies the changed browser files parse.Verification
git diff --checkpython3 scripts/ruff_lint.py --diff origin/master— no new violations on added/modified linesnode --check static/ui.js && node --check static/messages.jspython3 -m py_compile api/compression_recovery.py api/models.py api/routes.py api/streaming.py api/session_recovery.py api/webui_session_db.py tests/test_compression_recovery_action.py tests/test_auto_compression_terminal_failure.py./scripts/test.sh tests/test_compression_recovery_action.py -v— 9 passed./scripts/test.sh tests/test_auto_compression_terminal_failure.py -v— 14 passed./scripts/test.sh tests/test_issue4812_session_sse_contract_rfc.py -v— 33 passed./scripts/test.sh tests/test_compression_recovery_action.py tests/test_auto_compression_terminal_failure.py tests/test_auto_compression_card.py tests/test_live_to_final_anchor_visible_order.py tests/test_turn_duration_display.py tests/test_webui_state_db_reconciliation.py tests/test_issue4812_session_sse_contract_rfc.py— 217 passed1e9d42c05. The final blocker-only review raisedcontext_messagespersistence, verified as already covered bySession.save()extra fields and the disk JSON test.Risks / Follow-ups
Release Note
Compression-exhausted sessions now show a focused continuation recovery action instead of encouraging bare
continueretries that can replay exhausted context.Model Used
OpenAI GPT-5 Codex for implementation and verification, plus Claude Code CLI and Greptile as independent diff reviewers.