Add message editing with conversation branches - #45
Merged
milind-soni merged 2 commits intoAug 13, 2026
Conversation
Editing a user message forks the conversation, ChatGPT/Claude style: messages form a tree (parentId per message, activeLeafId per thread) and the visible chat is the path from root to the active leaf. Old branches stay reachable through a per-message version switcher. UI: a pencil icon on hover (or ArrowUp in an empty composer) turns the bubble into an inline editor — Enter resends, Esc cancels — and edited messages grow "< 2/2 >" controls to flip between versions. Server guarantees no two conversation tails ever generate at once: - an edit interrupts the in-flight turn and waits for it to settle (outlasting every driver's own cancel window) before forking - straggler events from a dead turn are dropped at the bus (turnId fence plus a busy guard), so they can never fold into the new branch - edits serialize per bot; branch switching is refused while a turn runs After a rewind the provider's native session still holds the abandoned branch, so resume cursors are invalidated and the surviving path is replayed inline for cursor-resuming drivers (transcript-replay drivers get it through the transcript as before). Legacy flat transcript files migrate on load: rows chain in array order and the last message becomes the active leaf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01634YWxEfsreuTSoDLCSy7b
Editing a live thread tried to interrupt, wait, force-settle, fence the dead turn's stragglers, and drain for 200ms before forking. That machinery had the failure it existed to prevent: an edit landing in the window before sendTurn (box provisioning can take ~90s) found nothing to interrupt, so the original turn started AFTER the fork and its reply landed on the new branch. Force-settling also only cleared the harness's busy flag while the driver still held the thread, so the edit's own turn was then rejected. Rewinding now requires an idle thread — the rule the version switcher already used, and the one the reference implementation uses (its revert action is disabled while the agent is working). The edit endpoint's checks through startTurn are synchronous, so two racing edits can't both start a turn, and liveTurns/staleTurns, waitForIdle, enqueueEdit, the force-settle and the drain window are all deleted (-100 lines). Also fixed: - the bus guard that dropped every event of an idle bot swallowed turn.completed, leaking the 4s screen poller into idle; deleted with the rest of the fencing. - `rewound` was cleared before dispatch, so a turn that failed early left the next message with no cursor AND no replay — total context loss. It now clears only once sendTurn resolves, and a rewound turn passes no resume cursor rather than relying on the wipe landing first. - sidebar preview and mascot state read bot.messages' tail, which is a hidden branch after a version switch; both now read the visible path. - the edit pencil is hidden while the bot works, matching the server rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
|
Looks great! Thanks for the contribution |
milind-soni
added a commit
to guilimasp/OpenMausBot
that referenced
this pull request
Aug 13, 2026
Both sides changed the composer's key handling: main added ArrowUp-on-empty to edit the last message (milind-soni#45), this branch added Shift+Enter for newlines. Kept both — ArrowUp first (it returns), then the Enter guard replacing the old unconditional send. The mention picker still intercepts Enter/Tab ahead of either, so tagging is unaffected.
milind-soni
added a commit
to guilimasp/OpenMausBot
that referenced
this pull request
Aug 13, 2026
Both sides changed the same Composer render: main passes onEditLast (milind-soni#45), this branch keys it by bot.id. Kept both — the key still remounts on switch so drafts don't follow you between conversations.
milind-soni
pushed a commit
that referenced
this pull request
Aug 13, 2026
Thanks @guilimasp — a draft now belongs to the conversation it was typed in. Merged main in; the composer keeps the onEditLast wiring from #45 alongside the per-bot key.
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.
What
Editing a user message forks the conversation, ChatGPT/Claude style. Hover a message for a pencil icon, or press ArrowUp in an empty composer to edit your last message. Enter resends, Esc cancels, and edited messages get
‹ 2/2 ›controls to flip between versions — each version keeps its own continuation, so nothing is lost.How
parentIdand each thread stores anactiveLeafId; the visible chat is the root→leaf path. Existing flatmessages-<threadId>.jsonfiles migrate transparently on load (rows chain in array order, last row becomes the leaf).POST /api/bots/:id/messages/:msgId/edit(fork + rerun the turn) andPOST /api/bots/:id/active-branch(switch versions, no new turn).GET /api/botsnow includesactiveLeafId.transcriptas before.No double generation, by construction
The classic failure mode of edit-in-place chat UIs is two conversation tails generating at once. This PR prevents it server-side:
Testing
server/branching.test.tse2e on the fake ACP CLI: real turn → edit → reply lands on the new branch → version switching; plus an edit against a hung turn proving the interrupt path never runs two turns at once.pnpm typecheck,pnpm test,pnpm buildall green; also smoke-tested in the browser against the dev server.🤖 Generated with Claude Code
https://claude.ai/code/session_01634YWxEfsreuTSoDLCSy7b