Skip to content

[Bug]: pty exit notification omits model, clobbers user's session model selection (falls back to agent's configured model) #57

Description

@tbontb-iaq

Description

When a PTY process spawned with notifyOnExit: true exits, the plugin sends the <pty_exited> notification via client.session.promptAsync with only agent: session.parentAgent and no model in the body (see dist/src/plugin/pty/notification-manager.js, sendExitNotification).

In OpenCode's model resolution chain (packages/opencode/src/session/prompt.ts, createUserMessage):

input.model ?? ag.model ?? currentModel(sessionID)

a model-less prompt falls back to the agent's configured model (ag.model). The turn is then answered by the agent's default model instead of the model the user currently has selected in the session. Worse, createUserMessage subsequently calls setAgentModel, which persists that resolved model back into the session row — so the user's manual model selection is permanently clobbered and all subsequent model-less injections (e.g. oh-my-openagent's TODO-continuation prompts, which inherit the model from the most recent message) also resolve to the agent's default model.

This is the plugin-side half of the problem tracked upstream at anomalyco/opencode#42893 ("Model swap gets clobbered by model-less prompts"). The TUI never triggers it because it always sends an explicit model on every prompt.

Reproduction Steps

  1. Configure a primary agent with a pinned model in opencode.json (e.g. agent "build" → model A)
  2. Start a session, then switch the model in the picker to model B (e.g. a stronger model for a heavy task)
  3. Ask the agent to run a long background render, e.g. pty_spawn with notifyOnExit: true
  4. While it runs, keep chatting (turns run on model B)
  5. The PTY exits → <pty_exited> notification is injected without a model
  6. The reply to the notification runs on model A, and the session row is rewritten to model A
  7. All following turns (including other plugins' model-less injections) run on model A until the user manually sends another message

Verified in opencode.db (session with agent pinned to glm-5.3, user switched to opencode/x-preview-f-free):

user      opencode/x-preview-f-free              ← user's manual message (model B)
user      opencode/x-preview-f-free              ← user's manual message (model B)
user      volcengine-agent-plan/glm-5.3          ← <pty_exited> injection (reverted to model A)
user      volcengine-agent-plan/glm-5.3          ← oh-my-openagent TODO CONTINUATION (inherited the clobbered model A)
user      opencode/x-preview-f-free              ← next manual message ("继续") re-pins model B

Expected Behavior

The <pty_exited> notification should preserve the session's currently selected model, so the wake-up turn is answered by the model the user picked, and the session's model selection survives background-process completion.

Actual Behavior

The notification is dispatched without a model, OpenCode resolves the turn to the agent's configured model, and the session's persisted model is overwritten — silently switching the user to a different (potentially metered) model for the remainder of the session.

Environment

  • OS: NixOS (Linux x86_64)
  • OpenCode Version: 1.18.21
  • Plugin Version: 0.3.6 (opencode-pty@latest)
  • Bun Version: (plugin runs under OpenCode's plugin host)

OpenCode Configuration

opencode.json (sanitized)
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    "opencode-pty@latest",
    "oh-my-openagent@latest"
  ]
}

Suggested Fix

sendExitNotification already has the parent session id; fetch the session's current model and pass it through:

async sendExitNotification(session, exitCode) {
    if (!this.client) return;
    try {
        const message = this.buildExitNotification(session, exitCode);
        // Preserve the user's currently selected model across the injected wake-up turn
        let model;
        try {
            const res = await this.client.session.get({ path: { id: session.parentSessionId } });
            const m = res?.data?.model ?? res?.model; // { id, providerID, variant }
            if (m?.providerID && m?.id) {
                model = { providerID: m.providerID, modelID: m.id, ...(m.variant ? { variant: m.variant } : {}) };
            }
        } catch { /* fall back to model-less prompt */ }

        await this.client.session.promptAsync({
            path: { id: session.parentSessionId } },
            body: {
                parts: [{ type: 'text', text: message }],
                ...(session.parentAgent ? { agent: session.parentAgent } : {}),
                ...(model ? { model } : {}),
            },
        });
    } catch {
        // Ignore notification errors
    }
}

This mirrors what the TUI does on every prompt (it always sends model: selectedModel). Note there is an inherent race if the user switches models between session.get and the dispatch, but that window is negligible compared to the current behavior of always reverting.

A deeper fix on the OpenCode side (guarding setAgentModel against model-less overwrites) is being tracked in anomalyco/opencode#42893 — this issue covers the plugin side.

Debug Logs

Click to expand debug logs

Not applicable — the misbehavior is fully explained by the missing model field in the promptAsync body (notification-manager.js L13–19), confirmed by inspecting the stored user-message model fields in ~/.local/share/opencode/opencode.db.

Additional Context

Related historical issue: #23 ("PTY exit notifications to use spawning conversation agent") fixed the agent half of this problem; the model half remains. Downstream amplification: oh-my-openagent's continuation hooks resolve the model from the most recent session message, so one model-less <pty_exited> injection poisons every subsequent model-less injection until the user manually sends a message.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions