Pre-flight checks
OpenCode version tested
1.18.30
opencode-quota version tested
4.9.0
Bug summary
The deterministic slash commands inject their output with session.prompt({ noReply: true }), which fires chat.message, but nothing publishes an end-of-turn event afterwards: the TUI inline handler (the default) returns right after the injection, and the server command path throws the COMMAND_HANDLED sentinel and fails with HTTP 500. Lifecycle-aware integrations (e.g. Herdr's opencode plugin) stay stuck on "working" until the next real prompt.
Steps to reproduce
-
Install opencode-quota 4.9.0 with the server plugin loaded ("plugin": ["@slkiser/opencode-quota@latest"]).
-
Run an OpenCode server with a plugin that logs bus events, e.g.:
// .opencode/plugin/event-log.js
export const EventLogPlugin = async () => ({
"chat.message": async ({ sessionID }) => console.log("chat.message", sessionID),
event: async ({ event }) => console.log(event.type, event.properties?.sessionID),
});
-
Reproduce the plugin's injection shape:
$sid = (Invoke-RestMethod -Method Post http://127.0.0.1:4096/session -ContentType application/json -Body '{}').id
Invoke-RestMethod -Method Post "http://127.0.0.1:4096/session/$sid/message" `
-ContentType application/json `
-Body '{"noReply":true,"parts":[{"type":"text","text":"quota output","ignored":true}]}'
-
Observe the event stream, or just run /quota in the TUI (default inline display) with a lifecycle watcher attached.
Expected behavior
Like any normal command, the session emits session.idle (and command.executed) after chat.message, so lifecycle watchers return to idle.
Actual behavior
The noReply injection produces no end-of-turn event:
chat.message ses_… <- watchers now report "working"
message.updated ses_…
message.part.updated ses_…
session.updated ses_…
(nothing else — stuck "working" until the next real prompt)
The server /command surface additionally returns 500 UnknownError (err_…) for /quota.
Relevant logs/output
Observed on opencode 1.18.30 + quota 4.9.0 with an event-logger plugin on the same bus:
- Raw noReply injection: last event is
session.updated, no idle after chat.message.
session.abort on the stuck session immediately emits session.status + session.idle, so the settle primitive exists and is cheap to call from the plugin.
If not tested on current production OpenCode, explain why
N/A — tested on current production release.
Pre-flight checks
OpenCode version tested
1.18.30
opencode-quota version tested
4.9.0
Bug summary
The deterministic slash commands inject their output with
session.prompt({ noReply: true }), which fireschat.message, but nothing publishes an end-of-turn event afterwards: the TUI inline handler (the default) returns right after the injection, and the server command path throws theCOMMAND_HANDLEDsentinel and fails with HTTP 500. Lifecycle-aware integrations (e.g. Herdr's opencode plugin) stay stuck on "working" until the next real prompt.Steps to reproduce
Install opencode-quota 4.9.0 with the server plugin loaded (
"plugin": ["@slkiser/opencode-quota@latest"]).Run an OpenCode server with a plugin that logs bus events, e.g.:
Reproduce the plugin's injection shape:
Observe the event stream, or just run
/quotain the TUI (default inline display) with a lifecycle watcher attached.Expected behavior
Like any normal command, the session emits
session.idle(andcommand.executed) afterchat.message, so lifecycle watchers return to idle.Actual behavior
The noReply injection produces no end-of-turn event:
The server
/commandsurface additionally returns500 UnknownError (err_…)for/quota.Relevant logs/output
Observed on opencode 1.18.30 + quota 4.9.0 with an event-logger plugin on the same bus:
session.updated, no idle afterchat.message.session.aborton the stuck session immediately emitssession.status+session.idle, so the settle primitive exists and is cheap to call from the plugin.If not tested on current production OpenCode, explain why
N/A — tested on current production release.