Server-lifecycle & state-lock hardening (bounded waits, EPERM leases, atomic server.json)
Surfaced by external reviews (GPT + DeepSeek). Grouped lifecycle/lock robustness items.
[Med] SessionEnd teardown lock has no acquisition deadline
teardownServerSession({ cwd }) acquires the server lock via acquireServerLock, which has no acquisition timeout (a held lock is treated as non-stale for up to 30 s, and a concurrent ensureServer may hold it while health-polling up to 10 s). The SessionEnd hook budget is 5 s (plugins/opencode/hooks/hooks.json), so teardown can be killed by the hook host before it disposes/clears server state.
Fix: pass a bounded lock-acquisition timeout for SessionEnd and fail safely with a diagnostic, or align the hook budget with a deliberately bounded lifecycle budget. Add a lock-contention hook test.
[Med] Atomics.wait blocks the Node event loop during state-lock contention
The state lock uses synchronous Atomics.wait(...) as its poll sleep (state.mjs), blocking the Node main thread for the poll interval during contention — unlike acquireServerLock, which uses async sleep. This freezes concurrent async work (e.g. an in-flight cancel's HTTP interrupt) while waiting on the state lock.
Fix: prefer an async poll where callers are async, or keep the blocking wait but bound/document it. (Severity is bounded — lock hold times are short — but it is a real event-loop stall.)
[Low] EPERM + 6 h lease TTL → "undead" leases
processIsAlive treats process.kill(pid, 0) EPERM as alive; with DEFAULT_LEASE_TTL_MS = 6h, a crashed lease owner whose PID is reused by a foreign-owned process yields a lease that blocks server teardown for up to 6 hours.
Fix: reduce the lease TTL (and/or add a companion heartbeat to extend it for long sessions), and don't treat EPERM as definitively alive for lease liveness.
[Low] saveServerSession is not atomic
saveServerSession writes server.json directly (unlike state persistence's temp-file + rename), so a crash/disk-full mid-write can corrupt lifecycle state and orphan a running server.
Fix: reuse the atomic-write helper (temp + rename).
Source: GPT report (P2 lock budget, P2 Atomics.wait, P3 EPERM/TTL, P3 server.json) and DeepSeek report (C4, C1, M2+C3, H3).
Server-lifecycle & state-lock hardening (bounded waits, EPERM leases, atomic server.json)
Surfaced by external reviews (GPT + DeepSeek). Grouped lifecycle/lock robustness items.
[Med] SessionEnd teardown lock has no acquisition deadline
teardownServerSession({ cwd })acquires the server lock viaacquireServerLock, which has no acquisition timeout (a held lock is treated as non-stale for up to 30 s, and a concurrentensureServermay hold it while health-polling up to 10 s). TheSessionEndhook budget is 5 s (plugins/opencode/hooks/hooks.json), so teardown can be killed by the hook host before it disposes/clears server state.Fix: pass a bounded lock-acquisition timeout for SessionEnd and fail safely with a diagnostic, or align the hook budget with a deliberately bounded lifecycle budget. Add a lock-contention hook test.
[Med]
Atomics.waitblocks the Node event loop during state-lock contentionThe state lock uses synchronous
Atomics.wait(...)as its poll sleep (state.mjs), blocking the Node main thread for the poll interval during contention — unlikeacquireServerLock, which uses asyncsleep. This freezes concurrent async work (e.g. an in-flight cancel's HTTP interrupt) while waiting on the state lock.Fix: prefer an async poll where callers are async, or keep the blocking wait but bound/document it. (Severity is bounded — lock hold times are short — but it is a real event-loop stall.)
[Low]
EPERM+ 6 h lease TTL → "undead" leasesprocessIsAlivetreatsprocess.kill(pid, 0)EPERMas alive; withDEFAULT_LEASE_TTL_MS = 6h, a crashed lease owner whose PID is reused by a foreign-owned process yields a lease that blocks server teardown for up to 6 hours.Fix: reduce the lease TTL (and/or add a companion heartbeat to extend it for long sessions), and don't treat
EPERMas definitively alive for lease liveness.[Low]
saveServerSessionis not atomicsaveServerSessionwritesserver.jsondirectly (unlike state persistence's temp-file + rename), so a crash/disk-full mid-write can corrupt lifecycle state and orphan a running server.Fix: reuse the atomic-write helper (temp + rename).
Source: GPT report (P2 lock budget, P2 Atomics.wait, P3 EPERM/TTL, P3 server.json) and DeepSeek report (C4, C1, M2+C3, H3).