Concurrency: unlocked state.json races (OpenCode adversarial review of PR #1)
Surfaced by an OpenCode adversarial self-review, triaged by Claude Code. Flagged as the highest-risk cluster: correctness of every concurrent operation (cancel, status, background-task completion) depends on these. Phase 2 added a server lock but there is no state-file lock.
4. lib/state.mjs — saveState load-then-write, no serialization
Reads previousJobs from disk, computes nextJobs from in-memory state, deletes orphaned files, writes new state. Two concurrent saveState calls → last-writer-wins: one writer's deletions can remove files for jobs the other still tracks.
6. opencode-companion.mjs handleCancel vs. tracked-jobs.mjs runTrackedJob
Both write job status unlocked. If cancel writes last, a successfully completed task is overwritten as cancelled and its result is lost; if the worker writes last, the cancel is lost. (High impact — silent data loss.)
7. lib/state.mjs — upsertJob → updateState → saveState unlocked
Every upsertJob (progress updater, completion, cancel, enqueue) is load-mutate-save with no lock. The main-process progress reporter, the task-worker process, and a cancel from another hook all race on state.json; one process's mutations are silently discarded by another's writeFileSync.
8. lib/server-lifecycle.mjs — ensureServer usage window unprotected
After ensureServer releases its lock, nothing prevents teardownServerSession (e.g. from session-lifecycle-hook.mjs) from disposing the server while a caller is mid-request. The startup-path guards don't cover the usage window → broken connection / connection refused with no retry.
Suggested direction
Introduce a single-writer discipline for state.json (file lock / atomic compare-and-swap / serialize through one owner), and a usage refcount or lease so the server isn't torn down mid-request.
Ref: PR #1.
Concurrency: unlocked
state.jsonraces (OpenCode adversarial review of PR #1)Surfaced by an OpenCode adversarial self-review, triaged by Claude Code. Flagged as the highest-risk cluster: correctness of every concurrent operation (cancel, status, background-task completion) depends on these. Phase 2 added a server lock but there is no state-file lock.
4.
lib/state.mjs—saveStateload-then-write, no serializationReads
previousJobsfrom disk, computesnextJobsfrom in-memory state, deletes orphaned files, writes new state. Two concurrentsaveStatecalls → last-writer-wins: one writer's deletions can remove files for jobs the other still tracks.6.
opencode-companion.mjshandleCancelvs.tracked-jobs.mjsrunTrackedJobBoth write job status unlocked. If cancel writes last, a successfully completed task is overwritten as
cancelledand its result is lost; if the worker writes last, the cancel is lost. (High impact — silent data loss.)7.
lib/state.mjs—upsertJob→updateState→saveStateunlockedEvery
upsertJob(progress updater, completion, cancel, enqueue) is load-mutate-save with no lock. The main-process progress reporter, the task-worker process, and a cancel from another hook all race onstate.json; one process's mutations are silently discarded by another'swriteFileSync.8.
lib/server-lifecycle.mjs—ensureServerusage window unprotectedAfter
ensureServerreleases its lock, nothing preventsteardownServerSession(e.g. fromsession-lifecycle-hook.mjs) from disposing the server while a caller is mid-request. The startup-path guards don't cover the usage window → broken connection / connection refused with no retry.Suggested direction
Introduce a single-writer discipline for
state.json(file lock / atomic compare-and-swap / serialize through one owner), and a usage refcount or lease so the server isn't torn down mid-request.Ref: PR #1.