fix: isolate edit rewind from discarded context - #8145
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Dependencies ( Design-Verdict: CONCERNS Sound fix for a real replay-leak, but a hand-rolled two-phase commit over Watch
[DESIGN-REVIEWED] ac833c8 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All verification is done. The diff confines itself to the rewind endpoint (every mechanism it calls — First-Principles-Verdict: CONCERNS The rewind path is fixed at cause level, but the same replay defect stays live in edit-resend, which the Mochi chat panel still calls today. What this change shipsIntent: editing a past prompt must truly discard the suffix from every replay channel — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] ac833c8 |
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @chenmingwei23 overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Rebuild a rewound session from persisted retained history after clearing the native resume sid, so discarded turns cannot re-enter the replacement context. Maintainer drive on top of Premshay's rebase, folding in two GPT review rounds (five blocking findings total): - Reserve the slot before the awaited durable boundaries: publish the dispatch task (gated on an internal event) before the first await, so a concurrent send takes the queue path instead of starting a competing turn the commit would erase; the commit removes only the pre-await queue snapshot, and a rejected rewind hands arrived entries to the canonical queue drain. - Check _save_slot_to_history's return value: its guards refuse the write (False) when the session was deleted or rebound mid-save; rewind now 503s instead of dispatching from state that was never persisted. - Deny app-authenticated rewind of a channel-linked slot: its effective session is a foreign conversation the app does not own, and the rewind would clear that session's native identity (404, anti-enumeration). - Force the session-map durability point endpoint-side after the discard (state.sessions.aflush, 503 on failure), so a gateway exit before the debounced flush cannot resurrect the discarded conversation; the shared discard keeps its existing semantics for its other callers. - Refuse the commit when the slot was rebound to another transcript while persistence was in flight, so a late commit cannot overwrite state injected by a concurrent rebinding. The save itself goes through the live slot with expected_history_key, so a rebind visible at write time refuses the write before it can rewrite the former transcript. - Ask the discard with skip_if_busy: an inbound channel turn holds the session semaphore while slot.running reads False, and an unconditional discard would tear down its provider mid-reply; the refusal surfaces as a retryable 409 rewind_session_busy. - Give the three identical anti-enumeration 404s the shared machine code slot_not_found (error-code contract ratchet; baseline regenerated, chat_rewind.py 15 -> 13). - Shield the history save against handler cancellation: the worker thread finishes the destructive rewrite regardless, so a client disconnect mid-save now waits for the worker's outcome and completes the matching live-state commit (and the reserved dispatch still runs the edited prompt) instead of abandoning a landed rewrite with stale live state. Original change authored by Premshay. Round 9 (same-repo #8145): the commit closure no longer restores the prospective slot's pre-save persistence witnesses; the save stamps the post-rewrite truth on the live slot (_pending_rewrite cleared, fresh _disk_* / frozen-prefix cache), and restoring stale copies re-armed a second destructive rewrite and moved the monotone _disk_tail_ts floor backwards. Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
a860fed to
ac833c8
Compare
|
Kiro Crew [operator: chenmingwei23#pr-drive-5395] Fixed the GPT round-9 blocking finding (chat_rewind.py:393, verified real) in ac833c8. Finding: after a successful rewind the commit closure copied the prospective slot's PRE-save persistence witnesses back onto the live slot. The save runs on the LIVE slot and stamps the post-rewrite truth (_pending_rewrite cleared, _disk_window_len/disk_meta*/_frozen_prefix_cache matching the truncated file); restoring the stale copies re-armed _pending_rewrite -- so the next flush repeated the destructive rewrite and could discard a cross-process append (workflow/cron) that landed in between -- and moved the monotone _disk_tail_ts floor backwards (state.py note_disk_tail documents that floor as forward-only). Fix: the commit closure no longer touches the persistence witnesses; the live slot keeps what the save stamped. Both commit call sites (success path and cancellation-after-landed-save) run only after a landed save, so the post-save values are authoritative at each. Test: test_rewind_commit_keeps_the_post_save_persistence_witnesses -- the mocked save stamps witnesses on the live slot like the real one; asserts the commit leaves _pending_rewrite False and keeps the advanced _disk_tail_ts. Full local gates green (35 rewind tests, 1140 session/context/chat/error-contract, isort/flake8/mypy/black). Note: this finding is NOT in the operator-accepted residue class (narrow races vs the non-atomic rewrite) -- it was a genuine defect in the round-6 commit-closure refactor, introduced on the superseded #5395 and carried here, hence fixed rather than overridden. |
|
/ai-review override gpt ac833c8: Operator-accepted residue class: narrow races vs the non-atomic rewind rewrite (channel cold-start after discard; rebind during save; slot-close vs in-flight rewrite). Outcomes recoverable: suffix archived, canonical history retained per spec. Rounds 5-8 on superseded #5395 each landed a variant prescribing feature removal or cross-module serialization, out of scope; serialization deferred to tracked follow-up f-20260903-01. |
Human judgment recorded@chenmingwei23 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
Problem / Motivation
Edit + Send visually rewound a dashboard conversation, but the next turn could still receive the discarded suffix through native ACP session resume, generic session-history reconstruction, or queued successors.
Supersedes #5395 (fork PR by @Premshay, who remains the git author of this identical commit). The fork PR converged on the code but could not converge on process: fork review lanes re-review from scratch on every roll and do not honor the recorded /ai-review override marker (target=gpt head=002066e35, actor chenmingwei23), and the marker is voided by every rebase while main moves several times a day. Same-repo lanes honor the override mechanism, which is what this PR restores.
Why it matters
Editing a prompt must create a real conversation boundary: the replacement turn may use only the retained prefix and revised prompt, including after a process restart or in another open client. Without it, discarded context silently re-enters the model's view.
What changed (motivation -> approach -> change)
The rewind request is one-shot and built only from the retained slot prefix. It clears the native conversation identity durably before saving the rewritten history (endpoint-side aflush), rejects the request if that boundary cannot be persisted (503) or was refused (save guards, rebind fence, busy session 409), removes queued successors (snapshot-only, arrivals survive), and notifies other clients via queue_cancel. The slot is reserved before the awaited boundaries so concurrent sends queue instead of racing; a client disconnect mid-save still commits a landed rewrite and dispatches the edited prompt; app-authenticated rewind of a channel-linked slot is refused (404, anti-enumeration). Carries all 12 verified blocking findings fixed across six GPT review rounds plus one First Principles round on #5395.
Accepted residue (operator ruling, recorded via the override marker on #5395): narrow races against the non-atomic rewrite (channel cold-start after discard; slot-close vs in-flight rewrite). Outcomes are recoverable -- the dropped suffix is archived, canonical history is retained per spec -- and strictly smaller than main's rewind, which has no such fences at all. Serializing rebinding/flushing with persistence is deferred to a tracked follow-up (local backlog f-20260903-01: extract the prepare/commit boundary onto _ChatSlot and migrate edit_resend onto it).
Tests
Manual verification
The focused endpoint tests cover durable-boundary failure, native-session discard, retained-prefix reconstruction, and queue removal. Manual Edit + Send verification remains appropriate for a live dashboard session.
Why no screenshot: No rendered UI changed; this fixes prompt/session state assembly.
Related Issues
Supersedes #5395. No linked issue: no existing issue matches this bug fix.
Pattern harvest
Rule candidate: review-prompt
Pattern: a destructive edit that removes content in one layer while the data stays reachable through a parallel replay channel (native session resume, history reconstruction, queued successors) -- every replay path must be cleared, durably, before the replacement is dispatched.
Checklist