Repository navigation
Closed
Description
Description
Shell tool timeout fires but the kill + exit-code + tool-state finalization chain deadlocks, leaving the tool part permanently running and blocking the parent session indefinitely.
Root cause analysis
After cloning the source and inspecting packages/opencode/src/tool/shell.ts and packages/core/src/cross-spawn-spawner.ts, the issue is in the timeout handling chain:
- Shell commands on macOS are spawned with
detached: true, creating a new process group. - When timeout fires, OpenCode calls
handle.kill({ forceKillAfter: "3 seconds" }):if (exit.kind === "timeout") { expired = true yield* handle.kill({ forceKillAfter: "3 seconds" }).pipe(Effect.orDie) }
handle.kill()internally does:process.kill(-pid, "SIGTERM")(kill the process group)Effect.timeoutOrElse(attempt, { duration: "3 seconds", orElse: () => send("SIGKILL") })- Then
Deferred.await(signal)— wait for the process to actually exit
- If the process group has lingering child processes (e.g.
bunspawningtsgo,node, or file watchers), the exit signal may not arrive even afterSIGKILL. - When
handle.kill()does not resolve,Effect.orDiedoes not help because the Effect itself is still running. - The shell tool never returns a result, so the tool part stays
status: "running"in the database. - The child session never reaches
idleorerror, so the parenttask()call blocks indefinitely.
Observed case
- Command:
bun run i18n:sync && bun run typecheck && bunx oxlint src/features/gateway-playground/index.tsx src/components/layout/components/app-sidebar.tsx src/hooks/use-sidebar-data.ts - Working directory: project
web/defaultdirectory - Timeout parameter:
120000(2 minutes) - Actual behavior: Tool part remained
status: "running"for 19+ minutes. Nocompletedorerrorstate was written. Child session stayedbusy. Parent session blocked ontask(). - Same command run manually in terminal: Completed successfully in ~2 minutes with exit code 0.
- No lingering processes: After the issue,
psshowed nobun,tsgo, oroxlintprocesses still running, yet the database tool part was stillrunning.
This confirms the process was already dead but the tool state was never finalized.
Expected behavior
- After timeout, the shell tool should always finalize the tool part as
completed(with timeout metadata) orerror, regardless of whetherhandle.kill()succeeds. - If
handle.kill()does not resolve within theforceKillAftergrace period, the tool should still write a terminal state to the database. - The child session should receive
session.errororsession.idle. - The parent
task()call should receive a structured failure instead of blocking forever.
Suggested fix
Add a hard finalization guarantee after handle.kill():
if (exit.kind === "timeout") {
expired = true
yield* handle.kill({ forceKillAfter: "3 seconds" }).pipe(
Effect.timeoutOrElse({
duration: "5 seconds",
orElse: () => Effect.sync(() => {
// Force-write tool state even if kill cleanup is stuck
console.error("[shell] kill cleanup timed out, force-finalizing tool part")
}),
}),
Effect.orDie,
)
}Or wrap the entire post-timeout block in a hard timeout that guarantees the tool returns a result.
Plugins
oh-my-openagent
OpenCode version
1.18.4
Steps to reproduce
- Launch a subagent through the Task tool.
- Have it run a compound shell command like
bun run i18n:sync && bun run typecheck && bunx oxlint ...withtimeout: 120000. - If the command's process tree has any child processes that don't exit cleanly with the parent, the timeout fires but
handle.kill()hangs. - Observe: tool part stays
runningindefinitely, child session staysbusy, parenttask()blocks forever.
Operating System
macOS
Terminal
OpenChamber / OpenCode server
Metadata
Metadata
Assignees
Labels
No labels
Activity
github-actions commentedon Jul 22, 2026
This issue might be a duplicate of existing issues. Please check:
If this is intentional (e.g. you meant to provide additional technical detail), please consider closing one and linking the other, or adding a comment to #38276 with the root cause analysis instead.