Repository navigation
Plannotator Inbox, step 7: the Pi and OpenCode connections (plannotator_inbox and the reply wake) - #1763
Merged
Conversation
…raced the second row)
backnotprop
force-pushed
the
feat/inbox-pi-opencode
branch
from
October 7, 2026 18:04
51ad708 to
7691dd7
Compare
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.
The story
Pi and OpenCode now get the Plannotator Inbox the way Claude Code got it in step 6: a separate
plannotator_inboxtool built from the running Inbox's own/mcptool list, and the reply wake, so a person's Send in the Inbox reaches the Pi or OpenCode 2 session that asked as a new turn once that session is idle (OpenCode 1 gets the tool only). Both hosts share one node-runtime link (packages/shared/inbox/agent-link.ts) over step 6's contract (packages/shared/inbox/connection.ts, reused, not forked); the switch is off by default on both, and nothing is registered, polled or said where noinbox/inbox.jsonexists. It is proved on the real hosts: the installed Pi CLI in RPC mode and the installed OpenCode 2 package, each against the compiled Inbox, with a scripted model endpoint in place of a provider.Plan step 7 (
research/plans/plannotator-inbox-2026-10-06/PLAN.md). Branched fromorigin/mainafter step 6 (#1758) and rebased onto step 5 (#1760); no window change, so it does not stack on another lane.What changed
packages/shared/inbox/agent-link.ts(new):discoverInboxTools(once at load/setup, only where the registry exists;tools/listbounded at 2 s, the mod's bound; a stopped Inbox is described by the list it last answered with, kept ininbox/connection-tools.json),InboxAgentConnection(the tool: re-reads the registry on every call, checks health against itsserverSession, starts a stopped Inbox withplannotator inbox --background, proxies to/mcpwithproject_path,agent_session,agent_host,agent_namefilled; no binary: "install Plannotator" once),InboxWake(one per session: 1 s tick, bridge long-poll with the 5 to 60 s backoff that never gives up, two idle ticks, a re-check with the Inbox, a mkdir claiminbox/claims/<reply id>for exactly-once across processes,inboxWakeText,deliveredacknowledged; never interrupts). Imports only./connectionandnode:modules so Pi can vendor it.packages/shared/inbox/connection.ts:mcpAnswerOfmoved here from the mod (pure, now shared);INBOX_TOOL_WAKE_NOTEmade host-neutral;inboxAgentTool(tools, { wakes: false })for OpenCode 1. The mod's byte-pinned copyapps/hook/hooks/mod/inbox-contract.tsre-pasted;apps/hook/hooks/mod/inbox.tsimportsmcpAnswerOffrom it.apps/pi-extension/inbox.ts(new), called at the end of the factory inindex.ts(the factory returns the bounded discovery for Pi to await only when the switch is on and the registry exists).pi.registerToolwithdefaultActive: false, made active by the firstsession_startwith a UI (inactive in print/JSON). Listeners once at load. Busy = notctx.isIdle(), queued messages, an "Ask this session" question running, or (once Pi has emittedagent_settled, 0.80.4+) a run not yet settled; older Pi settles on idle. Deliverypi.sendUserMessage(text, { deliverAs: "followUp" }), confirmed by that user message'smessage_start. Nothing aborts a run, the rule ofpi-session-bridge.ts's run protection.vendor.shvendorsconnectionandagent-linkintogenerated/inbox/;inbox.tsadded tofiles.apps/opencode-plugin/inbox.ts(new). OpenCode 2 (server.ts→registerInboxOpenCode2): the tool throughctx.tool.transform, signed by and woken in the ROOT session (resolveRootSession); a wake per root session made on its first tool call or firstsession.execution.startedin this process; busy from execution events with the 20 s cross-check (session.wait, 300 ms bound;session.waitalone without an event stream); deliverysession.prompt({ sessionID, id, text, delivery: "queue" }), never steer. OpenCode 1 (index.ts): the tool only, built with its owntool()and zod (inboxToolArgs), description without the wake line.tests/parity/vendor-parity.test.ts: it matched only.jsimports, and the package imports.tseverywhere, so it checked nothing; it now reads.ts(and dynamic) imports, single-file vendored outputs, and thegenerated/inbox/prefix.tests/helpers/scripted-model.ts(an OpenAI-compatible streamed endpoint that keeps every request),tests/helpers/inbox-world.ts(temp HOME and data dir, the compiled binary or the CLI from source on PATH, the person's Send through the window's route)..github/workflows/inbox-pi.yml(new, path-gated: compile, Node 24, Pi floor 0.79.1, the Pi proofs and the parity test, transcripts uploaded); the "OpenCode 2 installed package" job intest.ymlgains a compile step and the OpenCode proofs on the same packed tarball, transcripts uploaded.AGENTS.md: the Inbox section (step 7 built; a "Pi and OpenCode (step 7)" paragraph), thePLANNOTATOR_INBOX_TOOLrow.What it makes real
PLAN.md step 7: the tool on Pi and OpenCode 2 registered only with the registry and the switch on (off by default); the wake once per reply as a Pi follow-up or an OpenCode 2 queued prompt, with the stable header, the fixed instruction line and the reply verbatim (never the thread, never the system prompt); take-over; silence without a registry; "install Plannotator" once; Pi without
agent_settledstill settles; OpenCode 1 tool only. Window record: the thread's existing "Delivered to , " now reads "Delivered to Pi" and "Delivered to OpenCode" (theagent_namethe connection signs with); no UI change.Proofs (transcripts in
.local/proof/, uploaded by both CI jobs)All against
plannotatorcompiled from this branch with the release flags, under tempPLANNOTATOR_DATA_DIRand HOME,PLANNOTATOR_BROWSER=none.apps/pi-extension/inbox.test.ts(the installed Pi 0.84.4 CLI in RPC mode through Pi's ownRpcClient,-ethis extension from source, a scripted model; 6 tests, 43 s):plannotator_inbox, noinbox/folder, no notice; registry with the switch off: no tool; both: the tool with exactly the Inbox's actions (8, guides included), filled arguments absent.send_messagelands a thread signedhost: pi, name: Pi, the Pi session id; the person's Send arrives once as the user message the model receives last:Plannotator Inbox: <subject> (<reply id>),INBOX_WAKE_INSTRUCTION, the reply verbatim; the thread reads Delivered to Pi with the row'schecked_at; the wake'ssend_messagewithreply_tolands in the same thread.--session): 1 wake across both, one claim.plannotatoron PATH or in~/.local/bin, the first result carriesINBOX_INSTALL_TEXT, the second only "not running".PI_TEST_OLD_CLI, noagent_settledevent seen): 1 wake, delivered.apps/opencode-plugin/inbox-v2.test.ts(opencode22.0.19 serving, the packed@plannotator/opencodeinstalled through a throwaway registry exactly as the smoke does, the scripted model as its provider, OpenCode's/api/eventstream recorded; 2 tests, 14 s): silence, the switch, the tool from/mcp;send_messagesignedhost: opencode, name: OpenCode; the reply waits 4 s while the person's turn runs; then ONE session row withdelivery: "queue"carrying the wake text; Delivered to OpenCode; a prompt typed into the wake's turn reaches the model, 1 wake row, 0session.execution.interrupted; the answer lands in the thread.apps/opencode-plugin/inbox.test.ts(the plugin's OpenCode 1 entry with its owntool()/zod against a real Inbox): no tool without a registry or with the switch off; with both, the zod-produced schema hasactionrequired, no filled arguments, no wake line; a call lands a thread.apps/hook/hooks/mod/inbox.test.ts+inbox-contract.test.ts+packages/server/inbox-bridge.test.tsagainst the binary (17 pass);scripts/test-claude-code-mod.shwith a tempCLAUDE_CONFIG_DIR(24 pass);claude plugin validate apps/hookpasses.Commands:
Gates
bun run typecheck: pass.bun run check:release-version: v0.28.7 consistent.bun test packages apps/hook tests scripts apps/pi-extension apps/opencode-pluginunder a temp data dir withFORCE_COLORunset: 6628 pass, 1462 skip, 0 fail (709 files). With this machine'sFORCE_COLOR=3,annotate CLI on an occupied fixed portfails on main's code too (itsResolved:line starts with an ANSI escape); untouched here.tests/parity/vendor-parity.test.ts(now real) passes;bun pm pack --dry-runinapps/pi-extension: 261 files, 43.98 MB unpacked, includesinbox.ts(8.17 KB),generated/inbox/agent-link.ts(19.22 KB),generated/inbox/connection.ts(14.00 KB); noinbox.html.apps/opencode-plugin: 13 files, 56.98 MB unpacked (dist/server.js211.29 KB,dist/index.js0.57 MB); noinbox.html.origin/main..HEAD: no leaks. Semgrep 1.172.0 with.semgrep.ymlover every new and changed source: no findings.packages/uiandpackages/coreuntouched (no ui smoke needed).What it leaves out
An Inbox server inside Pi; the OpenCode 1 wake (no way to wake an idle OpenCode 1 session); Windows for the proofs (the world's
plannotatorwrapper is/bin/sh); a toast for OpenCode 2 (it has no toast surface; its one notice, a reply that could not be delivered, goes to the log, because a transcript notice would be a steer row left in an idle session, the #1734 trap).Deviations from the plan, with reasons
claude --continueruns twice. Two processes on one Pi session (pi --sessiontwice) or one OpenCode session are rare, a lease is a second file protocol with its own staleness rules, and the mkdir claim alone already guarantees one delivery (proof 4). Which process delivers is whichever claims first.session.promptresolving means OpenCode durably admitted the row (its API: "Durably admit one session input and schedule agent-loop execution"); Pi'ssendUserMessagereturns nothing, so Pi waits for the user message'smessage_start(15 s watchdog while idle, then a notice with the thread link).Plannotator Inbox: <subject> (<reply id>)"), because "as a message from the plannotator plugin" is false on Pi and OpenCode. This changes the Claude Code tool description too (still under the 2,048-character cut, asserted by the mod proof).mcpAnswerOfmoved into the shared contract rather than being copied a third time.inboxAgentTool(tools, { wakes: false })for OpenCode 1 (and an OpenCode 2 host withoutsession.prompt), so its description never promises a reply that cannot arrive.RUNNING_CROSS_CHECK_MS), read in code.plannotatortool: nothing could wake that session, and the description says replies arrive by themselves.inbox/connection-tools.json(the mod keeps it in$.store); it lives in the Inbox folder, souninstall --purgeremoves it with the claims.PLANNOTATOR_BIN(as the OpenCode CLI bridge already does), thenplannotatoron PATH, then~/.local/bin/plannotator. "Install Plannotator" is said in the first result only; later results in the session say the Inbox is not running.inbox-pi.ymljob (build, compile, two Pi installs, about 45 s of proofs) and two steps on the existing OpenCode 2 job (a compile and about 15 s of proofs locally; a cold runner reinstalls the plugin per world, about 47 s each as the smoke measured). plannotator-ops to check runner time, per LANE-RULES.inbox/claims/gains one small folder per delivered reply, as the mod's claims folder does; no pruning added.After the first CI run
tests/e2e/inbox-guides.spec.ts:162failed in CI (run 37659138886). Not caused by this branch, which changes no server, shim, store or window code. The spec opened the window before the two guide sends. A list already on screen holds a later arrival behind "N new" (held order,App.tsxadopts only an empty list), so the second row raced. A 2 s pause between the sends reproduces it locally on main's code. The fix opens the window after both sends (commit "Inbox guides e2e"). Rebased onto 12f21b6. Fullbun run test:e2e:inbox: 51 passed. merge-tree clean.