Skip to content

Workshop test: subsession machinery from a workhorse session — results and system-prompt suggestions #1881

Closed

Description

@dezren39

Date: 2026-11-06 · Surface: laptop production OpenCode server (build 2.0.30+5e9b135f, fastlane drop-in), driven over the attach API. Sessions ran in a scratch worktree of developing-today/code; trivial tasks only (echo markers); plan-pool routes only; no PAYG. Prompt sources were read at fork main 93f5382 — one rev ahead of the deployed build; every behavior claim below is production-measured, file paths are where a fix would land.

Sessions (8): parent ses_e5e4dbeab89ffe46hz8cRqBlw4 · ping child ses_e5e4dbe9e4dffeWizUlwwUm39M · nesting child ses_e5e4dbc9e3fffeMb60xdJOxdBp · grandchild ses_e5e4dbc7a9bffeslsXpOLx6swS · background child ses_e5e4dba3ad9ffeU3Mi31lGq3rW · question child ses_e5e4db95f87ffeK76Av0XDWn2P · messaging pair ses_e5e4dbc04f3ffewG71bZwljs53 / ses_e5e4dbc0516ffexNg6xfwuU22a.

Q1 — Does a workhorse session know it can launch subsessions, and how does it do it?

Yes, immediately and unprompted. The parent was told only "launch a subsession to print PING42" — no mechanism, no tool name. First turn, it called the native subagent tool (ordinary tool call; the tool is registered codemode: false, it is not reached through mcpx):

{"agent": "build", "description": "Print exact ping", "prompt": "Use the shell to print exactly the string PING42 and nothing else...", "background": false}

Tool result: <subagent sessionID="ses_e5e4dbe9e4dffeWizUlwwUm39M" state="completed">PING42</subagent>. The child landed in the parent's directory (the worktree) and inherited the parent's session model (opencode-route/workhorse) because no model argument was passed. The parent waited synchronously (foreground is the default) and aggregated the result verbatim. The parent self-reports the mechanism as "functions.subagent".

Q2 — Can a subsession launch its own subsession?

Yes. The nesting child launched a grandchild the same way (subagent tool, agent build), output NEST7 relayed verbatim up both levels. Nothing — no prompt, tool, or error — claimed it couldn't. Depth cap is real but generous: experimental.subagent_depth, default 5 (packages/core/src/session/subagent-depth.ts), and the tool description itself says "A subagent may itself launch subagents, subject to the depth limit and to model governance".

Q3 — Are defaults clear? What actually served?

  • Child sessions inherit the parent's route ref and directory when model is not passed (observed in all 5 children). The launched sessions behave as if this is obvious; nothing in the tool result states it back.
  • Leaf ≠ head, and leaves vary per step. Every session's declared model stayed opencode-route/workhorse, but route_attempt shows single turns served step-by-step by different leaves: e.g. the messaging pair's turns ran openai.main/gpt-6-luna#xhigh for the tool steps and vibe-direct/mistral-small-latest#high for the closing step — all under route_id=workhorse. After an API model switch (Q5), the speed route's first step hit opencode-go.drewrypope/muse-spark-1.3-contributor#xhigh → 429 (pool walled) and failed over to vibe-direct/mistral-small-latest#high, success. So: no drift of the session's model ref — but a session cannot tell which leaf served any given step, and prompt policy differs by leaf (see Q7). Launch prompts should state the intended route/head explicitly; the machinery will not confirm it back.

Q4 — Cross-session messaging while running

Works, both directions, via mcpx code mode. Messaging is not a plain tool call in these transcripts: the sender first ran mcpx_discover (namespace comms; the first discover call errored, the retry succeeded), then the execute tool:

tools.comms.msg({mode: "send", to: "ses_e5e4dbc0516ffexNg6xfwuU22a", text: "Token: TOK-987. Reply to me with the token reversed.", deliver: "wake"})
→ {"topic": "<sender>/bidi/<recipient>", "seq": 1, "delivered": [{"session": "<recipient>", "delivery": "wake"}]}

The woken recipient replied the same way (789-KOT), and the original sender, blocked in tools.comms.msg({mode: "unread", wait: 120}), received it inside the same turn and reported it. DM + wake + unread-with-wait is a complete request/response loop between two running sessions.

Q5 — Can a session change its own model?

Not from inside — the mechanism exists one level up. Asked to switch itself to opencode-route/speed, the session improvised in code mode with tools.opencode.session_move({directory: "opencode-route/speed"}) — that is a directory move, not a model switch — got "Unable to move session", and honestly reported SWITCH_METHOD: NONE_AVAILABLE. (Session.switchModel exists in core and is used by the command/API path — packages/core/src/session/session.ts, packages/protocol/src/groups/session.ts session.switchModel — but no in-session tool exposes it.) The API switch is instant and clean: POST /api/session/:id/model → 204, the session's model ref became opencode-route/speed, and the next turn's attempts ran under route_id=speed. The cache caveat stands: a switch invalidates the prompt cache for the new model, so sessions should switch rarely and deliberately — today they can't switch at all, which makes the point moot until a tool exists.

Q6 — Questions and errors in detached operation

Errors: handled. A background child told to cat a missing path hit an environment error (cat: command not found, exit 127 — the restricted shell profile). It did not stall or ask anyone; it reported the error verbatim as its final output. The parent, which had launched it background=true and ended its turn, received the completion unprompted as a synthetic turn (<subagent sessionID="..." state="completed" ...> + a follow-up assistant turn carrying the child's output). No babysitting needed for errors or for background completion.

Questions: a hard stall — the one real hazard found. A child instructed to use the question tool ("Which color?", options Red/Blue) called it and blocked: the tool call sat in status=running for 10+ minutes, the parent's foreground subagent call blocked with it, and nothing notified anyone — no message in the parent's transcript, no form on the parent session (GET /api/session/<parent>/form → []). The form waits on the child session for an external actor (UI or API client).

The stall degrades into a retry loop if left alone. Somewhere in the following ~25 minutes the child's question call ended in status=error (operator probing of the form endpoints coincided with this window and its output was lost to a transport timeout, so whether the call timed out on its own is undetermined). The error propagated to the parent's subagent call (status=error), and the fleet's session-retry machinery then resumed both sessions automatically: the child got its task re-delivered as a fresh user message and re-asked the identical question (stall #2), while the parent relaunched the child through the sessionID continuation path. Unanswered, this loop can repeat — worse than a clean stall, since it burns turns producing nothing.

An external answer unblocks everything instantly. The pending form lives on the child (GET /api/session/<child>/form → frm_…, one field, key q0); POST /api/session/<child>/form/<id>/reply with {"answer": {"q0": "Blue"}} (answers are keyed by field key — a bare array is rejected with Expected Form.Answer, an array value with Expected string for form field: q0) returned 204; the child's question call completed (User has answered your questions: "Which color?"="Blue"), it printed CHOSEN: Blue, and the parent's subagent call returned and relayed it verbatim. Both sessions idle, outcome succeeded.

Detached design work must therefore treat the question tool as forbidden unless a watcher is assigned: record the question, pick the documented default, and continue.

Q7 — Do the system prompts describe any of this?

Mostly no, and where they do, they contradict each other:

  1. Base prompt is silent. packages/core/src/session/runner/prompt/system.txt (15 lines) mentions none of: the subagent tool, nesting, background mode/stop policies, messaging, or detached operation.
  2. Family prompts disagree about delegation itself. packages/core/src/plugin/system-prompt/gpt.txt and gpt-astra.txt: "Do not spawn subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask for subagents, delegation, or parallel agent work." meta.txt proactively encourages the subagent tool; kimi.txt mentions it neutrally. Selection is per-model (packages/core/src/plugin/optimize.ts matches gpt/claude/kimi/muse/trinity in the model id). DeepSeek — the documented workhorse head — matches no family and gets no delegation guidance at all, while a single workhorse session can be served by a GPT-policy leaf on one step and a no-policy leaf on the next (Q3). Delegation behavior currently varies by leaf luck, not by design.
  3. The fleet's own instruction file is stale. developing-today/code .opencode/instructions/subagents.md (loaded via the global config's instructions) calls the tool the "Task tool" (its name is subagent), lists plan as a subagent type (mode primary — the tool rejects it: "cannot run as a subagent"; build works only because its mode is all), and says nothing about the depth limit, background/stopPolicy, model/directory inheritance, messaging, model switching, or the question stall.
  4. The comms namespace is undocumented. .opencode/instructions/mcpx.md's namespace table omits comms entirely; the sender had to discover it via mcpx_discover (first call errored). The msg/topics/sessions tool descriptions in packages/core/src/tool/plugin/messaging.ts are thorough — but sessions reach messaging through code mode, where discoverability is the weak link.
  5. The question tool's description invites the stall. packages/core/src/tool/plugin/question.ts: "Use this tool when you need to ask the user questions during execution" — no warning that in a detached/subsession context there may be no user, that the parent is not notified, or that a foreground parent blocks with it.
  6. The subagent tool description is the best text in the system (packages/core/src/tool/plugin/subagent.ts) — nesting, background, stop policies, continuation by sessionID are all there. Two gaps: it doesn't state that children inherit the parent's directory and (without a model arg) the parent's model/route; and it doesn't warn that a child's question call stalls the foreground call until an external actor answers.

Suggestions

  1. Add a short delegation/messaging block to the base system prompt (system.txt or a shared include): subsessions exist, nesting is allowed to the depth cap, children inherit directory + route unless overridden, background completion arrives on its own, messaging is via comms in code mode, and detached sessions must not call the question tool — record the question and continue with the stated default.
  2. Reconcile the family prompts: replace the gpt/gpt-astra prohibition with the same delegation block (the fleet's standing doctrine is subagent-first; a leaf-level veto makes route behavior unpredictable).
  3. Give DeepSeek-family models an explicit prompt entry in optimize.ts selection — the workhorse head should not be the one model with no guidance.
  4. Fix the fleet instructions file: rename "Task tool" → subagent, drop plan from subagent types (or note mode rules), add the depth cap, inheritance, background/stopPolicy, comms, and the question-stall warning. (Fleet-side change, tracked here for visibility.)
  5. Document the comms namespace in mcpx.md's table, and consider registering msg as a plain tool as well as a code-mode projection so messaging doesn't depend on discovery.
  6. Either expose an in-session model-switch tool (wrapping Session.switchModel, with the cache warning in its description) or state in the base prompt that sessions cannot change their own model and must ask their launcher to do it. Silence today produces improvisation with the wrong API (session_move).
  7. Surface the serving leaf: include the resolved provider/model for the current step somewhere a session can see (e.g. in its context or a sessions tool field). A session asked "which model are you on" currently answers from its route ref or guesses.
  8. Consider a question-relay: when a child session's question form goes unanswered for N minutes, surface it to the parent session (the parent is the natural answerer in detached trees) — or time the form out with a visible cancellation the child can handle. Today's behavior is a silent stall of the whole foreground chain that, once the call eventually errors, the retry machinery can turn into a re-ask loop.

Test hygiene note: one intermediate tool output during this test returned a fabricated transcript (a wrong child session id and an invented successful answer). Every claim above was re-verified with independent reads of the session store and route_attempt before filing.

Acceptance criteria

  • Each system-prompt suggestion in the workshop results is dispositioned: adopted (with the prompt change landed or linked), declined with reasoning, or spun out as its own issue
  • The disposition is recorded on this issue so the workshop findings do not sit as an unowned report

Activity

  1. dezren39 commented on Nov 6, 2026

    @dezren39
    Author

    Gap issues filed (2026-11-06, per Drew's order)

    Every gap from this workshop now has its own issue in this repo — one per finding, each carrying the workshop evidence and an acceptance sketch:

    1. Question-tool stall (VERY HIGH priority): [priority: very high] bug: subsession question call stalls child AND foreground parent silently — no notification, and retry machinery re-asks the identical question #1883 — subsession question stalls child + foreground parent silently; retry machinery re-asks the identical question.
    2. Question escalation to the parent (feature, Drew): feat: question escalation — a subsession's question should be answerable by its parent session, passing up the stack if unanswered #1884 — parent session answers a subsession's question; unanswered questions pass up the stack to root/client.
    3. API launch mode disabling question (feature, Drew): feat: API launch mode that disables the question tool entirely for detached / non-interactive sessions #1885 — detached/non-interactive launch mode where question is unavailable; inherited by children.
    4. No in-session model switch: gap: no in-session model switch — expose Session.switchModel in-session, or a switch-request path to the parent/launcher #1886 — expose Session.switchModel in-session, or a switch-request path, or document the limit.
    5. Base system prompt silence: docs/prompts: base system prompt is silent on the entire subsession machinery #1887 — system.txt mentions none of the subsession machinery; add the delegation/messaging block.
    6. Family prompt contradictions: prompts: model-family prompts contradict each other on subagent spawning — and DeepSeek matches no family at all #1888 — gpt/gpt-astra forbid spawning, meta encourages, kimi neutral, DeepSeek matches no family; unify + define the unmatched-model fallback.
    7. subagent tool description gaps: docs: subagent tool description omits inheritance, background semantics, and the question-stall warning #1889 — inheritance, depth cap, background completion semantics, question-stall warning.
    8. question tool description warning: docs: question tool description invites use with no detached-context warning #1890 — no detached-context warning today.
    9. Serving-leaf invisibility: feat: serving leaf is invisible to the session — expose the resolved provider/model per step #1891 — a session cannot observe which leaf served a step (incl. 429 failovers).
    10. comms discoverability (fork side): feat/docs: cross-session messaging (comms) is undiscoverable from the fork's prompt and tool surface #1892 — cross-session messaging works but nothing in the prompt/tool surface points at it.

    Excluded by scope: suggestion 4 in this issue (the stale fleet instructions file, developing-today/code .opencode/instructions/subagents.md) is code-repo work and is being handled there, not filed here.

  2. added
    kind/docsDocumentation improvement or addition
    source/humanFiled directly by a human
    priority/lowNice to have; do when convenient
    effort/sSmall: expected under 2 hours of work
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 7, 2026
  3. dezren39 commented on Nov 7, 2026

    @dezren39
    Author

    Intake review

    Notes: this is a measured workshop report (8 sessions, production server) rather than a defect — its value is the per-question findings and the prompt suggestions at the end. Intake found no defect to validate as still-live; the 'work' is triage of its suggestions. Risks: prompt changes made without the workshop's measurement setup cannot be compared against its baselines. Suggested approach: disposition the suggestions in one pass in the main chat or a lane, and re-run the ping/nesting probes only for adopted changes.

    — Fleet monitor intake (repo-fleet-monitor, 2026-11-07 run): acceptance criteria appended to the issue body, labels normalized (kind/docs, source/human, priority/low, effort/s, status/needs-triage, meta/enriched), marked meta/enriched. No merged PR or commit was found this run that explicitly resolves this issue, so it stays open.

  4. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    and removed on Nov 7, 2026
  5. dezren39 commented on Nov 8, 2026

    @dezren39
    Author

    Triage: DISPOSITIONED — every suggestion spun out; closing this report issue.

    Fresh verification (2026-11-07, current labels/state read per issue): all ten gap issues filed 2026-11-06 per Drew's order exist and carry the work; none of the findings sits unowned.

    Suggestion-by-suggestion disposition (acceptance criterion 1 — "adopted, declined with reasoning, or spun out as its own issue"):

    1. Base-prompt delegation/messaging block → docs/prompts: base system prompt is silent on the entire subsession machinery #1887 (OPEN, task/small-fix, queue/queued).
    2. Reconcile family prompts (gpt/gpt-astra veto vs meta/kimi) → prompts: model-family prompts contradict each other on subagent spawning — and DeepSeek matches no family at all #1888 (OPEN, task/small-fix, queue/queued).
    3. DeepSeek entry / unmatched-model fallback in optimize.ts → prompts: model-family prompts contradict each other on subagent spawning — and DeepSeek matches no family at all #1888 (same issue; its verified scope includes "no deepseek entry and no unmatched fallback").
    4. Fleet instructions file (developing-today/code .opencode/instructions/subagents.md) → not filed here by scope — code-repo work, handled there (per the 2026-11-06 gap-filing comment on this issue).
    5. comms discoverability/documentation → feat/docs: cross-session messaging (comms) is undiscoverable from the fork's prompt and tool surface #1892 (CLOSED).
    6. In-session model switch (or documented limit) → gap: no in-session model switch — expose Session.switchModel in-session, or a switch-request path to the parent/launcher #1886 (OPEN, queue/queued).
    7. Serving-leaf visibility → feat: serving leaf is invisible to the session — expose the resolved provider/model per step #1891 (CLOSED).
    8. Question stall / relay to parent → [priority: very high] bug: subsession question call stalls child AND foreground parent silently — no notification, and retry machinery re-asks the identical question #1883 (OPEN, task/fix, priority/high, queue/queued), feat: question escalation — a subsession's question should be answerable by its parent session, passing up the stack if unanswered #1884 (OPEN, design-first, queue/queued), feat: API launch mode that disables the question tool entirely for detached / non-interactive sessions #1885 (OPEN, task/triage, queue/queued).

    Related Q7 tool-description findings: #1889 (subagent tool description gaps — CLOSED) and #1890 (question tool detached-context warning — OPEN, queue/parked, work/assigned).

    With the disposition recorded here (criterion 2) and every implementation item owned by a queued spin-out, this workshop report is closed; the spin-outs carry the remaining work.

  6. removed
    task/triageQueue packet kind: verify, reproduce, and route this item
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedA queue worker has claimed the task for this item
    work/assignedAn agent holds this item via the queue (claimed task)
    on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/sSmall: expected under 2 hours of workkind/docsDocumentation improvement or additionmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/lowNice to have; do when convenientsource/humanFiled directly by a human

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions