Repository navigation
Description
Summary
ctx.event.subscribe() in a Promise plugin is not reliably location-scoped: on one shared-server run every plugin instance received session events from all locations, so a plugin that tracks "is a session in my directory running" reflected activity from unrelated projects. On another server run (same machine, same config, hours earlier) the same plugin correctly received only its own location's events.
Environment
- opencode version: v2.1.4 (channel=latest; server process that showed the bug started on v2.1.4; the scoped behavior was observed on a server process started earlier that day when the installed binary was v2.1.2/v2.1.4 mid-update — auto-update channel)
- OS: Linux 7.0.0-38-generic x86_64 (Ubuntu)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/bash
- Install/channel: official installer,
latestchannel, shared background service (client-server architecture) - Active plugins: local server plugins in
~/.config/opencode/plugins/(status_monitor.ts— the affected one;orca-opencode-status.js); reproduction below uses a minimal standalone plugin
Reproduction
- Save this plugin to
~/.config/opencode/plugins/loc-events.ts:
import fs from "node:fs"
import { Plugin } from "@opencode/plugin"
export default Plugin.define({
id: "loc-events",
setup(ctx) {
const dir = ctx.location.directory
const log = (m: string) =>
fs.appendFileSync("/tmp/loc-events.log", `${new Date().toISOString()} [${dir}] ${m}\n`)
log("loaded")
void (async () => {
for await (const event of ctx.event.subscribe()) {
const e = event as {
type: string
location?: { directory?: string }
data?: { sessionID?: string }
}
log(`${e.type} loc=${e.location?.directory ?? "-"} sid=${e.data?.sessionID ?? "-"}`)
}
})()
},
})- Restart the shared service (
opencode service restart), open two TUIs in different directories (e.g.~/projectAand~/projectB). - Send a prompt in
projectA. - Watch
/tmp/loc-events.log.
Expected Behavior
The plugin instance loaded in projectB only receives events belonging to projectB's location — or, at minimum, the behavior is consistent between server runs. Per packages/core/src/plugin/host.ts, the plugin host subscribes through the location-aware bus (bus.subscribe() → local(...) filter in packages/core/src/bus.ts), which scopes delivery to the ambient location when one is in scope.
Actual Behavior
All instances receive an identical, unscoped stream: session.execution.started, session.step.*, session.tool.*, shell.created, etc. for sessions of other locations. Aggregated per-instance event counters captured at the same wall-clock second from three concurrent instances (directories /home/stanislavus, /home/stanislavus/PycharmProjects/coding-agent-status, /tmp/opencode) are byte-identical — all three counted the same session's events, and that session belonged to only one of the three locations:
2026-10-24T12:07:49.489Z [opencode] events: shell.exited=1 shell.deleted=1 session.tool.success=1 session.step.ended=1 session.usage.updated=1 session.step.started=1 session.reasoning.started=1 session.reasoning.delta=4
2026-10-24T12:07:49.489Z [stanislavus] events: shell.exited=1 shell.deleted=1 session.tool.success=1 session.step.ended=1 session.usage.updated=1 session.step.started=1 session.reasoning.started=1 session.reasoning.delta=4
2026-10-24T12:07:49.491Z [coding-agent-status] events: shell.exited=1 shell.deleted=1 session.tool.success=1 session.step.ended=1 session.usage.updated=1 session.step.started=1 session.reasoning.started=1 session.reasoning.delta=4
Additionally, earlier the same day on a different server process (started 05:12 UTC, before the binary auto-updated to v2.1.4), the scoping worked: a foreign location's session.execution.started did not reach another location's plugin instance, verified via state-change timestamps of a per-directory status plugin. After the next server start (11:03 UTC, v2.1.4) the streams became global. Same machine, same config, same plugin API — inconsistent between server runs.
Session event envelopes on v2.1.4 do carry event.location.directory, so plugins can filter manually; the concern is that the built-in scoping silently differs between runs.
Additional Context
- Real-world impact: a fleet status plugin (one dashboard card per project directory, heartbeats over HTTP) showed every project as "running" whenever any single session on the shared server was active.
- Workaround: filter every session event by
event.location.directory === ctx.location.directory, falling back to thesession.createdevent'sdata.location.directory, then toctx.session.get({ sessionID })lookups (fail open when a session cannot be attributed). - For contrast, the HTTP endpoint
GET /api/eventis explicitly documented as "Subscribe to native events and plugin RPC events across all server locations" — global delivery there is intended; the plugin-context subscription is the one whose scoping flipped. - Frequency: consistent within a server run; differs between runs. Observed once going scoped → unscoped after a server restart.
- Possibly related observation on v2.0.27 (fixed by v2.1.4):
form.created/form.repliedwere not delivered toctx.event.subscribe()at all while othersession.*events kept flowing (verified with a pendingquestion-tool form). Not reproducible on v2.1.4, mentioning only as delivery-reliability context.
Activity
opencode-agent commentedon Oct 24, 2026
Thanks for the detailed report and the minimal plugin. I think this is a duplicate of #56290.
That issue was reproduced on
dev. Plugins are set up once per loaded location in the shared server, butctx.event.subscribe()gives every instance events from all locations. Your identical per-instance counters across the three directories show exactly that. The cause is that plugin loading drops the location from the plugin's context, so the location filter in the bus never applies. The run where scoping seemed to work most likely had only one location loaded at the time.Possible duplicates and related items, ordered from most to least likely:
Until a fix lands, your workaround of filtering on
event.location.directory === ctx.location.directoryis the right approach.The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
benjaminkincses commentedon Oct 24, 2026
related to #56290 but not the same symptom. that one is per-connection duplication, this one is cross-location leak: every instance sees every location's events. the run-to-run flip fits bus.ts local(): when the subscribe stream runs with no ambient Location, onNone passes everything through unfiltered. so scoping here depends on which scope materializes the stream, not on ctx.location.directory.
opencode-agent commentedon Oct 25, 2026
Closing this as a duplicate of #56290, since there's been no reply saying how this one is different. Both report the same thing: every plugin instance in the shared server gets events from every location through
ctx.event.subscribe(). Please follow #56290 for updates. Two open PRs aim to fix it: #56346 and #57186.Until then, filtering on
event.location.directory === ctx.location.directoryshould keep working. If you find this behaves differently from #56290, please comment here and we can reopen it. Thanks again for the detailed report and the minimal plugin.benjaminkincses commentedon Oct 26, 2026
this was closed as a duplicate for having no reply saying how it differs. there is one, 2026-10-24: #56290 is per-connection duplication, this one is cross-location leak, every instance sees every location events. mechanism noted there too, bus.ts local() with no ambient Location passes everything through onNone. per your own closing comment, reopening makes sense here.