Repository navigation
feat(core): tell the agent who sent a message (experimental) - #60042
kitlangton wants to merge 2 commits into
Conversation
Messages one session sends another carry metadata["opencode.origin"]. With experimental.message_context on, the agent reads a <message-origin/> line before the text, naming the sender session by its current title or the tool. The stored text and API responses are unchanged. The subagent tool and subagent commands send their parent session as the origin.
There was a problem hiding this comment.
Looks sound. I ran the four changed test files in packages/core (75 pass) and tsc --noEmit on core (clean). I also checked that every path that builds a model request (runner, preview, compaction trigger and precompute) reads history with the flag applied, while title generation and sessions.context keep reading metadata as stored.
Public API: adds the optional experimental.message_context boolean to config (schema, OpenAPI spec and generated client types). It also starts a metadata convention, opencode.origin: on admission, a session origin's title is always replaced with the sender's stored title and any extra fields are dropped. The subagent tool and command subagents now store that key on the child's prompt, so sessions.context returns it. Everything is additive and backwards compatible; only callers that already used the opencode.origin key would see it rewritten.
A non-blocking note on the design: the flag works by removing the origin when history is loaded, but SessionHistory.load keeps it and toLLMMessages always writes the line. A future model-request path that reads through load (or store.context) would show the origin even with the flag off. That's fine for now, since every current path is covered.
Resolve titles among top-level sessions in the caller's project and compare
them exactly, so other projects, child sessions, and same-title sessions
elsewhere never appear as candidates or turn into a false ambiguity.
Count unanswered questions across the target's inbox and promoted history,
then match them against answers recorded in the asking session, so the
three-question cap and the duplicate guard survive delivery.
Stop the answer leg from waking the asker: replies are steered without
resume so two sessions cannot ping-pong into unattended runs. The question
leg still wakes the target, and the ask tool says the answer arrives at the
asker's next step.
Stamp `opencode.origin` ({kind, sessionID, title}) on both legs, matching
the shared cross-session provenance key in anomalyco#60042. That PR is still open, so
the same shape is written literally rather than imported.
The self-stamped origin would drift from anomalyco#60042, which sets the title server-side at admission; adopt the shared key once that PR merges.
The agent can now tell when a message came from another session or a tool rather than the person, and which one.
A message can say who sent it in its metadata, and the agent reads that as one line before the text. What was typed, what is stored and what the API returns stay exactly as sent.
💡 Why
Sessions message each other: a subagent gets its task from its parent, one session hands work to another, and scripts report back. Today every such message looks to the agent like the person typed it, so senders type "From X:" into the text by hand, and a continued subagent gets no hint at all.
🆕 Now you can
metadata["opencode.origin"]set to{ kind: "session", sessionID }or{ kind: "tool", name }.<message-origin session="ses_…" title="Fix login"/>(or<message-origin tool="release-bot"/>) before the text.🚦 How it ships
Off by default, behind
experimental.message_context. With it off, requests to the model are byte-for-byte what they are today. The origin is still stored, so turning it on later applies to earlier messages too.The title the agent reads is the one saved when the message arrived and never changes, so renaming or deleting the sender can't rewrite earlier messages and bust the receiver's prompt cache. A sender with no title yet is named by its session ID alone. Clients show the sender's current title from the session ID.
📊 Proof
session="…"with no title, even after it gets one ✅titletool="…"✅[User]:line with the origin before the text ✅sessions.context(HTTP, SDK, plugins)14 new tests, plus origin checks added to the subagent tool, subagent command and prompt admission tests.
👀 Read:
session/history.ts,loadForModel. The flag is read only where model requests are built (the runner, preview, compaction), inside the session's own location.loadandsessions.contextstay plain reads of stored metadata. The rest is routine.The generated SDK gains only the optional
experimental.message_contextboolean; routes, prompt input and message types are unchanged.Checks run
bun run testin packages/core over the session, prompt, subagent, config, compaction, runner, title, instructions, execution and generate tests: 741 pass, 0 failtest/plugin/andtest/schedule.test.ts; its only failures were 6 DigitalOcean OAuth tests that fail the same way ondevwithout this changebun run typecheckin packages/core, packages/schema, packages/client: no errorsbun run check:generatedin packages/client and packages/protocol: cleanoxlinton the changed files: 0 warnings, 0 errorsAdded by the opencode bot (AI-generated).
Related: #58367, #52297
Public API: adds the optional
experimental.message_contextconfig flag and theopencode.originprompt metadata key (backwards compatible)