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.
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
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
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
Track setTimeout timers in notifyParentSession using a completionTimers Map.
Clear all timers on shutdown() and when tasks are deleted via session.deleted.
This prevents the BackgroundManager instance from being held in memory by
uncancelled timer callbacks.
Fixes#1043
Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
* fix: prevent zombie processes with proper process lifecycle management
- Await proc.exited for fire-and-forget spawns in tmux-utils.ts
- Remove competing process.exit() calls from LSP client and skill-mcp-manager
signal handlers to let background-agent manager coordinate final exit
- Await process exit after kill() in interactive-bash timeout handler
- Await process exit after kill() in LSP client stop() method
These changes ensure spawned processes are properly reaped and prevent
orphan/zombie processes when running with tmux integration.
* fix: address Copilot review comments on process cleanup
- LSP cleanup: use async/sync split with Promise.allSettled for proper subprocess cleanup
- LSP stop(): make idempotent by nulling proc before await to prevent race conditions
- Interactive-bash timeout: use .then()/.catch() pattern instead of async callback to avoid unhandled rejections
- Skill-mcp-manager: use void+catch pattern for fire-and-forget signal handlers
* fix: address remaining Copilot review comments
- interactive-bash: reject timeout immediately, fire-and-forget zombie cleanup
- skill-mcp-manager: update comments to accurately describe signal handling strategy
* fix: address additional Copilot review comments
- LSP stop(): add 5s timeout to prevent indefinite hang on stuck processes
- tmux-utils: log warnings when pane title setting fails (both spawn/replace)
- BackgroundManager: delay process.exit() to next tick via setImmediate to allow other signal handlers to complete cleanup
* fix: address code review findings
- Increase exit delay from setImmediate to 100ms setTimeout to allow async cleanup
- Use asyncCleanup for SIGBREAK on Windows for consistency with SIGINT/SIGTERM
- Add try/catch around stderr read in spawnTmuxPane for consistency with replaceTmuxPane
* fix: address latest Copilot review comments
- LSP stop(): properly clear timeout when proc.exited wins the race
- BackgroundManager: use process.exitCode before delayed exit for cleaner shutdown
- spawnTmuxPane: remove redundant log import, reuse existing one
* fix: address latest Copilot review comments
- LSP stop(): escalate to SIGKILL on timeout, add logging
- tmux spawnTmuxPane/replaceTmuxPane: drain stderr immediately to avoid backpressure
* fix: address latest Copilot review comments
- Add .catch() to asyncCleanup() signal handlers to prevent unhandled rejections
- Await proc.exited after SIGKILL with 1s timeout to confirm termination
* fix: increase exit delay to 6s to accommodate LSP cleanup
LSP cleanup can take up to 5s (timeout) + 1s (SIGKILL wait), so the exit
delay must be at least 6s to ensure child processes are properly reaped.
- BackgroundManager.shutdown() now aborts all running child sessions via
client.session.abort() before clearing state, preventing orphaned
opencode processes when parent exits
- Add onShutdown callback to BackgroundManager constructor, used to
trigger TmuxSessionManager.cleanup() on process exit signals
- Interactive bash session hook now aborts tracked subagent opencode
sessions when killing tmux sessions (defense-in-depth)
- Add 4 tests verifying shutdown abort behavior and callback invocation
Closes#1240
- Add permission: [{ permission: 'question', action: 'deny' }] to session.create()
in background-agent and delegate-task for SDK-level blocking
- Add subagent-question-blocker hook as backup layer to intercept question tool
calls in tool.execute.before event
- Ensures subagents cannot ask questions to users and must work autonomously
- Add session.status re-check in stability detection before completing
- Reset stablePolls if session is not idle (agent still working)
- Fix statusText to show CANCELLED instead of COMPLETED for non-completed tasks
fix: pass model parameter when resuming background tasks
Ensure resumed tasks maintain their original model configuration
from category settings, preventing unexpected model switching.
The resume method was not passing the stored model from the task,
causing Sisyphus-Junior to revert to the default model when resumed.
This fix adds the model to the prompt body in resume(), matching
the existing behavior in launch().
Fixes#826
OpenCode API returns different structures for user vs assistant messages:
- User: info.model = { providerID, modelID } (nested)
- Assistant: info.modelID, info.providerID (flat top-level)
Previous code only checked nested format, causing model info loss when
continuation hooks fired after assistant messages.
Files modified:
- todo-continuation-enforcer.ts
- ralph-loop/index.ts
- sisyphus-task/tools.ts
- background-agent/manager.ts
Added test for assistant message model extraction.
feat(concurrency): prevent background task races and leaks
Summary
Fixes race conditions and memory leaks in the background task system that could cause "all tasks complete" notifications to never fire, leaving parent sessions waiting indefinitely.
Why This Change?
When background tasks are tracked for completion notifications, the system maintains a pendingByParent map to know when all tasks for a parent session are done. Several edge cases caused "stale entries" to accumulate in this map:
1. Re-registering completed tasks added them back to pending tracking, but they'd never complete again
2. Changing a task's parent session left orphan entries in the old parent's tracking set
3. Concurrent task operations could cause double-acquisition of concurrency slots
These bugs meant the system would sometimes wait forever for tasks that were already done.
What Changed
- Concurrency management: Added proper acquire/release lifecycle with cleanup on process exit (SIGINT, SIGTERM)
- Parent session tracking: Fixed cleanup order. Now clears old parent's tracking before updating parent ID
- Stale entry prevention: Only tracks tasks that are actually running; actively cleans up completed tasks
- Renamed registerExternalTask → trackTask: Clearer name (the old name implied external API consumers, but it's internal)
Update BackgroundManager to rename the method for tracking external tasks, improving clarity and consistency in task management. Adjust related tests to reflect the new method name.