Skip to content

feat(server): fork threads and side chats - #9436

Closed
amanthanvi wants to merge 27 commits into
pingdotgg:mainfrom
amanthanvi:t3code/side-chats-forking-server
Closed

amanthanvi wants to merge 27 commits into
pingdotgg:mainfrom
amanthanvi:t3code/side-chats-forking-server

Conversation

@amanthanvi

@amanthanvi amanthanvi commented Sep 3, 2026 •

Copy link
Copy Markdown

This is the first half of a two-PR split of #9396. It holds the contracts, server, and shared client-state changes; the web and mobile surfaces are in #9437, which depends on this one.

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

ThreadForkOrigin records the source thread, turn, message, and forked-at time. The thread and thread shell gain optional fork and sideChat fields. A new thread.fork client command decides to thread.created, so there is no new event type. thread.meta.update takes an optional sideChat for promotion. Provider session start takes forkFrom, which is mutually exclusive with resumeCursor. ServerProvider.sessionFork comes from each adapter's declared capability and is one of any-turn, latest-turn, or unsupported. Omitting it means unsupported. The keybinding command ids are chat.sideChat (mod+shift+b) and chat.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 adds fork_json and side_chat to projection_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 passes forkFrom and 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/fork and the source turn as lastTurnId, never falling back to a resume. Claude resumes the source session with forkSession and a fresh session id. OpenCode uses session.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 run across the 34 test files this branch touches: 939 tests passed
  • One failure in AgentSessionScanner.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 /var to /private/var
  • contracts, client-runtime, and server typechecks pass; vp fmt and vp lint clean on changed files
  • The fork projection test pins each lineage field with json_extract rather than asserting the column is non-null, so a projector that wrote the wrong shape would fail it
  • Fork titles skip deleted siblings, so a deleted "Source (1)" no longer reserves that suffix. Reverting the filter fails the test that covers it

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Screenshots are not applicable because there are no UI changes
  • Video is not applicable because there are no animation or interaction changes

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.fork command and side-chat threads across server, provider, and client layers

  • Adds ThreadForkCommand and ThreadForkOrigin schemas to orchestration.ts, plus forkFrom on ProviderSessionStartInput and sessionFork capability metadata (any-turn, latest-turn, unsupported) on server-provider snapshots.
  • The decider (decider.ts), normalizer (Normalizer.ts), and ProviderCommandReactor validate fork sources (existence, completion, message match, capability/turn boundary), resolve the source provider instance, and emit thread.created events with inherited lineage.
  • Codex (any-turn), Claude (latest-turn), and OpenCode (latest-turn) adapters gain native session forking; Cursor, Grok, and Antigravity report unsupported. All provider drivers now stamp adapter capabilities into provider identity snapshots.
  • Migration 048 (048_ProjectionThreadForks.ts) adds fork_json and side_chat columns to projection_threads; projection handlers and snapshot queries persist and decode fork lineage and side-chat state.
  • Client runtime (threadShell.ts) hides side chats from visible thread lists while their parent exists, groups them by parent, and promotes orphaned side chats to visible threads when the parent is deleted or archived. Adds forkThread command builder and chat.sideChat keybinding default (mod+shift+b).
  • Risk: ProviderService.startSession now rejects fork requests with mismatched source instances, unsupported providers, missing resume state, or a combined resumeCursor + forkFrom input. 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.fork in the decider and normalizer (source-turn validation, latest-turn provider rules, inherited metadata), projection persistence of fork lineage and sideChat, 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: ProviderCommandReactor passes forkFrom (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 helpers getThreadTurnState and getForkSourceHead support validation and those races.

Tests and harness mocks are updated across orchestration, checkpoints, and keybindings (default chat.sideChat → mod+shift+b; chat.forkThread unbound).

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

    • Added thread forking from completed turns, with automatic titles and preserved source lineage.
    • Added side chats that appear alongside their parent thread and become visible if the parent is unavailable.
    • Added provider-aware session forking, including latest-turn and any-turn workflows where supported.
    • Added keyboard shortcuts for side chats (mod+shift+b) and thread forking (mod+alt+f).
    • Added clearer reasons for disabled context-menu actions.
  • Documentation

    • Added glossary definitions for forks and side chats.

Closes discussions

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants