Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e968c5f
feat(contracts): add sessionForkOnResume + maxInputCharactersPerTurn …
chuks-qua May 21, 2026
d8d5500
feat(providers): declare session-fork + input-char capabilities
chuks-qua May 21, 2026
fc338b9
fix(providers): include missed test fixtures + barrel-export SessionF…
chuks-qua May 21, 2026
f1f5b3e
feat(shared): add handoff paths resolver with ULID utility
chuks-qua May 21, 2026
429c91c
feat(db): add messages.isInternal flag + filter from user-facing queries
chuks-qua May 21, 2026
abe54d6
fix(message-repo): filter is_internal on remaining user-facing lookups
chuks-qua May 21, 2026
08b02ba
feat(handoff): add handoff pipeline shared types
chuks-qua May 21, 2026
6c50629
feat(handoff): add provider error classifier with tests
chuks-qua May 21, 2026
4169d3e
feat(handoff): vendored /handoff prompt with mode + budget tailoring
chuks-qua May 21, 2026
3e84c78
feat(handoff): add HandoffStorage with ULID dirs + attachment copy + DI
chuks-qua May 21, 2026
ffc04b6
feat(contracts): add provider side-channel handoff methods
chuks-qua May 21, 2026
607d963
feat(claude-provider): implement runSideChannelQuery for path B
chuks-qua May 21, 2026
e4ce726
feat(cursor-provider): implement runHiddenTurn for path A with disreg…
chuks-qua May 21, 2026
9e73c37
feat(handoff): add path-D deterministic adapter wrapping handoff-builder
chuks-qua May 21, 2026
dac05f2
feat(handoff): add HandoffPipelineService orchestrating B->A->D ladder
chuks-qua May 21, 2026
6bb4392
feat(agent-service): use HandoffPipelineService in createBranchedThread
chuks-qua May 22, 2026
2c955ac
feat(handoff): wipe thread handoff files on hard delete
chuks-qua May 22, 2026
a07517f
feat(handoff): add thread.handoff push channel + emit from AgentService
chuks-qua May 22, 2026
9806612
feat(web): subscribe threadStore to thread.handoff push
chuks-qua May 22, 2026
87703e3
feat(composer): rename branch to fork in UI copy and swap to GitFork …
chuks-qua May 22, 2026
7f50242
feat(composer): per-thread fork-mode state and pre-fill textarea on u…
chuks-qua May 22, 2026
b9e7040
feat(child-thread): skeleton placeholder while handoff is generating
chuks-qua May 22, 2026
2f5a7c7
fix(composer): remove em-dashes from comments
chuks-qua May 22, 2026
0a5262f
fix(composer): validate slash commands against in-flight provider at …
chuks-qua May 22, 2026
d758e27
feat(settings): add chat.handoff.notifyOnLocalFallback toggle
chuks-qua May 22, 2026
91a2ec1
feat(web): add HandoffFallbackBanner for local-fallback handoff detec…
chuks-qua May 22, 2026
d2d13c0
feat(handoff): add handoff.regenerate RPC stub for v1
chuks-qua May 22, 2026
46c9534
feat(handoff): guard pipeline on parent in-flight + missing session
chuks-qua May 22, 2026
26d996c
feat(handoff): post-write budget validation + section-boundary trunca…
chuks-qua May 22, 2026
7106f1d
feat(handoff): cap attachment copy at 25MB with reference-only fallback
chuks-qua May 22, 2026
45932e9
feat(handoff): drop artifact when child thread vanishes mid-generation
chuks-qua May 22, 2026
3a9f5af
test(handoff): end-to-end branch flow integration test
chuks-qua May 22, 2026
764e7cf
docs(handoff): document chat fork handoff pipeline
chuks-qua May 22, 2026
7185de2
fix(handoff): guard composer queued-send + Cursor turn collision; add…
chuks-qua May 22, 2026
fe72af6
fix(desktop): hoist electronProcess decl above await to avoid TDZ on …
chuks-qua May 22, 2026
bb70795
fix(handoff): make HandoffStorage DI-resolvable by removing Function-…
chuks-qua May 22, 2026
7f1f63e
feat(ui): rename remaining 'Branched/Branching' copy + test filenames…
chuks-qua May 22, 2026
7bed827
feat(handoff): trigger-aware banner copy + View doc affordance
chuks-qua May 22, 2026
f3ffcc0
fix(handoff): legacy catch path persists artifact to disk so View doc…
chuks-qua May 22, 2026
bc038eb
fix(handoff): visually de-emphasize fallback banner relative to Inter…
chuks-qua May 22, 2026
08431f1
fix(claude-provider): classify post-restart session-not-found as tran…
chuks-qua May 22, 2026
701ef29
fix(handoff): orchestrator was calling non-existent providerRegistry.…
chuks-qua May 22, 2026
324e079
fix(handoff): dismissible fallback banner + wider doc dialog with met…
chuks-qua May 22, 2026
daac13c
fix(handoff): describeError logging + classifier covers SDK wrapper e…
chuks-qua May 22, 2026
b2dc1ee
fix(claude-provider): forward abort signal via event listener (AbortC…
chuks-qua May 22, 2026
58c7824
feat(claude-provider): session-less side-channel fallback when parent…
chuks-qua May 22, 2026
8c50b26
fix(handoff): run side-channel handoff in parent thread's worktree, n…
chuks-qua May 22, 2026
605978f
fix(handoff): stop asking model to write the file via a tool it doesn…
chuks-qua May 22, 2026
602b5b8
feat(handoff): content-preserving prompt + anchor-role framing + temp…
chuks-qua May 22, 2026
c5b1a5e
feat(handoff): quote /handoff skill verbatim; stop forcing rigid sect…
chuks-qua May 22, 2026
45a4794
fix(cursor-provider): drop path A settle timeout 30s -> 10s for bette…
chuks-qua May 22, 2026
98691b9
docs(context): add domain glossary for chat fork handoff vocabulary
chuks-qua May 22, 2026
4eb46b6
docs(handoff): cursor SDK migration handoff for path B unlock
chuks-qua May 22, 2026
25a4d05
Merge remote-tracking branch 'origin/main' into feat/chat-branch-enha…
chuks-qua May 22, 2026
a9ea6cb
fix(tests): wire HandoffStorage mock into cleanup tests + defensive s…
chuks-qua May 22, 2026
17cce50
fix: address CodeRabbit review feedback
chuks-qua May 22, 2026
84a21fc
docs(plans): commit chat-fork-handoff plan + prototype for future-age…
chuks-qua May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ Integrated terminals and provider subprocesses use **`EnvService`** plus **`Prot
- **shadcn/ui docs:** https://ui.shadcn.com/
- **Tailwind CSS 4:** https://tailwindcss.com/docs
- **Codex provider docs:** `apps/server/src/providers/codex/` - uses `codex app-server` JSON-RPC 2.0 protocol (see ARCHITECTURE.md)
- **Chat fork handoff:** [docs/guides/chat-fork-handoff.md](docs/guides/chat-fork-handoff.md)

## Performance Requirements

Expand Down
156 changes: 156 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Context: Mcode Glossary

Domain terms used across this repo, resolved during design conversations.
This file is a glossary only. No implementation details, no architecture, no
specs. For those see `ARCHITECTURE.md`, `docs/plans/`, and
`docs/guides/`.

## Chat threading

### Thread
A single chat conversation between a user and an AI agent. Threads belong to
workspaces and run against a provider. Distinct from a git branch even though
threads can be associated with a worktree.

### Fork (verb), forked thread (noun)
The act of branching a conversation from a specific message in a parent
thread, creating a new child thread that picks up from that anchor point.
Renamed from "branch" in UI copy to disambiguate from git branches. The
schema field is already named `forked_from_message_id`.

### Parent thread / child thread
The pre-existing thread a fork is created from is the **parent**. The newly
created thread is the **child**. The parent is unaffected by the fork; the
child inherits the parent's context via a handoff.

### Fork anchor
The specific message in the parent thread that a fork is created from. Has a
**role**, either `user` (forking from your own message; intent is "retry")
or `assistant` (forking from the agent's reply; intent is "follow up about
what was just said"). The role shapes how the handoff is framed.

## Handoff

### Handoff
A markdown document summarizing the parent thread, written to disk and
inlined into the child thread's first provider turn so the new agent picks
up with the parent's context. The handoff replaces what would otherwise be a
verbose transcript replay.

### Handoff pipeline
The orchestration layer that produces a handoff for a given fork. Routes
through one of three paths (B / A / D) based on the parent provider's
capability and live availability.

### Side-channel
A provider call made out-of-band from the user-visible conversation, used to
generate a handoff. Does not appear in the parent thread's UI. Distinct from
a "hidden turn". Side-channels typically use a separate SDK process.

### Hidden turn
A message persisted with `is_internal: 1` so it doesn't render in the UI but
is still present in the provider's session state. Used in path A to inject
the handoff request into the parent's session without polluting the user's
view of the conversation.

### Disregard turn
A second hidden turn following a hidden handoff request, instructing the
model to ignore the previous request and continue normally. Specific to
path A's session-mutation cleanup.

## The B/A/D ladder

### Path B (clean side-channel)
Resumes the parent provider's session in a forked SDK process to generate
the handoff. The original session is untouched. Used when the provider
declares `sessionForkOnResume: "clean"` (Claude).

### Path B-prime (sessionless side-channel)
Variant of path B that runs without `resume:` when the parent's session
isn't available (e.g. after a server restart). Provides the conversation
history as text in the prompt instead. Same artifact ladder step ("B") from
the caller's perspective.

### Path A (mutating resume)
Injects hidden turns directly into the parent thread's session. Used when
the provider declares `sessionForkOnResume: "mutating"` (Cursor today).
Cleaned up with a disregard turn.

### Path D (deterministic)
Local builder that produces the handoff from message rows without invoking
any provider. Used as the universal fallback. Lowest fidelity but always
available.

### Ladder step
A label on a produced handoff artifact (`"B" | "A" | "D"`) identifying which
path generated it. Stored in `handoff.json` for diagnostics and the
fallback banner copy.

## Modes and budgets

### Full mode
The default handoff structure produced by path B / A when the child
provider's per-turn input window is large enough (≥ 8000 characters).
Includes the model's choice of sections. The prompt does not enforce a
fixed list.

### Minimal mode
Compact handoff structure triggered when the child provider's
`maxInputCharactersPerTurn` is below 8000 (Cursor's 4000-char floor today).
Drops ancillary sections to keep the inlined doc within budget.

### Character budget
The size constraint applied to the inlined portion of a handoff. Sourced
from the child provider's declared `maxInputCharactersPerTurn`, minus
reserved overhead for system prompt + the user's first message. Used as
characters, not tokens. Tokens vary per model and are not portable.

### Overflow
The portion of a handoff that exceeds the inline budget. Written to the
user's OS temp directory at
`os.tmpdir()/mcode-handoff-overflow-<threadId>-<ts>.md` with a pointer
embedded in the inlined version. The next agent can `Read` it on demand.

## Provider capabilities

### Session fork behavior
Declared per provider as `"clean" | "mutating" | "unsupported"`. Determines
which ladder path the orchestrator dispatches to:

- **clean**: `resume:` spawns a fork without mutating the original (Claude)
- **mutating**: `resume:` mutates the session forward (Cursor today)
- **unsupported**: provider can't fork sessions; pipeline goes directly to
path D (Codex, Copilot today)

### Per-turn input cap
The maximum input characters a provider accepts per turn. Declared as
`maxInputCharactersPerTurn`. Drives both the handoff budget and the
mode (full vs minimal) selection.

## Internal / hidden state

### Internal message
A message persisted with `is_internal: 1`. Excluded from the user-visible
timeline and from queries by default. Used for hidden turns and for the
synthetic system message anchoring a handoff at sequence 1 in a child
thread.

### Provenance metadata
The `handoff.json` sidecar accompanying every `handoff.md`. Records which
path produced the doc, when, against which provider, with which classified
error (if any). Used by the View doc dialog and by the fallback banner copy.

## Related but distinct

### Cross-provider switch (deferred)
Swapping a thread's provider mid-conversation. Uses the same handoff
primitive as a fork but with the implicit anchor being the thread's last
message, and the same thread continues with the new provider. Not yet
implemented; tracked in `docs/plans/2026-05-21-chat-branch-handoff-pipeline.md`.

### Compaction
A separate mechanism that summarizes a thread's older turns into a single
compact summary stored on the thread itself, used to keep long threads
within their own provider's context window. Distinct from a fork handoff,
though the orchestrator does consult `last_compact_summary` when building a
deterministic path-D handoff.
8 changes: 6 additions & 2 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ function startViteDevServer() {
});
}

// Declared here (rather than below the await Promise.all) so the vite-exit
// handler in startViteDevServer can safely reference it during startup. If
// vite fails fast (e.g. binary missing), the handler fires while the await
// is still pending, so a `let` declaration after the await would be in TDZ.
let electronProcess = null;

// Run Vite startup and esbuild (main/preload) watch in parallel
const [devServerUrl, watchContexts] = await Promise.all([
startViteDevServer(),
Expand Down Expand Up @@ -216,8 +222,6 @@ console.log(`[dev] Web dev server is ready at ${devServerUrl}`);
// Step 2: Spawn Electron
// -------------------------------------------------------------------------

let electronProcess = null;

/** Spawn (or restart) the Electron process. */
function spawnElectron() {
if (electronProcess) {
Expand Down
1 change: 1 addition & 0 deletions apps/server/drizzle/0008_brave_big_bertha.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `messages` ADD `is_internal` integer DEFAULT 0 NOT NULL;
Loading
Loading