OpenCode native subagent tracking - #2390
Conversation
Record how OpenCode's task tool exposes child sessions to a plugin and how the current integration mishandles them, ahead of implementing task records for OpenCode. Captured live against OpenCode 1.18.30 with a probe plugin planted beside Entire's own: tool.execute.before(task) on the parent, session.created for the child carrying parentID, the parent's task part binding callID to the child session ID in state.metadata, the child's full turn, then tool.execute.after(task) with the same metadata. The child is a real session that `opencode export` returns with exact per-message tokens. Today the plugin treats the child as a new top-level session: the checkpoint lands on the child, the parent logs "no files modified", and the parent's hooks re-fire mid-turn. The one-pager proposes the event mapping and lists the gaps (background tasks, task_id resumption). The probe script builds an isolated repo, links the global plugin deps when the version matches, records every plugin signal to events.jsonl, and prints a timeline plus Entire's view after a commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M297724RX5YB0M87H3CW5XN3
…y children Add --scenario to the probe (single, concurrent, readonly) and record the two extra live runs. Two children launched in one message keep distinct callID→child bindings through the task part metadata, and a read-only explore child emits the same signals with no file parts. Both still surface today as separate top-level Entire sessions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29A38G9K2ZV6X1JGYV7VV81
…nt tracking One task record per `task` tool call on the parent session, keyed on the call ID, with the child's exported transcript declared and its exact tokens attached. The plugin learns child sessions from parentID and stops forwarding their lifecycle, fires subagent-start from the parent's running task part and subagent-stop from tool.execute.after. Two generic lifecycle changes: Event.DeferredCompletion records an in-flight marker at start, and event-files-only capture is keyed on transcript unavailability rather than on CompletionWithoutLaunch, which leaves Copilot unchanged. Plan reviewed chunk by chunk against the code: mock analyzer agent shape, macOS realpath normalization in tests, dupl-safe launch-marker helper, the committed dogfood plugin copy, GetSessionState's nil-nil contract, and the existing CopyTranscriptToEntireTmp helper. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29BBH8RHNJE2DTC115VK365
docs/superpowers is gitignored on purpose — the Copilot and Codex planning documents there were never committed — and the previous commit force-added these two against that convention. Untrack them; they stay on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29BF09G4XSKZGSMR729JT0X
…apes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mise run lint runs shellcheck at its default level, which flags the A && B || C idiom (SC2015) that the probe used for its PASS/WARN lines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29DM6V90DK7BQ0PF8XEVATG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the HookNameSubagentStart/HookNameSubagentStop constants and the subagentStartRaw/subagentStopRaw payload types the plugin will send for OpenCode's task tool. Not yet wired into ParseHookEvent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The parent's `task` tool part binds the tool call ID to the child session ID before the child finishes, so subagent-start records a DeferredCompletion marker rather than a normal launch; completion arrives separately from subagent-stop (tool.execute.after). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…script subagent-stop fires from tool.execute.after, once, at true completion, so it is the authoritative final capture regardless of whether the start hook was ever seen. It exports the child session, declares the transcript path on the event, and computes its token usage; an export failure degrades the record (SubagentTranscriptUnavailable) rather than dropping the completion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t comments Collapse subagentStartRaw/subagentStopRaw into one subagentRaw type and extract parseSubagentPayload (read + validate + resolve parent transcript path) so both ParseHookEvent cases only build their event. Sharpen the SubagentTranscriptUnavailable doc comment and the eventFilesOnly comment in handleSubagentStopFinal to cover both causes (Copilot CLI's contract vs. OpenCode's failed fetch), and correct the attachSubagentTranscript comment: OpenCode does implement TranscriptFetcher, but neither the SessionEnd sweep nor condensation calls it, so a failed export here permanently loses a transcript that still exists in OpenCode's store. Note the follow-up (lazy FetchTranscript at condensation). Update AGENT.md's proposed mapping table to match what was actually implemented and drop the now-false "single-signal agent" caveat. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…session Task launches spawn a real child OpenCode session, and the plugin previously treated it like a new user session — firing session-start/turn-start/turn-end for it and putting the checkpoint on the child instead of the parent. Learn child session IDs from parentID on session.created/session.updated and guard the event switch (and the one-time context injection) so a child never fires lifecycle hooks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fire subagent-start from the parent's task tool part once it reaches running with the child session ID bound in state.metadata.sessionId — the first signal that safely joins the tool call to its child, since tool.execute.before has no child ID yet and session.created events for sibling tasks can interleave. Fire subagent-stop synchronously from tool.execute.after on true completion (skipping background tasks and a child's own nested task calls), since `opencode run` exits on the parent's idle right after and an async hook would be killed before finishing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…adata
Code review fixes on the child-session tracking added for subagent hooks:
- Gate the parentID-learning line on event.type.startsWith("session.") —
AssistantMessage.parentID names the parent USER MESSAGE, so an unguarded
check added a msg_… id to childSessions on every assistant message.updated,
growing the set unboundedly.
- Also learn child identity from the two places that positively name it: the
parent's task part (state.metadata.sessionId) and tool.execute.after's
output.metadata.sessionId, so a child whose own session.created this
process never saw (resumed, or after a restart) is still suppressed.
- Stop clearing childSessions/announcedTasks on session.deleted: a child's
own deletion never reaches that case (the guard returns first), so the
clear only ever fired for an unrelated top-level session, which would drop
a live child back to top-level mid-task. The sets now live for the process
and are cleared only on server.instance.disposed.
- Reuse the hoisted props/info in the switch cases instead of re-deriving
them, and drop the session.status case's shadowing local `props`.
- Wrap tool.execute.after's body in try/catch, matching the event handler.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M29H9HHNAQZHQZK3SDZT901D
Some models' write tool refuses to create parent directories, and the child then asks for permission instead of writing, which leaves nothing to commit and skips the checkpoint half of the probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29HBFAMXZKR7TQ7PND9EKJP
Adds SimulateOpenCodeSubagentStart/Stop to the OpenCode hook runner and TestEnv, and a new integration test driving the real hook binary: subagent-start leaves an in-flight task record, subagent-stop completes it with the child's exported files and exact tokens, a stop with no prior export leaves the record transcript-unavailable, the child never gets its own session state, and the commit materializes task.json and the child's transcript under the parent checkpoint's tasks/ subtree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29HK5E0R20JAN74PJ3RN6ZW
Extends the copilot-cli task-record assertion to opencode, and adds a session-count check that the child never becomes a session of its own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Documents the shipped subagent-start/subagent-stop hooks in the agent guide's event table and "declaring a subagent transcript" section, adds an OpenCode Producers entry to sessions-and-checkpoints.md, and updates the OpenCode AGENT.md one-pager from "proposed" to "implemented": the mapping table, the plugin-side suppression rationale (including that a TUI-driven child session is fully suppressed too), and a new Gaps note about the macOS unresolved-symlink path-normalization limitation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s, tighten docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29JPH73P7SG607MDK7SZZ9J
There was a problem hiding this comment.
🟡 Changes recommended
One critical and four moderate findings remain unresolved, along with a documentation nit.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds native OpenCode subagent tracking, preserving child transcripts and token usage as task records on the parent session.
Changes:
- Adds plugin hooks and child-session suppression.
- Exports transcripts and records exact token usage.
- Extends shared lifecycle handling for deferred and unavailable completions.
- Adds integration, E2E, documentation, and probe coverage.
Open findings: Critical (1 vote): process-wide call-ID guards can suppress new sessions. Moderate (1 vote): the embedded plugin has the same issue. Moderate (3 votes): staged export read failures can leave records incomplete. Moderate (1 vote): token-calculation failures have the same failure mode. Moderate (1 vote): resumed child sessions can produce cumulative per-call records. Nit (1 vote): documentation incorrectly promises recursive nested-task mapping.
File summaries
| File | Reviewed change |
|---|---|
e2e/tests/subagent_commit_flow_test.go |
Verifies one parent session and materialized task records. |
docs/architecture/sessions-and-checkpoints.md |
Documents OpenCode task-record lifecycle. |
docs/architecture/agent-guide.md |
Updates agent lifecycle and transcript guidance. |
cmd/entire/cli/lifecycle.go |
Adds deferred task markers and transcript-aware final capture. |
cmd/entire/cli/lifecycle_test.go |
Tests deferred completion and declared-transcript capture. |
cmd/entire/cli/integration_test/opencode_subagent_test.go |
Covers native OpenCode task-record integration. |
cmd/entire/cli/integration_test/hooks.go |
Adds OpenCode subagent hook test helpers. |
cmd/entire/cli/agent/opencode/types.go |
Defines OpenCode hook and export payload types. |
cmd/entire/cli/agent/opencode/lifecycle.go |
Parses hooks, exports child transcripts, and attaches tokens. |
cmd/entire/cli/agent/opencode/lifecycle_test.go |
Tests OpenCode lifecycle and export behavior. |
cmd/entire/cli/agent/opencode/hooks.go |
Registers OpenCode lifecycle hooks. |
cmd/entire/cli/agent/opencode/hooks_test.go |
Tests plugin hook behavior. |
cmd/entire/cli/agent/opencode/entire_plugin.ts |
Tracks tasks and suppresses child sessions. |
cmd/entire/cli/agent/event.go |
Adds deferred-completion and transcript-availability fields. |
.opencode/plugins/entire.ts |
Updates the dogfood plugin implementation. |
Review details
Suppressed comments (4)
cmd/entire/cli/agent/opencode/AGENT.md:163
- These lines claim nested task mapping is recursive, but the plugin returns early for every event whose session is already in
childSessions; a task started by a child therefore emits no lifecycle hooks and cannot be mapped recursively. Please document nested subagents as currently suppressed/out of scope, or implement child-to-grandchild propagation before promising recursive support.
- **Nested subagents** are off by default (`subagent_depth: 1`); when enabled,
`parentID` chains and the same mapping applies recursively.
cmd/entire/cli/agent/opencode/entire_plugin.ts:264
announcedTasksis process-scoped and is only cleared on server disposal, but OpenCodecallIDs are not globally unique. If a new parent session reuses a call ID from an earlier session, this guard suppresses itssubagent-start, so no deferred/in-flight task record is created (the stop path can only reconstruct it later). Key this set by the parent session ID plus call ID, or clear it when switching parent sessions.
if (part.type === "tool" && part.tool === "task" && part.callID &&
part.state?.status === "running" && part.state?.metadata?.sessionId &&
!announcedTasks.has(part.callID)) {
announcedTasks.add(part.callID)
childSessions.add(part.state.metadata.sessionId)
cmd/entire/cli/agent/opencode/lifecycle.go:265
- A token-calculation failure has the same incomplete-record failure mode as the read failure above: the declared transcript remains present, so final capture still attempts analysis and can fail rather than completing the task without token metadata. Set
SubagentTranscriptUnavailablebefore returning so this path follows the documented fail-open behavior.
usage, err := a.CalculateTokenUsage(data, 0)
if err != nil {
logging.Warn(logCtx, "opencode: could not compute subagent token usage",
slog.String("subagent_id", event.SubagentID), slog.String("error", err.Error()))
return
cmd/entire/cli/agent/opencode/lifecycle.go:254
- OpenCode's
task_idresumption can reuse one child session across multiple call IDs, but this path exports from the beginning and calculates tokens from offset zero. A later call therefore receives the cumulative earlier transcript/files and token total under a new task record, which is not exact per-call tracking. Please either slice export and token usage at the call boundary or model the reused child as one multi-turn record rather than advertising one exact record per call.
path, err := a.fetchAndCacheExport(ctx, event.SubagentID)
if err != nil {
logging.Warn(logCtx, "opencode: could not export subagent transcript; completing task without it",
slog.String("session_id", event.SessionID),
slog.String("tool_use_id", event.ToolUseID),
slog.String("subagent_id", event.SubagentID),
slog.String("error", err.Error()))
event.SubagentTranscriptUnavailable = true
return
}
event.SubagentTranscriptPath = path
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (part.type === "tool" && part.tool === "task" && part.callID && | ||
| part.state?.status === "running" && part.state?.metadata?.sessionId && | ||
| !announcedTasks.has(part.callID)) { | ||
| announcedTasks.add(part.callID) |
| data, err := a.ReadTranscript(path) | ||
| if err != nil { | ||
| logging.Warn(logCtx, "opencode: could not read exported subagent transcript for token usage", | ||
| slog.String("subagent_id", event.SubagentID), slog.String("error", err.Error())) | ||
| return | ||
| } |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Entire-Checkpoint: 01M29KBP9YH5JWQY7NATB9C7GM
https://entire.io/gh/entireio/cli/trails/1316
Summary
tasktool subagents as durable task records on the parent session, one record per task call, with the child's exported transcript and exact token usageEvent.DeferredCompletionrecords an in-flight marker at a fully-identified start, and event-files-only capture is keyed onSubagentTranscriptUnavailablerather thanCompletionWithoutLaunch(Copilot sets both, so it is unchanged)cmd/entire/cli/agent/opencode/AGENT.md) capturing the verified OpenCode 1.18.30 contractHow it works
The embedded plugin learns child sessions from
parentIDonsession.*events and from the parent's task-part metadata, and forwards no lifecycle hooks for them. It firessubagent-startfrom the parent'staskpart once it isrunningwithstate.metadata.sessionId, andsubagent-stopsynchronously fromtool.execute.after. The Go adapter exports the child withopencode export, declares that file as the transcript, attaches its token usage, and emits aFinal+CompletionWithoutLaunchSubagentEnd; the shared final capture extracts files from the declared transcript and completes the record exactly once. Condensation materializestasks/<callID>/{task.json, agent-<child>.jsonl}under the parent checkpoint.Verified
google/gemini-2.5-flash-lite; the Anthropic key stored in OpenCode on this machine is rejected and Gemini Flash hit its free-tier quota): one Entire session, checkpoint on the parent titled with the user's prompt,tasks/<callID>/task.json+ child transcript materialized, exact tokens, two concurrent children kept distinct with disjoint files.TestOpenCodeSubagentTaskRecord), and Vogon canary pass.mise run lintclean.TestSubagentCommitFlowe2e now asserts a task record and a single session; not run in this PR.Judgment call to confirm
Child sessions are suppressed plugin-side, so a user who opens a child session directly in the OpenCode TUI and drives it by hand is not tracked at all (no session, no task record). That is deliberate for v1 and documented in
AGENT.md; flagging it here so a human can move it.Known gaps
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS) are out of scope and ignored.SubagentTokensaggregate is a follow-up; record-level tokens are exact./var/folders/...vs git's/private/var/...) is dropped from the record'sfilesby the shared normalizer; the parent's turn-end still attributes the file. Agent-agnostic.opencode exportat stop time completes the record as transcript-unavailable; a lazy fetch at condensation is a follow-up.TestRunStatus_CheckpointPushDisabledDestinationsfails onorigin/maintoo (unrelated).Claude Sonnet 5co-author trailer; a squash merge normalizes it.🤖 Generated with Claude Code
Note
Medium Risk
Changes checkpoint/session attribution for OpenCode subagents and shared subagent final-capture semantics; failures degrade to transcript-unavailable records rather than blocking the parent session.
Overview
Adds native OpenCode
tasksubagent tracking so child sessions no longer become separate Entire sessions or steal checkpoints from the parent.The embedded plugin (
entire.ts) trackschildSessionsfromparentIDand task metadata, suppresses all child lifecycle hooks (including context injection), and fires syncsubagent-start(parent task partrunningwithsessionId) andsubagent-stop(tool.execute.afterfor foregroundtask).The OpenCode Go adapter parses those hooks, sets
DeferredCompletionon start andFinal+CompletionWithoutLaunchon stop, **opencode export**s the child for transcript path and token usage, and validates subagent IDs.Shared lifecycle gains
recordDeferredTaskLaunch/Event.DeferredCompletion(in-flight task marker, no pre-task baseline) and keys event-files-only final capture onSubagentTranscriptUnavailableinstead ofCompletionWithoutLaunchso OpenCode can still scan a declared child export.Docs, probe script, integration/e2e tests, and plugin template tests cover the contract.
Reviewed by Cursor Bugbot for commit a1bc976. Configure here.