Skip to content

[FEATURE]: Plugin API: read session messages (v1 client.session.messages parity) #57424

Open

Description

@jurgenhaas

Feature hasn't been suggested before.

Describe the enhancement you want to request

In the v1 plugin API, a plugin could read a session transcript: the server() input carried a client with working client.session.messages(...) and client.session.get(...), plus serverUrl. Several published plugins rely on this (e.g. @gotgenes/opencode-session-context, @gotgenes/opencode-agent-identity build agent-attribution and session-reading tools on it).

In v2 (verified on 2.1.0 by probing a live setup(ctx) context), that capability is gone with no replacement:

  • ctx.session exposes hook, list, create, get, remove, switchAgent, switchModel, prompt, generate, command, compact, synthetic, interrupt, update, move, wait, context — no way to read messages.
  • ctx.session.context() is not the transcript: after ctx.session.synthetic() added a message it still returned an empty array.
  • A two-level walk of the whole ctx for anything matching /message|history|transcript|part/i finds zero hits.
  • Tool execute(args, context) receives ["sessionID","messageID","agent","progress","signal"] — no client either.
  • Unlike v1, the context exposes no serverUrl/port/credential, so a plugin cannot even fall back to the HTTP API (GET /api/session/:id/message exists and works) without out-of-band configuration — that half of the gap is [FEATURE]: Plugin API: bridge session revert, and expose server self-endpoint to plugins #51599.

Request: bridge session message reading into the plugin session domain — e.g. ctx.session.messages(sessionID) returning what the HTTP endpoint returns (messages with roles, agent/model attribution, parts). That restores v1 parity for transcript-reading plugins (attribution, session summarization, quota/retry tooling, cross-session context) and is read-only, so it adds no new mutation surface.

Workarounds considered and why they fall short:

Activity

  1. opencode-agent commented on Oct 20, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up and for probing the live setup(ctx) context!

    You're asking for a read-only way for v2 plugins to get a session's stored messages, e.g. ctx.session.messages(sessionID), returning what GET /api/session/:id/message returns, so transcript-reading plugins work again as they did with v1's client.session.messages.

    You're right that this can't be done as a plugin today. The public ctx.session domain doesn't include messages, even though core already has a session messages read that the HTTP endpoint uses. Maintainers will consider it.

    Related issues:

  2. iceteaSA commented on Nov 4, 2026

    @iceteaSA

    Data for this request, from a plugin that must find the last assistant message of a different session (the last answer of a subagent).

    Today the only read is ctx.session.context(sessionID). It loads and decodes the full model window. We measured the CPU time of the main thread on an actual 2.2.5 host with seeded sessions:

    Messages context, warm median Read of the latest 64 messages, warm median
    1,000 12.5 ms 4.0 ms
    10,000 128.1 ms 4.2 ms
    25,000 317.9 ms 3.8 ms

    To find the last assistant message, we read the latest 64 messages, newest first. When that page does not give the answer, we read context as before. At 10,000 messages, this changed the CPU time for each call from 141.5 ms to 4.7 ms (−96.6%). We compared the result with the full read 104 times, and it was equal each time. These tests included compaction, part updates and plugin reloads. On a shared service with long sessions, the full reads cause freezes of 0.6 to 3.6 s each.

    A plugin cannot get the same result from a cache that events update. The events do not give an atomic watermark, so the plugin cannot know that it did not receive the last event. In our test, a cache that did not receive one last event gave the expired text.

    The host has the necessary reads at this time:

    • Session.messages with limit, order, type and an ID cursor (packages/core/src/session/store.ts:43-52,102-134 at v2.2.5).
    • One message by ID (store.ts:137-138,195-200).
    • The HTTP route uses them (packages/server/src/handlers/message.ts:36-78).

    Only the plugin SessionDomain does not include them (packages/plugin/src/promise/session.ts:201-220).

    Request (read-only):

    • ctx.session.messages({ sessionID, limit, order, cursor, type }), with the same codecs and limits as the HTTP route.
    • ctx.session.message({ sessionID, messageID }).

    These do not give the plugin a service URL or a credential, and they add no write operation.

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