Adds a configurable maxToolCalls limit (default: 200) that automatically
cancels background tasks when they exceed the threshold. This prevents
runaway subagent loops from burning unlimited tokens, as reported in #2571
where a Gemini subagent ran 809 consecutive tool calls over 3.5 hours
costing ~$350.
The circuit breaker triggers in the existing tool call tracking path
(message.part.updated/delta events) and cancels the task with a clear
error message explaining what happened. The limit is configurable via
background_task.maxToolCalls in oh-my-opencode.jsonc.
Fixes#2571
- MCP teardown race: add shutdownGeneration counter to prevent
in-flight connections from resurrecting after disconnectAll
- MCP multi-key disconnect race: replace disconnectedSessions Set
with generation-based Map to track per-session disconnect events
- MCP clients: check shutdownGeneration in stdio/http client
creators before inserting into state.clients
- BackgroundManager: call clearTaskHistoryWhenParentTasksGone after
timer-based task removal in scheduleTaskRemoval and notifyParentSession
- BackgroundManager: clean completedTaskSummaries when parent has
no remaining tasks
- Plugin dispose: remove duplicate tmuxSessionManager.cleanup call
since BackgroundManager.shutdown already handles it via onShutdown
Test expected timers only after allComplete, but H2 fix intentionally
decoupled per-task cleanup from sibling completion state. Updated
assertion to expect timer after individual task notification.
H3: cancelTask(skipNotification=true) now schedules task removal.
Previously the early return path skipped cleanup, leaking task objects
in this.tasks Map permanently. Extracted scheduleTaskRemoval() helper
called from both skipNotification and normal paths.
H2: Per-task completion cleanup timer decoupled from allComplete check.
Previously cleanup timer only ran when ALL sibling tasks completed. Now
each finished task gets its own removal timer regardless of siblings.
H1+C2: TaskHistory.clearAll() added and wired into shutdown(). Added
clearSession() calls on session error/deletion and prune cycles.
taskHistory was the only data structure missed by shutdown().
Tests: 10 pass (3 cancel + 3 completion + 4 history)
- Check response.error and !response.data after session.get() to fail closed
- Prevents unlimited spawning when SDK returns non-throwing error responses
- Added regression tests for SDK error and missing data scenarios
Implements question-denied session permission rules when creating child
sessions via background task delegation. This prevents subagents from
asking questions by passing explicit permission configuration during
session creation.
🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode
When a completed session is no longer returned by session.status(),
allStatuses[sessionID] is undefined. Previously this fell through to
a 'still running' log, leaving the task stuck as running forever.
Match the sync-session-poller pattern: only continue (skip completion
check) when sessionStatus EXISTS and is not idle. When undefined,
fall through to validateSessionHasOutput + checkSessionTodos +
tryCompleteTask, same as idle.
Align runtime defaults, tests, docs, and generated artifacts with the newer GPT-5.4 baseline. Keep think-mode and prompt-routing expectations consistent after the model version bump.
When a background task completes and the parent session is waiting for
user input, promptAsync() fails with an aborted error. Previously the
notification was silently dropped — lost forever.
Fix: queue the notification text in-memory on the BackgroundManager
when promptAsync fails with an aborted/idle error. On the user's next
message to that session, the queued notifications are injected into the
chat context before the agent sees the message.
- BackgroundManager: add pendingNotifications map + queuePendingNotification()
and injectPendingNotificationsIntoChatMessage() methods
- background-notification hook: add chat.message handler that calls injection
- chat-message.ts: wire backgroundNotificationHook.chat.message into the
message processing chain
- Add tests covering queue-on-abort and next-message delivery
The extracted handleSessionIdleBackgroundEvent was never imported by
manager.ts — dead code from incomplete refactoring (d53bcfbc). Replace
the inline session.idle handler (58 LOC) with a call to the extracted
function, remove unused MIN_IDLE_TIME_MS import, and add 13 unit tests
covering all edge cases.
- Revert getMessageDir to original join(MESSAGE_STORAGE, sessionID) behavior
- Fix dead subagentSessions.delete by capturing previousSessionID before tryFallbackRetry
- Add .unref() to process cleanup setTimeout to prevent 6s hang on Ctrl-C
- Add missing isUnstableAgent to fallback retry input mapping
- Fix process-cleanup tests to use exit listener instead of SIGINT at index 0
- Swap test filenames in compaction-aware-message-resolver to exercise skip logic correctly
Reorder tool permission spread so getAgentToolRestrictions() comes
last, allowing agent-specific restrictions to override defaults.
Fixes all 3 sites: task-starter.ts (startTask), manager.ts (startTask
and resume paths).
Previously, defaults like call_omo_agent:true would stomp agent
restrictions (e.g., explore's call_omo_agent:false) due to JS
spread semantics.