Skip to content

Preserve cancelled partial turn payloads - #4647

Closed
franksong2702 wants to merge 17 commits into
nesquena:masterfrom
franksong2702:franksong2702/fix-cancel-preserve-partial
Closed

franksong2702 wants to merge 17 commits into
nesquena:masterfrom
franksong2702:franksong2702/fix-cancel-preserve-partial

Conversation

@franksong2702

@franksong2702 franksong2702 commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Thinking Path

  • Cancelling a live run already persists captured partial work on the backend.
  • The terminal cancel SSE event still only carried a status/message, so the browser had to race a follow-up /api/session fetch before it could render the canonical persisted state.
  • That race is especially visible for reasoning-only or tool-only partial turns, where the fallback cancel marker can hide the useful partial work until the fetch succeeds.
  • This PR carries a redacted canonical session snapshot on the cancel terminal event and lets the frontend apply it before falling back to /api/session.

What Changed

  • Added a redacted terminal cancel payload helper that embeds the persisted full-message session snapshot after cancel finalization.
  • Emitted that session payload only from cancel_stream() after it persists the canonical partial/cancel state, while worker-side cancel exits stay metadata-only so they cannot make the browser trust an incomplete snapshot.
  • Taught the frontend cancel handler to prefer the embedded session payload, preserve projected anchor-scene fields, and render before using the /api/session fallback.
  • Added regression coverage for the cancel SSE payload, frontend cancel ordering, and anchor-scene settlement ordering.
  • Rebuilt the old draft branch as a clean one-commit diff on latest master; no contributor CHANGELOG.md entry is included.

Why It Matters

Users should not lose sight of already-streamed reasoning, tool activity, or partial assistant text just because they stopped a run. The cancel terminal event now has enough canonical state for the UI to render the preserved partial turn immediately instead of briefly collapsing to only a cancel marker.

Contract Routing

Task type: streaming/recovery cancellation fix
Touched areas: backend SSE cancel payload, frontend cancel terminal handler, anchor-scene settlement ordering
Relevant public docs:

  • AGENTS.md
  • CONTRIBUTING.md
  • docs/CONTRACTS.md
  • docs/UIUX-GUIDE.md
  • DESIGN.md
  • docs/rfcs/webui-run-state-consistency-contract.md
  • docs/rfcs/live-to-final-assistant-replies.md
    Scope boundaries: this preserves already-persisted cancel state in the terminal event; it does not redesign cancellation semantics or the full assistant turn anchor model.
    Evidence needed before claiming done: backend cancel payload regression, frontend ordering regression, anchor settlement ordering regression, JS syntax check, adjacent cancel-stream regressions.

Verification

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 33 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

Risks / Follow-ups

  • The embedded payload is redacted and best-effort; existing /api/session fallback behavior remains in place if the payload is absent.
  • This changes the terminal cancel event payload shape by adding optional session and session_id fields, without removing the existing status/message fields.
  • This does not change cancellation semantics, stream ownership, or the live-to-final assistant turn anchor model.

Model Used

OpenAI Codex GPT-5 with local repository inspection, regression tests, and GitHub CLI/API publishing.

@franksong2702
franksong2702 force-pushed the franksong2702/fix-cancel-preserve-partial branch 3 times, most recently from b4b1ed3 to 64e5bb0 Compare June 25, 2026 15:11
@franksong2702 franksong2702 changed the title [codex] Preserve cancelled partial turn payloads Preserve cancelled partial turn payloads Jun 25, 2026
@franksong2702
franksong2702 marked this pull request as ready for review June 25, 2026 15:11
@greptile-apps

greptile-apps Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR embeds a redacted canonical session snapshot inside the terminal cancel SSE event so the browser can render preserved partial reasoning/tool rows immediately, eliminating the race where a follow-up /api/session GET could arrive before the cancel marker was visible. Worker-side cancel exits retain metadata-only payloads; only cancel_stream() injects the full snapshot after _cs.save() completes.

  • Adds _cancel_event_payload() helper that owns session/session_id injection and _redacted_session_payload_with_full_messages() as a best-effort wrapper, keeping all cancel event construction in one place.
  • Teaches the frontend cancel handler to prefer the embedded payload and skip the /api/session GET when it is present, while preserving the anchor-scene projection and ephemeral-field carry-forward ordering.
  • Adds regression tests verifying the payload shape, the worker/cancel_stream split, and the front-end ordering invariant.

Confidence Score: 5/5

Safe to merge — the embedded session path is strictly additive and all pre-existing fallback paths remain intact.

The cancel event shape change is backward-compatible (new optional fields only), worker paths are unchanged in behavior, and the frontend always falls through to the /api/session GET when the embedded payload is absent or rejected. The only findings are style-level guard improvements in the frontend helper.

No files require special attention; the two style suggestions in static/messages.js are optional hardening.

Important Files Changed

Filename Overview
api/streaming.py Adds two new helpers (_cancel_event_payload, _redacted_session_payload_with_full_messages) and wires them correctly: cancel_stream() embeds the session after save(); worker paths stay metadata-only. All exception paths leave _cancel_session_payload as None, causing graceful fallback.
static/messages.js Refactors the cancel handler to prefer the embedded session payload via _applyCancelSessionPayload, then falls back to the /api/session GET. Logic is sound but the guard inside _applyCancelSessionPayload checks S.session.session_id rather than sessionPayload.session_id, and the null-object check uses the typeof-null quirk without an explicit !== null guard.
tests/test_cancelled_turn_status.py Adds two structural regression tests: one verifying the frontend ordering invariant (embedded payload tried before GET), one verifying worker blocks never call _cancel_event_payload with a session argument.
tests/test_issue1361_cancel_data_loss.py Adds an integration test seeding reasoning/tool-call state, calling cancel_stream(), and asserting the queue item carries partial messages and the cancel marker. Preconditions now use explicit assert hasattr() before assignment.
tests/test_live_to_final_anchor_visible_order.py Updates the anchor-scene ordering test to match refactored _applyCancelSessionPayload internals and adds index-based ordering assertions for embedded → GET → fallback-apply paths.
tests/test_session_lost_response_regression.py Trailing blank-line cleanup only — no logic changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant W as Streaming Worker
    participant CS as cancel_stream()
    participant Q as SSE Queue
    participant B as Browser

    Note over W,CS: User triggers cancel

    CS->>CS: cancel_event.set()
    W->>W: detect cancel_event
    W->>W: _finalize_cancelled_turn(s)
    W->>Q: put cancel [metadata-only, no session]

    CS->>CS: acquire locks, snapshot buffers
    CS->>CS: _build_partial_message (reasoning + tool calls)
    CS->>CS: _cs.messages.append(cancel marker)
    CS->>CS: _cs.save()
    CS->>CS: "_cancel_session_payload = _redacted_session_payload_with_full_messages(_cs)"
    CS->>Q: put cancel [with embedded session snapshot]

    Q->>B: SSE cancel event (first arrival)

    alt Embedded session payload present
        B->>B: _applyCancelSessionPayload(payload.session)
        B->>B: "S.session = payload, render partial turn immediately"
    else No embedded session
        B->>B: _applyCancelSessionPayload(null) → false
        B->>B: "GET /api/session?session_id=..."
        B->>B: _applyCancelSessionPayload(data.session)
        B->>B: render
    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 W as Streaming Worker
    participant CS as cancel_stream()
    participant Q as SSE Queue
    participant B as Browser

    Note over W,CS: User triggers cancel

    CS->>CS: cancel_event.set()
    W->>W: detect cancel_event
    W->>W: _finalize_cancelled_turn(s)
    W->>Q: put cancel [metadata-only, no session]

    CS->>CS: acquire locks, snapshot buffers
    CS->>CS: _build_partial_message (reasoning + tool calls)
    CS->>CS: _cs.messages.append(cancel marker)
    CS->>CS: _cs.save()
    CS->>CS: "_cancel_session_payload = _redacted_session_payload_with_full_messages(_cs)"
    CS->>Q: put cancel [with embedded session snapshot]

    Q->>B: SSE cancel event (first arrival)

    alt Embedded session payload present
        B->>B: _applyCancelSessionPayload(payload.session)
        B->>B: "S.session = payload, render partial turn immediately"
    else No embedded session
        B->>B: _applyCancelSessionPayload(null) → false
        B->>B: "GET /api/session?session_id=..."
        B->>B: _applyCancelSessionPayload(data.session)
        B->>B: render
    end
Loading

Reviews (16): Last reviewed commit: "Merge origin/master into PR #4647" | Re-trigger Greptile

Comment thread api/streaming.py Outdated
Comment thread api/streaming.py
Comment thread tests/test_issue1361_cancel_data_loss.py
@nesquena-hermes nesquena-hermes added the size:M Medium PR (≤10 files, ≤250 LOC) label Jun 25, 2026
@franksong2702
franksong2702 force-pushed the franksong2702/fix-cancel-preserve-partial branch from 64e5bb0 to 9017940 Compare June 26, 2026 09:22
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed a refreshed #4647 follow-up on top of current master at 9017940ce.

What changed since the prior head:

  • Kept embedded cancel session snapshots limited to cancel_stream() after the canonical partial/cancel state is persisted.
  • Made worker-side cancel exits emit metadata-only cancel events, so a worker-wins-lock path cannot cause the browser to trust an incomplete session snapshot and skip the later canonical update.
  • Added a regression guard covering that worker/cancel_stream split.

Verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 33 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed a base refresh for #4647 at 6e4ff837.

What changed:

  • Merged current origin/master into franksong2702/fix-cancel-preserve-partial.
  • The merge was clean; the new base commits did not overlap the PR's cancel/live-to-final files.
  • No product-scope changes were made beyond refreshing the branch.

Local verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 33 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

GitHub checks are now running on the refreshed head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at a21be942.

What changed:

  • Merged current origin/master (ffef6fa4) into franksong2702/fix-cancel-preserve-partial.
  • The merge was clean; the new base commits did not overlap this PR's cancel/live-to-final files.
  • No product-scope changes were made beyond refreshing the branch.

Local verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 34 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

GitHub checks are now running on the refreshed head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 099448d7.

What changed:

  • Merged current origin/master (87acf25e) into franksong2702/fix-cancel-preserve-partial.
  • The merge was clean and only brought in the latest base release files: CHANGELOG.md, static/ui.js, and tests/test_issue4856_android_scroll_regression.py.
  • No product-scope changes were made beyond refreshing the branch.

Local verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • node --check static/ui.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_issue4856_android_scroll_regression.py -q — 108 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 34 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

GitHub checks are now running on the refreshed head.

Frank Song and others added 2 commits June 26, 2026 22:30
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Follow-up pushed at 868c67c7.

What changed since the base refresh:

  • Addressed the cancel payload test feedback in f5a53fcb1 by making the required reasoning/tool capture dictionaries explicit preconditions before asserting persisted payload fields.
  • Addressed the helper drift feedback in 868c67c7 by centralizing optional session / session_id injection inside _cancel_event_payload(..., session=...).
  • Kept worker-side cancel events metadata-only; only cancel_stream() attaches the persisted session snapshot after _cs.save().

Verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • node --check static/ui.js
  • git diff --check
  • ./scripts/test.sh tests/test_issue1361_cancel_data_loss.py -q — 19 passed.
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_issue4856_android_scroll_regression.py -q — 108 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 34 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

GitHub readback: all visible checks are passing; PR is MERGEABLE / CLEAN.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at b9399c36.

What changed:

  • Merged current origin/master (b9cea92f) into franksong2702/fix-cancel-preserve-partial.
  • The merge was clean; the new base release is perf(#4759): parallelize fresh sidebar boot fetches #4992 / v0.51.679, focused on fresh sidebar boot fetches.
  • No product-scope changes were made beyond refreshing the branch.

Local verification:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • node --check static/ui.js
  • node --check static/sessions.js
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_issue4856_android_scroll_regression.py -q — 108 passed.
  • ./scripts/test.sh tests/test_streaming_race_fix.py tests/test_session_save_mode.py tests/test_issue2157_sessions_list_stale_stream_state.py -q — 34 passed.
  • ./scripts/test.sh tests/test_issue4759_parallel_sidebar_boot_fetch.py tests/test_issue4766_sidebar_source_pushdown.py tests/test_session_sidebar_resilience.py -q — 26 passed.
  • ./scripts/test.sh tests/test_cancel_interrupt.py tests/test_issue893_cancel_preserves_partial.py tests/test_issue1298_cancel_and_activity.py tests/test_stale_stream_writeback.py tests/test_sprint51.py -q — 45 passed.

GitHub checks are now running on the refreshed head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 4e42eb324.

  • Merged current origin/master (60f6779e) into franksong2702/fix-cancel-preserve-partial.
  • The merge brought in the Release YJ session-index ownership fix (CHANGELOG.md, api/models.py, tests/test_session_index.py); the cancel-preservation files were unchanged by this merge.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py api/models.py
    • node --check static/messages.js
    • node --check static/ui.js
    • node --check static/sessions.js
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py -q — 35 passed
    • ./scripts/test.sh tests/test_session_index.py::test_background_rebuild_old_thread_finally_preserves_new_same_target_owner tests/test_session_index.py::test_background_index_rebuild_skips_after_session_dir_switch -q — 2 passed
  • GitHub readback after push: PR open, non-draft, head 4e42eb324, base 60f6779e, MERGEABLE; checks are running on the new head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 166461b1e.

  • Merged current origin/master (0585b3573) into franksong2702/fix-cancel-preserve-partial.
  • This pulled in the current base release train through Release YS; the PR-specific cancel payload diff still remains scoped to the cancel terminal event path and its regression tests.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py api/models.py api/config.py api/routes.py api/route_session_list_cache.py api/system_health.py
    • node --check static/messages.js
    • node --check static/boot.js
    • node --check static/workspace.js
    • node --check static/ui.js
    • node --check static/sessions.js
    • git diff --cached --check
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
    • ./scripts/test.sh tests/test_route_session_list_cache_extraction.py tests/test_5001_active_profile_recovery.py tests/test_issue4945_markdown_table_copy.py -q — 20 passed
  • GitHub readback after push: PR open, non-draft, head 166461b1e, base 0585b3573, MERGEABLE; checks are running on the new head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at e411b410d.

  • Merged current origin/master (173eb6771) into franksong2702/fix-cancel-preserve-partial.
  • The PR-specific cancel payload diff still remains scoped to the cancel terminal event path and its regression tests after the refresh.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py api/auth.py api/config.py api/routes.py api/route_session_list_cache.py api/system_health.py
    • node --check static/messages.js
    • node --check static/boot.js
    • node --check static/onboarding.js
    • node --check static/panels.js
    • node --check static/sessions.js
    • node --check static/ui.js
    • node --check static/i18n.js
    • git diff --cached --check
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
    • ./scripts/test.sh tests/test_5021_boot_model_redirect_budget.py tests/test_issue4877_fresh_blank_boot_workspace_bind.py tests/test_issue1362_codex_oauth_onboarding.py tests/test_auth_session_persistence.py tests/test_byok_model_dropdown.py -q — 74 passed
    • ./scripts/test.sh tests/test_3866_project_context_memory_section.py tests/test_dashboard_link_ui.py tests/test_issue4756_session_visit_model_refresh.py tests/test_model_default_boot_precedence.py tests/test_session_metadata_fast_path.py -q — 51 passed
  • GitHub readback after push: PR open, non-draft, head e411b410d, base 173eb6771, MERGEABLE; checks are running on the new head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 42d314f8e.

  • Merged current origin/master (d38aa51a) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the completion-notification preview release; the PR-specific cancel payload diff still remains scoped to the cancel terminal event path and its regression tests.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py
    • node --check static/messages.js
    • git diff --cached --check
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
    • ./scripts/test.sh tests/test_pwa_notification_controls.py -q — 9 passed
  • GitHub readback after push: PR open, non-draft, head 42d314f8e, base d38aa51a, MERGEABLE; checks are running on the new head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 53b537b2f.

  • Merged current origin/master (1b3b49b33) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the extension sidecar runtime diagnostics release; the PR-specific cancel payload diff still remains scoped to the cancel terminal event path and its regression tests.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py
    • node --check static/messages.js
    • node --check static/panels.js
    • git diff --cached --check
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
    • ./scripts/test.sh tests/test_extensions_settings_panel.py -q — 13 passed
  • GitHub readback after push: PR open, non-draft, head 53b537b2f, base 1b3b49b33, MERGEABLE; checks are running on the new head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at 259bba4da.

  • Merged current origin/master (2d9422adf) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the extension gallery post-install guidance release; the PR-specific cancel payload diff still remains scoped to the cancel terminal event path and its regression tests.
  • Local verification passed:
    • python3 -m py_compile api/streaming.py
    • node --check static/messages.js
    • node --check static/panels.js
    • node --check static/i18n.js
    • git diff --cached --check
    • git diff --check HEAD~1 HEAD
    • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
    • ./scripts/test.sh tests/test_extensions_settings_panel.py -q — 16 passed
  • GitHub readback after push: PR open, non-draft, head 259bba4da, base 2d9422adf, MERGEABLE; checks are running on the new head.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Deep-reviewed + gate-clean — held for your visual sign-off (crown-jewel cancel-render surface, so not auto-shipped per the standing rule).

  • Codex regression gate: SAFE TO SHIP — the cancel snapshot is built under the per-session agent lock with recursive redaction (no aliasing/TOCTOU), consumed through the same renderMessages()/renderMd escaped path (no new innerHTML sink), the backward-compatible /api/session fallback is preserved when no snapshot is embedded, and a stale-terminal-event guard prevents overwriting a newer active stream.
  • Full pytest suite: 10769 passed, 0 failed. node -c + ESLint runtime gate CLEAN.
  • Rebased clean onto current master.

Recommendation to the maintainer: ship — clean UX-recovery win (cancel now shows your preserved partial work instead of a flash of nothing). Awaiting a quick visual confirm before release.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Refreshed this branch against current origin/master again.

Local verification passed:

  • python3 -m py_compile api/streaming.py api/routes.py api/route_approvals.py
  • node --check static/messages.js
  • git diff --cached --check
  • git diff --check origin/master...HEAD
  • git diff --check HEAD~1 HEAD
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py -q — 91 passed
  • ./scripts/test.sh tests/test_gateway_approval_legacy_path.py -q — 14 passed

GitHub checks are running on the refreshed head.

@franksong2702

Copy link
Copy Markdown
Contributor Author

CI readback for the refreshed head c008257da270ed067f69d084cf129008cec08f7a is clean.

  • browser-smoke: success
  • lint: success
  • Python shards 3.11, 3.12, and 3.13: all success
  • PR remains open, non-draft, MERGEABLE / CLEAN

No additional branch changes from me here; this is ready for the pending visual sign-off/reviewer path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at a7515c598.

  • Merged current origin/master (db15233c4) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the memory-section hover path titles release (static/panels.js, tests/test_3866_project_context_memory_section.py, CHANGELOG.md).
  • The PR-specific cancel payload diff remains scoped to the cancel terminal event path and its regression tests.

Local verification passed:

  • python3 -m py_compile api/streaming.py
  • node --check static/messages.js
  • node --check static/panels.js
  • git diff --check origin/master...HEAD
  • git diff --check HEAD~1 HEAD
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_3866_project_context_memory_section.py -q — 104 passed

GitHub readback after push: PR open, non-draft, head a7515c598, base db15233c4, mergeable; checks are running on the refreshed head.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at b381ad580.

  • Merged current origin/master (4584068b7) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the session-visit model-refresh coalescing release (api/config.py, tests/test_issue4756_session_visit_model_refresh.py, CHANGELOG.md).
  • The PR-specific cancel payload diff remains scoped to the cancel terminal event path and its regression tests.

Local verification passed:

  • python3 -m py_compile api/streaming.py api/config.py
  • node --check static/messages.js
  • git diff --check origin/master...HEAD
  • git diff --check HEAD~1 HEAD
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_issue4756_session_visit_model_refresh.py -q — 108 passed

GitHub readback after push: PR open, non-draft, head b381ad580, base 4584068b7, mergeable; checks are running on the refreshed head.

@franksong2702

Copy link
Copy Markdown
Contributor Author

CI readback for refreshed head b381ad58007449b27ec4639b38271df44813a2a7 is clean.

  • browser-smoke: success
  • lint: success
  • Python shards 3.11, 3.12, and 3.13: all success
  • PR remains open, non-draft, MERGEABLE / CLEAN

No additional branch changes from me here; this is ready for the pending visual sign-off/reviewer path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@franksong2702

Copy link
Copy Markdown
Contributor Author

Pushed another base refresh for #4647 at bb3e9c5f2.

  • Merged current origin/master (d968c741f) into franksong2702/fix-cancel-preserve-partial.
  • The new base change is the read-side sidecar self-heal release (api/models.py, tests/test_session_lost_response_regression.py, CHANGELOG.md).
  • The PR-specific cancel payload diff remains scoped to the cancel terminal event path and its regression tests.
  • While validating the merge, git diff --check HEAD~1 HEAD caught a trailing blank line at EOF in the newly merged base test file; I removed that whitespace-only issue in the merge commit before pushing.

Local verification passed:

  • python3 -m py_compile api/streaming.py api/models.py
  • node --check static/messages.js
  • git diff --check origin/master...HEAD
  • git diff --check HEAD~1 HEAD
  • git diff --check
  • ./scripts/test.sh tests/test_cancelled_turn_status.py tests/test_issue1361_cancel_data_loss.py tests/test_live_to_final_anchor_visible_order.py tests/test_session_lost_response_regression.py -q — 122 passed

GitHub readback after push: PR open, non-draft, head bb3e9c5f2, base d968c741f, mergeable; checks are running on the refreshed head (browser-smoke and lint already succeeded at readback time).

@franksong2702

Copy link
Copy Markdown
Contributor Author

CI readback for refreshed head bb3e9c5f2a13f6f3ddb6fc0ecd5ebe82972532cf is clean.

  • browser-smoke: success
  • lint: success
  • Python shards 3.11, 3.12, and 3.13: all success
  • PR remains open, non-draft, MERGEABLE / CLEAN

No additional branch changes from me here; this is ready for the pending visual sign-off/reviewer path.

nesquena-hermes added a commit that referenced this pull request Jun 27, 2026
Release: preserve cancelled partial turn payloads (#4647)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.703 🎉 — thank you @franksong2702!

Since this PR is from your fork, I shipped it via a maintainer release branch (push-to-fork-branch can't update the PR), preserving your authorship with a Co-authored-by trailer and crediting you in the CHANGELOG.

Your fix is exactly right: embedding the canonical redacted session snapshot in the terminal cancel event so the preserved partial reasoning/tool/text renders immediately, skipping the GET race that briefly collapsed the turn to a bare "Task cancelled" marker (#4076, #1361).

Full gate before ship:

  • Codex: SAFE TO SHIP — no regression risk; snapshot is stable because messages is materialized by redaction before the per-session lock releases.
  • Opus: SHIP — confirmed the snapshot is a fully detached deep copy (redaction rebuilds every container, so no aliasing/TOCTOU), the lock discipline is correct, and the _applyCancelSessionPayload extraction is faithful with zero regression to the existing cancel-marker / partial-message persistence.
  • Full suite + your CI 11/11 green.

One small maintainer hardening on top of your fix: the embedded snapshot is now only applied when its session_id matches the active view (belt-and-suspenders against a stray payload). Closing as shipped — reopen if you spot anything off after upgrading.

iosub pushed a commit to iosub/HERMES-hermes-webui2 that referenced this pull request Jun 27, 2026
…ot (nesquena#4647)

Cancelling a running turn briefly collapsed the already-streamed reasoning,
tool cards, and text to a bare "Task cancelled" marker before a follow-up
GET /api/session repopulated them — a visible race between the terminal
cancel SSE event and the refetch (nesquena#4076, nesquena#1361).

The terminal cancel event now carries a canonical, redacted session snapshot
captured under the per-session agent lock right after the cancelled turn is
persisted (including the recovered partial-assistant message). The frontend
applies that embedded snapshot immediately via _applyCancelSessionPayload and
skips the GET round-trip. The snapshot is a fully detached deep copy (redaction
rebuilds every container) so it cannot alias or be staled by concurrent writes;
it is only applied when its session_id matches the active view; and a
missing/failed snapshot degrades cleanly to the existing GET fallback.

The seven other cancel call sites are standardized via _cancel_event_payload()
(message + type + status), backward-compatible (no session key when none).

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M Medium PR (≤10 files, ≤250 LOC)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants