Repository navigation
Edit and regenerate answer the edited message; restore files on rewind (#24) - #95
Merged
Merged
Conversation
Compact Conversation and the compaction before a switch to a smaller model sent an ordinary message asking the model for a summary. The SDK session then carried that summary on top of the full history it was meant to replace, so the context grew, and the model-switch notice claimed a compaction that never happened. Both now send `/compact` with the same keep-list. The CLI summarises and starts the session again from the summary (checked against the bundled CLI: a compact_boundary comes back and the next turn sends only the summary), and the turn shows its own "Context compacted" notice. The switch notice says whether it worked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k SDK session Edit and Regenerate sent the literal prompt "regenerate" into a resumed SDK session that still held the original, unedited history. The edited text never reached the model, the reply the user threw away stayed in its context, and memory recall, skill relevance, the title and the evaluator all ran on the word "regenerate". Every turn now goes in as one streamed SDKUserMessage carrying a uuid we mint, which the CLI keeps as the transcript uuid (verified against the bundled CLI). The user row records it with the session and the run's cwd (`metadata.sdkTurn`), and the reply records the last main-thread chain entry its turn wrote (`metadata.sdkTailUuid`) — no migration. A regenerate, after an edit too, sends the row's own text and attachments and resumes with `resumeSessionAt` at the previous reply's tail: the same session id, exactly the kept history. The first message, and replies from before the join or from another session, start a fresh session primed with the kept rows as text; a cut the CLI refuses (one from before a compaction) is recognised before any model call and retried once that way. forkSession is not used — a fork starts without the file history. File checkpointing is turned on for runs whose workspace outlives the turn (a project, or an agent's persistent key), so the join can name a checkpoint; restoring comes next. The edit button is hidden while a reply streams, and the page sends '' rather than a placeholder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit and Regenerate can now put back the files the dropped replies changed. Before
anything changes, the page asks the server for a preview: a short-lived control session
(`$lib/engine/rewind.server`) resumes the conversation's SDK session in the run's own cwd
with checkpointing on and an input that never yields, so no prompt is sent and no model is
called, and answers `rewindFiles(uuid, { dryRun: true })` from the file history it loads at
start-up. It works after a restart, which a live Query handle would not.
The dialog lists the files and line counts with "Also restore these files" ticked, flags
files git reports as uncommitted, and for an imported repository with uncommitted changes
in those files needs an explicit "overwrite them" before Continue works. It is not shown
when there is nothing to restore (no project, an older message, a turn that changed no
files), and says why when a checkpoint exists but cannot be used.
On confirm the server checks everything again — the caller's own message, a checkpoint
from the current session, a cwd inside their sandbox that still exists, no live turn and
no other rewind in the conversation, every file inside the workspace, the acknowledgement
— re-runs the dry run, restores, and only then edits or truncates the rows. A restore that
fails leaves the conversation as it was. Edit and truncate are refused while a turn runs,
and truncate now cuts by sequence rather than timestamp.
The real bundled CLI is exercised against a synthetic session on disk in
engine.rewind.spec.ts (no credentials); the rest is pinned through stubs, the database and
the page on both projects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New docs/chat/chat.md covers what the model sees after an edit or regenerate, when files are checkpointed, the restore dialog and its guards, compaction, and exactly which SDK behaviours were verified against the bundled CLI and how. The chat and context specs, README, the desktop parity table and the #24 section of the re-audit are updated; the re-audit's two open questions for #24 are answered there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A real `rewindFiles` can refuse tracked files because a symlink, hard link or moved
parent directory is in the way (`RewindFilesResult.skippedLinks`); the dry run never
reports these. The count was logged and dropped, and `filesRestored` was the previewed
count, so a partial restore read as a full one.
`applyMessageRewind` now reports the previewed files less the skipped ones, the edit and
regenerate results carry `skippedLinks`, and the chat page shows a dismissable notice
("Restored 3 of 5 files. 2 were not restored: ...") when any were skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cut back to the kept history only happened on the regenerate request itself. If an edit or regenerate truncated the rows and that request never reached the CLI (a dropped connection, a failed setup, a restart), the next ordinary message resumed the full, unedited session: the model saw the original message and the discarded reply while the page showed the edited one. Edit and regenerate now mark the row they cut back to (`metadata.sdkCutPending`) in the same transaction as the cut. `planTurn` treats any turn in a conversation with a pending cut like a regenerate: resume at the previous reply's `sdkTailUuid`, with the same fresh fallback, and put the user rows after that reply (the ones the cut session never saw) in front of the text as a short preamble. A slash command gets nothing in front, so `/compact` still reads as a command. `recordTurnJoin` clears the mark up to its row once the turn's session has started from the cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing held the finding 80 fix in place: a revert to the old "please summarise" prompt passed the whole suite. The `/compact` prompt and the model-switch notice move out of the chat page into `$lib/chat/compact-command`, and `tests/chat.compact.spec.ts` pins both: the prompt starts with the command, the notice says whether compaction ran or failed, and on the page Compact Conversation posts exactly that prompt as a new turn (not a regenerate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/chat/chat.md explains what happens when an edit's reply never starts (the next message still cuts the session back, with unanswered messages carried as text), the `sdkCutPending` key, slash commands sent while a cut is pending, and the notice after a restore that left linked files alone. The #24 section of the re-audit notes both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #22 (`@` file mentions and the `/` palette in ChatComposer), and with it the #14 right rail and #9 gateway that #22 had already merged. Conflicts: - README.md: kept #24's edit/regenerate/restore/compact paragraph and #22's model-picker and composer-shortcut paragraphs, in that order; added docs/chat/chat.md and docs/llm/llm.md to the domain-doc list. - docs/chat/spec.md: kept #24's "Editing, regenerating and restoring files" section and invariants beside #22's `@`/palette invariants. Semantic fixes in the merge: - The palette's `/compact` calls compactContext, which #24 switched to the CLI's own `/compact`. tests/chat.composer-mentions.spec.ts still expected the old "Please compact this conversation" prompt; it now expects compact-command's prompt. - docs/chat/spec.md's `/compact` row, the chat-page-commands.ts comment and the #22 status in parity-reaudit said `/compact` asks for a summary / routing it to the SDK was still open; they now say it runs the SDK's `/compact`. docs/chat/chat.md names the Compact button's new home (the context ring in the chat header, #14) and `/compact`. Checked, no change needed: stream/+server.ts refuses an unrunnable model (#9) before planTurn (#24), so an edit's pending cut survives a refused send; gateway turns carry sdkTailUuid and resumeSessionAt like subscription turns; `/model` goes through maybeCompactBeforeModelSwitch with #9's engine context limits and #24's `/compact`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Fallback #24 wraps the route's `runEngineStream(` call in `runWithResumeFallback`, so the source check for `await runEngineStream(` found nothing and compared against -1. Look for the call itself, and assert both markers exist so a missing one cannot pass as "before". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Picked up the interrupted attempt (substantial uncommitted work), verified and hardened it, then finished tests, docs and commits. Edit and Regenerate no longer send the literal prompt 'regenerate' into the unedited SDK session. Every turn is now one streamed SDKUserMessage with a uuid we mint, recorded as the join on messages.metadata (sdkTurn on the user row, sdkTailUuid on the reply), so no migration was needed. A regenerate, including after an edit, sends the row's own text and attachments and resumes with resumeSessionAt at the previous reply's tail. That keeps the same session id and exactly the kept history. A cut the CLI refuses (for example one from before a compaction) is recognised before any model call and retried once on a fresh session primed with the kept history. File checkpointing is on for stable workspaces (a project or an agent persistentKey). A short-lived control session (resume, the run's cwd, enableFileCheckpointing, an input that never yields, no model call) runs rewindFiles dryRun for the preview and the real call on confirm. The restore dialog defaults on, lists the files with +/- totals, flags uncommitted files, needs an explicit overwrite for imported repos with uncommitted changes, and is hidden when there is nothing to restore. The server re-checks every guard, and a failed restore leaves the conversation unchanged. 'Compact Conversation' and the compaction before a model switch now send the SDK's /compact, and the switch notice is honest. The SDK behaviour was verified by running the bundled CLI against a stub Messages API: our uuid becomes the transcript uuid and the checkpoint key; the Write tool is checkpointed; resumeSessionAt drops the later turn and keeps the session id; /compact works as a plain-text prompt; a pre-compaction cut is refused with 'No message found'. The results are written up in docs/chat/chat.md.
After review: This round fixed all three should_fix items from the review. The branch still carries the earlier #24 work: file checkpointing, the rewind control session, and edit/regenerate cutting back the SDK session with resumeSessionAt. (1) Pending cut: editUserMessage and truncateAfterMessage now stamp the pivot row with metadata.sdkCutPending, in the same transaction as the cut. planTurn treats any turn in a conversation with a pending cut like a regenerate: it resumes at the previous reply's sdkTailUuid, keeps the same fresh fallback, and puts unanswered user rows after that reply in front of the text as a short preamble. Slash commands get no preamble, so /compact still works as a command. recordTurnJoin clears the stamp up to its row once the turn's session starts. So an edit whose stream POST fails, followed by a new message, no longer reaches the unedited session. (2) Finding 80 is now pinned. The /compact prompt and the model-switch notice moved into src/lib/chat/compact-command.ts, and the new tests/chat.compact.spec.ts pins the wording and a page-level click on Compact Conversation, asserting the POST content is exactly the /compact command with regenerate=false. (3) skippedLinks is passed through. applyMessageRewind reports filesRestored as the previewed files minus the skipped ones. BranchResult carries skippedLinks, and the page (one reportFileRestore line per handler) shows a dismissable notice rendered by RewindPreviewDialog, for example 'Restored 3 of 5 files. 2 were not restored: they are links...'. No migration was added. bun run check reports 0 errors and bun run build passes.
Closes #24
What was addressed
Audit findings (from the 2026-09-22 subsystem audit) were re-checked against the code before any change; "not a bug" means the claim did not hold up.
Verification
bun run check: 0 errorsbun run build: passesDocs updated
Follow-ups / residual risk
🤖 Generated with Claude Code