anomalyco / anomalyco/opencode

[BUG]: Status.get() returns idle for a session absent from the status map

Open
#43,093 1 comment 0 reactions 1 assignee View on GitHub

@neriousy is already working on this.

Since Aug 17, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

Describe the bug

packages/opencode/src/session/status.tsset(sessionID, status) publishes Event.Status on transition, and on type === "idle" publishes Event.Idle and deletes the map entry. get() then returns { type: "idle" } for any session with no entry, so "absent" and "idle" are indistinguishable to every consumer.

That makes idle a positive claim about a session that may not exist. The status map is InstanceState.make(… new Map()) — per-process and non-durable — so after a host restart every session reads idle, including sessions that were in-flight when the process died. Any consumer gating completion, cleanup, or notification on idle sees a fleet-wide false quiesce at exactly the moment the truth is unknown. The failure is silent and reads as health.

Worth documenting alongside: idle fires on all termination shapes — clean, error, and interrupt — so it is a quiesce barrier, not a success signal. The absent/idle conflation invites reading it as success.

Expected behavior

Absent and idle should be distinguishable — get() returning undefined (or an explicit unknown) for a missing entry, with idle reserved for an observed transition. Consumers can then treat unknown as "must check durably" rather than "finished".

The durable side already carries the discriminator: message.data.time.completed, set at packages/opencode/src/session/processor.ts:595, is persisted and survives restart — present on 49,961 of 50,000 sampled assistant messages on a live database. A session whose latest assistant message lacks it was in-flight when the process died. The information exists; the status surface collapses it.

For contrast within the same area, the field validation in packages/opencode/src/plugin/shared.ts throws precisely on every malformed input rather than returning a default that reads as success.

Steps to reproduce
  1. Start a session and let it go idle, or start one and kill the process mid-turn.
  2. Call the status surface for a session ID that has no map entry (any session after a restart, or any session that has already gone idle once).
  3. Observe { type: "idle" } returned for a session the process has no state for.

Note: these steps are derived from the mechanism rather than from a recorded run — the finding came from reading the code path.

Environment
  • Version: reproduces on current dev
  • Platform: Linux (not platform-specific — the mechanism is in-memory state plus a defaulted read)
Additional context

Found while integrating an external supervisor process that consumes opencode session status to decide when a background task has finished. It surfaced there as a false-completion vector: the supervisor's candidate check reads idle, and on a host restart every session presents as idle at once.

A consumer can defend against this by treating "absent from the status map" as a distinct unknown state rather than as idle, but only if it already assumes the surface is lossy. A consumer taking get() at face value has no way to tell the two apart.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.