Skip to content

Let an agent send a message to another live opencode-skein session #82

Closed

Description

@androidand
Proposal

Why

session-peer-awareness shipped discovery (peers — who else is working here) but deliberately
did not ship messaging: "Enforcement... should stay [elsewhere]. Awareness, not enforcement." That
was the right scope for that change. But nothing has since filled the messaging half, and the gap
is not hypothetical — it's the single piece of claude-peer-protocol's eventual scope that has no
dependency on the unproven Claude-compatibility hypothesis at all. Two opencode-skein sessions on
one machine sending each other a short message is a same-app problem: both speak the same HTTP
API already.

There is prior art, and it is informative about the gap rather than a solution: an untracked,
unregistered file, packages/opencode/src/plugin/skein-peers.ts, sketches exactly this — a
list_agents/send_message tool pair. It should not be built on directly (see What's wrong with
it, below), but it correctly identifies that send_message is the missing half, and its
prompt-injection approach (POST to the target's /session/{id}/prompt_async with a provenance
prefix) is the right shape for the same-instance case.

What's wrong with the prior art

  • list_agents duplicates peers under a different tool name instead of extending it — exactly
    the "parallel concept instead of extending an existing tool" anti-pattern the original request
    warned against by name.
  • Its registry read (fetch(${ctx.worktree}/../.skein/peers.json)) is not a valid fetch call —
    no scheme, so it fails outright — and it invents a .skein/peers.json file nothing writes.
  • send_message's target discovery hardcodes http://localhost:4096, assuming exactly one
    opencode instance on a fixed port. Real deployments run multiple instances on different ports
    across different directories (fleet-instance-presence's whole reason for existing).
  • No exclusion of the caller's own session/descendants (the bug class session-peer-awareness
    1.2 specifically fixed for peers).
  • No delivery semantics: no distinction between "the HTTP POST returned 200" and "the message was
    actually injected and will be seen," no handling of a target that disappears mid-send, no
    provenance beyond a bare text prefix.

What Changes

1. Extend peers, don't parallel it

The existing peers tool gains a sibling send_peer_message tool (or an optional parameter on
peers itself — implementation detail to resolve in tasks, but the model stays one thing: a
PeerRef/PeerMessage pair, not a second roster). No new list_agents tool. Resolve a target by
session id or an unambiguous prefix of its title; an ambiguous match is refused, never guessed.

2. Two delivery paths, one tool surface

  • Same-directory (same store): the target is already visible via session/peers.ts's
    resolvePeers — no HTTP hop needed; inject directly through the local session/prompt API,
    exactly like session.synthetic/session.prompt already do internally.
  • Cross-instance (different directory/port), once fleet-instance-presence Phase 4's
    remote-instance control API exists:
    resolve the target instance's real baseURL from presence
    data (never a hardcoded port) and deliver through that instance's own HTTP API, the same pattern
    Phase 4 already establishes for cancel/pause.

A caller does not need to know which path applies — the tool resolves it from where the target
actually is.

3. Explicit provenance, explicit delivery status

An injected message carries structured provenance (sending session id/title, not just a text
prefix) so the receiving agent can distinguish it from a human prompt, matching the same
"metadata not content, provenance always explicit" posture session-peer-awareness and
fleet-instance-presence already hold. Delivery is fire-and-forget (same pattern as the
background-subagent result injection in task.ts — awaiting a full reply would block the sender
on the peer's whole turn), so the tool result reports "accepted for delivery" honestly rather than
claiming a stronger guarantee it cannot back up; a mid-turn target is refused outright rather than
risked, and a disappeared target is reported as unreachable rather than accepted.

4. Retire the prior art

Once this ships, packages/opencode/src/plugin/skein-peers.ts is superseded. Delete it as part of
this change rather than leaving it as an unregistered, confusing duplicate.

Non-Goals

  • Not the Claude-compatible cross-session protocol — that's claude-peer-protocol, which is
    expected to extend this same Peer/PeerMessage model with a Claude-Code-reachable harness
    adapter once its spike proves the mechanism, not reinvent a tool surface.
  • Not solving the roster-accuracy problem for cross-instance targets — that's
    fleet-instance-presence Phase 6; this change consumes that data, it doesn't fix it.
  • No broadcast, no channels, no persistent message history — a single short message to a single
    named peer, same restraint the original request asked for.

Dependencies

  • Same-directory delivery: none beyond what session-peer-awareness already shipped.
  • Cross-instance delivery: depends on fleet-instance-presence Phase 4 (remote-instance control
    API) for the target-instance HTTP pattern. Can ship same-directory-only first and add
    cross-instance delivery once Phase 4 lands, rather than blocking entirely.
  • claude-peer-protocol depends on this change's Peer/PeerMessage model existing first, so it
    has something to extend rather than something to duplicate.

Impact

  • New: send_peer_message tool (or peers extension) in packages/opencode/src/tool/, sharing
    packages/opencode/src/session/peers.ts's data model.
  • Deleted: packages/opencode/src/plugin/skein-peers.ts.
  • Modified: packages/opencode/src/session/peers.ts (delivery, not just resolution), instance HTTP
    API surface for the cross-instance path (co-designed with fleet-instance-presence Phase 4).

Disposition (2026-09-18)

Archived — superseded/obsolete. Shipped as send_peer_message under claude-peer-messaging; 3.1/3.2 (unreachable/disappeared targets) handled there and by skein-pool's deadline.

Tasks

Slice 1: Domain model

  • 1.1 Added resolveTarget (id-or-unambiguous-title-prefix resolution over the existing
    Peer roster) and formatPeerMessage (structured provenance envelope) to
    session/peers.ts, rather than a separate PeerMessage struct — the roster this reuses
    (resolvePeers) is already harness-implicit (opencode-skein is the only source today), so
    claude-peer-protocol adds a harness field to Peer when it has a second harness to
    represent, instead of this slice speculatively adding one now with only one value.
    • File: packages/opencode/src/session/peers.ts
    • Validation: bun run typecheck

Slice 2: Same-directory delivery

  • 2.1 Added send_peer_message tool: resolves by exact session id or unambiguous
    case-insensitive title prefix (ambiguous → refused with the candidate list; exact id always
    short-circuits title matching). Targets are drawn from the same resolvePeers roster the
    peers tool uses, so caller/descendant exclusion is inherited, not reimplemented.
    • File: packages/opencode/src/tool/send-peer-message.ts, registered in
      packages/opencode/src/tool/registry.ts
    • Validation: bun test test/session/peers.test.ts (resolveTarget cases: exact id, unambiguous
      prefix, ambiguous refusal, not-found, id-shadows-title-prefix)
  • 2.2 Delivery via TaskPromptOps.prompt (the same ctx.extra.promptOps primitive
    task.ts uses for background-subagent result injection — see
    subagent-notification-reliability for the parent-wake-up reliability question this
    shares), with formatPeerMessage's structured provenance envelope rather than a bare
    prefix.
    • File: packages/opencode/src/tool/send-peer-message.ts
    • Validation: bun test test/session/peers.test.ts (formatPeerMessage — provenance separate
      from message text)
  • 2.3 Scope-adjusted from the original wording: delivery is fire-and-forget (forked, matching
    task.ts's own background-result-injection pattern) rather than awaited, because awaiting
    the full prompt() call would block the sender on however long the peer's entire reply
    takes to generate — wrong for a lightweight coordination message. The honest claim available
    without that block is "accepted for delivery" (target existed and was not mid-turn at send
    time), not "confirmed injected". A literally busy (mid-generation) target is refused
    up front rather than risking joining/racing its turn, the same foreign-turn hazard
    loop.ts guards against — awaiting-permission/stalled/cancelling are not mid-generation
    and are still valid targets.
    • File: packages/opencode/src/tool/send-peer-message.ts
    • Validation: covered by the tool's own not-found/ambiguous/busy/unreachable result branches;
      see specs/peer-messaging/spec.md for the corresponding requirement update

Slice 3: Cross-instance delivery (depends on fleet-instance-presence Phase 4)

  • 3.1 Once fleet-instance-presence Phase 4's remote-instance control API exists, resolve a
    cross-instance target's real baseURL from presence data and deliver through that
    instance's HTTP API.
    • File: packages/opencode/src/session/peers.ts
    • Validation: integration test with two instances on different ports
  • 3.2 Handle target-instance-unreachable and target-session-disappeared distinctly from a
    same-directory failure.
    • Validation: unit test for each failure mode

Slice 4: Retire prior art

  • 4.1 Deleted packages/opencode/src/plugin/skein-peers.ts — unregistered, unwired to any
    plugin loader, and fully superseded by Slices 1-2.
    • File: packages/opencode/src/plugin/skein-peers.ts
    • Validation: git status shows it removed; grep -rn skein-peers packages/opencode/src empty

Slice 5: Verification

  • 5.1 bun run typecheck (opencode, sdk, tui all clean) and bun test test/tool/ test/session/
    — 762 pass (2 pre-existing, unrelated failures confirmed present on a clean checkout before
    this change: a stale snapshot from subagent-background-default, fixed opportunistically
    since it was a one-line, safe --update-snapshots; and a known-flaky
    test/session/prompt.test.ts timing test, left as-is — see prior session notes).
  • 5.2 Live check: two same-directory sessions exchange a message end to end.
    • Done 2026-09-05 — and it caught a real bug on the first attempt: the tool was constructed
      in tool/registry.ts's custom-tool assembly (with a node-graph layer) but never added to
      the separate, hand-maintained builtin array that determines what the model actually sees.
      The LLM's own error made this unambiguous: "Model tried to call unavailable tool
      'send_peer_message'." Same bug, same fix, as the already-shipped peers tool — see
      session-peer-awareness task 4.2. Fixed in packages/opencode/src/tool/registry.ts
      (both added to builtin).
    • Also fixed along the way: target resolution was scoped to resolvePeers's "actively
      working" roster, which — correctly for the awareness tool, wrongly here — excludes idle
      sessions. Messaging an idle session ("when you get back to this, X changed") is the normal
      case, not an edge case, so resolveMessageTargets (new, in session/peers.ts) is a
      sibling projection that includes idle sessions while keeping the same
      caller/descendant/directory exclusions.
    • Validation: packages/opencode/test/session/prompt.test.ts — "send_peer_message delivers
      into a second, idle session in the same directory" — a real model turn calls the tool
      through the real ToolRegistry/SessionTools.resolve path targeting a deliberately idle
      session, and the test polls the receiving session's actual message history for the
      delivered, provenance-tagged text. Not a mock — the real registered tool, the real forked
      delivery path.
    • Cross-instance delivery (Slice 3) remains not live-checked — still blocked on
      fleet-instance-presence Phase 4.

Unchecked items above: see Disposition in proposal.md (2026-09-18).

Plan changes

7 done

Activity

  1. androidand commented on Sep 17, 2026

    @androidand
    OwnerAuthor

    Archived 2026-09-18 — see the change's Disposition section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions