fix: send_message_to_parent fails because child cannot learn its own child_session_id #333
Labels
No labels
backend
bug
chore
duplication
effort:complex
effort:medium
effort:trivial
enhancement
frontend
fullstack
priority:high
ready-for-agent
refactor
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dries/ocman#333
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A child/subagent session calling
send_message_to_parentfails. The last failing call was:The child passed the literal placeholder
child_session_id=current. That string is looked up verbatim byGetChildSession("current")ininternal/mcp/tools_comm.go:116→ returnssql.ErrNoRows→ the tool returnschild session not found: current. The message never reaches the parent.Root cause
There is no way for a child agent to discover its own
child_session_id.send_message_to_parentrequires a realchild_session_idthat exists instate.db'schild_sessionstable (internal/mcp/tools_comm.go:106-136).childID = resp.IDininternal/mcp/launcher.go:171).internal/mcp/composer.go:168-175) tells it to "call send_message_to_parent when done" without telling it what child_session_id to pass.get_current_session_idreturns the most-recent OpenCode session ID, not framed as the child's own id, and the skill (.opencode/skills/ocman-session-splitting/SKILL.md) never instructs the child to resolve it.So the agent guesses
"current"and the call fails.Fix
Inject the child's own
child_session_idinto the composed prompt. The prompt is composed before the session exists, butlaunchWithPortknowschildID(== OpenCode session id) before it sends the first message (internal/mcp/launcher.go:171→ send at:189-196). Append a line to the composed prompt afterchildIDis known, e.g.:Update the "Reporting Back" section of the composed prompt to state the concrete id.
Also fix (drive-by)
internal/mcp/composer.go:168and:175emit two byte-identical "## Reporting Back" paragraphs. Delete the duplicate.Verification
send_message_to_parent ... child_session_id=<childID>).TestSendMessageToParent_DeliversToParent(internal/mcp/tools_test.go:930) still passes.Research: can the server identify the caller without the client passing an id?
Investigated whether MCP
_meta, HTTP headers, or an MCP-session-id → child-session map could letsend_message_to_parentknow its ownchild_session_idwithout the child passing it. All rejected. The prompt-injection fix (injectchildIDinto the composed prompt) stands as the only self-contained solution.Rejected:
_meta/ HTTP headers /HTTPContextFuncThe server can read these — mcp-go v0.43.2 exposes
WithHTTPContextFunc(streamable_http.go:113) and always putsr.Headerin handler context (:393). But they only help if the client stamps its session identity, and OpenCode doesn't.Decompiled from the OpenCode binary (1.17.15), the tool-call site is:
arguments= only the model-supplied tool args. No session id._metaonly forprogressToken, never a conversation/session id.Per the MCP spec's Statelessness section, any cross-request identifier "MUST be referenced by an explicit identifier the client passes on each request" — the client simply doesn't pass one.
Rejected: track an MCP-session-id → child-session map
Two independent blockers:
No MCP session id exists in our config. We use
WithStateLess(true)(server.go:121) →StatelessSessionIdManager:Generate()returns""(streamable_http.go:1299-1301) andValidate()ignores client ids (:1303-1306). Each request is an ephemeral session whose "life is the same as the request" (:335-336). Nothing to key a map on.resp.ID(the child id) comes from the OpenCode platform HTTP API on a different connection from the MCP transport; they never meet.Even stateful, the granularity is wrong. If we flipped to
WithStatefulfor a stableMcp-Session-Id, it still wouldn't identify the OpenCode conversation. OpenCode keeps one MCP client per configured server, shared across all conversations (binary registry keyed by server name:clients[serverName],defs[serverName],status[serverName]). So the mapping is1 mcp-session-id → N opencode-sessions— we still couldn't tell which child called. This matches the spec: "an open connection... is not a conversation or session... a server must not treat connection identity as a proxy for conversation continuity."Conclusion
The only correlation point is that the child id equals the OpenCode session id (
launcher.go:171,resp.ID), known at launch. There is no transport-level channel to hand it back, so it must reach the child as content — i.e. injected into the composed prompt. Any header/_meta/map approach would require an upstream OpenCode change we don't control.No longer relevant after PR #338: child results are delivered to parents automatically, and get_current_session_id now identifies the calling session for explicit mid-task messages.