Repository navigation
feat(ai): Ask this session for OpenCode 2 (host-neutral pull bridge) - #1671
Merged
Merged
Conversation
…ridge Adds the "pull" transport for Ask this session: a host that runs the Plannotator server as a separate process (the OpenCode plugin's CLI child today, the Claude Code mod next) long-polls POST /api/ai/bridge/poll for questions and posts answer deltas, done, error, status and interrupt results to POST /api/ai/bridge/event. Token from the host's environment (scrubbed after reading), loopback Host only, no Origin, bounded 25s long-poll, re-send until acknowledged, gone after 30s of silence. Both runtimes serve it through createAIEndpoints; Bun takes the env config, Pi accepts the same config as an option. OpenCode 2: /plannotator-review, -annotate and -last answer as a real turn (session.prompt with our own message id, streamed from the event stream, session.wait + session.context fallback), with busy wait / interrupt and Stop mapped to session.interrupt on our own turn only. Plan review answers from context (session.generate, "Quick answer from this session") because submit_plan is a pending tool call; every bridge on that session reports blocked so nothing interrupts the review. OpenCode 1 is not covered (a second adapter over a different event model). No version bumps.
# Conflicts: # AGENTS.md
…at CLI start The --tailscale path called takeEnvPullSessionBridgeConfig to discard the host's config, but the take caches it, so createAIRuntime served it anyway and tailnet peers could type into the agent session. Add an explicit discard. Also take the config at the top of the CLI, before git/gh/sem, the auto-update wrapper, or an agent terminal (AI disabled, archive) can inherit the token.
…he last status report The server asks only when the host last reported ready, but that report can be a status tick old. If the user started a run in between, a steered question would land inside it, and Stop would then interrupt the user's run. Refuse with busy; the reviewer gets the usual wait / interrupt choice.
1 task
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.
Follow-up to #1668 (core + Pi). Ask AI in OpenCode 2 can now be answered by the OpenCode session that opened Plannotator, and the transport it uses is host-neutral so the Claude Code mod can reuse it.
What works
/plannotator-annotateopenai/gpt-5.5) and tests/plannotator-review/plannotator-lastagent_busy, then wait orsession.interruptsession.interrupton our turn only; the next question then runssession.generate, no tools, nothing written to the transcript)submit_plan, transcript unchanged, then approved normallyruntime: "cli"or a Node host)The pull bridge (host-neutral)
Server half:
packages/ai/session-bridge-pull.ts(vendored to Pi). Host half:packages/ai/session-bridge-pull-client.ts. The full contract is documented in CLAUDE.md under "Ask AI Provider Defaults".PLANNOTATOR_SESSION_BRIDGE_TOKEN(32 or more characters),PLANNOTATOR_SESSION_BRIDGE_HOSTand an optionalPLANNOTATOR_SESSION_BRIDGE_MODES(turn,transient).process.env, so agent jobs and terminals never inherit the token.--tailscale.POSTtohttp://127.0.0.1:<port>withAuthorization: Bearer <token>./api/ai/bridge/polltakes{ status?, modes?, waitMs? }. It long-polls for up to 25 s, answers early when there is work, and returns{ commands: [ask | cancel | interrupt], closing?, superseded? }./api/ai/bridge/eventtakes one event or{ events }. Events:started,delta,tool,done,error,status,interrupted.409 ask_not_active, which tells the host to stop it.403.Originheader:403.401.404. The plugin treats that as "older binary", so there is no version-skew breakage.readyuntil the host's first poll, so a question asked early waits for that poll. No first request within 30 s, or 30 s with no open poll after that, meansgone, and a running question then fails withsession_gone.cancel. On a decision or shutdown,detach()drops only unconfirmed questions, and a running turn is never stopped.createAIEndpoints. Bun lifts the idle timeout for the poll.OpenCode 2 adapter (
apps/opencode-plugin/opencode-session-bridge.ts)session.prompt({ id, text, delivery: "steer", metadata: { source: "plannotator-ask" } }). The id is generated in OpenCode's own ascendingmsg_format, so it sorts correctly in the transcript.session.inbox.deliveredfor our id, streams fromtext.delta, and falls back totext.endedfor blocks that arrive whole. Tool names come fromtool.input.started, andexecution.*ends the answer. Without an event stream (#44788) it usessession.waitplussession.context.session.waitprobe. The plugin domain has noactivecall.markPlanReviewPendingmakes every bridge on that session reportblockedwhilesubmit_planwaits. In that state, interrupt is refused and the review flows fall back to quick answers.Live check of the pending tool call (task item 3)
submit_planis pending. A prompt sent during the tool call stays in the inbox until the tool returns (verified live).session.generatedoes work during the pending call.@opencode/ai'snormalizeToolHistoryfills the unfinished call with an error result for every provider protocol (source:packages/ai/src/tool-history.ts).generatestill offers the model its tools. In a first live probe, a question without guidance came back empty because the model chose a tool call. So the transient question now carriesSESSION_ASK_TRANSIENT_NOTE("plain text only, tools unavailable…"). With it, 3 of 3 live answers came back as text. An empty answer is reported as a failure, never shown blank.Why OpenCode 1 is not included
It is not a small lift:
client.session.promptblocks for the whole turn, streaming comes frommessage.part.updatedthrough the plugineventhook, and status and abort have their own endpoints. It would be a second adapter, not a reuse of this one./plannotator-*insidecommand.execute.before, mostly on the embedded runtime. Whether that hook holds the session while the review is open needs its own live testing.The installed OpenCode 1.18.23 could do that testing, but it is a separate piece of work. The V1 bundle compiles the shared CLI code unchanged and never passes a bridge.
Risks
generateoffers the session's tools. A model that calls a tool anyway yields a "quick answers cannot run tools" failure the reviewer can retry. Quick answers also cannot be cancelled upstream; we only drop the result.session.waitprobing (about 1 s granularity), and answers arrive whole instead of streamed.Tests
packages/ai/session-bridge-pull.test.ts: a fake host over the real client. Covers streaming, a question asked before the host connects, busy wait and interrupt, Stop before and after pickup, a host that disappears or never connects, transient questions, wrong token / Origin / rebinding Host / no bridge, supersede and dispose, detach, and the env scrub.apps/opencode-plugin/opencode-session-bridge.test.ts: the adapter over a fakectx.sessionandctx.event.apps/opencode-plugin/session-bridge-cli.test.ts: plugin ↔ a stub CLI child that is a real pull-bridge server. Covers the token hand-off, the ready-file port, the answer round trip, and dispose.packages/server/ai-runtime.sessionBridge.test.ts(env takeover, scrub, guards, remote mode off).bun run typecheck: passes.bun test: 5837 pass, 0 fail.bun run --cwd apps/review build && bun run build:hook && bun run build:opencode && bun run build:pi: all pass.session.command,prompt), with the Plannotator HTTP API acting as the browser.No version bumps.