Skip to content

[FEATURE]: Ephemeral, tool-capable sessions for the V2 plugin API #58060

Open

Description

@mossgowild

Related issues

The problem

Plugins often need to run a short, tool-using agent turn as an internal step: consolidate memory, review a diff, classify files, etc. The V2 plugin API has no way to do that without creating a normal, persisted session.

ctx.generate.text is not enough. The docs describe it as generating text "without creating a session, invoking tools, or adding to session history". A tool loop needs a session.

The only workaround is a child session:

const child = await ctx.session.create({ parentID, agent, model })
try {
  await ctx.session.prompt({ sessionID: child.id, text })
  await ctx.session.wait({ sessionID: child.id })
  const messages = await ctx.session.context({ sessionID: child.id })
} finally {
  await ctx.session.remove({ sessionID: child.id })
}

This is not a temporary session:

  1. parentID is hierarchy, not visibility. The child is persisted from creation and appears in unfiltered GET /api/session / ctx.session.list, and in any client that does not filter by parentID.
  2. Nothing guarantees cleanup. If the plugin is unloaded, crashes, or the server exits mid-run, the session and its messages, tool output and snapshots remain. remove only runs if the plugin reaches finally.
  3. Parentage and visibility are conflated. A background controller may be top-level; a child may need to be visible.
  4. The write happens before plugin hooks, so a hook cannot prevent persistence.

Proposed API

Add two independent properties to ctx.session.create (and POST /api/session):

const temp = await ctx.session.create({
  title: "memory-consolidation",
  agent: "build",
  hidden: true,      // keep it out of user-facing pickers/history
  ephemeral: true,   // do not leave a durable session/message record
})

await ctx.session.prompt({ sessionID: temp.id, text: "..." })
await ctx.session.wait({ sessionID: temp.id })
const messages = await ctx.session.context({ sessionID: temp.id })

Semantics

  • hidden: true — excluded from user-facing session lists, pickers and live session events by default; addressable by ID; list APIs accept includeHidden; persistence unchanged. This is the shape proposed in Support session visibility independently of parentage #39911.
  • ephemeral: true — the session exists only for its lifetime. Tools, permissions, prompt, wait, context and interrupt work normally, but the server owns cleanup: on completion, cancellation, a TTL, plugin unload and process exit. After that there is no recoverable session or message row. Session snapshots and tool artifacts are cleaned up with it. Implies hidden.
  • parentID remains independent: an ephemeral/hidden session can be top-level; a child can be visible.
  • Optional convenience for the common one-shot case: ctx.session.run({ prompt }) returning the final assistant text, implemented on top of an ephemeral session. Not required if the create properties exist.

Observability: ephemeral does not have to mean "no logs". Server logs, traces and usage accounting can still record the run; the request is about not polluting user session history and not leaving a recoverable conversation.

Acceptance criteria

  • A plugin can create a session, run a prompt that calls tools, wait, read the result, and leave no user-facing session in GET /api/session or ctx.session.list.
  • Cleanup happens on completion, cancellation, plugin unload/crash and server exit, not only when the plugin reaches finally.
  • hidden sessions are excluded from the TUI/app root session lists but remain readable by ID.
  • hidden / ephemeral are independent of parentID.
  • Normal sessions behave exactly as today.

Environment

OpenCode 2.1.0, @opencode/plugin 2.1.2.

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up, @mossgowild!

    To restate it: you'd like plugins to be able to create sessions that run a full tool loop (prompt / wait / context) but are kept out of user-facing session lists (hidden). You'd also like those sessions to be cleaned up by the server, not by the plugin (ephemeral), with both settings separate from parentID.

    I checked the current code. ctx.session.create / POST /api/session has no visibility or lifetime field today, so the child-session plus remove in finally approach is the only option. As you describe, that session is persisted and listed while it runs, and it is left behind if the plugin or server goes away mid-run. A plugin can't work around this alone; it needs support in the session API. Maintainers will consider it.

    Related issues:

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