Repository navigation
Session routes throw raw ParseError when id carries opencode: deep-link scheme prefix (opencode%3Ases_...) #31145
Description
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 -> throwsCalled 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 containsopencode%3Ases_<id>(URL-encodedopencode: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
- Feature Request: Desktop app deep links (opencode://session/<id>) #6232 (closed) - introduced the
opencode://session/<id>deep-link scheme this prefix comes from - TUI hangs in infinite render loop after auto-resume abort fails with session ID parse error #29868, Historical sessions disappear - Expected a string starting with "ses", got "dummy" error #28486, TUI shows server error on
--continue --fork: Expected a string starting with "ses", got "dummy" #29262 - same validator, different (placeholder) triggers - Web deep-linked session pages can crash on initial render #21569 - different deep-link crash (web
messagesmemo race) - CLI --session flag rejects external session IDs that don't start with 'ses' #29964 (closed) - external session ids rejected by
startsWith("ses")(CLI--session)
OpenCode version
Observed on a build from 2026-06-05 (analysis pinned to commit 4519a1da on main); CLI 1.16.2 installed locally.
Activity
tiffanychum commentedon Jun 7, 2026
I'd like to work on this. Root cause:
getWorkspaceRouteSessionIDinworkspace-routing.tspasses the raw URL path segment directly toSessionID.make()without decoding or stripping theopencode:scheme prefix. Fix: add normalization inworkspace-routing.tsand aSessionIDFromPathtransform schema for HttpApi params. PR coming shortly.github-actions commentedon Aug 7, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.