Background fallback retry notifications were queued as bare internal user messages, so OpenCode could treat the notification as a new default-agent turn. Reuse the same parent prompt context resolver used by completion notifications for retrying and retry-ready wakes, and pin regression coverage for Hephaestus parent sessions plus missing-context fallbacks.
When a background subagent emits [ALL BACKGROUND TASKS COMPLETE], the
plugin queues a parent-wake that ultimately calls
dispatchInternalPrompt against the parent session. If the user submits
a new prompt inside the ~250 ms post-dispatch hold window, both writes
land on the same OpenCode session-storage file at the same instant.
OpenCode's @parcel/watcher (which the plugin itself does not depend on,
but does indirectly trigger) batches those events into a TSFN callback
and dispatches them into a JS env that the renderer has just torn down
because the session view re-mounted around the user's new message ->
napi_fatal_error / SIGABRT on macOS arm64. Removing the plugin removes
the parent-wake, which is why removing OmO eliminates the crash.
Mitigation:
- Before flushPendingParentWake calls dispatchInternalPrompt, inspect
the parent session's message tail. If the most recent message is a
user message added inside PARENT_WAKE_USER_MESSAGE_IN_PROGRESS_WINDOW_MS
(default 2_000 ms), reschedule instead of dispatching. The user's own
prompt will drive the model; queued notifications will be re-flushed
on the next idle.
- Best-effort unref() of the long-lived pending-retry and dispatched-
wake bookkeeping setTimeouts. They previously pinned the host event
loop and prolonged the teardown window during which the watcher race
can fire.
The new option userMessageInProgressWindowMs is wired through
BackgroundManager via a module-level constant and is independently
testable.
Regression test parent-wake-user-message-race.test.ts covers:
- fresh user message -> dispatch deferred
- latest message is assistant -> dispatch proceeds
- user message older than window -> dispatch proceeds
- window=0 disables the guard
This is a surface-level mitigation of the most-likely root cause from
the audit; a deeper fix (singleton guard against plugin
double-instantiation under @opencode-ai/plugin@local reload, dispose
lifecycle for OpenCode plugin reload) is out of scope here.
External review on PR #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)
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.
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.
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.
Replace the inlined parent-wake coalescing logic in manager.ts with delegation to the ParentWakeNotifier extracted in c1ccf8d09. The four timer Maps and the related methods now live in their own module with a narrow public API, while BackgroundManager retains the wiring point and the enqueue-callback bridge.
Closes HIGH-9 (step 2: integration)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Fixes BackgroundManager to launch promptAsync before invoking the blocking tmux callback.
- Adds regression test to ensure promptAsync is called before tmux callback.
Keep prompt reservations briefly after successful dispatch so rapid idle/message/error transitions cannot inject the same follow-up twice.
Route all production session prompt calls through the shared gate, restore skipped background resume state, release holds after abort/recovery paths, and preserve Ralph/ULW loop state when a dispatch is deferred.
Add regression coverage for session routing, static prompt route auditing, team-mode live messaging, model suggestion retries, call-omo-agent reuse, background parent wakes, runtime fallback, compaction recovery, Atlas, and Ralph/ULW loops.
When many background tasks complete in rapid succession while the parent
session is idle, each completion fired its own promptAsync call, stacking
N consecutive `<system-reminder>` user messages with no assistant turn
between. Hyperplan + many parallel explore subagents made this very
visible to the user.
Route the idle-path through the existing pendingParentWakes queue with a
100ms debounce window. Notifications arriving during the debounce join
the same batch, the 150ms settle window also coalesces newcomers, and a
single batched prompt fires to the parent. Busy-path semantics are
unchanged (still 1s retry).
Prior attempts (1c05c60dc, ea55c385b, a337635e3) all coalesced only the
busy-defer path, leaving the idle-immediate-send path uncoalesced.
Sync the PR branch with the newest dev branch and resolve the new import-level conflicts in background-agent manager and runtime-fallback tests. Preserve both the delegated bootstrap coverage from this branch and the newer upstream test utilities and runtime wiring changes, then re-verify the affected delegated fallback suites and typecheck.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The circuit breaker in manager.ts uses recordToolCall to detect when a subagent gets stuck repeating identical tool_use blocks. It passed partInfo.state?.input as the tool-input signature. When a model (Kimi K2.6 in the reporter's case) emits duplicate tool_use parts faster than the tool actually starts running, state.input is still null, so loop-detector falls back to the bare 'tool::__unknown-input__' signature. As soon as one part has state.input populated (next event), the signature flips to 'tool::{actual-args}' and the consecutive counter resets to 1, repeatedly. The breaker never reaches its 20-call threshold.
Add a top-level input?: Record<string, unknown> field to the local MessagePartInfo interface and prefer state.input when present, falling back to the part's own input when state is still pre-running. The OpenCode part payload carries the tool input as soon as the tool_use block is generated, so this fallback restores signature stability across the model's repeated emissions.
Verification: added 2 regression tests in manager-circuit-breaker.test.ts. Test 1 (reproduce) emits 20 part.updated events with only top-level input and asserts the task is cancelled by the breaker — fails before the fix, passes after. Test 2 confirms that when state.input IS present, it still wins over the top-level input (precedence preserved). All 10 manager-circuit-breaker tests pass, all 20 loop-detector tests pass, typecheck clean.
Handle OpenCode session events that carry the session ID under properties.info.id or properties.info.sessionID so background tasks and continuation hooks do not miss idle/error/delete events.
Add regression coverage for nested session.idle events completing background tasks and waking continuation hooks.
Sync the PR branch with the latest dev branch and resolve the remaining conflict in sync-task.test.ts while preserving both the new upstream poll-recovery coverage and this branch's delegated bootstrap cleanup and isolation coverage. Re-verified the affected delegated fallback suites and typecheck after the merge resolution.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Reconcile the latest dev branch changes with the delegated child-session fallback work. Preserve the upstream background-agent updates while keeping the delegated bootstrap cleanup and compatibility wiring fixes intact, then re-verify the affected regression suites and typecheck.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
MessageAbortedError/worker shutdown could race with scheduled removeTask, leaving background_output's manager.getTask returning 'Task not found' even though the task had completed cleanly.
Fix: add completedTaskArchive (max 500, FIFO eviction). On removeTask, archive non-running/pending tasks with sessionId. getTask falls back to archive on active-map miss. addTask clears stale archive entries on re-registration.
Fixes#3895
Sub-agent sessions created via the task tool or call_omo_agent tool were
being created without a model. The resolved model was only passed as a
promptAsync body override, which opencode core ignores, causing fallback
to the client's system default model (often a reasoning model that hangs).
OpenCode's session.create API supports model: { id, providerID, variant }
at creation time. This fix ensures the resolved category/agent model is
passed during session creation across all paths:
- createSyncSession() (delegate task / category-based)
- createOrGetSession() (call_omo_agent / direct agent calls)
- BackgroundManager.startTask() (background tasks)
This guarantees each sub-agent session is created with the correct model
regardless of whether promptAsync honors its model override.
Capture delegated child-session retry context before the first prompt so fallback recovery still works when session history is empty. Align background and sync launch paths around the same bootstrap contract, clear session-scoped fallback state on every terminal path, and lock the behavior with regression coverage for first-prompt retries, exhaustion, isolation, and cleanup.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Agent names like \hephaestus\ cause 'Agent not found' errors because
sanitizeSubagentType() was only called in subagent-resolver.ts but not
in the direct manager.launch() path or background-executor.ts.
- manager.ts: strip leading/trailing backslash/quote chars from input.agent
before validation so \hephaestus\ → hephaestus
- background-executor.ts: call sanitizeSubagentType(args.subagent_type)
instead of passing raw value to manager.launch()
- agent-display-names.ts: reuse sanitizeSubagentType in stripAgentListSortPrefix
- Add unit tests for all three fix points
Fixes: sessions dying with 'Agent not found: \hephaestus\'