anomalyco / anomalyco/opencode
[FEATURE]:Expose session metadata in the V2 API so programmatic run sessions are attributable
@rekram1-node is already working on this.
Since Aug 4, 2026.
- 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 defined — packages/core/src/session/sql.ts, SessionTable:
metadata: text({ mode: "json" }).$type<Record<string, unknown>>(),
The only writer is the V1 projector — packages/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:
- Add
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional)to thesession.createpayload inpackages/protocol/src/groups/session.ts. - Add the same field to
Session.Infoinpackages/schema/src/session.tsand maprow.metadatainfromRow()inpackages/core/src/session/info.ts. - Have
runNonInteractivepass a default marker, e.g.metadata: { origin: "run" }, throughresolveSessionTarget()inpackages/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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.