fix(delegate-task): start child prompts reliably#4074
Conversation
Preserve delegated child prompt/bootstrap metadata for early runtime fallback before OpenCode has persisted the first user turn. Bind prompt gate calls to the SDK session receiver and keep completed background task lookup visible across plugin manager instances.
There was a problem hiding this comment.
No issues found across 11 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Although the AI review found no issues and all tests pass, the changes involve significant additions (global task registry, session bootstrap state) and modifications to fallback logic, method binding, and cleanup paths, making it impossible to guarantee zero regressions with 100% certainty,...
Re-trigger cubic
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Requires human review: Given the extensive changes across 13 files including core session management, background task registry, runtime fallback, and prompt binding, the risk of subtle regressions in these interdependent subsystems is too high to meet the 100% no-regression guarantee required by your approval criteria.
Re-trigger cubic
Add optional system and tools fields to DelegatedChildSessionBootstrap so callers can stash the original delegated context alongside retry parts. Backward compatible - existing callers stay unchanged.
…trap retry When the first prompt fails before any durable user message persists, runtime fallback retry was rebuilding the request from parts alone and losing the delegated agent system prompt and tool gates. Now it threads bootstrap.system and bootstrap.tools into the retry body alongside the captured retry parts, so the retried prompt keeps the same scope as the initial delegate launch.
…ent across retries Three coupled gaps surfaced after the initial spawn fix: 1. fallback-retry-handler dropped task.skillContent and task.sessionPermission when rebuilding LaunchInput, so the retried background task lost the delegated system prompt and question-deny permission rule. 2. manager.startTask never bound the child sessionID to the resolved agent via setSessionAgent, leaving runtime fallback and other hooks with no idea which agent owned the new child session. 3. The fallback-to-general path in spawner.ts rebuilt the prompt body without going through buildFallbackBody, so bootstrap state, session tools, and session agent updates drifted apart. Persist skillContent and sessionPermission on BackgroundTask, bind setSessionAgent/updateSessionAgent at session creation and on fallback, and route the FALLBACK_AGENT retry through buildFallbackBody so the prompt body, bootstrap tools, and session registries all agree.
… prompt dispatch call_omo_agent sync path created the child OpenCode session and went straight into promptAsync without registering child session agent, session tools, or bootstrap state. If first dispatch failed before any durable user message persisted, runtime fallback could not reconstruct the original prompt or the agent identity for that child session. Bind setSessionAgent and setSessionTools to the child session id with the same tool restrictions that the prompt body sends, register a delegated child session bootstrap with the prompt text, fallback chain, and prompt tools, then clean bootstrap + session tools in finally for sessions this call created.
… and prompt dispatch Two call sites built the sync delegate tool gate independently: sync-prompt-sender's prompt body construction and sync-task's bootstrap registration. Drift between them would let bootstrap claim one tool set while the actual prompt sent a different one. Extract buildSyncPromptTools and route both call sites through it so the registered bootstrap and the dispatched prompt always agree.
Tests that mock an AgentFactory were using an `as AgentFactory` cast and a separate mutation of `mockFactory.mode` to satisfy the type. Replace with Object.assign so the factory type is constructed correctly without casts. Also type the empty discoveredSkills fixture so its element type is inferred from the function signature instead of collapsing to never[].
There was a problem hiding this comment.
2 issues found across 19 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/background-agent/manager.ts">
<violation number="1" location="src/features/background-agent/manager.ts:761">
P2: Session agent state is set for new child sessions but not cleared in pre-start cancellation/binding-failure cleanup paths, which can leave stale session→agent mappings.</violation>
</file>
<file name="src/features/background-agent/spawner.ts">
<violation number="1" location="src/features/background-agent/spawner.ts:128">
P2: Store the normalized agent in session state to avoid mismatches with the agent actually used for prompt dispatch.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…bort and normalize stored agent Two adjacent gaps cubic flagged on the previous diff: 1. spawner.startTask stored input.agent (potentially prefixed with sort marker and ZWSP) in setSessionAgent, but the prompt body used the stripped/normalized form. The session-agent registry therefore did not match what promptAsync actually dispatched. Capture the normalized agent once at the top of startTask and use it for setSessionAgent plus the launch log lines. 2. manager.startTask wrote setSessionAgent(sessionID, input.agent) before the cancelled and stale-attempt cleanup branches, but those branches only cleared subagentSessions and the delegated bootstrap. The session->agent mapping survived as orphan state after an aborted launch. Call clearSessionAgent inside both early-return paths so nothing remains tied to a session we just aborted. Adds focused tests for both: spawner persistence parity with promptAsync and manager cancellation cleanup leaving getSessionAgent undefined.
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Requires human review: The changes introduce new global state (delegated child session bootstrap, process-local task registry) and alter session agent/tools management across multiple files; while tests pass, the risk of subtle regressions in these stateful interactions prevents 100% certainty.
Re-trigger cubic
… sync cleanup External review on PR code-yeongyu#4074 noted that adding setSessionAgent for child sessions left sessionAgentMap holding entries after the session was deleted or after the sync call_omo_agent executor cleaned up other owned state. The map only grows; entries never get reused but they do accumulate across long-running plugin instances. Close both gaps: - BackgroundManager.handleEvent for session.deleted now calls clearSessionAgent for the deleted session id on both the early-return no-task branch and the cascade tail. This pairs with the existing clearDelegatedChildSessionBootstrap and SessionCategoryRegistry.remove so all owned session state is dropped together. - sync-executor finally for createdSessionForExecution now calls clearSessionAgent alongside the existing subagentSessions, syncSubagentSessions, and deleteSessionTools cleanup so sessions this executor created cannot leak their agent mapping. Adds focused tests: - BackgroundManager.handleEvent - session.deleted cascade > should clear session agent state for deleted sessions to prevent map leak - executeSync > registers child-session bootstrap and tracked prompt state before sync prompt dispatch (extended assertion for cleanup)
Summary
Fixes delegated OpenCode child sessions so
call_omo_agent/ delegate-task creates and starts the child prompt reliably instead of leaving an empty created session.Root Cause
session.errorafterpromptAsyncreturns, before the first user prompt is durably persisted.session.promptAsync/session.promptwithout binding the SDK receiver, which can break OpenCode SDK internals.background_output(bg_...)could miss completed tasks after a manager/plugin reload because completed task state only lived on the manager instance.Changes
Testing
bun test->6976 pass,1 skip,0 failbun run typecheck-> passbun run build-> pass96 pass,0 failsrc/features/background-agent/manager.test.tsafter registry-leak fix ->170 pass,0 failgit diff --check origin/dev..HEAD-> cleanManual QA
opencode servein an isolated tmux session using provider/model settings copied from~/.config/opencode/opencode.json, with the plugin loaded from this worktree.POST /session/{id}/prompt_asyncprompt asking Sisyphus to callcall_omo_agentwithsubagent_type="explore"andrun_in_background=true.bg_8a5d88adand child sessionses_1d03b6848ffe82CC66gQKBoaoF.background_outputreturndelegate-task-bootstrap-qa-apitopiafrom the child task./tmp/omo-delegate-task-fix-20260516T075417Z/scenarios-and-qa.mdIndependent Review
APPROVE.Summary by cubic
Fixes delegated child prompts so
delegate-taskreliably creates, starts, and retries child sessions, even when OpenCode errors before the first user message is saved. Also adds a process-local background task registry that survives manager reloads and preserves the original delegated context (system, tools, permission, agent) across retries.Bug Fixes
skillContent(system), tools, andsessionPermissionthrough runtime fallback and background retries; use bootstrap when history has no user prompt; clear bootstrap on success, error, cancel, retry, and shutdown.general; clear the session-agent mapping on pre-start abort and stale-attempt cleanup; keep session tools in sync; dispatchsession.promptAsync/session.promptwith the bound SDK receiver.call_omo_agentregisters bootstrap and session tools/agent before the first prompt, then cleans them up afterward; sharedbuildSyncPromptToolskeeps bootstrap and dispatch aligned.New Features
getTaskfalls back to the registry when not in memory.Written for commit d331861. Summary will update on new commits. Review in cubic