list opencode sessions from every project, not just the serve's own
An opencode session in a git directory never appeared in /chat. The list read GET /session,
which answers for ONE project — the one the request's directory resolves to, and with no
x-opencode-directory header that is the serve's own cwd, DATA_PATH/opencode_server. Not a git
checkout, so it resolves to the catch-all project `global`, along with every other non-git
directory. That is why the default chat dir listed fine and nothing looked broken: a cwd that
IS a checkout gets its own project, and chat pwds are checkouts.
Measured on the live serve before changing anything: /session returned 8 sessions, /api/session
13, the five missing ones being an old project's. A session created in a git directory came back
0 times from /session and 1 from /api/session.
/api/session spans projects, so that is now the list. The per-id reads stay on /session — they
answer for any session regardless of project, verified 200 with and without the header.
The trap, and the reason listSessions normalises rather than returning the response: the two
surfaces disagree in silence. /session carries the working directory as top-level `directory`,
/api/session as `location.directory` with no top-level field, inside a {data: …} envelope.
Swapping the endpoint without the mapping leaves `directory` undefined on every session, which
the cwd filter turns into an empty list — the same shape as the metadata.officer.cwd bug this
filter already had once.
Verified against the live serve: with the mapping, a session in a git directory and one in the
general chat dir both resolve to their cwd, and every session carries a directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,8 @@ export const chatRouter = createRouter();
|
|||||||
|
|
||||||
// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
|
// The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default
|
||||||
// general_chat_sessions dir. Claude groups sessions by cwd, so this selects which project group we read.
|
// general_chat_sessions dir. Claude groups sessions by cwd, so this selects which project group we read.
|
||||||
// (OpenCode sessions all live in the one fixed server and ignore cwd.)
|
// OpenCode runs on one fixed serve, but each session records the directory its turn ran in, so cwd
|
||||||
|
// selects there too.
|
||||||
const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getGeneralChatSessionsCwd(email);
|
const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getGeneralChatSessionsCwd(email);
|
||||||
|
|
||||||
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
||||||
|
|||||||
@@ -7,18 +7,17 @@ import { logger } from './logger';
|
|||||||
// fixed pm2-managed server's HTTP API (never the DB directly). Returns the same shapes as the Claude
|
// fixed pm2-managed server's HTTP API (never the DB directly). Returns the same shapes as the Claude
|
||||||
// reader, tagged harness:'opencode', so chat.ts can merge both harnesses transparently.
|
// reader, tagged harness:'opencode', so chat.ts can merge both harnesses transparently.
|
||||||
//
|
//
|
||||||
// Sessions do NOT all live in one project. That claim was true when turns went through the serve and
|
// Sessions do NOT all live in one project. Each records the directory its turn ran in — the serve takes
|
||||||
// inherited its directory; turns are now `opencode run --dir <cwd>` subprocesses, so each session
|
// it per request as `x-opencode-directory` — and one serve holds sessions for many. That is why the cwd
|
||||||
// records the directory it ran in and one serve happily lists sessions across many. Verified: a single
|
// filter has to read `directory` rather than assume, and why the list has to come from a read that spans
|
||||||
// serve returned 7 sessions spread over several directories, which is also why the cwd filter has to
|
// projects (`client.listSessions`, which is where the project-scoping trap is written up).
|
||||||
// read `directory` rather than assume.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List OpenCode sessions for a working directory, so each context (/chat pwd, email account, project)
|
* 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.
|
* 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
|
* Filters on the session's own `directory`, which is what OpenCode records from the directory the turn
|
||||||
* with `--dir`. It used to filter on `metadata.officer.cwd`, a tag whose only writer
|
* ran in. 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
|
* (`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
|
* 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
|
* the `!cwd` escape never fired either and there was no configuration in which an OpenCode session
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// One connection per `opencode serve` base URL, for the REST reads the chat list and transcript need.
|
// One connection per `opencode serve` base URL, for the REST reads the chat list and transcript need.
|
||||||
// The SDK-style `/session/*` route family is used (feature-complete, incl. DELETE).
|
// The SDK-style `/session/*` route family is used for the per-id reads and writes (feature-complete,
|
||||||
|
// incl. DELETE) — those answer for any session regardless of project, verified 200 with and without a
|
||||||
|
// directory header. The LIST is the exception and goes through `/api/session`; see `listSessions`.
|
||||||
//
|
//
|
||||||
// This used to also hold a shared SSE subscription (`GET /event`) demultiplexed per session, plus
|
// This used to also hold a shared SSE subscription (`GET /event`) demultiplexed per session, plus
|
||||||
// `createSession`, `postMessage`, `abort` and `isServerHealthy` — the client half of a serve-based turn
|
// `createSession`, `postMessage`, `abort` and `isServerHealthy` — the client half of a serve-based turn
|
||||||
@@ -12,10 +14,30 @@ class ServerConnection {
|
|||||||
|
|
||||||
// ── Session history (REST; OpenCode's SQLite store is the source of truth) ──
|
// ── Session history (REST; OpenCode's SQLite store is the source of truth) ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every session the serve knows, across every project.
|
||||||
|
*
|
||||||
|
* `GET /session` — the endpoint this used — answers for ONE project: the one the request's directory
|
||||||
|
* resolves to, which with no `x-opencode-directory` header is the serve's own cwd
|
||||||
|
* (`DATA_PATH/opencode_server`). That directory is not a git checkout, so it resolves to the catch-all
|
||||||
|
* project `global`, and so does every other non-git directory — which is why the general chat dir
|
||||||
|
* listed fine and nothing looked wrong. A cwd that IS a git checkout gets its own project and its
|
||||||
|
* sessions were simply never returned. Measured on the live serve: `/session` 8, `/api/session` 13,
|
||||||
|
* and a session created in a git directory came back 0 times from `/session` and 1 from `/api/session`.
|
||||||
|
*
|
||||||
|
* Chat pwds are project directories, so that is the ordinary case, not the edge one.
|
||||||
|
*
|
||||||
|
* The two surfaces disagree about the shape, and the difference is silent: `/session` carries the
|
||||||
|
* working directory as top-level `directory`, `/api/session` as `location.directory` with no top-level
|
||||||
|
* field at all, wrapped in `{data: …}`. Normalised here so callers keep reading `directory` — reading
|
||||||
|
* the wrong one yields `undefined` for every session, which filters the list down to nothing.
|
||||||
|
*/
|
||||||
async listSessions(): Promise<OpenCodeSessionInfo[]> {
|
async listSessions(): Promise<OpenCodeSessionInfo[]> {
|
||||||
const res = await fetch(`${this.baseUrl}/session`);
|
const res = await fetch(`${this.baseUrl}/api/session`);
|
||||||
if (!res.ok) throw new Error(`opencode GET /session → ${res.status}`);
|
if (!res.ok) throw new Error(`opencode GET /api/session → ${res.status}`);
|
||||||
return (await res.json()) as OpenCodeSessionInfo[];
|
const body = (await res.json()) as { data?: ApiSessionInfo[] } | ApiSessionInfo[];
|
||||||
|
const sessions = Array.isArray(body) ? body : (body.data ?? []);
|
||||||
|
return sessions.map((s) => ({ ...s, directory: s.directory ?? s.location?.directory ?? null }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One session's own record. The only place its working directory can be read on resume. */
|
/** One session's own record. The only place its working directory can be read on resume. */
|
||||||
@@ -72,6 +94,9 @@ export type OpenCodeSessionInfo = {
|
|||||||
directory?: string | null;
|
directory?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** `/api/session`'s shape: the same record, with the directory one level down. Normalised by `listSessions`. */
|
||||||
|
type ApiSessionInfo = OpenCodeSessionInfo & { location?: { directory?: string | null } };
|
||||||
|
|
||||||
export type OpenCodeStoredPart = {
|
export type OpenCodeStoredPart = {
|
||||||
type?: string;
|
type?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user