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.
Tasks with no progress.lastUpdate were silently skipped in
checkAndInterruptStaleTasks, causing them to hang forever when the model
hangs before its first tool call. Now falls back to checking startedAt
against a configurable messageStalenessTimeoutMs (default: 10 minutes).
Closes#1769
Reasoning/thinking models (Oracle, Claude Opus) were being killed by the
stale timeout because lastUpdate was only refreshed on tool-type events.
During extended thinking, no tool events fire, so after 3 minutes the
task was incorrectly marked as stale and aborted.
Move progress initialization and lastUpdate refresh before the tool-type
conditional so any message.part.updated event (text, thinking, tool)
keeps the task alive.
Records task history at 6 status transitions (pending, running×2, error,
cancelled, completed). Exports TaskHistory from background-agent barrel.
Passes backgroundManager and sessionID through compaction hook chain.
In-memory tracker that survives BackgroundManager's cleanup cycles.
Records agent delegations with defensive copies, MAX 100 cap per parent,
undefined-safe upsert, and newline-sanitized formatForCompaction output.
Child session creation was injecting permission: { question: 'deny' } which
conflicted with OpenCode's child session permission handling, causing subagent
sessions to hang with 0 messages after creation (zombie state).
Remove the permission override from all session creators (BackgroundManager,
sync-session-creator, call-omo-agent) and rely on prompt-level tool restrictions
(tools.question=false) to maintain the intended policy.
Closes#1711
- fix(background): include "interrupt" status in all terminal status checks (3 files)
- fix(background): display "INTERRUPTED" instead of "CANCELLED" for interrupted tasks
- fix(cli): add error recovery grace period in poll-for-completion
- fix(lsp): use JSONC parser for config loading to support comments
All changes verified with tests and typecheck.
Update notification systems to display INTERRUPTED status.
Add interrupt handling to background_output tool (terminal status).
Add interrupt-specific status note to formatTaskStatus.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Change promptAsync catch blocks to set status = "interrupt" instead of "error".
This distinguishes prompt errors from stale timeouts (cancelled) and TTL expirations (error).
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Deny question prompts in CLI run mode since there's no TUI to answer them
- Inherit parent session permission rules in background task sessions
- Force deny questions while preserving other parent permission settings
- Add test coverage for permission inheritance behavior
🤖 Generated with assistance of OhMyOpenCode
Add permission: [{ permission: 'question', action: 'deny', pattern: '*' }]
to client.session.create() call to prevent background sessions from
asking questions that go unanswered, causing hangs.
- Change PROMETHEUS_PERMISSION bash from 'allow' to 'deny' to prevent unrestricted bash execution
- Prometheus is a read-only planner and should not execute bash commands
- The prometheus-md-only hook provides additional blocking as backup
When the main session is aborted while background tasks are running,
notifyParentSession() would attempt to call session.messages() and
session.prompt() on the aborted parent session, causing exceptions
that could crash the TUI.
- Add isAbortedSessionError() helper to detect abort-related errors
- Add abort check in session.messages() catch block with early return
- Add abort check in session.prompt() catch block with early return
- Add test case covering aborted parent session scenario
Fixes TUI crash when aborting main session with running background tasks.
When promptWithModelSuggestionRetry() fails, the session was not being aborted, causing the polling loop to wait forever for an idle state. Added session.abort() calls in startTask() and resume() catch blocks.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Add skipNotification option to cancelTask method
- Apply skipNotification to background_cancel tool
- Prevents unwanted notifications when user cancels via tool
* refactor(background-agent): optimize cache timer lifecycle and result handling
Ultraworked with Sisyphus
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* refactor(background-task): simplify tool implementation and expand test coverage
Ultraworked with Sisyphus
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(background-task): fix BackgroundCancel tool parameter handling
Correct parameter names and types in BackgroundCancel tool to match actual usage patterns. Add comprehensive test coverage for parameter validation.
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Unify slot ownership: processKey() owns the slot until task.concurrencyKey is set.
Previously, startTask() released the slot on errors (session.create catch,
createResult.error), but processKey() catch block didn't release, causing slot
leaks when errors occurred between acquire() and task.concurrencyKey assignment.
Changes:
- Remove all pre-transfer release() calls in startTask()
- Add conditional release in processKey() catch: only if task.concurrencyKey not set
- Add validation for createResult.data?.id to catch malformed API responses
This fixes 'Task failed to start within timeout' errors caused by exhausted
concurrency slots that were never released.
- Change session title from 'Task: {desc}' to '{desc} (@{agent} subagent)'
- Move session_id to structured <task_metadata> block for better parsing
- Add category tracking to BackgroundTask type and LaunchInput
- Add tests for new title format and metadata block
- Add stubNotifyParentSession implementation to stub manager's notifyParentSession method
- Add stubNotifyParentSession calls to checkAndInterruptStaleTasks tests
- Add messages mock to client mocks for completeness
- Fix timer-based tests by using real timers (fakeTimers.restore) with wait()
- Increase timeout for tests that need real time delays