From 22bcd7d4b9cf15c3e413e6567e2cbe855cd0ea87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 10 Aug 2026 01:58:46 +0000 Subject: [PATCH] list opencode sessions at all, and give a resumed one its directory back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, one cause: the session type declared two fields opencode 1.17.9 does not return. `GET /session` returns `directory` at the top level. There is no `location` object and no `metadata`. Re-verified by reading the live server rather than the type. So `metadata.officer.cwd` was compared against `undefined` for every session and the list filter matched nothing — and since `cwdOf` substitutes a default when no `?cwd=` is given, the `!cwd` escape never fired either. There was no configuration in which an OpenCode session appeared in /chat. Confirmed against the running server: 7 sessions present, 0 returned, and the `OpenCode` badge in SessionList was unreachable code. Now 1 of 7 is listed under the default chat dir, the other 6 correctly filtered to their own directories. And `location?.directory ?? ''` was likewise always '', so resuming a session reported no cwd and relocated the conversation to the default chat dir — which matters because OpenCode rebuilds its working-directory system prompt every turn. Detail now reads the session's own record via a new `getSession`, alongside the transcript. `officerMeta` and the `metadata` tag are gone rather than fixed: the only writer of that tag (`client.createSession`) has no callers, because the sidecar creates sessions with `opencode run --dir`. Tagging would have been a second source of truth for something `directory` already answers. docs/opencode-parity.md B1 and B3. Its suggested fix — derive from `location.directory` — was written against a field that does not exist; the doc asked for the version to be checked first, and this is why. Co-Authored-By: Claude Opus 5 --- src/servers/api/chat/opencode-sessions.ts | 31 ++++++++++++++------ src/servers/api/chat/opencode/client.ts | 35 +++++++++++++++++------ 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/servers/api/chat/opencode-sessions.ts b/src/servers/api/chat/opencode-sessions.ts index 2597f851..0877277f 100644 --- a/src/servers/api/chat/opencode-sessions.ts +++ b/src/servers/api/chat/opencode-sessions.ts @@ -1,6 +1,6 @@ import type { ClaudeSessionSummary, ClaudeSessionDetail, ClaudeChatMessage } from './claude-sessions'; import { ensureServer } from './opencode/server-manager'; -import { getConnection, officerMeta } from './opencode/client'; +import { getConnection } from './opencode/client'; import { logger } from './logger'; // The OpenCode analog of claude-sessions.ts. OpenCode's own store is the source of truth, read via the @@ -9,23 +9,29 @@ import { logger } from './logger'; // tagged harness:'opencode', so chat.ts can merge both harnesses transparently. /** - * List OpenCode sessions tagged with the given logical cwd (via `metadata.officer.cwd`), so each - * context (/chat pwd, email account, project) sees only its own. With no cwd, returns all. Never - * throws — returns [] if the server is unavailable. + * List OpenCode sessions for a working directory, so each context (/chat pwd, email account, project) + * sees only its own. With no cwd, returns all. Never throws — returns [] if the server is unavailable. + * + * Filters on the session's own `directory`, which is what OpenCode records when the runner starts it + * with `--dir`. It used to filter on `metadata.officer.cwd`, a tag whose only writer + * (`client.createSession`) has no callers — so the comparison was against `undefined` for every session + * and the list was ALWAYS empty. `cwdOf` in chat.ts substitutes a default when no `?cwd=` is given, so + * the `!cwd` escape never fired either and there was no configuration in which an OpenCode session + * appeared in /chat. Verified against the live server: 7 sessions present, 0 returned. */ export async function listOpenCodeSessions(cwd?: string): Promise { try { const { baseUrl } = await ensureServer(); const sessions = await getConnection(baseUrl).listSessions(); return sessions - .filter((s) => !cwd || officerMeta(s.metadata).cwd === cwd) + .filter((s) => !cwd || s.directory === cwd) .map((s) => { const created = s.time?.created ?? Date.now(); const updated = s.time?.updated ?? created; return { id: s.id, title: s.title || '(untitled)', - cwd: s.location?.directory ?? '', + cwd: s.directory ?? '', createdAt: new Date(created).toISOString(), updatedAt: new Date(updated).toISOString(), messageCount: 0, // the session list endpoint doesn't include a turn count @@ -42,7 +48,11 @@ export async function listOpenCodeSessions(cwd?: string): Promise { try { const { baseUrl } = await ensureServer(); - const stored = await getConnection(baseUrl).getMessages(sessionId); + const conn = getConnection(baseUrl); + // Both reads, together: the transcript, and the session record that carries its directory. OpenCode + // needs the cwd on EVERY turn (it rebuilds its working-directory system prompt each time), so a + // resume that reports '' silently relocates the conversation to the default chat dir. + const [stored, info] = await Promise.all([conn.getMessages(sessionId), conn.getSession(sessionId)]); const messages: ClaudeChatMessage[] = []; let modelId = ''; @@ -81,7 +91,12 @@ export async function loadOpenCodeSession(sessionId: string): Promise { + const res = await fetch(`${this.baseUrl}/session/${sessionId}`); + if (!res.ok) return null; + return (await res.json()) as OpenCodeSessionInfo; + } + async getMessages(sessionId: string): Promise { const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`); if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`); @@ -167,20 +174,32 @@ class ServerConnection { } } -// Shapes returned by the `/session/*` read endpoints (verified against opencode 1.17.9). +// Shapes returned by the `/session/*` read endpoints. +// +// Re-verified against the running opencode 1.17.9 on 2026-08-10 by reading `GET /session` directly. The +// keys it actually returns are: +// +// id, slug, projectID, directory, path, summary, cost, tokens, title, agent, model, version, time, +// permission +// +// Two fields this type used to declare are NOT among them, and both were load-bearing: +// +// - `location.directory` — there is no `location` object. The working directory is top-level +// `directory`. Every read of `location?.directory ?? ''` therefore produced '', which is why a +// resumed OpenCode session lost its cwd. +// - `metadata` — never returned, and nothing writes it. The session list filtered on +// `metadata.officer.cwd`, so it matched nothing and no OpenCode session was ever listed. +// +// Kept deliberately narrow: only the fields we read. Adding one means confirming it against a live +// server first — the previous pair were plausible and wrong, and cost two user-visible defects. export type OpenCodeSessionInfo = { id: string; title?: string; time?: { created?: number; updated?: number }; - location?: { directory?: string | null }; - metadata?: Record; + /** Absolute working directory the session belongs to. Top-level; there is no `location` wrapper. */ + directory?: string | null; }; -// Our own namespaced session metadata (stored under the free-form `metadata.officer` key). -export type OfficerSessionMeta = { cwd?: string }; -export const officerMeta = (m?: Record): OfficerSessionMeta => - (m?.officer as OfficerSessionMeta) ?? {}; - export type OpenCodeStoredPart = { type?: string; text?: string;