feat(opencode): session-to-session messaging — communicate between two running sessions - #38944
Draft
iceteaSA wants to merge 5 commits into
Draft
feat(opencode): session-to-session messaging — communicate between two running sessions#38944iceteaSA wants to merge 5 commits into
iceteaSA wants to merge 5 commits into
Conversation
6 tasks
iceteaSA
force-pushed
the
session-to-session
branch
2 times, most recently
from
July 26, 2026 10:38
017a9a0 to
50e2f8d
Compare
This was referenced Jul 26, 2026
iceteaSA
force-pushed
the
session-to-session
branch
from
July 26, 2026 12:22
50e2f8d to
89868d4
Compare
3 tasks
Lets a parent agent (via tools) or the human (via TUI esc) steer, gracefully cancel, or hard-abort a single running Task subagent mid-run, without affecting the parent or sibling subagents. A per-instance Interrupt service holds one pending interrupt per child plus a terminal record; the child consumes it at its own runLoop turn boundary. Steer injects a frame and the child adapts and continues; cancel injects a frame, records a terminal, and force-breaks within a grace window; abort cancels the BackgroundJob immediately. Interrupt tools reach any descendant through a bounded ancestry walk that fails closed off the caller's subtree. Off by default behind OPENCODE_EXPERIMENTAL_SUBAGENT_INTERRUPT.
Lets a subagent message the agent that spawned it — ask a question and block for the answer, or send a fire-and-forget update — instead of only being able to ask the human. The parent replies with the same tool. A send wakes the parent's parked task wait through a background-job channel and parks on a reply Deferred held by a per-instance Messaging service, so no new session state is needed. Replies are bounded by a timeout, the channel is separate from the promotion channel, and a missing parent fails fast. Off by default behind OPENCODE_EXPERIMENTAL_AGENT_MESSAGING. Injected parent messages carry the parent session model and variant explicitly, so an injected message cannot fall through to the agent default and switch the session model.
…communication Lets the sibling subagents one parent spawns message each other directly instead of routing everything through the parent. The surface is one composable primitive — a per-child allow-list (`message_allow`) — so the parent builds whatever graph it wants: hub, mesh, or chain. Peer sends are fire-and-forget; the synchronous round-trip stays parent-only, which is what keeps siblings deadlock-free. Slugs are parent-owned handles resolved through a registry, authorized at send time against both the allow-list and true sibling-hood. Recipients drain a FIFO inbox at their own turn boundary, batched so an M-member graph costs O(1) turns per drain. The interrupt tools also accept a slug task_id, resolved through the same registry and still subject to the full ancestry check. Off by default behind OPENCODE_EXPERIMENTAL_AGENT_MESSAGING.
…ions Lets two separate top-level sessions — different windows or OS processes on the same machine — send each other messages. There is no parent/child relationship, so both sides opt in with an explicit mutual-consent handshake: invite mints a one-time token, accept consumes it and writes durable bidirectional consent, then either side can msg. Peers are addressed by session_id, never by slug, because slugs carry no uniqueness guarantee. Send persists a capsule row to a shared table; receive has two halves — a session taking a turn drains its inbox at the turn boundary, and an idle session is woken by a per-instance poller that claims its rows. Delivered rows are hard-deleted and a reaper reopens rows claimed by a crashed process. Incoming messages render as an external-context frame and are treated as untrusted input. The poller forks lazily from inside the run loop so it captures the live fiber's instance context; forking at layer-build time leaves that context absent and the receive path dies silently while unit tests still pass. Off by default behind OPENCODE_EXPERIMENTAL_S2S.
iceteaSA
force-pushed
the
session-to-session
branch
from
August 2, 2026 11:44
89868d4 to
7316ade
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.
Issue for this PR
Closes #38965
Related to #19215 (agent-to-agent communication primitives). Not "Closes" — this is an experimental, flag-gated primitive.
Re-submission of #32693, closed by the automated cleanup bot on 2026-07-17 while still active. The branch has been force-pushed since (rebased onto current
dev), so GitHub refuses an author-side reopen — same situation as #35195. No maintainer decision was involved in the closure.Type of change
What does this PR do?
Lets two separate top-level sessions — different windows / OS processes on the same machine — send each other messages. This is distinct from the subagent primitives in the base PRs: there is no parent/child relationship, so the two sides opt in with an explicit mutual-consent handshake first. Gated behind
OPENCODE_EXPERIMENTAL_S2S(off by default).A new
s2stool drives it:invitemints a one-time 10-minute token; the peer runsacceptwith that token (shared out-of-band), which writes a durable bidirectional consent record; then either side runsmsgto send. Peers are addressed by their globally-unique session_id (ses_…), never by slug — slugs are not unique.leavetears the pair down.How a message actually gets from one process to the other:
s2s_inboxtable in the sharedopencode.db(tables:s2s_inboxdurable mailbox,s2s_tokenone-time invites,s2s_allowdirectional consent).external-contextframe taggedsource="sibling-session", and are treated as untrusted input (attributes escaped at the sink).Why the receive path is built the way it is (the non-obvious part): the messaging/poller services are per-instance — they resolve through a request-scoped context reference that is only present inside a live request/run fiber. An earlier version forked the poller at layer-build time, where that reference is absent, so it died on its first tick and the entire recipient path was silently dead in a real process while every unit test passed (the tests inject the context). The fix is to (a) drain in-context at the runLoop boundary, and (b) lazily fork the per-instance wake-poller from inside
SessionPrompt.loop, capturing the live fiber's context — plus making theS2SStoreservice a direct member of the prompt-serving layer groups so aserviceOptionlookup in the forked fiber actually finds it. No service was made process-global.Addressing by session_id (not slug) is deliberate:
session.slughas no uniqueness guarantee — an earlier draft added a UNIQUE index on it and that broke new-session creation on a real DB once the small random-slug space saturated. Slugs stay as parent-owned handles for the subagent primitives only.How did you verify your code works?
invite/accepthandshake, then messages both directions. Confirmed at the DB level — consent rows written in both directions, capsule persisted, and the row hard-deleted on delivery on each side; the reply surfaced in the recipient's context as asource="sibling-session"frame. Both halves exercised, including waking a fully idle peer.test/s2sdirectory (store, poller, capsule, lifecycle, frame-escaping, the cross-process topology repro, and an in-process fork repro pinning the per-instance-context behavior), plus the messaging/coordinator/interrupt suites it shares files with — green locally.bun typecheckclean on the s2s surface (the repo's pre-existingmcp/catalog.tserrors on dev are unrelated).leavenot revoking cross-process consent), all fixed and re-reviewed clean.devthree more times, each with the full suite re-run. One fix landed in that window: severaltest/s2sfiles shared Effect's process-globalmemoMapacross files, so a router registered by one suite leaked into another and produced order-dependent failures. AtestEffectIsolatedSharedhelper now allocates a file-localMemoMap, keeping intra-file service identity without the cross-file leak.Screenshots / recordings
Minor TUI surface (an inbox marker line in the transcript + a session-list tweak), reusing the marker render path from #38942.
Checklist