Skip to content

[FEATURE]: Plugin API: awaited preflight hook before context assembly (restart-resume bypasses session.prompt) #58572

Open

Description

@WhiteGiverMa

Feature hasn't been suggested before.

Describe the enhancement you want to request

Problem

When the OpenCode service restarts and automatically resumes a suspended session, the resumed session's first model request bypasses every plugin hook that runs on the normal prompt path. Plugins that build per-agent state at admission time start the resumed turn with empty state and no supported way to rebuild it.

The path, on current dev (24b5ed8bcc):

  • prepareResume in packages/core/src/session/execution/restart.ts publishes SessionEvent.Synthetic ("The server restarted while you were working…") and gates execution.resume. This path never reaches SessionPrompt.prepare, so session.prompt — which only fires in packages/core/src/session/prompt.ts — does not run.
  • The resumed turn's first model request still triggers session.context, but by then the plugin has had no initialization point.

There is a second, structural gap: session.context receives a mutable array of system parts with no provenance. SessionModelRequest.baseTranscript assembles the system array as [agent.system ?? host default prompt, initial], and earlier hooks may already have modified or reordered parts. Given [GUARD, DEFAULT, INITIAL], a plugin cannot distinguish "GUARD was added as an independent part" from "the agent's system text was rewritten to GUARD and DEFAULT is separate". A plugin that owns the agent's system text therefore cannot replace exactly its own region while preserving independent parts added by other plugins. Copying the host default template or guessing indices does not solve this.

What this breaks in practice

A plugin that takes over an agent's system prompt via a marker-delimited owned region seeds its state at admission (the session.prompt path). After a service restart:

  1. The plugin process restarts with empty in-memory state.
  2. The resumed session's first request arrives via the synthetic/resume path — no session.prompt, no seed.
  3. At session.context, the system parts contain no marker, and the plugin cannot prove which part is the native system text it owns. It must fail closed and block the request.
  4. The next ordinary user message re-seeds via admission and everything works — so the observable behavior is "the first request after every restart fails, then the session self-heals."

Fail-closed is correct behavior for a plugin that cannot prove ownership. The missing capability is on the host side.

Proposed API

An awaited primary-request preflight hook: after plugin activation completes and before the per-request agent snapshot is selected, run an awaited hook that exposes sessionID. Plugins establish their owned state there; the existing session.context hook then renders the final text with this request's model and tools.

A candidate wiring point is packages/core/src/session/runner/llm.ts: inside prepareContext, before context.select. Public types would live in packages/plugin/src/{promise,effect}/session.ts.

The hook contract:

  • Awaited: the request does not proceed until the hook settles, so asynchronous initialization (config loads, snapshot capture) completes deterministically.
  • Runs on every primary-request path: ordinary prompts, restart resume, and any future synthetic-driven execution. Uniformity is the point.
  • Read-only beyond sessionID: the hook establishes plugin-side state; it does not mutate the request.
  • Idempotency belongs to the plugin: the hook may fire more than once per session; plugins key state by session/agent and make initialization idempotent.

Independently, the provenance gap could be addressed by tagging system parts with their origin (agent config, host default, or the plugin/hook that added them). That would let ownership-aware plugins do precise replacement even without a preflight hook, but the preflight hook is the more general fix.

Related issues

Reproduction evidence

Observed on OpenCode v2.0.26 and v2.1.2 (Linux and Windows) with a plugin implementing marker-based system-prompt ownership. The plugin's live regression harness drives a real opencode serve process with a mock provider: two sessions share one managed agent, the service is SIGKILLed mid-conversation and restarted. The resume request records zero admissions and fails at context with no marker present; a subsequent ordinary user message re-seeds and succeeds. With a non-empty unique agents.<id>.system configured, the plugin can reseed from its pre-takeover snapshot and the resume succeeds — confirming the gap is exactly the missing initialization point, not the resume mechanics.

Activity

  1. opencode-agent commented on Oct 24, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and the regression evidence!

    To restate the request: you're asking for an awaited plugin hook that runs before context assembly on every primary request path, including the restart resume that skips session.prompt. That would let plugins rebuild their state before session.context. Separately, you suggest tagging system parts in session.context with where they came from (agent config, host default, or the plugin that added them), so plugins can safely replace only their own part.

    I checked the current code, and the restart resume path (prepareResume → SessionEvent.Synthetic) does bypass SessionPrompt.prepare as you describe. A plugin can't work around this today, so it needs a change to the plugin API. Maintainers will consider it.

    Related issues:

  2. WhiteGiverMa commented on Oct 24, 2026

    @WhiteGiverMa
    Author

    This report is generated by AI, but the request was discovered from a strange production bug in my plugin. Doc included, and AI too, contact me immediately if any problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions