60645f8a53
Guarding the *aim* of a rewind still leaves every other way of aiming it wrong terminal. All three reported incidents (#70516, #80763, #82756) ended at the same write — `replace_messages()` in the `prompt.submit` truncation path — and all three were unrecoverable for the same reason: the rows are DELETEd, which also evicts them from the FTS index, so there is no `active=0` archive and nothing to restore from. The codebase already draws this distinction and already has the safe half of it. `archive_and_compact` is documented as "the durability-preserving alternative to replace_messages"; `rewind_to_message` — the `/undo` path — soft-deletes to `active=0, compacted=0` and keeps the rows "on disk for audit / forensic inspection". The desktop rewind is the same user-facing operation as `/undo` and was the one taking the destructive branch. `replace_messages(..., archive_dropped=True)` flips the DELETE to a content-preserving `UPDATE messages SET active = 0`, reusing the existing transaction and the existing `active=0, compacted=0` marking so the dropped turns stay readable via `get_messages(..., include_inactive=True)` and stay out of session search (`compacted=0` = "the user took it back", vs compaction's `compacted=1` = "summarized away, still discoverable"). The live transcript is byte-identical either way — only the durability of the dropped turns changes. The parameter defaults to False, so the fork handler, the ACP adapter and `gateway/session.py` keep their current semantics untouched; a test pins that. `active_only=True` stays on the call: #80216 still applies, and archiving must not disturb rows an earlier compaction deliberately archived. Test doubles for `replace_messages` in the gateway suite are widened to the real signature — they are stand-ins for SessionDB, and a double that does not accept what production passes silently converts this write into a 5008. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>