anomalyco / anomalyco/opencode

[FEATURE]:Expose session metadata in the V2 API so programmatic run sessions are attributable

Open
#40,384 1 comment 0 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Aug 4, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Feature hasn't been suggested before.
  • I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request

Audited against: branch v2 @ 42e8d11 (2026-08-03)


Summary

The V2 session table already has a free-form metadata JSON column, but nothing in the V2 read or write path touches it, and opencode2 run does not set it. As a result there is no way to tell, from the session store, whether a session was started interactively or by a script.

I'd like session.create to accept metadata, Session.Info to return it, and run to populate a default origin marker.

Motivation

I drive OpenCode programmatically via opencode run from scripts and scheduled jobs, alongside normal interactive use in the same projects. Downstream I want to:

  • separate automated runs from human ones when querying cost and token totals
  • filter automated runs out of session pickers and history views
  • attribute a session back to the job that created it (job ID, workflow name, run number)

Today none of this is possible without heuristics like matching on --title strings, or filtering with denied permissions.

Current behaviour

The storage exists but is unreachable from V2.

The column is definedpackages/core/src/session/sql.ts, SessionTable:

metadata: text({ mode: "json" }).$type<Record<string, unknown>>(),

The only writer is the V1 projectorpackages/core/src/session/projector.ts, sessionRow():

metadata: info.metadata,

sourced from the V1 SessionInfo schema (packages/schema/src/v1/session.ts), which has:

metadata: optional(Schema.Record(Schema.String, Schema.Any)),

V2 never reads it. Session.Info in packages/schema/src/session.ts has no metadata field, and fromRow() in packages/core/src/session/info.ts does not map the column.

V2 cannot write it. The session.create payload in packages/protocol/src/groups/session.ts is:

payload: Schema.Struct({
  id: Session.ID.pipe(Schema.optional),
  agent: Agent.ID.pipe(Schema.optional),
  model: Model.Ref.pipe(Schema.optional),
  location: Location.Ref.pipe(Schema.optional),
}),

and there is no session.update / session.metadata endpoint in the V2 group to set it after the fact. (V1 has both Session.setMetadata and a PATCH handler; V2 dropped them.)

run sets nothing. resolveSessionTarget() in packages/cli/src/session-target.ts creates with exactly:

client.session.create({
  agent: prepared.agent,
  model: prepared.model,
  location: { directory: location.directory, workspaceID: location.workspaceID },
})

So a session created by opencode2 run is byte-identical in shape to one created by the TUI.

Proposed change

Option A, preferred — surface the column that already exists:

  1. Add metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional) to the session.create payload in packages/protocol/src/groups/session.ts.
  2. Add the same field to Session.Info in packages/schema/src/session.ts and map row.metadata in fromRow() in packages/core/src/session/info.ts.
  3. Have runNonInteractive pass a default marker, e.g. metadata: { origin: "run" }, through resolveSessionTarget() in packages/cli/src/session-target.ts.

No migration is needed since the column is already in the schema, and V1-projected sessions keep whatever metadata they already carry.

Option B — a dedicated typed origin field ("tui" | "run" | "acp" | "api" | "sdk") on Session.Info. Stricter and queryable without JSON extraction, but it needs a real column and it only answers the origin question, not the "which job made this" question.

I lean toward A because it covers both, and B can be layered on later if the enum turns out to be worth having on its own.

Relationship to #4489

#4489 asks for --ephemeral / --no-session so throwaway runs never hit the store at all. That is the opposite remedy for an overlapping problem: suppression versus attribution.

They are complementary rather than competing. Suppression loses the cost and token data, which is part of what I want to keep. If both land, --ephemeral handles "I don't care about this run" and metadata handles "I care about this run and want to know where it came from."

Notes

Audited by checking out v2 at 42e8d11. Worth confirming this is the branch the @opencode-ai/cli@next beta ships from: the beta and dev branches at the same date have no run command in packages/cli/src/commands/commands.ts at all, so the audit above would not apply to them.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.