Skip to content

fix: ring the bell when an opencode session asks a question - #14

Merged
omartelo merged 1 commit into
mainfrom
fix/opencode-question-waiting
Aug 11, 2026
Merged

omartelo merged 1 commit into
mainfrom
fix/opencode-question-waiting

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

An opencode session blocked on an interactive question looked exactly like one that was working: spinner, and the tool line reading question. No bell, no toast, no desktop notification.

Only permissions were reported. A question rides an event of its own, question.asked, and the module listened for permission.asked by name — so the last thing the card heard was the busy from the tool call that asked.

What a real run shows

opencode serve 1.18.16, driven through its own HTTP API, SSE on /event:

ask     +  130ms session.status {"type":"busy"}
        + 1665ms question.asked {"id":"que_…","sessionID":"ses_…"}
        …45s of silence — no status at all while the question is pending

reply   question.replied  → session.status busy   (+129ms)
reject  question.rejected → session.status idle   (+81ms)

So the spinner is not stale for a moment; it is stale for as long as the user takes. And the recovery needs nothing from us: answering and dismissing each produce a status of their own, which is why question.replied / question.rejected are not registered here.

Why by suffix, not by name

opencode asks in four spellings today — permission.asked, question.asked, and a .v2. variant of each — and its catalogue is not exhaustive: the same run emits server.heartbeat, which appears in none of the 94 event schemas its /doc publishes. Enumerating names is precisely how this bug happened.

The failure modes are not symmetric. A surplus bell is cleared by the next status report, which a reply produces within ~100ms. A name nobody enumerated is a session waiting behind a card that says it is busy.

Tests

tests/opencode.test.mjs covers all four .asked types, an unknown one (elicitation.asked — the point of the rule), the bound (question.asked.dismissed must report nothing), and a question raised inside a sub-session. Checked against the pre-fix module: the four new cases fail there and pass here, and permission.asked passes in both.

# tests 126
# pass 126
# fail 0

Contract

docs/session-state.md and docs/providers.md here now describe the rule instead of the name. The canonical contract in the lich repository (docs/hooks/session-state.md:42) still lists permission.asked in its opencode column — a follow-up there, no lich code change: waiting is an existing state and the payload is unchanged.

An opencode session blocked on an interactive question looked exactly
like one that was working: spinner, and the tool line reading `question`.
No bell, no toast, no desktop notification — the session waited until
somebody happened to look at it.

Only permissions were reported. A question rides an event of its own,
`question.asked`, and the module listened for `permission.asked` by name,
so the last thing the card heard was the `busy` from the tool call that
asked. A run against a stub confirms nothing follows it: opencode reports
no status at all while the question is pending, so the spinner is not
stale for a moment, it is stale for as long as the user takes.

Matched by suffix rather than by name. opencode asks in four spellings
today — `permission.asked`, `question.asked` and a `.v2.` variant of each
— and its catalogue is not exhaustive: a real run emits types
(`server.heartbeat`) that its published schema does not list. Enumerating
names is precisely how this bug happened, and the failure modes are not
symmetric. A surplus bell is cleared by the next status report, which a
reply produces within ~100ms; a name nobody enumerated is a session
waiting behind a card that says it is busy.

Neither `question.replied` nor `question.rejected` is registered: both
are followed by a status of their own — `busy` on an answer, `idle` on a
dismissal — so the card re-arms itself, and a second report would only
say what the first already did.
@omartelo
omartelo merged commit ef9cc81 into main Aug 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant