diff --git a/src/servers/api/chat/opencode/client.ts b/src/servers/api/chat/opencode/client.ts index 83f80e73..999fb10a 100644 --- a/src/servers/api/chat/opencode/client.ts +++ b/src/servers/api/chat/opencode/client.ts @@ -146,6 +146,12 @@ class ServerConnection { return (await res.json()) as OpenCodeSessionInfo[]; } + async getSession(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}`); diff --git a/src/servers/channels/send-opencode.ts b/src/servers/channels/send-opencode.ts index 8f400241..3297bcab 100644 --- a/src/servers/channels/send-opencode.ts +++ b/src/servers/channels/send-opencode.ts @@ -1,7 +1,7 @@ import type { ChatEvent } from '@@/api/chat/types'; import { logger } from '@@/api/chat/logger'; import { ensureServer } from '@@/api/chat/opencode/server-manager'; -import { getConnection } from '@@/api/chat/opencode/client'; +import { getConnection, officerMeta } from '@@/api/chat/opencode/client'; import { createEventMapper } from '@@/api/chat/opencode/event-mapper'; import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state'; @@ -38,7 +38,7 @@ function officerSystemPrompt(cwd: string): string { return [ `Session working directory: ${cwd}`, '', - `For this session, treat \`${cwd}\` as your current working directory. Resolve relative paths, globs, and file references against it, and run shell commands from inside it (\`cd\` into it, or use absolute paths beneath it). When the user says "here", "this directory", or gives a relative path, they mean a location within \`${cwd}\`.`, + `For this session, treat \`${cwd}\` as your current working directory — this overrides any other working directory the environment reports. Resolve relative paths, globs, and file references against it, and run shell commands from inside it (\`cd\` into it, or use absolute paths beneath it). When the user says "here", "this directory", or gives a relative path, they mean a location within \`${cwd}\`.`, '', `Unless the user explicitly directs you elsewhere, keep your work focused on \`${cwd}\`, its files, and its subdirectories.`, ].join('\n'); @@ -52,17 +52,24 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr // Resolve the OpenCode session: a known mapping, or — when resuming from history — the sessionKey is // itself the OpenCode session id (`ses_…`); otherwise create a new one. - let opencodeSessionId = + const existingId = getOpenCodeSession(params.sessionKey) ?? (params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ?? params.resumeSessionId; - if (!opencodeSessionId) { - // Tag the session with its logical cwd so it can be listed in the right place (OpenCode has no - // per-session directory — every session runs in the fixed server's cwd). - opencodeSessionId = await conn.createSession(params.cwd ? { officer: { cwd: params.cwd } } : undefined); + + // The session's working directory is bound at creation (metadata.officer.cwd). msg.cwd only rides + // the first message, so for an existing/resumed session read the bound cwd back and reuse it — this + // keeps the system prompt (and session tagging) consistent across every turn. + let sessionId: string; + let cwd = params.cwd; + if (existingId) { + sessionId = existingId; + const bound = officerMeta((await conn.getSession(existingId))?.metadata).cwd; + if (bound) cwd = bound; + } else { + sessionId = await conn.createSession(cwd ? { officer: { cwd } } : undefined); } - setOpenCodeSession(params.sessionKey, opencodeSessionId); - const sessionId = opencodeSessionId; + setOpenCodeSession(params.sessionKey, sessionId); let unsub = () => {}; const mapper = createEventMapper((event: ChatEvent) => { @@ -72,7 +79,7 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr unsub = conn.subscribe(sessionId, mapper); const { providerID, modelID } = splitModel(params.model ?? ''); - const system = params.cwd ? officerSystemPrompt(params.cwd) : undefined; + const system = cwd ? officerSystemPrompt(cwd) : undefined; // Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above. conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => {