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.
- Replace all response.data ?? [] with (response.data ?? response)
pattern across 14 files to handle SDK array-shaped responses
- Normalize SDK parts in parts-reader.ts by injecting sessionID/
messageID before validation (P1: SDK parts lack these fields)
- Treat unknown part types as having content in
recover-empty-content-message-sdk.ts to prevent false placeholder
injection on image/file parts
- Replace local isRecord with shared import in parts-reader.ts
- isTodo: allow optional id to match Todo interface, preventing
todos without ids from being silently dropped
- messageHasContentFromSDK: treat unknown part types as empty
(continue) instead of content (return true) for parity with
existing storage logic
- readMessagesFromSDK in recover-empty-content-message-sdk: wrap
SDK call in try/catch to prevent recovery from throwing
- Make id field optional in all Todo interfaces (TodoInfo, Todo, TodoItem)
- Fix null-unsafe comparisons in todo-sync.ts to handle missing ids
- Add test case for todos without id field preservation
- All tests pass and typecheck clean
TaskToastManager entries were never removed when tasks completed via
error, session deletion, stale pruning, or cancelled with
skipNotification. Ghost entries accumulated indefinitely, causing the
'Queued (N)' count in toast messages to grow without bound.
Added toastManager.removeTask() calls to all 4 missing cleanup paths:
- session.error handler
- session.deleted handler
- cancelTask with skipNotification
- pruneStaleTasksAndNotifications
Closes#1866
OpenCode 1.2.0+ changed reasoning-delta and text-delta to emit
'message.part.delta' instead of 'message.part.updated'. Without
handling this event, lastUpdate was only refreshed at reasoning-start
and reasoning-end, leaving a gap where extended thinking (>3min)
could trigger stale timeout.
Accept both event types as heartbeat sources for forward compatibility.
OpenCode uses 'busy'/'retry'/'idle' session statuses, not 'running'.
The stale timeout guard checked for type === 'running' which never
matched, leaving all background tasks vulnerable to stale-kill even
when their sessions were actively processing.
Change sessionIsRunning to check type !== 'idle' instead, protecting
busy and retrying sessions from premature termination.
The stale detection was checking lastUpdate timestamps BEFORE
consulting session.status(), causing tasks to be unfairly killed
after 3 minutes even when the session was actively running
(e.g., during long tool executions or extended thinking).
Changes:
- Reorder pollRunningTasks to fetch session.status() before stale check
- Skip stale-kill entirely when session status is 'running'
- Port no-lastUpdate handling from task-poller.ts into manager.ts
(previously manager silently skipped tasks without lastUpdate)
- Add sessionStatuses parameter to checkAndInterruptStaleTasks
- Add 7 new test cases covering session-status-aware stale detection
Both parent-session-notifier.ts and notify-parent-session.ts now include
parentTools in the promptAsync body, ensuring tool restrictions are
consistently applied across all notification code paths.
Pass parentTools from session-tools-store through the background task
lifecycle (launch → task → notify) so that when notifyParentSession
sends promptAsync, the original tool restrictions (e.g., question: false)
are preserved. This prevents the Question tool from re-enabling after
call_omo_agent background tasks complete.
Call setSessionTools(sessionID, tools) before every prompt dispatch so
the tools object is captured and available for later retrieval when
background tasks complete.