Description
Session routes throw an unguarded Effect Schema ParseError when a session id arrives carrying the opencode: deep-link scheme prefix (URL-encoded as opencode%3A). The server logs:
ERROR service=server ref=err_cee4144f error=Expected a string starting with "ses", got "opencode%3Ases_169659db9ffeX6V47C8c3Sclz7" cause=Error: Expected a string starting with "ses", got "opencode%3Ases_169659db9ffeX6V47C8c3Sclz7"
opencode%3Ases_... is URL-encoded opencode:ses_.... Note the value contains a valid ses_... id - it just has the opencode://session/<id> deep-link scheme prefix prepended (added in #6232) and is never stripped/decoded before reaching the SessionID validator.
This is distinct from the existing placeholder-id reports (#28486 / #29262 got "dummy", #29868 got "%7Bid%7D" = uninterpolated {id}): those are placeholder strings, whereas this is a real id wrapped in the deep-link scheme. They do share a common theme - several code paths feed un-normalized strings straight into SessionID.make, which throws a raw Effect ParseError instead of degrading gracefully.
Root cause
The opencode: scheme prefix is never stripped (nor is the path segment decodeURIComponent'd) before the id reaches Schema.isStartsWith("ses"). Two paths fail:
1. Validator - packages/core/src/session/schema.ts:
export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
Schema.brand("SessionID"),
...
)
(re-exported as SessionV2.ID, aliased SessionID in packages/opencode/src/session/schema.ts). The error text itself is formatted by Effect's ParseResult.ts (getDefaultTypeMessage -> Expected ${description}, actual ${value}), with the filter description a string starting with "ses".
2. Workspace-routing middleware - packages/opencode/src/server/shared/workspace-routing.ts (getWorkspaceRouteSessionID):
const id =
url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1] ??
url.pathname.match(/^\/experimental\/session\/([^/]+)\/background$/)?.[1]
if (!id) return null
return SessionID.make(id) // id still carries the opencode: prefix -> throws
Called from .../httpapi/middleware/workspace-routing.ts for every /session/* route, so the throw fires before the handler.
3. Effect HttpApi path param - .../httpapi/groups/session.ts, every endpoint with params: { sessionID: SessionID } validates the raw :sessionID path segment the same way.
The only existing decodeURIComponent in the server (.../middleware/instance-context.ts) decodes the directory param, not session ids. There is no code that strips the opencode: scheme from a session id.
(Contrast: the TUI route TuiHttpApi.selectSession guards with if (!sessionID.startsWith("ses")) return BadRequest{}, so it fails cleanly there - but the workspace-routing / HttpApi paths surface the raw ParseError.)
Steps to reproduce
- Run
opencode serve.
- Issue a session request whose path segment carries the deep-link scheme, e.g. a request derived from an
opencode://session/<id> deep link, so the path contains opencode%3Ases_<id> (URL-encoded opencode:ses_<id>).
- The server responds with an unexpected error; logs show
Expected a string starting with "ses", got "opencode%3Ases_...".
Suggested fix
Normalize (decode + strip scheme) before validation. Primary site - getWorkspaceRouteSessionID:
if (!id) return null
const decoded = decodeURIComponent(id)
const bare = decoded.startsWith("opencode:") ? decoded.slice("opencode:".length) : decoded
return SessionID.make(bare)
And apply the equivalent for the HttpApi params: { sessionID } path (e.g. a SessionIDFromPath transform schema that decodes + strips the scheme), so both entry points normalize identically. More broadly, consider having session routes return 400 BadRequest instead of letting SessionID.make throw a raw ParseError to the server error handler (the same hardening would improve #28486 / #29262 / #29868).
Related
OpenCode version
Observed on a build from 2026-06-05 (analysis pinned to commit 4519a1da on main); CLI 1.16.2 installed locally.
Description
Session routes throw an unguarded Effect Schema
ParseErrorwhen a session id arrives carrying theopencode:deep-link scheme prefix (URL-encoded asopencode%3A). The server logs:opencode%3Ases_...is URL-encodedopencode:ses_.... Note the value contains a validses_...id - it just has theopencode://session/<id>deep-link scheme prefix prepended (added in #6232) and is never stripped/decoded before reaching theSessionIDvalidator.This is distinct from the existing placeholder-id reports (#28486 / #29262
got "dummy", #29868got "%7Bid%7D"= uninterpolated{id}): those are placeholder strings, whereas this is a real id wrapped in the deep-link scheme. They do share a common theme - several code paths feed un-normalized strings straight intoSessionID.make, which throws a raw EffectParseErrorinstead of degrading gracefully.Root cause
The
opencode:scheme prefix is never stripped (nor is the path segmentdecodeURIComponent'd) before the id reachesSchema.isStartsWith("ses"). Two paths fail:1. Validator -
packages/core/src/session/schema.ts:(re-exported as
SessionV2.ID, aliasedSessionIDinpackages/opencode/src/session/schema.ts). The error text itself is formatted by Effect'sParseResult.ts(getDefaultTypeMessage->Expected ${description}, actual ${value}), with the filter descriptiona string starting with "ses".2. Workspace-routing middleware -
packages/opencode/src/server/shared/workspace-routing.ts(getWorkspaceRouteSessionID):Called from
.../httpapi/middleware/workspace-routing.tsfor every/session/*route, so the throw fires before the handler.3. Effect HttpApi path param -
.../httpapi/groups/session.ts, every endpoint withparams: { sessionID: SessionID }validates the raw:sessionIDpath segment the same way.The only existing
decodeURIComponentin the server (.../middleware/instance-context.ts) decodes thedirectoryparam, not session ids. There is no code that strips theopencode:scheme from a session id.(Contrast: the TUI route
TuiHttpApi.selectSessionguards withif (!sessionID.startsWith("ses")) return BadRequest{}, so it fails cleanly there - but the workspace-routing / HttpApi paths surface the rawParseError.)Steps to reproduce
opencode serve.opencode://session/<id>deep link, so the path containsopencode%3Ases_<id>(URL-encodedopencode:ses_<id>).Expected a string starting with "ses", got "opencode%3Ases_...".Suggested fix
Normalize (decode + strip scheme) before validation. Primary site -
getWorkspaceRouteSessionID:And apply the equivalent for the HttpApi
params: { sessionID }path (e.g. aSessionIDFromPathtransform schema that decodes + strips the scheme), so both entry points normalize identically. More broadly, consider having session routes return400 BadRequestinstead of lettingSessionID.makethrow a rawParseErrorto the server error handler (the same hardening would improve #28486 / #29262 / #29868).Related
opencode://session/<id>deep-link scheme this prefix comes from--continue --fork: Expected a string starting with "ses", got "dummy" #29262 - same validator, different (placeholder) triggersmessagesmemo race)startsWith("ses")(CLI--session)OpenCode version
Observed on a build from 2026-06-05 (analysis pinned to commit
4519a1daonmain); CLI1.16.2installed locally.