Skip to content

Release: compression-recovery action — focused continuation on compression_exhausted (#5538) - #5646

Merged
nesquena-hermes merged 37 commits into
masterfrom
ship-5538
Jul 5, 2026
Merged

nesquena-hermes merged 37 commits into
masterfrom
ship-5538

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release: compression-recovery action (#5538) — one-click focused continuation on compression_exhausted

Ships #5538 (@franksong2702) implementing the #4685 recovery slice: when a long conversation hits compression_exhausted, the terminal error shows a Start focused continuation action that opens a fresh linked session (same workspace/model/profile/toolset, empty model-facing transcript). Bare "continue"/"go on"/继续 on an exhausted conversation is intercepted; substantive prompts pass through; repeated clicks/tabs converge on the same child.

Gate folds (applied on top of the contributor's head)

Fable + Codex flagged the stale-card edge; folded in before ship:

  • Map the stale-card 409 (recovery already cleared) to a neutral info toast + retire the card (data-compression-recovery-consumed), instead of a raw error toast.
  • finally-guard (retiredRecoveryCard) so the retired button is not re-enabled.
  • Consumed-state CSS so the retired card is visibly de-emphasized (no lingering cursor:progress).
  • Regression test for all three.

Full authoritative gate (converged clean)

Gate Result
ESLint runtime + node --check ✅ CLEAN
Browser smoke (/, /#settings, /#sessions) ✅ CLEAN — 0 console errors
Full pytest suite (-p no:xdist) ✅ 12140 passed, 0 failed
Codex (regression, reproduce) ✅ SAFE TO SHIP — 0 findings
Fable-UX ✅ SHIP-UX — 0 blockers

Both round-1 gate-cert COREs (recovery-child lineage isolation + profile-scoped reuse) verified fixed by independent code read. #5592 soft-partial layering preserved. Nathan approved the concept + reviewed desktop/mobile screenshots.

Attribution: Co-authored-by: Frank Song + CHANGELOG credit.

Frank Song and others added 30 commits July 4, 2026 21:42
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>
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>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
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>
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>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Frank Song and others added 7 commits July 5, 2026 16:22
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>
… on compression_exhausted (#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>
@nesquena-hermes
nesquena-hermes merged commit 1870f77 into master Jul 5, 2026
18 checks passed
@nesquena-hermes
nesquena-hermes deleted the ship-5538 branch July 5, 2026 21:19
@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ships the compression-recovery action (#5538): when a session hits compression_exhausted, users see a Start focused continuation card that opens a fresh linked session preserving workspace/model/profile/toolset but with an empty model-facing transcript. Bare continuation phrases are intercepted client-side (and blocked server-side as a safety net), while repeated clicks and multi-tab scenarios converge on the same child via persisted markers on the recovery session.

  • New API endpoint POST /api/session/compression-recovery/start creates or idempotently returns a focused child session, guarded by a process-level lock and profile-scoped deduplication via find_compression_recovery_session.
  • Recovery clear/restore logic in _handle_chat_start optimistically clears the recovery flag before starting a run and restores it on any failure (exception, 501, or ≥400 status), with the restore explicitly persisted via session.save().
  • Stale-card 409 handling in startCompressionRecovery maps a cleared-recovery response to a neutral info toast and retires the card in-place, guarded by a finally block so the button is not re-enabled after retirement.

Confidence Score: 4/5

Safe to merge with awareness of the stale-child reuse behaviour on repeated exhaustion events.

The clear/restore logic around _start_run, the profile-scoped deduplication, and the stale-card 409 path are all well-implemented and tested. The one behavioural gap worth tracking is that find_compression_recovery_session matches purely on (source_session_id, action, profile) without any recovery-event timestamp — so if source session S is exhausted a second time after a successful mid-session run, the Start focused continuation action silently reopens the original child C1 rather than creating a new one. The server-side 409 for generic continuations also has no dedicated client-side handler for the stale-session-state path.

api/models.py (find_compression_recovery_session) and api/routes.py (server-side 409 for generic continuations) are the two areas worth a second look.

Important Files Changed

Filename Overview
api/compression_recovery.py New module implementing recovery metadata stamping, payload building, and the generic-continuation intent classifier; logic is sound with one cosmetic issue (re.UNICODE is redundant in Python 3).
api/models.py Adds four recovery fields to Session; find_compression_recovery_session correctly de-duplicates across memory+disk, but the oldest-match selection means a second exhaustion event on the same source session silently reuses the first recovery child.
api/routes.py New /api/session/compression-recovery/start endpoint and generic-continuation 409 guard added; clear/restore logic around _start_run is correct, but the server-side 409 for generic continuations has no dedicated client-side handler for the stale-session-state edge case.
api/streaming.py Stamps recovery metadata on both inline and exception compression_exhausted error paths; change is minimal and correctly attached to the right error classification branches.
static/ui.js Adds _compressionRecoveryHtml, startCompressionRecovery, intercept helpers, and the 409 stale-card retirement branch; XSS safety via esc() is maintained throughout and the finally guard correctly skips re-enabling a retired button.
static/messages.js Adds pre-send intercept hook and attaches _compressionRecovery to assistant error messages; changes are minimal and correctly guard with typeof checks before each call.
tests/test_compression_recovery_action.py New test file covering intent classification, generic-continuation blocking, clear/restore round-trips, deduplication, profile scoping, DB sidecar round-trips, and the stale-card 409 path; coverage is thorough.
api/session_recovery.py Correctly plumbs all four new recovery fields through _state_db_row_to_sidecar with appropriate null-safe defaults.
api/webui_session_db.py Adds the four new recovery fields to _METADATA_FIELDS so they are included in metadata-only loads; straightforward and correct.
static/style.css Adds compression-recovery card styles including the consumed-state attribute selector and responsive layout; no issues.
tests/test_auto_compression_terminal_failure.py Existing tests extended to assert recovery metadata is present on exhausted session payloads; additions are consistent with the new fields.
docs/troubleshooting.md Adds a well-structured troubleshooting entry covering symptom, why, diagnostic commands, fix, and when-to-file-bug sections.
CHANGELOG.md Changelog entry added as part of the release process; accurate summary of the feature.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User
    participant JS as Browser
    participant API as /api/chat/start
    participant SR as /api/session/compression-recovery/start
    participant STR as Streaming engine
    participant DB as Session store

    STR->>DB: stamp_compression_exhausted_recovery(session)
    STR->>JS: SSE error event with compression_recovery
    JS->>JS: render recovery card

    alt User types generic continuation
        U->>JS: send()
        JS->>JS: shouldInterceptCompressionRecoveryContinuation()
        JS->>U: scroll to card + warning toast
    end

    alt User clicks Start focused continuation
        U->>JS: startCompressionRecovery(btn)
        JS->>SR: POST session_id
        SR->>DB: find_compression_recovery_session()
        alt No existing child
            SR->>DB: new Session empty transcript
            SR->>JS: "200 created=true"
        else Existing child found
            SR->>JS: "200 created=false"
        end
        JS->>JS: loadSession(new_sid)
        alt 409 recovery already cleared
            SR->>JS: 409
            JS->>JS: "retire card consumed=1"
            JS->>U: info toast
        end
    end

    alt User sends substantive prompt
        U->>JS: send()
        JS->>API: POST chat/start
        API->>DB: clear_compression_recovery(session)
        API->>STR: _start_run(session)
        STR->>DB: session.save()
    end
Loading
%%{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 U as User
    participant JS as Browser
    participant API as /api/chat/start
    participant SR as /api/session/compression-recovery/start
    participant STR as Streaming engine
    participant DB as Session store

    STR->>DB: stamp_compression_exhausted_recovery(session)
    STR->>JS: SSE error event with compression_recovery
    JS->>JS: render recovery card

    alt User types generic continuation
        U->>JS: send()
        JS->>JS: shouldInterceptCompressionRecoveryContinuation()
        JS->>U: scroll to card + warning toast
    end

    alt User clicks Start focused continuation
        U->>JS: startCompressionRecovery(btn)
        JS->>SR: POST session_id
        SR->>DB: find_compression_recovery_session()
        alt No existing child
            SR->>DB: new Session empty transcript
            SR->>JS: "200 created=true"
        else Existing child found
            SR->>JS: "200 created=false"
        end
        JS->>JS: loadSession(new_sid)
        alt 409 recovery already cleared
            SR->>JS: 409
            JS->>JS: "retire card consumed=1"
            JS->>U: info toast
        end
    end

    alt User sends substantive prompt
        U->>JS: send()
        JS->>API: POST chat/start
        API->>DB: clear_compression_recovery(session)
        API->>STR: _start_run(session)
        STR->>DB: session.save()
    end
Loading

Reviews (1): Last reviewed commit: "release: compression-recovery action — o..." | Re-trigger Greptile

Comment thread api/models.py
Comment on lines +3320 to +3384
def find_compression_recovery_session(
source_session_id: str,
action: str,
source_profile=_COMPRESSION_RECOVERY_PROFILE_UNSET,
):
"""Return an existing focused recovery child for ``source_session_id``.

The recovery-start endpoint is a retryable UI action. A persisted marker on
the child session makes double-clicks, repeated calls, and cache reloads
converge on the same continuation instead of creating duplicate siblings.
"""

source_sid = str(source_session_id or "").strip()
recovery_action = str(action or "").strip()
if not source_sid or not recovery_action:
return None

matches = []
seen_ids: set[str] = set()
try:
with LOCK:
memory_sessions = list(SESSIONS.values())
for session in memory_sessions:
sid = str(getattr(session, "session_id", "") or "").strip()
if sid:
seen_ids.add(sid)
if _compression_recovery_child_matches(session, source_sid, recovery_action, source_profile):
matches.append(session)
except Exception:
logger.debug("Failed to scan cached compression recovery sessions", exc_info=True)

try:
persisted_ids = _persisted_session_ids_snapshot()
except Exception:
persisted_ids = frozenset()
for sid in persisted_ids:
if sid in seen_ids:
continue
try:
meta = Session.load_metadata_only(sid)
except Exception:
logger.debug("Failed to inspect compression recovery session %s", sid, exc_info=True)
continue
if not meta or not _compression_recovery_child_matches(meta, source_sid, recovery_action, source_profile):
continue
try:
matches.append(get_session(sid))
except Exception:
matches.append(meta)

if not matches:
return None

def _sort_key(session):
try:
created_at = float(getattr(session, "created_at", 0) or 0)
except (TypeError, ValueError):
created_at = 0.0
try:
updated_at = float(getattr(session, "updated_at", 0) or 0)
except (TypeError, ValueError):
updated_at = 0.0
return (created_at, updated_at, str(getattr(session, "session_id", "") or ""))

return sorted(matches, key=_sort_key)[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale recovery child reused after a second exhaustion event

find_compression_recovery_session matches solely on (compression_recovery_source_session_id, action, profile) and returns the oldest match. If source session S is exhausted once, C1 is created; later a substantive prompt in S clears the recovery flag and starts a new run; if that run also hits compression_exhausted, the second stamp_compression_exhausted_recovery stamps S again. The next click on "Start focused continuation" calls find_compression_recovery_session(S.id, "start_focused_continuation"), which finds C1 (its markers are permanent and never cleared), and the user is silently redirected to that old child — which may already contain unrelated work — instead of getting a fresh session.

The fix would be to scope the child lookup to the specific recovery event, e.g., by including a recovery_event_id or compression_recovery_created_at in both the payload and the child markers, and matching on it during the lookup.

Comment thread api/routes.py
Comment on lines +20246 to +20258
recovery = compression_recovery_payload_for_session(s)
if recovery and not attachments and is_generic_continuation_intent(msg):
return j(
handler,
{
"error": "This session exhausted context compression. Start a focused continuation, then describe the next narrow task.",
"type": "compression_recovery_required",
"recommended_recovery_action": recovery.get("recommended_action"),
"compression_recovery": recovery,
"session_id": getattr(s, "session_id", body["session_id"]),
},
status=409,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Server-side 409 for generic continuation lacks a dedicated client-side handler

When the JS intercept in send() does not fire — for example, when a session was loaded before compression_recovery was present on the server-side session object, so Object.prototype.hasOwnProperty.call(S.session,'compression_recovery') returns false AND no _compressionRecovery message is found — the API call proceeds. The server correctly returns a structured 409 with type: "compression_recovery_required", but send() has no branch that inspects e.status === 409 on this code path and shows the focused-continuation hint. The result is that the existing generic-error toast fires with the raw string rather than the scroll-to-card hint.

if not raw:
return ""
# Keep CJK letters while dropping punctuation/emoji/noise around short intents.
return re.sub(r"[\W_]+", " ", raw, flags=re.UNICODE).strip()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 re.UNICODE is the implicit default for str patterns in Python 3 and has no effect here. Dropping it removes a small maintenance confuser — readers might assume it's doing something meaningful beyond the default.

Suggested change
return re.sub(r"[\W_]+", " ", raw, flags=re.UNICODE).strip()
return re.sub(r"[\W_]+", " ", raw).strip()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@cutter-sh

cutter-sh Bot commented Jul 5, 2026

Copy link
Copy Markdown

🎬 Cutter preview — PR #5646

Start focused continuation
Start focused continuation — Focused continuation opens a fresh linked session with an empty transcript ready for the next narrow task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant