Skip to content

fix(opencode-plugin): persist agent/model context across injected turns#24

Merged
aannoo merged 4 commits intoaannoo:mainfrom
mmkzer0:fix/agent-model-persistence
Apr 22, 2026
Merged

fix(opencode-plugin): persist agent/model context across injected turns#24
aannoo merged 4 commits intoaannoo:mainfrom
mmkzer0:fix/agent-model-persistence

Conversation

@mmkzer0
Copy link
Copy Markdown
Contributor

@mmkzer0 mmkzer0 commented Apr 20, 2026

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 through promptAsync with only body.parts, omitting body.agent and body.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

  • Track currentAgent and currentModel in plugin state and refresh on each chat.message
  • Scope the currentAgent/currentModel writes behind an isBoundSession check so foreign-session chat.message events can't cross-contaminate the bound session's injection context
  • Pass agent and model on every injected promptAsync call
  • Preserve fix(opencode-plugin): close TOCTOU race in deliverPendingToIdle #23 delivery hardening in this branch (promptAsync failure handling and deliveryInFlight cleanup on session reset)
  • Keep the original bootstrap payload available across transforms (OpenCode transform mutations are prompt-local and not persisted to stored session history)
  • Add reconcileInFlight guard to prevent overlapping interval reconciles
  • Remove dead helper code and normalize --detail shell args with String(x ?? "")
  • Add an explicit runtime-contract comment near the promptAsync as any cast

Known limitation: Tab-switch without submit

currentAgent/currentModel only update when the user submits a turn (chat.message fires). 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)

parseCliArgValue and parseCliModelArg are currently null in PTY-launched OpenCode because the plugin process sees OpenCode binary argv, not outer hcom opencode --agent/--model argv.

This PR documents that behavior. Effective turn-to-turn tracking still works through the chat.message hook. Long-term fix is to seed launch agent/model from the opencode-start payload.

Validation

  • cargo test -q
  • cargo build -q
  • Real OpenCode usage after patch: follow-up injected turns remain on the intended agent/model, with no observed default fallback drift

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).
Copilot AI review requested due to automatic review settings April 20, 2026 18:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / currentModel in plugin state and include them in client.session.promptAsync injections.
  • Add a reconcileInFlight guard to prevent overlapping reconcile ticks, and clean up/reset state on session reset.
  • Improve robustness/observability: normalize --detail args, add logging, clear bootstrap text after first injection, and document the promptAsync typing gap.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/opencode_plugin/hcom.ts Outdated
Comment on lines +430 to +432
if (input.agent) currentAgent = input.agent
const resolvedModel = normalizePromptModel(input.model)
if (resolvedModel) currentModel = resolvedModel
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,
})
}

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +173
body: {
agent: currentAgent ?? undefined,
model: currentModel ?? undefined,
parts: [{ type: "text", text: formatted }],
},
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
…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.
@mmkzer0 mmkzer0 force-pushed the fix/agent-model-persistence branch from ef19290 to 1ee3f6a Compare April 20, 2026 19:25
aannoo added 2 commits April 21, 2026 02:42
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.
@aannoo aannoo force-pushed the fix/agent-model-persistence branch from 20005a9 to c42622f Compare April 21, 2026 15:39
@aannoo aannoo merged commit 64a4529 into aannoo:main Apr 22, 2026
10 checks passed
@mmkzer0 mmkzer0 deleted the fix/agent-model-persistence branch April 22, 2026 10:08
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.

3 participants