fix(opencode-plugin): persist agent/model context across injected turns#24
Conversation
Follow-up to aannoo#23. Track and inject agent/model on hcom-delivered promptAsync turns to prevent default fallback drift on follow-up turns, while keeping delivery hardening intact. Also includes plugin cleanup (bootstrap one-shot injection, reconcile overlap guard, safer --detail args, dead-code removal, and PTY argv limitation notes).
There was a problem hiding this comment.
Pull request overview
Fixes OpenCode plugin turn injection so hcom-injected follow-up turns preserve the intended agent/model context instead of drifting to OpenCode defaults.
Changes:
- Track
currentAgent/currentModelin plugin state and include them inclient.session.promptAsyncinjections. - Add a
reconcileInFlightguard to prevent overlapping reconcile ticks, and clean up/reset state on session reset. - Improve robustness/observability: normalize
--detailargs, add logging, clear bootstrap text after first injection, and document thepromptAsynctyping gap.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (input.agent) currentAgent = input.agent | ||
| const resolvedModel = normalizePromptModel(input.model) | ||
| if (resolvedModel) currentModel = resolvedModel |
There was a problem hiding this comment.
currentAgent/currentModel are global and are updated on every chat.message regardless of which session the hook event belongs to. If OpenCode emits chat.message for multiple sessions in the same process, this can cross-contaminate agent/model context and cause deliverPendingToIdle(sessionA) to inject using sessionB’s agent/model. Consider scoping these fields per-session (e.g., Map<sessionID, {agent, model}>) or at minimum only updating them when input.sessionID === sessionId (and clearing them when sessionId changes / on bind).
| if (input.agent) currentAgent = input.agent | |
| const resolvedModel = normalizePromptModel(input.model) | |
| if (resolvedModel) currentModel = resolvedModel | |
| const isBoundSession = | |
| !input.sessionID || !sessionId || input.sessionID === sessionId | |
| if (isBoundSession) { | |
| if (input.agent) currentAgent = input.agent | |
| const resolvedModel = normalizePromptModel(input.model) | |
| if (resolvedModel) currentModel = resolvedModel | |
| } else { | |
| log("DEBUG", "plugin.chat_message_ignored_foreign_session", instanceName, { | |
| session_id: input.sessionID, | |
| bound_session_id: sessionId, | |
| }) | |
| } |
| body: { | ||
| agent: currentAgent ?? undefined, | ||
| model: currentModel ?? undefined, | ||
| parts: [{ type: "text", text: formatted }], | ||
| }, |
There was a problem hiding this comment.
deliverPendingToIdle(sid) injects agent/model using the global currentAgent/currentModel, which are not keyed to sid. If more than one session can be active, this can send an injection under the wrong agent/model for the target session. Use agent/model values associated with the specific sid being delivered to (e.g., look up from a per-session map) or validate that sid === sessionId before injecting.
…hat.message - Guard currentAgent/currentModel writes behind isBoundSession check so foreign-session chat.message events cannot cross-contaminate context used by deliverPendingToIdle Why needed: currentAgent/currentModel were plugin-global; if OpenCode emits chat.message for multiple sessions in one process, a foreign session's turn would overwrite the values injected into the bound session's next promptAsync call.
ef19290 to
1ee3f6a
Compare
Scope plugin delivery and event handling to the bound OpenCode session so foreign session.status, permission.replied, and session.deleted events cannot drive delivery, status, or teardown for the wrong hcom worker. Seed current agent/model during opencode-start by parsing stored launch_args from the instance row and returning agent/model in both bind and rebind responses, so resumed sessions keep the intended identity before the next chat.message hook. Add focused regression coverage for launch arg parsing and the rebind payload path.
Restore the pre-PR behavior of injecting the original opencode-start bootstrap payload into transformed prompt messages instead of replacing it with a compact summary. OpenCode chat transform mutations are prompt-local and synthetic text parts are not persisted back to session history, so bootstrapText must stay cached after the first injection. Keep the small defensive output.messages fallback, but do not introduce a separate hcom context payload.
20005a9 to
c42622f
Compare
Summary
Follow-up to #23.
Workers launched with
hcom opencode --agent <name>could run turn 1 on the intended agent/model, then drift on follow-up hcom-injected turns to OpenCode's default agent/model path. This surfaced during multi-agent orchestration, where frequent hook deliveries made fallback behavior visible in real usage.Root cause
deliverPendingToIdle()injected throughpromptAsyncwith onlybody.parts, omittingbody.agentandbody.model.When those fields were absent, OpenCode resolved agent context through its default path (
defaultAgent()), which could diverge from the launched worker agent/model on subsequent injected turns.Fix
currentAgentandcurrentModelin plugin state and refresh on eachchat.messageisBoundSessioncheck so foreign-sessionchat.messageevents can't cross-contaminate the bound session's injection contextagentandmodelon every injectedpromptAsynccallpromptAsyncfailure handling anddeliveryInFlightcleanup on session reset)reconcileInFlightguard to prevent overlapping interval reconciles--detailshell args withString(x ?? "")promptAsyncas anycastKnown limitation: Tab-switch without submit
currentAgent/currentModelonly update when the user submits a turn (chat.messagefires). If the user Tab-switches the TUI agent but doesn't submit, an hcom message arriving in that window still runs on the previously-submitted agent. The plugin has no way to observe TUI-only state — the agent selection is client-side until submit. Fully closing this window requires an upstream OpenCode change (exposing the TUI's current agent selection to plugins).CLI parse limitation (known)
parseCliArgValueandparseCliModelArgare currently null in PTY-launched OpenCode because the plugin process sees OpenCode binary argv, not outerhcom opencode --agent/--modelargv.This PR documents that behavior. Effective turn-to-turn tracking still works through the
chat.messagehook. Long-term fix is to seed launch agent/model from theopencode-startpayload.Validation
cargo test -qcargo build -q