chat: keep OpenCode session cwd consistent across turns + strengthen the override
msg.cwd only rides the first message, but OpenCode's system prompt is rebuilt every turn — so turn 2+ reverted to general_chat_sessions and the model fell back to the server's real cwd. Fix: the cwd is bound at creation (metadata.officer.cwd), so for an existing/resumed session read it back (new client.getSession) and reuse it for the system prompt every turn. Also make the prompt explicit that it overrides any other working directory the environment reports. Verified: a session created with a cwd reads the same cwd back on a later turn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,12 @@ class ServerConnection {
|
|||||||
return (await res.json()) as OpenCodeSessionInfo[];
|
return (await res.json()) as OpenCodeSessionInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSession(sessionId: string): Promise<OpenCodeSessionInfo | null> {
|
||||||
|
const res = await fetch(`${this.baseUrl}/session/${sessionId}`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return (await res.json()) as OpenCodeSessionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> {
|
async getMessages(sessionId: string): Promise<OpenCodeStoredMessage[]> {
|
||||||
const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`);
|
const res = await fetch(`${this.baseUrl}/session/${sessionId}/message`);
|
||||||
if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`);
|
if (!res.ok) throw new Error(`opencode GET /session/${sessionId}/message → ${res.status}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ChatEvent } from '@@/api/chat/types';
|
import type { ChatEvent } from '@@/api/chat/types';
|
||||||
import { logger } from '@@/api/chat/logger';
|
import { logger } from '@@/api/chat/logger';
|
||||||
import { ensureServer } from '@@/api/chat/opencode/server-manager';
|
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 { createEventMapper } from '@@/api/chat/opencode/event-mapper';
|
||||||
import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state';
|
import { getOpenCodeSession, setOpenCodeSession } from '@@/api/chat/opencode/state';
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ function officerSystemPrompt(cwd: string): string {
|
|||||||
return [
|
return [
|
||||||
`Session working directory: ${cwd}`,
|
`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.`,
|
`Unless the user explicitly directs you elsewhere, keep your work focused on \`${cwd}\`, its files, and its subdirectories.`,
|
||||||
].join('\n');
|
].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
|
// 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.
|
// itself the OpenCode session id (`ses_…`); otherwise create a new one.
|
||||||
let opencodeSessionId =
|
const existingId =
|
||||||
getOpenCodeSession(params.sessionKey) ??
|
getOpenCodeSession(params.sessionKey) ??
|
||||||
(params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ??
|
(params.sessionKey.startsWith('ses_') ? params.sessionKey : undefined) ??
|
||||||
params.resumeSessionId;
|
params.resumeSessionId;
|
||||||
if (!opencodeSessionId) {
|
|
||||||
// Tag the session with its logical cwd so it can be listed in the right place (OpenCode has no
|
// The session's working directory is bound at creation (metadata.officer.cwd). msg.cwd only rides
|
||||||
// per-session directory — every session runs in the fixed server's cwd).
|
// the first message, so for an existing/resumed session read the bound cwd back and reuse it — this
|
||||||
opencodeSessionId = await conn.createSession(params.cwd ? { officer: { cwd: params.cwd } } : undefined);
|
// 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);
|
setOpenCodeSession(params.sessionKey, sessionId);
|
||||||
const sessionId = opencodeSessionId;
|
|
||||||
|
|
||||||
let unsub = () => {};
|
let unsub = () => {};
|
||||||
const mapper = createEventMapper((event: ChatEvent) => {
|
const mapper = createEventMapper((event: ChatEvent) => {
|
||||||
@@ -72,7 +79,7 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr
|
|||||||
unsub = conn.subscribe(sessionId, mapper);
|
unsub = conn.subscribe(sessionId, mapper);
|
||||||
|
|
||||||
const { providerID, modelID } = splitModel(params.model ?? '');
|
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.
|
// Fire the turn; assistant tokens + tool calls stream back over the SSE subscription above.
|
||||||
conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => {
|
conn.postMessage(sessionId, providerID, modelID, params.prompt, system).catch((err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user