Skip to content

fix(delegate-task): start child prompts reliably#4074

Merged
code-yeongyu merged 9 commits into
devfrom
fix/delegate-task-spawn
May 16, 2026
Merged

fix(delegate-task): start child prompts reliably#4074
code-yeongyu merged 9 commits into
devfrom
fix/delegate-task-spawn

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented May 16, 2026

Copy link
Copy Markdown
Owner

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

  • OpenCode can emit session.error after promptAsync returns, before the first user prompt is durably persisted.
  • OMO runtime fallback then had no last user prompt to retry for a delegated child session.
  • The shared prompt gate also extracted session.promptAsync / session.prompt without 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

  • Bind OpenCode SDK prompt methods inside the shared prompt gate before dispatch.
  • Add delegated child-session bootstrap state used by runtime fallback only when session history has no persisted user prompt yet.
  • Register and clear bootstrap state for background and sync delegate-task child sessions.
  • Add a process-local background task registry so completed task output remains readable across manager reloads, with prompt redaction and bounded archive size.
  • Clear active registry entries on shutdown, while archiving terminal task statuses.

Testing

  • bun test -> 6976 pass, 1 skip, 0 fail
  • bun run typecheck -> pass
  • bun run build -> pass
  • Focused delegated/fallback/background suites -> 96 pass, 0 fail
  • Full src/features/background-agent/manager.test.ts after registry-leak fix -> 170 pass, 0 fail
  • git diff --check origin/dev..HEAD -> clean

Manual QA

  • Ran opencode serve in an isolated tmux session using provider/model settings copied from ~/.config/opencode/opencode.json, with the plugin loaded from this worktree.
  • Sent a real HTTP POST /session/{id}/prompt_async prompt asking Sisyphus to call call_omo_agent with subagent_type="explore" and run_in_background=true.
  • Observed background task bg_8a5d88ad and child session ses_1d03b6848ffe82CC66gQKBoaoF.
  • Observed background_output return delegate-task-bootstrap-qa-apitopia from the child task.
  • QA notes: /tmp/omo-delegate-task-fix-20260516T075417Z/scenarios-and-qa.md

Independent Review

  • External GPT-5.5 xhigh reviewer verdict: APPROVE.
  • Earlier blocker around active registry leakage on shutdown was fixed and re-reviewed.

Summary by cubic

Fixes delegated child prompts so delegate-task reliably 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

    • Preserve delegated launch context across retries: carry skillContent (system), tools, and sessionPermission through runtime fallback and background retries; use bootstrap when history has no user prompt; clear bootstrap on success, error, cancel, retry, and shutdown.
    • Bind and normalize the child session’s agent on creation; update on fallback to general; clear the session-agent mapping on pre-start abort and stale-attempt cleanup; keep session tools in sync; dispatch session.promptAsync/session.prompt with the bound SDK receiver.
    • call_omo_agent registers bootstrap and session tools/agent before the first prompt, then cleans them up afterward; shared buildSyncPromptTools keeps bootstrap and dispatch aligned.
  • New Features

    • Process-local background task registry across manager reloads:
      • Redacts prompts for active and completed entries; caps completed archive at 100.
      • Archives terminal tasks on shutdown and forgets active ones; getTask falls back to the registry when not in memory.

Written for commit d331861. Summary will update on new commits. Review in cubic

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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[].

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/features/background-agent/manager.ts
Comment thread src/features/background-agent/spawner.ts Outdated
…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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@code-yeongyu code-yeongyu merged commit 25d8054 into dev May 16, 2026
8 checks passed
@code-yeongyu code-yeongyu deleted the fix/delegate-task-spawn branch May 16, 2026 16:00
pull Bot pushed a commit to ehagerty/oh-my-openagent that referenced this pull request May 16, 2026
… 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant