Strip obvious comments, over-defensive guards, and dead branches across
the five delegate-task executor files while preserving all metadata
propagation behavior added in prior commits. Regression tests remain
green (328 pass / 0 fail).
The execute field with { task_id, task_dir } was defined but never referenced anywhere in the codebase. Removing dead code simplifies the type surface and prevents accidental future misuse.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Oracle flagged that the previous test file monkey-patched process.kill
and relied on mock.module for 5 modules. Running it after manager.test.ts
in the same Bun process reproduced 2 failures - the test resolution of
`./session-kill` specifier interacted badly with manager.test.ts's
`../../shared/tmux` barrel mock.
Solution: refactor stale-session-sweep.ts to expose
`sweepStaleOmoAgentSessionsWith(deps)` that accepts a SweepDeps record
(isInsideTmux, getTmuxPath, listCandidateSessions, killSession,
processAlive, currentPid, log). The public `sweepStaleOmoAgentSessions()`
still uses runtime-built deps so call sites are unchanged.
The test file now imports the pure function directly and constructs a
fixture with fake deps. Zero mock.module calls, zero process.kill
patching, zero cache-bust dynamic imports. 8 tests (up from 6) run
deterministically in any order with any neighbor.
Before: combined run with manager.test.ts = 2 fail, 50 pass.
After: combined run with manager.test.ts = 0 fail, 54 pass.
Oracle noted that loadSweeper() monkey-patches process.kill without
ever restoring it. Added afterEach hook to set process.kill back to the
captured original. Individual file runs already passed, and
script/run-ci-tests.ts confirms the full CI suite - 4781 pass, 0 fail
across 491 files - but this makes the test file safe under non-isolated
local runs as well.
Oracle flagged: staleSweepCompleted was set to true BEFORE
sweepStaleOmoAgentSessions() ran, so any throw from the first
invocation would permanently disable stale cleanup for the rest
of the process lifetime.
Fix:
- Move staleSweepCompleted=true into the try-block success branch.
- Add staleSweepInProgress guard so concurrent onSessionCreated calls
do not invoke sweep twice in parallel (sweep is idempotent, but the
guard prevents doubled log noise).
- finally{} clears the inProgress flag regardless of outcome.
- cleanup() resets both flags.
Two new tests cover: retry after a thrown first attempt, and single
invocation when subsequent spawns follow a successful first sweep.
Oracle flagged a regression introduced in PR #3507 commit 21554be8:
event.ts routed session.error through tmux pane cleanup BEFORE the
existing session-recovery / model-fallback logic ran.
Problem: when session.error was recoverable (context window limit,
quota rate limit, provider fallback), the recovery/fallback code would
successfully continue the SAME session - but by then its tmux pane had
already been destroyed. User-visible symptom is exactly the original
complaint - 'screen appears but streaming stops working' after an
auto-retry.
Fix is the minimal revert: remove the onSessionError funnel from
event.ts and drop onSessionError from the manager. Fatal errors that
actually end a session still fire session.deleted, which continues to
trigger cleanup correctly. Non-fatal error streams stay attached to
the surviving pane.
The log line was misplaced at the end of sweepStaleIsolatedSessionsOnce
where it said 'cleanup complete' after the stale sweep, which was
misleading. Per Oracle review.
Follow-up to PR #3507 addressing the Oracle-noted operational limitation:
per-PID isolated session names (getIsolatedSessionName(process.pid)) mean
that when an opencode process is SIGKILL'd (or the machine hard-reboots),
the old omo-agents-<old-pid> tmux session survives forever because nothing
is around to kill it.
Added sweepStaleOmoAgentSessions() that:
1. Lists tmux sessions matching /^omo-agents-(\d+)$/
2. For each, checks process.kill(pid, 0) to detect a dead PID
3. Skips our own PID
4. Calls killTmuxSessionIfExists for every session whose owner process is gone
Wired into TmuxSessionManager.onSessionCreated() as a one-shot (guarded by
staleSweepCompleted flag) so it runs lazily on the first subagent spawn when
isolation="session". The flag is reset in cleanup() so subsequent process
restarts re-run the sweep.
6 new tests cover: outside-tmux no-op, no matching sessions, multiple dead
PIDs, current PID skip, live PID skip, list-sessions failure.
Manual E2E verified on real tmux:
- Created omo-agents-99999, sweep killed it
- Spawned our own omo-agents-<pid>, closeTmuxPane returned true even after
pane auto-destroy from Ctrl+C
- Final tmux list-sessions shows zero omo-agents-* orphans
CI test suite exited 1 despite 0 failing tests because process-cleanup.test.ts
assertions left process.exitCode=1 in place. The afterEach hook only reset to
originalExitCode (which starts undefined), not 0, so Bun picked up exitCode=1
on shutdown and reported the shared batch as failing.
Explicitly set process.exitCode = 0 in beforeEach and afterEach so each test
starts and ends with a clean exit state.
Oracle flagged the previous commit: "omo-agents" was a shared constant,
so when two plugin instances ran in the same tmux server they wrote into
the same session. One instance's cleanup would then kill-session on the
shared name and tear down the other instance's live attached panes.
Replace the const ISOLATED_SESSION_NAME with getIsolatedSessionName(pid)
which defaults to process.pid, so every opencode process owns its own
"omo-agents-<pid>" session. spawnTmuxSession and cleanup both resolve
the name through this helper. Discovery is straightforward from the
host tmux via 'tmux list-sessions | grep omo-agents-'.
Manager test covers two concurrent managers and asserts each kills a
per-pid session name, proving they no longer collide on a global name.
The existing layout.test.ts relied on mock.module("bun", ...) registered
at the top level, but test-setup.ts calls mock.restore() + restoreModuleMocks()
in afterEach, so every test except the first one lost its mocks. CI has
been red on this file since e303feef.
Two changes:
1. layout.ts now imports spawn from the existing spawn-process helper
instead of "bun" directly, matching the pattern established for
closeTmuxPane and killTmuxSessionIfExists. This does not change
runtime behavior - spawn-process just re-exports Bun's spawn.
2. layout.test.ts registers module mocks inside beforeEach and uses the
?test=UUID cache-busting dynamic-import pattern so the mocks apply
on every test run, not just the first.
All 4 layout.test.ts cases now pass.
After send-keys C-c the subprocess running inside the pane (for example
"opencode attach") exits on SIGINT, which causes tmux to destroy the
pane automatically. The subsequent kill-pane then returns exit 1 with
stderr "can't find pane: %NN" even though the end state is exactly
what we wanted.
Before this fix closeTmuxPane reported failure for that branch, which
kept TmuxSessionManager's retryPendingCloses loop marking the (now
deleted) pane as still-pending forever and left stale entries behind
in the tracked sessions map. This is the behavior the user observed
as "screen opens, streaming runs, but cleanup doesn't finish" when
running with tmux.isolation="session".
Now we detect the "can't find pane" stderr and return true, treating
the auto-destroy path the same as an explicit successful kill.
Signal handlers covered SIGINT/SIGTERM/SIGBREAK/beforeExit/exit, but a
synchronous throw or a top-level rejected promise terminated the process
without letting TmuxSessionManager (or any other registered manager) run
its shutdown hook. That reliably left orphan tmux panes after an opencode
crash.
Added registration for uncaughtException and unhandledRejection that fan
out through the existing cleanupAll() path, set process.exitCode = 1,
and arm the same 6 second forced-exit guard we use for signals. Test
helpers hold process-level spies so the new tests do not leak listeners
between runs.
Three defects observed with tmux.isolation="session" where the omo-agents
session was left with orphan fish panes after subagents finished:
1. cleanup() never ran 'tmux kill-session -t omo-agents'. If any pane
lingered (for example because opencode attach stayed blocked on SSE),
the isolated session survived process shutdown. Now we explicitly kill
the shared session through killTmuxSessionIfExists when isolation is
"session".
2. session.error events bypassed tmux cleanup entirely. Only session.deleted
closed panes, so any provider error that did not escalate into a delete
left the pane behind. Added onSessionError on TmuxSessionManager, wired
from plugin/event.ts, which funnels through the same onSessionDeleted
close path for tracked sessions only.
3. retryPendingCloses() only ran when a new session was created. If the
main process went idle after a failed close, the pending session stayed
pending forever. TmuxPollingManager now accepts the retry callback and
fires it on every tick, alongside the existing stability-based close
sweep.
Manager tests cover isolation=session kill invocation, inline/window
isolation skipping the kill, the onSessionError happy + untracked paths,
and an isolated-session kill failure that must not break cleanup.
Adds killTmuxSessionIfExists(sessionName), a best-effort no-op when the
named session is absent. Drains both stdio streams so it does not leak
pipe buffers the way closeTmuxPane historically did.
Also exports ISOLATED_SESSION_NAME ("omo-agents") from session-spawn so
callers can tear down the shared isolated session without hard-coding
the name in multiple places.
closeTmuxPane spawned kill-pane with stdout: "pipe" but never drained the
stream, which could leave the subprocess hanging indefinitely when tmux
wrote anything to stdout (for example under --force-close race conditions).
- send-keys now uses stdout: "ignore" so there is no pipe to drain
- kill-pane keeps the pipe but drains stdout/stderr alongside proc.exited
- switch imports to the new spawn-process helper so the behavior is
covered by hermetic tests that mock the spawn boundary
Drop cleanup.ts, session-created-handler.ts, and session-deleted-handler.ts
which were never wired up; the lifecycle logic they contained lives inline
in TmuxSessionManager. Barrels trimmed to match.
Introduce a dedicated module that builds the two-window tmux layout team-mode relies on:
- "focus" window uses main-vertical for the lead-centric view
- "grid" window uses tiled so every member pane is visible at once
createTeamLayout spawns omo-team-<teamRunId>, registers pane titles with
color-coded labels, and returns the focus/grid window IDs plus a
pane-by-member map. removeTeamLayout tears the session down idempotently.
canVisualize short-circuits when TMUX is unset so callers degrade
gracefully outside a tmux context.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Awaiting the tmux callback blocked the prompt for up to 10s (waitForSessionReady downstream). During that window the spawned pane ran 'opencode attach' against an empty session and rendered a blank TUI. Users saw 'pane created but attach not working'.
Start promptWithModelSuggestionRetry immediately after session.create, then invoke the tmux callback as fire-and-forget. The session becomes active before the attach client connects.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
waitForSessionReady polled session.status for up to 10s before the pane was registered, but session.status only becomes visible after promptAsync starts. Blocking pane tracking on that signal caused the attach client to see an empty session and render a blank TUI.
Track the pane immediately after spawn, and run the readiness probe in the background purely for observability.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The zauc-mocks-hook variant previously asserted that session.created synchronously
ran the startup checks. After auto-update-checker was refactored to defer work to
the first session.idle via scheduleDeferredIdleCheck (5s timer), those assertions
never fired.
Mirror the mock+capture pattern from hook.test.ts so the test drives the deferred
callback synchronously, preserving the original invariants (hasChecked guard,
localDev toast, sisyphus wording) without waiting on real timers.