feat(server): fork threads and side chats - #9436
Closed
amanthanvi wants to merge 27 commits into
Closed
amanthanvi wants to merge 27 commits into
amanthanvi wants to merge 27 commits into
Conversation
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 changed
Forks and side chats are the same mechanism. A fork is a new thread that carries lineage back to the response it came from. A side chat is that same fork with a flag that keeps it out of the main lists while its parent exists.
Contracts
ThreadForkOriginrecords the source thread, turn, message, and forked-at time. The thread and thread shell gain optionalforkandsideChatfields. A newthread.forkclient command decides tothread.created, so there is no new event type.thread.meta.updatetakes an optionalsideChatfor promotion. Provider session start takesforkFrom, which is mutually exclusive withresumeCursor.ServerProvider.sessionForkcomes from each adapter's declared capability and is one ofany-turn,latest-turn, orunsupported. Omitting it means unsupported. The keybinding command ids arechat.sideChat(mod+shift+b) andchat.forkThread(unbound).Server
The decider inherits project, model, modes, branch, and worktree from the source, rejects a mid-turn or deleted source, numbers fork titles from live siblings only, and only lets threads with fork lineage carry
sideChat. Migration 051 addsfork_jsonandside_chattoprojection_threads; every thread read path carries them. Thread search and the project's first-thread lookup skip side chats. Fork creation does no provider work; the first turn passesforkFromand the provider service resolves the source's cursor, prefers the fork's own cursor once it exists, and rejects unsupported drivers or cross-instance sources.Adapters
Codex forks natively with
thread/forkand the source turn aslastTurnId, never falling back to a resume. Claude resumes the source session withforkSessionand a fresh session id. OpenCode usessession.fork. Cursor, Grok, and Antigravity declare the capability unsupported. After the native fork, latest-turn providers re-check that the source is still at the recorded turn, and stop the new session if it moved.Shared client state
Thread refs exclude side chats whose parent still exists, so every list on every client inherits the rule. By-id lookups still return them. An identity-stable atom groups side chats by parent. Worktree ownership checks read an unfiltered refs atom, so deleting a parent never offers to remove a worktree that a live side chat shares.
Docs
This PR adds glossary entries for fork and side chat.
Why
In a long thread, a user often wants to ask a side question without spending the main context. At a decision point, they may want to try a second direction without losing the first. Reusing the thread model keeps this small: no new event type, no transcript copying, and lineage that later work such as merge-back can read.
UI changes
None in this PR. The web and mobile surfaces are in the follow-up.
Verification
vp test runacross the 34 test files this branch touches: 939 tests passedAgentSessionScanner.test.ts("excludes sandboxes reached through a symlink into the worktrees dir") is pre-existing and unrelated: it fails the same way on a worktree that touches none of these files, because macOS resolves/varto/private/varvp fmtandvp lintclean on changed filesjson_extractrather than asserting the column is non-null, so a projector that wrote the wrong shape would fail itChecklist
Implemented by GPT-5.6 Sol via Codex and Claude Fable 5.1 via Claude Code, with independent designs and two rounds of adversarial review by Claude Opus 5 and GPT-5.6 Sol.
Note on the bot summaries below: both name the migration as 048, which was its number when they ran. It is 051 on the current branch, renumbered so it does not collide with the 050 that landed on main.
Note
Add
thread.forkcommand and side-chat threads across server, provider, and client layersThreadForkCommandandThreadForkOriginschemas to orchestration.ts, plusforkFromonProviderSessionStartInputandsessionForkcapability metadata (any-turn,latest-turn,unsupported) on server-provider snapshots.ProviderCommandReactorvalidate fork sources (existence, completion, message match, capability/turn boundary), resolve the source provider instance, and emitthread.createdevents with inherited lineage.fork_jsonandside_chatcolumns toprojection_threads; projection handlers and snapshot queries persist and decode fork lineage and side-chat state.forkThreadcommand builder andchat.sideChatkeybinding default (mod+shift+b).ProviderService.startSessionnow rejects fork requests with mismatched source instances, unsupported providers, missing resume state, or a combinedresumeCursor+forkFrominput. Provider drivers moved identity stamping after adapter construction, so any code reading capabilities from the stamped identity before adapter creation will see stale values.Macroscope summarized fb8a7b7.
Note
Medium Risk
Touches orchestration commands, projection read paths, and provider session lifecycle (fork boundaries and resume cursors); regressions could affect thread lists, search, or first-turn provider behavior.
Overview
Adds thread forks and side chats end-to-end on the server:
thread.forkin the decider and normalizer (source-turn validation, latest-turn provider rules, inherited metadata), projection persistence offorklineage andsideChat, and read models that surface those fields everywhere threads are loaded.Side chats stay out of normal project UX via a shared “top-level thread” rule in projection snapshot queries—active side chats whose parent still exists are omitted from thread search, first-thread selection, and similar list reads; orphaned or promoted side chats behave like ordinary threads.
Provider startup for forks is deferred until the first turn:
ProviderCommandReactorpassesforkFrom(or resumes an existing fork cursor), follows the source’s live provider instance/model, locks model changes until the inherited session starts, and for latest-turn drivers re-checks the source head (stopping and clearing stale sessions if the source advanced). New snapshot helpersgetThreadTurnStateandgetForkSourceHeadsupport validation and those races.Tests and harness mocks are updated across orchestration, checkpoints, and keybindings (default
chat.sideChat→mod+shift+b;chat.forkThreadunbound).Reviewed by Cursor Bugbot for commit dfb71ec. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
mod+shift+b) and thread forking (mod+alt+f).Documentation
Closes discussions