Conversation
Issue backnotprop#1519: Ask AI always starts a fresh provider session, even though the agent session that produced the plan/diff/document usually has the exploration and reasoning that led to it. A prior attempt (reverted) had the browser hand back a ParentSession on /api/ai/session, but no client ever built one and the plumbing to get there touched 29 files across the hook CLI, both editors, and five OpenCode plugin layers. The browser never needs the session id, cwd, or harness — the server already knows all three at launch. So /api/ai/session now takes a `forkOrigin: true` boolean instead, and the endpoint merges deps.originSession (server-side only) into the request's AIContext before the existing fork-or-fresh check runs. /api/ai/capabilities advertises which providers can fork it as `originFork: { agent, providerIds }`. originForkProviderIds matches registry entries against the origin's provider types by id OR name (`matchesAgentProvider`, packages/core/agents.ts) rather than id alone: a registry instance id can be custom (registered with its own instanceId), so id-only matching silently drops a provider registered that way. This is the same rule `findOriginAIProvider` already used client-side (packages/ui/utils/aiProvider.ts) to pick a default provider for an origin — extracting it into one shared predicate keeps the two in sync instead of re-implementing the match with different vocabulary on each side. ParentSession.agent is now typed as Origin (packages/core/agents.ts) instead of a bare string, removing the `as Origin` casts the reverted version needed. Pi vendors ai-context.ts/endpoints.ts verbatim into apps/pi-extension/generated, so vendor.sh also vendors agents.ts alongside them and rewrites the new `@plannotator/core/agents` import the same way it already does for ai-context.
createAIRuntime() and the three server bootstraps (plan, annotate, review) now
accept an optional originSession, forwarded straight into createAIEndpoints'
deps. It rides no bootstrap payload (/api/plan, /api/diff are untouched) —
only the AI runtime sees it, so /api/ai/session and /api/ai/capabilities can
act on it server-side per the previous commit.
The {sessionId, cwd, agent} ParentSession object and the `originSession` doc
comment both get one home instead of being restated per call site:
origin-session.ts's `buildOriginSession` is the single builder apps/hook and
apps/opencode-plugin will share (next two commits), and `OriginSessionOption`
is the one place `ServerOptions`/`AnnotateServerOptions`/`ReviewServerOptions`
document the field, via `extends` instead of copying the same four-line
comment three times. The package.json `exports` map gains an `./origin-session`
subpath so a Node-targeted bundle (apps/opencode-plugin's CLI entry points)
can import the builder without pulling in the package root's Bun-only modules.
Wires originSession into the seven hook-CLI launch sites that can genuinely name their invoking session: the direct `review`/`annotate` CLI (ancestor-PID transcript resolution), `annotate-last` (the transcript the rendered message was actually read from), the three OpenCode JSON-stdin bridge subcommands (opencode-plan/opencode-review/opencode-annotate-last, which already carry a sessionId/directory pair), and the ExitPlanMode hook-event branch (the event names its session id directly). Two small helpers replace what would otherwise be seven ad-hoc reconstructions: resolveInvokingClaudeSession() for the ancestor-PID cases, and toOpenCodeOriginSession() for the three bridge subcommands. Both delegate to the shared builder (buildOriginSession, packages/server/origin-session.ts) — apps/hook is Bun-only, so it can depend on @plannotator/server directly, the same builder apps/opencode-plugin's own toOriginSession wraps (next commit). Both return null outside their one supported harness — Codex's CODEX_THREAD_ID is deliberately never captured (no provider can fork a Codex thread), and neither Gemini nor any other origin is guessed at, since only claude-code and opencode ever appear in AIEndpointDeps.originSession.agent. resolveInvokingClaudeSession() resolves cwd via the PLANNOTATOR_CWD idiom used everywhere else in this file (the original working directory before a launcher shim `cd`s), not a bare process.cwd() — claude-agent-sdk.ts forks with `cwd: parent.cwd`, so a wrong cwd here would root the forked session in the wrong directory. The direct `annotate` CLI also gains a `--session-id <id>` value flag, following the same indexOf/splice idiom as the existing `--browser <name>` flag. It closes the one named surface from backnotprop#1519 ("plan review via hook, /plannotator-last, /plannotator-annotate") that the CLI-bridge fallback leg of /plannotator-annotate couldn't cover: that leg shells out to the plain `annotate` subcommand (no stdin-JSON channel the way opencode-plan/ opencode-annotate-last have), so this flag is the only way it can name the invoking OpenCode session. buildAnnotateCliArgs (apps/opencode-plugin) emits it next commit.
… commands
submit_plan (V1 and V2), the native /plannotator-review, /plannotator-annotate,
and /plannotator-last command handlers, and the embedded/CLI-bridge plan-review
runners all now carry the OpenCode session id (and cwd) one hop further, into
originSession on the server bootstrap call.
toOriginSession() wraps the shared builder (buildOriginSession,
packages/server/origin-session.ts) that apps/hook's own helpers use — the one
place the {sessionId, cwd, agent} object is actually constructed, replacing
what was otherwise the same construction repeated across the CLI bridge, the
native-command handlers, and the embedded runtime. It imports the
`@plannotator/server/origin-session` subpath rather than the package root:
index.ts and server.ts are bundled with `--target node` (they run under
Node.js-hosted OpenCode, not Bun), and the package root barrel pulls in
Bun-only modules (browser.ts, repo.ts, project.ts, integrations.ts) that a
Node-targeted bundle can't import — the origin-session submodule itself has
no such dependency.
sessionId and cwd previously traveled as separate parameters through
runPlanReview, runCliPlanReview and runEmbeddedPlanReview, joined into a
ParentSession only at the leaf (inside embedded.ts, and again — across the
process boundary — inside the hook CLI). runPlanReview (both the V1 and V2
copies) now joins them once, itself, into `originSession`, and threads that
one value down; runCliPlanReview still takes `cwd` separately since the CLI
bridge also needs it to spawn the child process, but the JSON payload's
sessionId/directory fields now read from originSession rather than raw
fields, so the two paths can't drift.
/plannotator-annotate's CLI-bridge fallback leg (used only when the embedded
runtime is unavailable) previously had no way to name the invoking session —
it shells out to the plain `annotate` CLI subcommand, and unlike
opencode-plan/opencode-annotate-last it has no stdin-JSON channel. The
existing value-flag parsing (--result-file <path>, the global --browser
<name>) shows this is a solved problem for the annotate CLI, so
buildAnnotateCliArgs now emits the --session-id <id> flag the previous commit
added, closing the one named backnotprop#1519 surface (plan review, /plannotator-last,
/plannotator-annotate) that OpenCode v2's always-CLI-bridge routing couldn't
reach.
The toggle is shown only when /api/ai/capabilities reports an originFork and
the *effective* AI provider — the resolved selection if it names a real,
currently known provider, else the server's own defaultProvider, never
providers[0] array order — is in its providerIds. useOriginFork() (packages/ui)
owns that gate once; posting `forkOrigin: true` on /api/ai/session is the only
new thing useAIChat does.
Because useAIChat's session depends on useOriginFork's `forkOrigin`, the app
can't compose a reset inside the gate hook itself (the same ordering problem
useAIProviderConfig already documents for provider switches) — so the emitted
`forkOrigin` is *derived* from the gate instead of held as independent state.
Switching to a non-forking provider clears it for free, with no explicit
reset and no stale "still forking" flag left armed. `toggleProps` is memoized
so its identity is stable across renders that don't change the gate inputs,
and `onToggle` hands the toggle's own `setWantsFork` straight through instead
of behind a needless wrapper — without that, a caller's `useCallback` around
`toggleProps.onToggle` (composing the reset) recomputes on every render
regardless of whether anything the toggle cares about changed.
OriginForkToggle (packages/ui/components/ai) is the one checkbox row, used by
both DocumentAIChatPanel and the review AIConfigBar/AITab/ReviewSidebar chain,
replacing what would otherwise be the same prop shape declared four times.
packages/editor/App.tsx and packages/review-editor/App.tsx each wire it with
the same few lines: a useOriginFork() call ahead of useAIChat, and a
handleForkOriginToggle that composes the toggle with resetSession — the editor
additionally withholds availability while the agent-terminal surface is
active, since forking into a provider chat isn't meaningful for the origin
agent's own TUI.
useAIChat.forkOrigin.test.tsx and useOriginFork.test.tsx share one DOM mount
harness (hookTestHarness.tsx) instead of each defining their own copy, and
the former keeps only the one test that asserts real behavior — that
forkOrigin: true actually reaches the wire body and no client-built
ParentSession rides with it — dropping two tests that only asserted key
absence on the `...(forkOrigin && { forkOrigin: true })` spread.
ruaridhw
force-pushed
the
ask-ai-fork-origin-session
branch
from
September 14, 2026 11:37
a6e1c9b to
b0810ab
Compare
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.
Closes #1519
Summary
The fork machinery merged in #363 was never reachable: no client ever populated
AIContext.parent, soshouldForkin/api/ai/sessionwas always false. This PR wires the dormant path end to end and exposes it as an opt-in toggle in the Ask AI panel — off by default, since a fresh session is often what you want.When a review/annotate surface was invoked from an agent session and the selected provider can fork that harness, the Ask AI panel shows a "Fork the <agent> session" checkbox. Toggling it on makes the next question fork the invoking session (full conversation history — "why did you choose X?", "what else did you consider?") instead of starting fresh with just the document snapshot.
How it works
The origin session never crosses the wire. It is resolved server-side at launch and merged into
context.parentinside/api/ai/session; the browser's only say in the matter is a single boolean.buildOriginSession(packages/server/origin-session.ts) is the one place that identity becomes aParentSession:session_id+cwdstraight off the hook payload/plannotator-last(Claude): session id from the resolved transcript pathplannotator review/annotate(Claude): ancestor-PID session-log walk, rooted atPLANNOTATOR_CWD || process.cwd()(no-op for a human running the CLI in a terminal)submit_plan,/plannotator-review,/plannotator-annotate,/plannotator-last:sessionIDthreaded through the embedded runtime, the CLI bridge, and v2's native commandscreateAIRuntime({ originSession })carries it intoAIEndpointDeps. It is never echoed into/api/planor/api/diff; those payloads are untouched./api/ai/capabilities(already fetched on load) gainsoriginFork: { agent, providerIds } | null.providerIdsis computed server-side: providers that declarecapabilities.forkand natively own the origin harness, matched on registry id or type name, so a custom instance id can't silently miss.forkOrigin: trueon/api/ai/session. The endpoint merges its ownoriginSessionintocontext.parent. The toggle's armed state is derived (wantsFork && available), so switching to a non-forking provider disarms it rather than silently answering fresh.Deliberate scope cuts
fork: false. No Codex thread id is captured and the Pi servers are untouched — recording an identity nothing can fork is dead weight.forkedresponse flag. IfforkSessionthrows, the pre-existing error path applies. Degradation reporting is a separate concern from [Feature Request] Ask AI: optionally fork the invoking agent session so the sidebar inherits its conversation history #1519 and would have needed its own UI in four components.context.parentstill passes through toshouldFork, exactly as it did before this PR. That passthrough predates [Feature Request] Ask AI: optionally fork the invoking agent session so the sidebar inherits its conversation history #1519 (feat(ai): AI backbone + inline chat for code review #363); tightening it is out of scope here.Test plan
bunx tsc --noEmitclean across all nine projects in thetypecheckscriptpackages/ai+packages/core— 302 pass, incl. new endpoint tests: forks whenforkOriginis set, doesn't without it, doesn't when the resolved provider can't fork,originForkreports the right ids (and matches a provider registered under a custom instance id),nullwith no origin sessionapps/opencode-plugin— 179 pass, incl.--session-idemission from the CLI bridgeapps/hook— 248 passpackages/review-editor— 549 passpackages/ui— 1407 pass (one unrelated pre-existing failure,Viewer consumer props > readOnly fenced code…, identical onmain)bun run build+ the pi-extension vendor steppackages/serversuite green per-file;agent-terminal*/ai-disabledpanic on node-pty under bun 1.4.2 locally, identically on unmodified upstreamNot manually verified
A real fork against a live Claude Code / OpenCode session (needs an interactive agent session plus a browser). The provider fork paths are unchanged from #363; what's new is that
context.parentfinally reaches them.