pi-orchestrator-config 4.3.2 → 4.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/extensions/cli-provider/agents/cursor.ts +28 -1
- package/extensions/cli-provider/agents/gemini.ts +2 -1
- package/extensions/cli-provider/providers.ts +1 -0
- package/extensions/cli-provider/runner.ts +22 -2
- package/extensions/providers/acpx-driver.ts +39 -25
- package/extensions/providers/claude-driver.ts +23 -14
- package/extensions/providers/cursor-acpx-driver.ts +33 -21
- package/extensions/providers/cursor-cli-driver.ts +85 -27
- package/extensions/providers/gemini-driver.ts +23 -14
- package/extensions/providers/index.ts +24 -3
- package/extensions/shared/provider-driver.ts +2 -0
- package/extensions/shared/session-cwd.ts +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Single extension that provides:
|
|
|
15
15
|
| **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
|
|
16
16
|
| **`list_models`** | List available models and providers for subagent model override |
|
|
17
17
|
| **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`, `/async-kill`). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `dev-docs/cli-provider.md`) |
|
|
18
|
-
| **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new`, empty `enabledModels`, trusted project merge — see `dev-docs/cli-provider.md` |
|
|
18
|
+
| **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new`, empty `enabledModels`, trusted project merge — see `dev-docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
|
|
19
19
|
| **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
|
|
20
20
|
| **`/async-status` command** | Show status of background agents — select one for live output streaming |
|
|
21
21
|
| **`/async-kill` command** | Kill async agents (overlay picker or by name/id) |
|
|
@@ -726,7 +726,8 @@ Then just run `pi-docker` from any project directory.
|
|
|
726
726
|
> `pi install` runs on each start.
|
|
727
727
|
> A `WARNING` on stderr is normal when the package is already cached in `~/.pi`.
|
|
728
728
|
> If pi misbehaves or the warning persists, verify network connectivity
|
|
729
|
-
> and run `pi install git:github.com/myk-org/pi-config`
|
|
729
|
+
> and run `pi install git:github.com/myk-org/pi-config` and
|
|
730
|
+
> `pi install git:github.com/myk-org/pi-config/packages/pi-vertex-claude` manually.
|
|
730
731
|
|
|
731
732
|
## Discord Bot
|
|
732
733
|
|
|
@@ -54,11 +54,35 @@ function discoverCursorModels(): DiscoveredCliModel[] {
|
|
|
54
54
|
return models;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Headless Cursor MCP: `--approve-mcps` is opt-in, not unconditional.
|
|
59
|
+
* `CLI_APPROVE_MCPS` wins; otherwise sidecar processes (`SIDECAR_PORT`, stamped
|
|
60
|
+
* by `startSidecar()` while running) get it because they have no TTY.
|
|
61
|
+
* Interactive pi omits the flag.
|
|
62
|
+
*/
|
|
63
|
+
export function shouldApproveCursorMcps(
|
|
64
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
65
|
+
): boolean {
|
|
66
|
+
const explicit = env.CLI_APPROVE_MCPS;
|
|
67
|
+
if (typeof explicit === "string" && explicit.length > 0) {
|
|
68
|
+
const on = ["1", "true", "yes", "on"].includes(explicit.toLowerCase());
|
|
69
|
+
log.debug(`cursor --approve-mcps CLI_APPROVE_MCPS=${explicit} -> ${on}`);
|
|
70
|
+
return on;
|
|
71
|
+
}
|
|
72
|
+
const sidecar = typeof env.SIDECAR_PORT === "string" && env.SIDECAR_PORT.length > 0;
|
|
73
|
+
log.debug(`cursor --approve-mcps SIDECAR_PORT set=${sidecar}`);
|
|
74
|
+
return sidecar;
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
export const cursorProvider: CliProviderDef = {
|
|
58
78
|
name: "cursor",
|
|
59
79
|
binary: "agent",
|
|
60
80
|
buildBaseArgs: (model, cwd) => {
|
|
61
|
-
// --trust: workspace; --force: auto-approve
|
|
81
|
+
// --trust: workspace; --force: auto-approve tools
|
|
82
|
+
const approveMcps = shouldApproveCursorMcps();
|
|
83
|
+
log.debug(
|
|
84
|
+
`cursor buildBaseArgs approveMcps=${approveMcps} modelOverride=${Boolean(model && model !== "default")}`,
|
|
85
|
+
);
|
|
62
86
|
const args = [
|
|
63
87
|
"--print",
|
|
64
88
|
"--trust",
|
|
@@ -69,6 +93,9 @@ export const cursorProvider: CliProviderDef = {
|
|
|
69
93
|
"--workspace",
|
|
70
94
|
cwd,
|
|
71
95
|
];
|
|
96
|
+
if (approveMcps) {
|
|
97
|
+
args.push("--approve-mcps");
|
|
98
|
+
}
|
|
72
99
|
if (model && model !== "default") {
|
|
73
100
|
args.unshift("--model", model);
|
|
74
101
|
}
|
|
@@ -81,7 +81,8 @@ export const geminiProvider: CliProviderDef = {
|
|
|
81
81
|
name: "gemini",
|
|
82
82
|
binary: "gemini",
|
|
83
83
|
buildBaseArgs: (model) => {
|
|
84
|
-
// --skip-trust:
|
|
84
|
+
// --skip-trust: skip TTY trust dialog (MCP still needs GEMINI_CLI_TRUST_WORKSPACE)
|
|
85
|
+
// --yolo: auto-approve all tool actions
|
|
85
86
|
const args = [
|
|
86
87
|
"--skip-trust",
|
|
87
88
|
"--yolo",
|
|
@@ -15,6 +15,7 @@ import { geminiProvider } from "./agents/gemini.js";
|
|
|
15
15
|
* |---------|--------|--------|--------|
|
|
16
16
|
* | Workspace trust | `--trust` | skipped by `-p` | `--skip-trust` |
|
|
17
17
|
* | Tool/command approve | `--force` (`--yolo` alias) | `--dangerously-skip-permissions` | `--yolo` |
|
|
18
|
+
* | Project MCP | `--approve-mcps` if `CLI_APPROVE_MCPS` or `SIDECAR_PORT` (`startSidecar()` stamps it while running) | n/a | `GEMINI_CLI_TRUST_WORKSPACE=true` unless parent already set |
|
|
18
19
|
*
|
|
19
20
|
* Trust alone is NOT enough: without auto-approve, tool calls that need
|
|
20
21
|
* confirmation hang or fail (no interactive user).
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { createLogger } from "../shared/logger.js";
|
|
6
7
|
import type { CliAgentName } from "./providers.js";
|
|
7
8
|
import { buildCliCommand } from "./providers.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger("cli_provider");
|
|
8
11
|
import {
|
|
9
12
|
parseCliOutput,
|
|
10
13
|
StreamJsonAccumulator,
|
|
@@ -54,9 +57,23 @@ const STRIP_ENV = [
|
|
|
54
57
|
"CURSOR_TRACE_ID",
|
|
55
58
|
];
|
|
56
59
|
|
|
57
|
-
function childEnv(): NodeJS.ProcessEnv {
|
|
60
|
+
function childEnv(agent: CliAgentName): NodeJS.ProcessEnv {
|
|
58
61
|
const env = { ...process.env };
|
|
59
62
|
for (const k of STRIP_ENV) delete env[k];
|
|
63
|
+
// --skip-trust skips the TTY dialog; Gemini still treats /tmp (and other
|
|
64
|
+
// unsigned folders) as untrusted and will not connect project MCP unless
|
|
65
|
+
// this env is set (same class of headless gap as Cursor --approve-mcps).
|
|
66
|
+
// Default true for headless MCP; preserve an explicit parent value (e.g. false).
|
|
67
|
+
if (agent === "gemini") {
|
|
68
|
+
if (env.GEMINI_CLI_TRUST_WORKSPACE === undefined) {
|
|
69
|
+
env.GEMINI_CLI_TRUST_WORKSPACE = "true";
|
|
70
|
+
log.debug("gemini spawn: defaulting GEMINI_CLI_TRUST_WORKSPACE=true for project MCP");
|
|
71
|
+
} else {
|
|
72
|
+
log.debug(
|
|
73
|
+
`gemini spawn: preserving parent GEMINI_CLI_TRUST_WORKSPACE=${env.GEMINI_CLI_TRUST_WORKSPACE}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
return env;
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -80,9 +97,12 @@ export function runCliAgent(opts: CliRunOptions): Promise<CliRunResult> {
|
|
|
80
97
|
return;
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
log.debug(
|
|
101
|
+
`runCliAgent spawn agent=${opts.agent} stream=${stream} timeoutMs=${timeoutMs ?? "none"} cwdBound=${Boolean(opts.cwd)}`,
|
|
102
|
+
);
|
|
83
103
|
const child = spawn(binary, argv, {
|
|
84
104
|
cwd: opts.cwd,
|
|
85
|
-
env: childEnv(),
|
|
105
|
+
env: childEnv(opts.agent),
|
|
86
106
|
stdio: ["pipe", "pipe", "pipe"],
|
|
87
107
|
windowsHide: true,
|
|
88
108
|
});
|
|
@@ -35,6 +35,7 @@ import { loadAcpxRuntime, type AcpxRuntimeModule } from "../acpx-provider/load-r
|
|
|
35
35
|
import { modelIdToDisplayName } from "../acpx-provider/runtime-models.js";
|
|
36
36
|
import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
|
|
37
37
|
import { fileLog } from "../shared/file-logger.js";
|
|
38
|
+
import { resolveAdapterCwd, adapterMemoryKey } from "../shared/session-cwd.js";
|
|
38
39
|
import { resolveBinary } from "../shared/resolve-binary.js";
|
|
39
40
|
|
|
40
41
|
const LOG_DOMAIN = "acpx-driver";
|
|
@@ -73,34 +74,42 @@ type AcpRuntimeHandle = Awaited<ReturnType<AcpxRuntime["ensureSession"]>>;
|
|
|
73
74
|
// Adapter
|
|
74
75
|
// ---------------------------------------------------------------------------
|
|
75
76
|
|
|
76
|
-
function createAcpxAdapter(
|
|
77
|
+
export function createAcpxAdapter(
|
|
77
78
|
config: AcpxConfig,
|
|
78
79
|
cwd: string,
|
|
79
80
|
runtime: AcpxRuntime,
|
|
80
|
-
cwdSlug: string,
|
|
81
81
|
initialHandle?: AcpRuntimeHandle,
|
|
82
82
|
): ProviderAdapterShape {
|
|
83
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
84
|
+
`createAcpxAdapter bootCwdBound=${Boolean(cwd)} agent=${config.agent}`);
|
|
83
85
|
const handles = new Map<string, AcpRuntimeHandle>();
|
|
84
86
|
const prevCumulative = new Map<string, { inputTokens?: number; outputTokens?: number; totalTokens?: number }>();
|
|
85
|
-
// Seed with the discovery handle so the first turn reuses it
|
|
87
|
+
// Seed with the discovery handle so the first turn reuses it when cwd matches boot
|
|
86
88
|
if (initialHandle) {
|
|
87
|
-
handles.set("default", initialHandle);
|
|
89
|
+
handles.set(adapterMemoryKey("default", cwd), initialHandle);
|
|
88
90
|
}
|
|
89
91
|
const pendingHandles = new Map<string, Promise<AcpRuntimeHandle>>();
|
|
90
92
|
const systemPromptSent = new Set<string>();
|
|
93
|
+
const knownSessionIds = new Set<string>();
|
|
91
94
|
|
|
92
|
-
function
|
|
95
|
+
function handleMapKey(modelId: string | undefined, turnCwd: string): string {
|
|
96
|
+
return adapterMemoryKey(modelId, turnCwd);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sessionKey(modelId: string | undefined, turnCwd: string): string {
|
|
100
|
+
const slug = createHash("sha256").update(turnCwd).digest("hex").slice(0, 12);
|
|
93
101
|
const model = modelId && modelId !== "default"
|
|
94
102
|
? `-${modelId.replace(/[^a-zA-Z0-9.-]/g, "_")}`
|
|
95
103
|
: "";
|
|
96
|
-
return `pi-${config.agent}${model}-${
|
|
104
|
+
return `pi-${config.agent}${model}-${slug}`;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
async function ensureHandle(
|
|
100
108
|
acpxModelId: string | undefined,
|
|
101
|
-
systemPrompt
|
|
109
|
+
systemPrompt: string | undefined,
|
|
110
|
+
turnCwd: string,
|
|
102
111
|
): Promise<AcpRuntimeHandle> {
|
|
103
|
-
const key = acpxModelId
|
|
112
|
+
const key = handleMapKey(acpxModelId, turnCwd);
|
|
104
113
|
|
|
105
114
|
const existing = handles.get(key);
|
|
106
115
|
if (existing) return existing;
|
|
@@ -118,11 +127,13 @@ function createAcpxAdapter(
|
|
|
118
127
|
sessionOpts.systemPrompt = systemPrompt;
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
131
|
+
`ensureSession agent=${config.agent} cwd=${turnCwd} boot=${cwd} model=${acpxModelId || "default"}`);
|
|
121
132
|
const handle = await runtime.ensureSession({
|
|
122
|
-
sessionKey: sessionKey(acpxModelId),
|
|
133
|
+
sessionKey: sessionKey(acpxModelId, turnCwd),
|
|
123
134
|
agent: config.agent,
|
|
124
135
|
mode: "persistent",
|
|
125
|
-
cwd,
|
|
136
|
+
cwd: turnCwd,
|
|
126
137
|
...(Object.keys(sessionOpts).length > 0
|
|
127
138
|
? { sessionOptions: sessionOpts }
|
|
128
139
|
: {}),
|
|
@@ -142,13 +153,17 @@ function createAcpxAdapter(
|
|
|
142
153
|
return {
|
|
143
154
|
startSession: async (opts: SessionStartOptions): Promise<SessionHandle> => {
|
|
144
155
|
const model = opts.model || "default";
|
|
156
|
+
const turnCwd = resolveAdapterCwd(opts, cwd);
|
|
145
157
|
const systemPrompt = opts.systemPrompt
|
|
146
158
|
? opts.systemPrompt
|
|
147
|
-
: buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
148
|
-
|
|
159
|
+
: buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd);
|
|
160
|
+
await ensureHandle(model, systemPrompt, turnCwd);
|
|
161
|
+
const sessionId = sessionKey(model, turnCwd);
|
|
162
|
+
knownSessionIds.add(sessionId);
|
|
149
163
|
return {
|
|
150
|
-
sessionId
|
|
164
|
+
sessionId,
|
|
151
165
|
model,
|
|
166
|
+
cwd: turnCwd,
|
|
152
167
|
};
|
|
153
168
|
},
|
|
154
169
|
|
|
@@ -157,13 +172,14 @@ function createAcpxAdapter(
|
|
|
157
172
|
prompt: string,
|
|
158
173
|
opts?: TurnOptions,
|
|
159
174
|
): Promise<TurnResult> => {
|
|
160
|
-
const
|
|
175
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
176
|
+
const handleKey = handleMapKey(handle.model, turnCwd);
|
|
161
177
|
const needsSystemPrompt = !systemPromptSent.has(handleKey);
|
|
162
178
|
const systemPrompt = needsSystemPrompt
|
|
163
|
-
? buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
179
|
+
? buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd)
|
|
164
180
|
: undefined;
|
|
165
181
|
|
|
166
|
-
const acpxHandle = await ensureHandle(handle.model, systemPrompt);
|
|
182
|
+
const acpxHandle = await ensureHandle(handle.model, systemPrompt, turnCwd);
|
|
167
183
|
if (needsSystemPrompt) {
|
|
168
184
|
systemPromptSent.add(handleKey);
|
|
169
185
|
}
|
|
@@ -259,7 +275,7 @@ function createAcpxAdapter(
|
|
|
259
275
|
const su = status.usage;
|
|
260
276
|
if (su?.cumulative) {
|
|
261
277
|
const c = su.cumulative;
|
|
262
|
-
const key = handle.model
|
|
278
|
+
const key = handleMapKey(handle.model, turnCwd);
|
|
263
279
|
const prev = prevCumulative.get(key) || { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
264
280
|
const deltaInput = (c.inputTokens ?? 0) - (prev.inputTokens ?? 0);
|
|
265
281
|
const deltaOutput = (c.outputTokens ?? 0) - (prev.outputTokens ?? 0);
|
|
@@ -287,7 +303,8 @@ function createAcpxAdapter(
|
|
|
287
303
|
},
|
|
288
304
|
|
|
289
305
|
stopSession: async (handle: SessionHandle): Promise<void> => {
|
|
290
|
-
const
|
|
306
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
307
|
+
const key = handleMapKey(handle.model, turnCwd);
|
|
291
308
|
const acpxHandle = handles.get(key);
|
|
292
309
|
if (acpxHandle) {
|
|
293
310
|
await runtime.close({ handle: acpxHandle, reason: "session stop" }).catch((err: unknown) => {
|
|
@@ -298,6 +315,7 @@ function createAcpxAdapter(
|
|
|
298
315
|
}
|
|
299
316
|
systemPromptSent.delete(key);
|
|
300
317
|
prevCumulative.delete(key);
|
|
318
|
+
knownSessionIds.delete(sessionKey(handle.model, turnCwd));
|
|
301
319
|
},
|
|
302
320
|
|
|
303
321
|
stopAll: async (): Promise<void> => {
|
|
@@ -315,15 +333,11 @@ function createAcpxAdapter(
|
|
|
315
333
|
pendingHandles.clear();
|
|
316
334
|
systemPromptSent.clear();
|
|
317
335
|
prevCumulative.clear();
|
|
336
|
+
knownSessionIds.clear();
|
|
318
337
|
},
|
|
319
338
|
|
|
320
339
|
hasSession: (sessionId: string): boolean => {
|
|
321
|
-
|
|
322
|
-
if (sessionKey(key === "default" ? undefined : key) === sessionId) {
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return false;
|
|
340
|
+
return knownSessionIds.has(sessionId);
|
|
327
341
|
},
|
|
328
342
|
};
|
|
329
343
|
}
|
|
@@ -467,7 +481,7 @@ export const AcpxDriver: ProviderDriver<AcpxConfig> = {
|
|
|
467
481
|
});
|
|
468
482
|
|
|
469
483
|
// Create adapter
|
|
470
|
-
const adapter = createAcpxAdapter(config, cwd, runtime,
|
|
484
|
+
const adapter = createAcpxAdapter(config, cwd, runtime, initialHandle);
|
|
471
485
|
|
|
472
486
|
return {
|
|
473
487
|
instanceId,
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
} from "../cli-provider/sessions.js";
|
|
47
47
|
import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
|
|
48
48
|
import { fileLog } from "../shared/file-logger.js";
|
|
49
|
+
import { resolveAdapterCwd, adapterMemoryKey, deleteKeysForCwd } from "../shared/session-cwd.js";
|
|
49
50
|
|
|
50
51
|
const LOG_DOMAIN = "claude-driver";
|
|
51
52
|
const DRIVER_KIND = "claude-cli";
|
|
@@ -82,14 +83,14 @@ function createClaudeAdapter(
|
|
|
82
83
|
instanceId: string,
|
|
83
84
|
): ProviderAdapterShape & {
|
|
84
85
|
bindPiSessionId: (sid: string) => void;
|
|
85
|
-
handleSessionStart: (reason: string, sid: string | null) => void;
|
|
86
|
+
handleSessionStart: (reason: string, sid: string | null, sessionCwd?: string) => void;
|
|
86
87
|
} {
|
|
87
88
|
let piSessionId = createProvisionalPiSessionId();
|
|
88
89
|
const systemPromptSent = new Set<string>();
|
|
89
90
|
const storedSystemPrompts = new Map<string, string>();
|
|
90
91
|
|
|
91
|
-
function sessionKeyFor(model: string): CliSessionKey {
|
|
92
|
-
return { cwd, agent: "claude", model, piSessionId };
|
|
92
|
+
function sessionKeyFor(model: string, turnCwd: string = cwd): CliSessionKey {
|
|
93
|
+
return { cwd: turnCwd, agent: "claude", model, piSessionId };
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
return {
|
|
@@ -97,21 +98,27 @@ function createClaudeAdapter(
|
|
|
97
98
|
if (sid) piSessionId = sid;
|
|
98
99
|
},
|
|
99
100
|
|
|
100
|
-
handleSessionStart: (reason: string, _sid: string | null) => {
|
|
101
|
+
handleSessionStart: (reason: string, _sid: string | null, sessionCwd?: string) => {
|
|
101
102
|
if (reason === "new" || reason === "resume") {
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
const turnCwd = sessionCwd || cwd;
|
|
104
|
+
deleteKeysForCwd(systemPromptSent, turnCwd);
|
|
105
|
+
deleteKeysForCwd(storedSystemPrompts, turnCwd);
|
|
104
106
|
}
|
|
105
107
|
},
|
|
106
108
|
|
|
107
109
|
startSession: async (opts: SessionStartOptions): Promise<SessionHandle> => {
|
|
108
110
|
const model = opts.model || "default";
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
+
const turnCwd = resolveAdapterCwd(opts, cwd);
|
|
112
|
+
const memKey = adapterMemoryKey(model, turnCwd);
|
|
113
|
+
if (opts.systemPrompt) storedSystemPrompts.set(memKey, opts.systemPrompt);
|
|
114
|
+
const key = sessionKeyFor(model, turnCwd);
|
|
111
115
|
const existingId = loadCliSessionId(key);
|
|
116
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
117
|
+
`startSession model=${model} cwd=${turnCwd} boot=${cwd}`);
|
|
112
118
|
return {
|
|
113
119
|
sessionId: existingId || `claude-${instanceId}-${model}`,
|
|
114
120
|
model,
|
|
121
|
+
cwd: turnCwd,
|
|
115
122
|
};
|
|
116
123
|
},
|
|
117
124
|
|
|
@@ -120,9 +127,10 @@ function createClaudeAdapter(
|
|
|
120
127
|
prompt: string,
|
|
121
128
|
opts?: TurnOptions,
|
|
122
129
|
): Promise<TurnResult> => {
|
|
123
|
-
const
|
|
130
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
131
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
124
132
|
const sessionId = loadCliSessionId(key);
|
|
125
|
-
const handleKey = handle.model
|
|
133
|
+
const handleKey = adapterMemoryKey(handle.model, turnCwd);
|
|
126
134
|
const needsSystemPrompt = !systemPromptSent.has(handleKey);
|
|
127
135
|
|
|
128
136
|
// Prefer the system prompt stored at startSession over rebuilding
|
|
@@ -130,7 +138,7 @@ function createClaudeAdapter(
|
|
|
130
138
|
if (needsSystemPrompt) {
|
|
131
139
|
systemPrompt =
|
|
132
140
|
storedSystemPrompts.get(handleKey) ||
|
|
133
|
-
buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
141
|
+
buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd);
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
// Apply system prompt to the prompt text
|
|
@@ -157,7 +165,7 @@ function createClaudeAdapter(
|
|
|
157
165
|
runCliAgent({
|
|
158
166
|
agent: "claude",
|
|
159
167
|
model: handle.model === "default" ? "default" : handle.model,
|
|
160
|
-
cwd,
|
|
168
|
+
cwd: turnCwd,
|
|
161
169
|
prompt: p,
|
|
162
170
|
sessionId: sid,
|
|
163
171
|
signal: opts?.signal,
|
|
@@ -215,9 +223,10 @@ function createClaudeAdapter(
|
|
|
215
223
|
},
|
|
216
224
|
|
|
217
225
|
stopSession: async (handle: SessionHandle): Promise<void> => {
|
|
218
|
-
const
|
|
226
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
227
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
219
228
|
clearCliSessionId(key);
|
|
220
|
-
const handleKey = handle.model
|
|
229
|
+
const handleKey = adapterMemoryKey(handle.model, turnCwd);
|
|
221
230
|
systemPromptSent.delete(handleKey);
|
|
222
231
|
storedSystemPrompts.delete(handleKey);
|
|
223
232
|
},
|
|
@@ -35,6 +35,7 @@ import { loadAcpxRuntime, type AcpxRuntimeModule } from "../acpx-provider/load-r
|
|
|
35
35
|
import { modelIdToDisplayName } from "../acpx-provider/runtime-models.js";
|
|
36
36
|
import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
|
|
37
37
|
import { fileLog } from "../shared/file-logger.js";
|
|
38
|
+
import { resolveAdapterCwd, adapterMemoryKey } from "../shared/session-cwd.js";
|
|
38
39
|
|
|
39
40
|
const LOG_DOMAIN = "cursor-acpx-driver";
|
|
40
41
|
const DRIVER_KIND = "cursor-acpx";
|
|
@@ -76,24 +77,30 @@ function createCursorAcpxAdapter(
|
|
|
76
77
|
config: CursorAcpxConfig,
|
|
77
78
|
cwd: string,
|
|
78
79
|
runtime: AcpxRuntime,
|
|
79
|
-
cwdSlug: string,
|
|
80
80
|
): ProviderAdapterShape {
|
|
81
81
|
const handles = new Map<string, AcpRuntimeHandle>();
|
|
82
82
|
const pendingHandles = new Map<string, Promise<AcpRuntimeHandle>>();
|
|
83
83
|
const systemPromptSent = new Set<string>();
|
|
84
|
+
const knownSessionIds = new Set<string>();
|
|
84
85
|
|
|
85
|
-
function
|
|
86
|
+
function handleMapKey(modelId: string | undefined, turnCwd: string): string {
|
|
87
|
+
return adapterMemoryKey(modelId, turnCwd);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sessionKey(modelId: string | undefined, turnCwd: string): string {
|
|
91
|
+
const slug = createHash("sha256").update(turnCwd).digest("hex").slice(0, 12);
|
|
86
92
|
const model = modelId && modelId !== "default"
|
|
87
93
|
? `-${modelId.replace(/[^a-zA-Z0-9.-]/g, "_")}`
|
|
88
94
|
: "";
|
|
89
|
-
return `pi-${config.agent}${model}-${
|
|
95
|
+
return `pi-${config.agent}${model}-${slug}`;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
async function ensureHandle(
|
|
93
99
|
acpxModelId: string | undefined,
|
|
94
|
-
systemPrompt
|
|
100
|
+
systemPrompt: string | undefined,
|
|
101
|
+
turnCwd: string,
|
|
95
102
|
): Promise<AcpRuntimeHandle> {
|
|
96
|
-
const key = acpxModelId
|
|
103
|
+
const key = handleMapKey(acpxModelId, turnCwd);
|
|
97
104
|
|
|
98
105
|
const existing = handles.get(key);
|
|
99
106
|
if (existing) return existing;
|
|
@@ -111,11 +118,13 @@ function createCursorAcpxAdapter(
|
|
|
111
118
|
sessionOpts.systemPrompt = systemPrompt;
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
122
|
+
`ensureSession agent=${config.agent} cwd=${turnCwd} boot=${cwd} model=${acpxModelId || "default"}`);
|
|
114
123
|
const handle = await runtime.ensureSession({
|
|
115
|
-
sessionKey: sessionKey(acpxModelId),
|
|
124
|
+
sessionKey: sessionKey(acpxModelId, turnCwd),
|
|
116
125
|
agent: config.agent,
|
|
117
126
|
mode: "persistent",
|
|
118
|
-
cwd,
|
|
127
|
+
cwd: turnCwd,
|
|
119
128
|
...(Object.keys(sessionOpts).length > 0
|
|
120
129
|
? { sessionOptions: sessionOpts }
|
|
121
130
|
: {}),
|
|
@@ -135,13 +144,17 @@ function createCursorAcpxAdapter(
|
|
|
135
144
|
return {
|
|
136
145
|
startSession: async (opts: SessionStartOptions): Promise<SessionHandle> => {
|
|
137
146
|
const model = opts.model || "default";
|
|
147
|
+
const turnCwd = resolveAdapterCwd(opts, cwd);
|
|
138
148
|
const systemPrompt = opts.systemPrompt
|
|
139
149
|
? opts.systemPrompt
|
|
140
|
-
: buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
141
|
-
|
|
150
|
+
: buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd);
|
|
151
|
+
await ensureHandle(model, systemPrompt, turnCwd);
|
|
152
|
+
const sessionId = sessionKey(model, turnCwd);
|
|
153
|
+
knownSessionIds.add(sessionId);
|
|
142
154
|
return {
|
|
143
|
-
sessionId
|
|
155
|
+
sessionId,
|
|
144
156
|
model,
|
|
157
|
+
cwd: turnCwd,
|
|
145
158
|
};
|
|
146
159
|
},
|
|
147
160
|
|
|
@@ -150,13 +163,14 @@ function createCursorAcpxAdapter(
|
|
|
150
163
|
prompt: string,
|
|
151
164
|
opts?: TurnOptions,
|
|
152
165
|
): Promise<TurnResult> => {
|
|
153
|
-
const
|
|
166
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
167
|
+
const handleKey = handleMapKey(handle.model, turnCwd);
|
|
154
168
|
const needsSystemPrompt = !systemPromptSent.has(handleKey);
|
|
155
169
|
const systemPrompt = needsSystemPrompt
|
|
156
|
-
? buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
170
|
+
? buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd)
|
|
157
171
|
: undefined;
|
|
158
172
|
|
|
159
|
-
const acpxHandle = await ensureHandle(handle.model, systemPrompt);
|
|
173
|
+
const acpxHandle = await ensureHandle(handle.model, systemPrompt, turnCwd);
|
|
160
174
|
if (needsSystemPrompt) {
|
|
161
175
|
systemPromptSent.add(handleKey);
|
|
162
176
|
}
|
|
@@ -207,7 +221,8 @@ function createCursorAcpxAdapter(
|
|
|
207
221
|
},
|
|
208
222
|
|
|
209
223
|
stopSession: async (handle: SessionHandle): Promise<void> => {
|
|
210
|
-
const
|
|
224
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
225
|
+
const key = handleMapKey(handle.model, turnCwd);
|
|
211
226
|
const acpxHandle = handles.get(key);
|
|
212
227
|
if (acpxHandle) {
|
|
213
228
|
await runtime.close({ handle: acpxHandle, reason: "session stop" }).catch((err: unknown) => {
|
|
@@ -217,6 +232,7 @@ function createCursorAcpxAdapter(
|
|
|
217
232
|
handles.delete(key);
|
|
218
233
|
}
|
|
219
234
|
systemPromptSent.delete(key);
|
|
235
|
+
knownSessionIds.delete(sessionKey(handle.model, turnCwd));
|
|
220
236
|
},
|
|
221
237
|
|
|
222
238
|
stopAll: async (): Promise<void> => {
|
|
@@ -233,15 +249,11 @@ function createCursorAcpxAdapter(
|
|
|
233
249
|
handles.clear();
|
|
234
250
|
pendingHandles.clear();
|
|
235
251
|
systemPromptSent.clear();
|
|
252
|
+
knownSessionIds.clear();
|
|
236
253
|
},
|
|
237
254
|
|
|
238
255
|
hasSession: (sessionId: string): boolean => {
|
|
239
|
-
|
|
240
|
-
if (sessionKey(key === "default" ? undefined : key) === sessionId) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return false;
|
|
256
|
+
return knownSessionIds.has(sessionId);
|
|
245
257
|
},
|
|
246
258
|
};
|
|
247
259
|
}
|
|
@@ -376,7 +388,7 @@ export const CursorAcpxDriver: ProviderDriver<CursorAcpxConfig> = {
|
|
|
376
388
|
});
|
|
377
389
|
|
|
378
390
|
// Create adapter
|
|
379
|
-
const adapter = createCursorAcpxAdapter(config, cwd, runtime
|
|
391
|
+
const adapter = createCursorAcpxAdapter(config, cwd, runtime);
|
|
380
392
|
|
|
381
393
|
return {
|
|
382
394
|
instanceId,
|
|
@@ -53,10 +53,28 @@ import {
|
|
|
53
53
|
} from "../cli-provider/sessions.js";
|
|
54
54
|
import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
|
|
55
55
|
import { fileLog } from "../shared/file-logger.js";
|
|
56
|
+
import { resolveAdapterCwd, adapterMemoryKey, deleteKeysForCwd } from "../shared/session-cwd.js";
|
|
56
57
|
|
|
57
58
|
const LOG_DOMAIN = "cursor-cli-driver";
|
|
58
59
|
const DRIVER_KIND = "cursor-cli";
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Per-turn history reseed. Cwd-scoped `session_start` must not force a fresh
|
|
63
|
+
* CLI session on a different workspace. `forceHistorySeedGlobal` is only
|
|
64
|
+
* `setForceHistorySeed()` (explicit adapter-wide override).
|
|
65
|
+
*/
|
|
66
|
+
export function cursorTurnNeedsHistorySeed(
|
|
67
|
+
turnCwd: string,
|
|
68
|
+
forceHistorySeedCwds: ReadonlySet<string>,
|
|
69
|
+
forceHistorySeedGlobal: boolean,
|
|
70
|
+
): boolean {
|
|
71
|
+
const cwdListed = forceHistorySeedCwds.has(turnCwd);
|
|
72
|
+
const needed = forceHistorySeedGlobal || cwdListed;
|
|
73
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
74
|
+
`cursorTurnNeedsHistorySeed cwd=${turnCwd} global=${forceHistorySeedGlobal} cwdListed=${cwdListed} needed=${needed}`);
|
|
75
|
+
return needed;
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
// ---------------------------------------------------------------------------
|
|
61
79
|
// Config
|
|
62
80
|
// ---------------------------------------------------------------------------
|
|
@@ -147,7 +165,7 @@ function buildPromptWithHistory(
|
|
|
147
165
|
// Adapter
|
|
148
166
|
// ---------------------------------------------------------------------------
|
|
149
167
|
|
|
150
|
-
function createCursorCliAdapter(
|
|
168
|
+
export function createCursorCliAdapter(
|
|
151
169
|
config: CursorCliConfig,
|
|
152
170
|
cwd: string,
|
|
153
171
|
instanceId: string,
|
|
@@ -155,28 +173,45 @@ function createCursorCliAdapter(
|
|
|
155
173
|
/** Bind the real pi session UUID (from sessionManager). */
|
|
156
174
|
bindPiSessionId: (sid: string) => void;
|
|
157
175
|
/** Handle session_start event (reseed, marker cleanup). */
|
|
158
|
-
handleSessionStart: (reason: string, sid: string | null) => void;
|
|
176
|
+
handleSessionStart: (reason: string, sid: string | null, sessionCwd?: string) => void;
|
|
159
177
|
/** Force history re-seed on next turn. */
|
|
160
178
|
setForceHistorySeed: (force: boolean) => void;
|
|
161
179
|
} {
|
|
180
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
181
|
+
`createCursorCliAdapter bootCwdBound=${Boolean(cwd)} instance=${instanceId}`);
|
|
162
182
|
let activePiSessionId: string | null = null;
|
|
163
183
|
const provisionalPiSessionId = createProvisionalPiSessionId();
|
|
164
184
|
let forceHistorySeed = false;
|
|
185
|
+
const forceHistorySeedCwds = new Set<string>();
|
|
165
186
|
const systemPromptSent = new Set<string>();
|
|
166
187
|
const storedSystemPrompts = new Map<string, string>();
|
|
167
188
|
const sessionKeys = new Map<string, CliSessionKey>();
|
|
168
189
|
|
|
190
|
+
function memoryKey(model: string | undefined, turnCwd: string): string {
|
|
191
|
+
return adapterMemoryKey(model, turnCwd);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function markerCwds(): string[] {
|
|
195
|
+
const found = new Set<string>([cwd]);
|
|
196
|
+
for (const key of sessionKeys.values()) {
|
|
197
|
+
if (key.cwd) found.add(key.cwd);
|
|
198
|
+
}
|
|
199
|
+
return [...found];
|
|
200
|
+
}
|
|
201
|
+
|
|
169
202
|
function resolvedPiSessionId(): string {
|
|
170
203
|
return activePiSessionId || provisionalPiSessionId;
|
|
171
204
|
}
|
|
172
205
|
|
|
173
|
-
function sessionKeyFor(model: string): CliSessionKey {
|
|
174
|
-
return { cwd, agent: "cursor", model, piSessionId: resolvedPiSessionId() };
|
|
206
|
+
function sessionKeyFor(model: string, turnCwd: string = cwd): CliSessionKey {
|
|
207
|
+
return { cwd: turnCwd, agent: "cursor", model, piSessionId: resolvedPiSessionId() };
|
|
175
208
|
}
|
|
176
209
|
|
|
177
210
|
function migrateMarkersToRealPiSessionId(sid: string): void {
|
|
178
211
|
if (!sid || sid === provisionalPiSessionId) return;
|
|
179
|
-
|
|
212
|
+
for (const markerCwd of markerCwds()) {
|
|
213
|
+
migrateAllCliSessionMarkers(markerCwd, provisionalPiSessionId, sid);
|
|
214
|
+
}
|
|
180
215
|
for (const [handleKey, prevKey] of sessionKeys) {
|
|
181
216
|
if (
|
|
182
217
|
!prevKey.piSessionId ||
|
|
@@ -202,9 +237,10 @@ function createCursorCliAdapter(
|
|
|
202
237
|
migrateMarkersToRealPiSessionId(sid);
|
|
203
238
|
},
|
|
204
239
|
|
|
205
|
-
handleSessionStart: (reason: string, sid: string | null) => {
|
|
240
|
+
handleSessionStart: (reason: string, sid: string | null, sessionCwd?: string) => {
|
|
206
241
|
const prevSid = activePiSessionId;
|
|
207
242
|
const resolvedSid = sid || activePiSessionId;
|
|
243
|
+
const turnCwd = sessionCwd || cwd;
|
|
208
244
|
if (sid) {
|
|
209
245
|
activePiSessionId = sid;
|
|
210
246
|
migrateMarkersToRealPiSessionId(sid);
|
|
@@ -215,22 +251,27 @@ function createCursorCliAdapter(
|
|
|
215
251
|
prevPiSessionId: prevSid,
|
|
216
252
|
nextPiSessionId: resolvedSid,
|
|
217
253
|
});
|
|
218
|
-
|
|
254
|
+
if (decision.forceHistorySeed) {
|
|
255
|
+
forceHistorySeedCwds.add(turnCwd);
|
|
256
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
257
|
+
`handleSessionStart reseed cwd=${turnCwd} (not adapter-global)`);
|
|
258
|
+
}
|
|
219
259
|
|
|
220
260
|
if (decision.action === "keep") {
|
|
221
261
|
return;
|
|
222
262
|
}
|
|
223
263
|
if (decision.action === "reseed" && resolvedSid) {
|
|
224
264
|
try {
|
|
225
|
-
clearCliSessionsForPiSession(
|
|
265
|
+
clearCliSessionsForPiSession(turnCwd, resolvedSid, {
|
|
226
266
|
includeLegacyDefault: prevSid == null || prevSid === "" || prevSid === "default",
|
|
227
267
|
});
|
|
228
268
|
} catch (err) {
|
|
229
269
|
fileLog(LOG_DOMAIN, "warn", LOG_DOMAIN,
|
|
230
270
|
`session_start marker cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
231
271
|
}
|
|
232
|
-
systemPromptSent
|
|
233
|
-
storedSystemPrompts
|
|
272
|
+
deleteKeysForCwd(systemPromptSent, turnCwd);
|
|
273
|
+
deleteKeysForCwd(storedSystemPrompts, turnCwd);
|
|
274
|
+
deleteKeysForCwd(sessionKeys, turnCwd);
|
|
234
275
|
}
|
|
235
276
|
},
|
|
236
277
|
|
|
@@ -240,13 +281,18 @@ function createCursorCliAdapter(
|
|
|
240
281
|
|
|
241
282
|
startSession: async (opts: SessionStartOptions): Promise<SessionHandle> => {
|
|
242
283
|
const model = opts.model || "default";
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
284
|
+
const turnCwd = resolveAdapterCwd(opts, cwd);
|
|
285
|
+
const memKey = memoryKey(model, turnCwd);
|
|
286
|
+
if (opts.systemPrompt) storedSystemPrompts.set(memKey, opts.systemPrompt);
|
|
287
|
+
const key = sessionKeyFor(model, turnCwd);
|
|
288
|
+
sessionKeys.set(memKey, key);
|
|
246
289
|
const existingId = loadCliSessionId(key);
|
|
290
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
291
|
+
`startSession model=${model} cwd=${turnCwd} boot=${cwd}`);
|
|
247
292
|
return {
|
|
248
293
|
sessionId: existingId || `cursor-${instanceId}-${model}`,
|
|
249
294
|
model,
|
|
295
|
+
cwd: turnCwd,
|
|
250
296
|
};
|
|
251
297
|
},
|
|
252
298
|
|
|
@@ -255,8 +301,9 @@ function createCursorCliAdapter(
|
|
|
255
301
|
prompt: string,
|
|
256
302
|
opts?: TurnOptions,
|
|
257
303
|
): Promise<TurnResult> => {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
304
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
305
|
+
const handleKey = memoryKey(handle.model, turnCwd);
|
|
306
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
260
307
|
// Read previous key BEFORE updating so legacy-marker adoption can compare categories
|
|
261
308
|
const prevKey = sessionKeys.get(handleKey);
|
|
262
309
|
sessionKeys.set(handleKey, key);
|
|
@@ -268,23 +315,30 @@ function createCursorCliAdapter(
|
|
|
268
315
|
sessionId = loadCliSessionId(key);
|
|
269
316
|
}
|
|
270
317
|
|
|
271
|
-
const
|
|
318
|
+
const reseedThisCwd = cursorTurnNeedsHistorySeed(
|
|
319
|
+
turnCwd,
|
|
320
|
+
forceHistorySeedCwds,
|
|
321
|
+
forceHistorySeed,
|
|
322
|
+
);
|
|
323
|
+
const needsSystemPrompt = !systemPromptSent.has(handleKey) || reseedThisCwd;
|
|
324
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
325
|
+
`sendTurn model=${handle.model} cwd=${turnCwd} boot=${cwd} reseed=${reseedThisCwd}`);
|
|
272
326
|
|
|
273
327
|
// Prefer the system prompt stored at startSession over rebuilding
|
|
274
328
|
let systemPrompt: string | undefined;
|
|
275
329
|
if (needsSystemPrompt) {
|
|
276
330
|
systemPrompt =
|
|
277
331
|
storedSystemPrompts.get(handleKey) ||
|
|
278
|
-
buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
332
|
+
buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd);
|
|
279
333
|
}
|
|
280
334
|
|
|
281
335
|
// Resolve history seed plan
|
|
282
336
|
const seedPlan = resolveCliHistorySeed({
|
|
283
337
|
hasCliSession: !!sessionId,
|
|
284
|
-
forceHistorySeed,
|
|
338
|
+
forceHistorySeed: reseedThisCwd,
|
|
285
339
|
});
|
|
286
340
|
// Stale marker after /resume: drop it so we open a fresh CLI chat
|
|
287
|
-
if (
|
|
341
|
+
if (reseedThisCwd && sessionId) {
|
|
288
342
|
clearCliSessionId(key);
|
|
289
343
|
}
|
|
290
344
|
const effectiveSessionId = seedPlan.useCliSession ? sessionId : null;
|
|
@@ -315,7 +369,7 @@ function createCursorCliAdapter(
|
|
|
315
369
|
runCliAgent({
|
|
316
370
|
agent: "cursor",
|
|
317
371
|
model: handle.model === "default" ? "default" : handle.model,
|
|
318
|
-
cwd,
|
|
372
|
+
cwd: turnCwd,
|
|
319
373
|
prompt: p,
|
|
320
374
|
sessionId: sid,
|
|
321
375
|
signal: opts?.signal,
|
|
@@ -335,7 +389,8 @@ function createCursorCliAdapter(
|
|
|
335
389
|
if (needsSystemPrompt) {
|
|
336
390
|
systemPromptSent.add(handleKey);
|
|
337
391
|
}
|
|
338
|
-
|
|
392
|
+
forceHistorySeedCwds.delete(turnCwd);
|
|
393
|
+
if (forceHistorySeed && reseedThisCwd) {
|
|
339
394
|
forceHistorySeed = false;
|
|
340
395
|
}
|
|
341
396
|
|
|
@@ -366,7 +421,8 @@ function createCursorCliAdapter(
|
|
|
366
421
|
if (needsSystemPrompt) {
|
|
367
422
|
systemPromptSent.add(handleKey);
|
|
368
423
|
}
|
|
369
|
-
|
|
424
|
+
forceHistorySeedCwds.delete(turnCwd);
|
|
425
|
+
if (forceHistorySeed && reseedThisCwd) {
|
|
370
426
|
forceHistorySeed = false;
|
|
371
427
|
}
|
|
372
428
|
return {
|
|
@@ -382,12 +438,14 @@ function createCursorCliAdapter(
|
|
|
382
438
|
},
|
|
383
439
|
|
|
384
440
|
stopSession: async (handle: SessionHandle): Promise<void> => {
|
|
385
|
-
const
|
|
441
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
442
|
+
const memKey = memoryKey(handle.model, turnCwd);
|
|
443
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
386
444
|
clearCliSessionId(key);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
445
|
+
sessionKeys.delete(memKey);
|
|
446
|
+
systemPromptSent.delete(memKey);
|
|
447
|
+
storedSystemPrompts.delete(memKey);
|
|
448
|
+
forceHistorySeedCwds.delete(turnCwd);
|
|
391
449
|
},
|
|
392
450
|
|
|
393
451
|
stopAll: async (): Promise<void> => {
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "../cli-provider/sessions.js";
|
|
43
43
|
import { buildExternalSystemPrompt } from "../shared/build-system-prompt.js";
|
|
44
44
|
import { fileLog } from "../shared/file-logger.js";
|
|
45
|
+
import { resolveAdapterCwd, adapterMemoryKey, deleteKeysForCwd } from "../shared/session-cwd.js";
|
|
45
46
|
|
|
46
47
|
const LOG_DOMAIN = "gemini-driver";
|
|
47
48
|
const DRIVER_KIND = "gemini-cli";
|
|
@@ -78,14 +79,14 @@ function createGeminiAdapter(
|
|
|
78
79
|
instanceId: string,
|
|
79
80
|
): ProviderAdapterShape & {
|
|
80
81
|
bindPiSessionId: (sid: string) => void;
|
|
81
|
-
handleSessionStart: (reason: string, sid: string | null) => void;
|
|
82
|
+
handleSessionStart: (reason: string, sid: string | null, sessionCwd?: string) => void;
|
|
82
83
|
} {
|
|
83
84
|
let piSessionId = createProvisionalPiSessionId();
|
|
84
85
|
const systemPromptSent = new Set<string>();
|
|
85
86
|
const storedSystemPrompts = new Map<string, string>();
|
|
86
87
|
|
|
87
|
-
function sessionKeyFor(model: string): CliSessionKey {
|
|
88
|
-
return { cwd, agent: "gemini", model, piSessionId };
|
|
88
|
+
function sessionKeyFor(model: string, turnCwd: string = cwd): CliSessionKey {
|
|
89
|
+
return { cwd: turnCwd, agent: "gemini", model, piSessionId };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
return {
|
|
@@ -93,21 +94,27 @@ function createGeminiAdapter(
|
|
|
93
94
|
if (sid) piSessionId = sid;
|
|
94
95
|
},
|
|
95
96
|
|
|
96
|
-
handleSessionStart: (reason: string, _sid: string | null) => {
|
|
97
|
+
handleSessionStart: (reason: string, _sid: string | null, sessionCwd?: string) => {
|
|
97
98
|
if (reason === "new" || reason === "resume") {
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const turnCwd = sessionCwd || cwd;
|
|
100
|
+
deleteKeysForCwd(systemPromptSent, turnCwd);
|
|
101
|
+
deleteKeysForCwd(storedSystemPrompts, turnCwd);
|
|
100
102
|
}
|
|
101
103
|
},
|
|
102
104
|
|
|
103
105
|
startSession: async (opts: SessionStartOptions): Promise<SessionHandle> => {
|
|
104
106
|
const model = opts.model || "default";
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
+
const turnCwd = resolveAdapterCwd(opts, cwd);
|
|
108
|
+
const memKey = adapterMemoryKey(model, turnCwd);
|
|
109
|
+
if (opts.systemPrompt) storedSystemPrompts.set(memKey, opts.systemPrompt);
|
|
110
|
+
const key = sessionKeyFor(model, turnCwd);
|
|
107
111
|
const existingId = loadCliSessionId(key);
|
|
112
|
+
fileLog(LOG_DOMAIN, "debug", LOG_DOMAIN,
|
|
113
|
+
`startSession model=${model} cwd=${turnCwd} boot=${cwd}`);
|
|
108
114
|
return {
|
|
109
115
|
sessionId: existingId || `gemini-${instanceId}-${model}`,
|
|
110
116
|
model,
|
|
117
|
+
cwd: turnCwd,
|
|
111
118
|
};
|
|
112
119
|
},
|
|
113
120
|
|
|
@@ -116,16 +123,17 @@ function createGeminiAdapter(
|
|
|
116
123
|
prompt: string,
|
|
117
124
|
opts?: TurnOptions,
|
|
118
125
|
): Promise<TurnResult> => {
|
|
119
|
-
const
|
|
126
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
127
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
120
128
|
const sessionId = loadCliSessionId(key);
|
|
121
|
-
const handleKey = handle.model
|
|
129
|
+
const handleKey = adapterMemoryKey(handle.model, turnCwd);
|
|
122
130
|
const needsSystemPrompt = !systemPromptSent.has(handleKey);
|
|
123
131
|
|
|
124
132
|
let systemPrompt: string | undefined;
|
|
125
133
|
if (needsSystemPrompt) {
|
|
126
134
|
systemPrompt =
|
|
127
135
|
storedSystemPrompts.get(handleKey) ||
|
|
128
|
-
buildExternalSystemPrompt({ systemPrompt: undefined },
|
|
136
|
+
buildExternalSystemPrompt({ systemPrompt: undefined }, turnCwd);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
let finalPrompt = prompt;
|
|
@@ -150,7 +158,7 @@ function createGeminiAdapter(
|
|
|
150
158
|
runCliAgent({
|
|
151
159
|
agent: "gemini",
|
|
152
160
|
model: handle.model === "default" ? "default" : handle.model,
|
|
153
|
-
cwd,
|
|
161
|
+
cwd: turnCwd,
|
|
154
162
|
prompt: p,
|
|
155
163
|
sessionId: sid,
|
|
156
164
|
signal: opts?.signal,
|
|
@@ -202,9 +210,10 @@ function createGeminiAdapter(
|
|
|
202
210
|
},
|
|
203
211
|
|
|
204
212
|
stopSession: async (handle: SessionHandle): Promise<void> => {
|
|
205
|
-
const
|
|
213
|
+
const turnCwd = resolveAdapterCwd(handle, cwd);
|
|
214
|
+
const key = sessionKeyFor(handle.model, turnCwd);
|
|
206
215
|
clearCliSessionId(key);
|
|
207
|
-
const handleKey = handle.model
|
|
216
|
+
const handleKey = adapterMemoryKey(handle.model, turnCwd);
|
|
208
217
|
systemPromptSent.delete(handleKey);
|
|
209
218
|
storedSystemPrompts.delete(handleKey);
|
|
210
219
|
},
|
|
@@ -59,6 +59,10 @@ import {
|
|
|
59
59
|
readPiSessionIdFromManager,
|
|
60
60
|
} from "../cli-provider/sessions.js";
|
|
61
61
|
import { fileLog } from "../shared/file-logger.js";
|
|
62
|
+
import {
|
|
63
|
+
enterSessionCwd,
|
|
64
|
+
resolveProviderStreamCwd,
|
|
65
|
+
} from "../shared/session-cwd.js";
|
|
62
66
|
import { isCliAgentName } from "../cli-provider/providers.js";
|
|
63
67
|
import {
|
|
64
68
|
isProvidersInitialized,
|
|
@@ -152,14 +156,19 @@ function makeStreamFunction(
|
|
|
152
156
|
const colonIdx = model.id.indexOf(":");
|
|
153
157
|
const driverModelId = colonIdx >= 0 ? model.id.substring(colonIdx + 1) : "default";
|
|
154
158
|
|
|
159
|
+
const turnCwd = resolveProviderStreamCwd(projectCwd);
|
|
160
|
+
log.debug(
|
|
161
|
+
`${kind}-${agent} stream cwd=${turnCwd} boot=${projectCwd} model=${driverModelId}`,
|
|
162
|
+
);
|
|
163
|
+
|
|
155
164
|
// Build system prompt
|
|
156
|
-
const systemPrompt = buildExternalSystemPrompt(context,
|
|
165
|
+
const systemPrompt = buildExternalSystemPrompt(context, turnCwd);
|
|
157
166
|
|
|
158
167
|
// Ensure session
|
|
159
168
|
const handle = await instance.adapter.startSession({
|
|
160
169
|
model: driverModelId,
|
|
161
170
|
systemPrompt,
|
|
162
|
-
cwd:
|
|
171
|
+
cwd: turnCwd,
|
|
163
172
|
});
|
|
164
173
|
|
|
165
174
|
// Extract latest user message
|
|
@@ -283,10 +292,22 @@ async function registerAcpxAgent(
|
|
|
283
292
|
const log = createLogger("providers");
|
|
284
293
|
log.debug("providers module loaded");
|
|
285
294
|
|
|
295
|
+
const cwdHookBound = new WeakSet<object>();
|
|
296
|
+
|
|
286
297
|
export default async function (pi: ExtensionAPI) {
|
|
287
298
|
// pi --help / --version — skip discovery
|
|
288
299
|
if (isPiMetaInvocation()) return;
|
|
289
300
|
|
|
301
|
+
// Always bind this AgentSession's cwd, even when the factory early-returns.
|
|
302
|
+
// Sidecar user sessions load this extension after the internal registrar
|
|
303
|
+
// already marked initialized — without this hook they never stash cwd (#768).
|
|
304
|
+
if (!cwdHookBound.has(pi)) {
|
|
305
|
+
cwdHookBound.add(pi);
|
|
306
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
307
|
+
enterSessionCwd(typeof ctx.cwd === "string" ? ctx.cwd : "");
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
// Idempotency guard — shims re-export this default, so it may be called
|
|
291
312
|
// multiple times (cli-provider shim, acpx-provider shim, providers dir).
|
|
292
313
|
// Only run once per session; session_shutdown resets so /new|/resume|/fork
|
|
@@ -358,7 +379,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
358
379
|
for (const inst of cliInstances.values()) {
|
|
359
380
|
const adapter = inst.adapter as any;
|
|
360
381
|
if (typeof adapter.handleSessionStart === "function") {
|
|
361
|
-
adapter.handleSessionStart(reason, sid);
|
|
382
|
+
adapter.handleSessionStart(reason, sid, ctx.cwd);
|
|
362
383
|
}
|
|
363
384
|
}
|
|
364
385
|
});
|
|
@@ -117,6 +117,8 @@ export interface SessionStartOptions {
|
|
|
117
117
|
export interface SessionHandle {
|
|
118
118
|
readonly sessionId: string;
|
|
119
119
|
readonly model: string;
|
|
120
|
+
/** Session/project cwd for this turn's CLI/ACPX spawn (#768). */
|
|
121
|
+
readonly cwd?: string;
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
/** Options for sending a turn. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn session cwd for CLI/ACPX spawn (#768).
|
|
3
|
+
*
|
|
4
|
+
* Pi stream() has no cwd. Sidecar sessions and extension before_agent_start
|
|
5
|
+
* stash the folder here. Drivers read it instead of process.cwd() from boot.
|
|
6
|
+
*
|
|
7
|
+
* Store lives on globalThis via Symbol.for so jiti, tsx, and sidecar share
|
|
8
|
+
* one AsyncLocalStorage even when this file is loaded more than once.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
|
+
import { createLogger } from "./logger.js";
|
|
13
|
+
|
|
14
|
+
const log = createLogger("providers");
|
|
15
|
+
|
|
16
|
+
/** Must match packages/pi-sidecar/src/session-cwd.ts — do not change one copy. */
|
|
17
|
+
export const SESSION_CWD_ALS_ID = "pi-config.sessionCwdAls";
|
|
18
|
+
const ALS_KEY = Symbol.for(SESSION_CWD_ALS_ID);
|
|
19
|
+
|
|
20
|
+
/** Unit separator — model ids and paths cannot contain this. */
|
|
21
|
+
const MEMORY_SEP = "\x1f";
|
|
22
|
+
|
|
23
|
+
function getAls(): AsyncLocalStorage<string> {
|
|
24
|
+
const g = globalThis as unknown as Record<
|
|
25
|
+
symbol,
|
|
26
|
+
AsyncLocalStorage<string> | undefined
|
|
27
|
+
>;
|
|
28
|
+
let als = g[ALS_KEY];
|
|
29
|
+
if (!als) {
|
|
30
|
+
als = new AsyncLocalStorage<string>();
|
|
31
|
+
g[ALS_KEY] = als;
|
|
32
|
+
log.debug("session cwd ALS created on globalThis");
|
|
33
|
+
}
|
|
34
|
+
return als;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Cwd bound for the current async turn, if any. */
|
|
38
|
+
export function getSessionCwd(): string | undefined {
|
|
39
|
+
const value = getAls().getStore();
|
|
40
|
+
const cwd = typeof value === "string" && value.length > 0 ? value : undefined;
|
|
41
|
+
log.debug(`getSessionCwd bound=${cwd !== undefined}`);
|
|
42
|
+
return cwd;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Bind cwd for the rest of this async resource (before_agent_start). */
|
|
46
|
+
export function enterSessionCwd(cwd: string): void {
|
|
47
|
+
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
48
|
+
log.warn("enterSessionCwd skipped: empty cwd");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (getSessionCwd()) {
|
|
52
|
+
log.debug(`enterSessionCwd skipped: ALS already bound cwd=${getSessionCwd()} incoming=${cwd}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
getAls().enterWith(cwd);
|
|
56
|
+
log.debug(`enterSessionCwd cwd=${cwd}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Run fn with cwd bound for the full async tree (sidecar prompt()). */
|
|
60
|
+
export function runWithSessionCwd<T>(cwd: string, fn: () => T): T {
|
|
61
|
+
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
62
|
+
log.warn("runWithSessionCwd empty cwd; running unbound");
|
|
63
|
+
return fn();
|
|
64
|
+
}
|
|
65
|
+
log.debug(`runWithSessionCwd cwd=${cwd}`);
|
|
66
|
+
return getAls().run(cwd, fn);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Stream path: ALS session cwd, else boot projectCwd. */
|
|
70
|
+
export function resolveProviderStreamCwd(bootCwd: string): string {
|
|
71
|
+
const fromAls = getSessionCwd();
|
|
72
|
+
if (fromAls) {
|
|
73
|
+
log.debug(`stream cwd from ALS=${fromAls} boot=${bootCwd}`);
|
|
74
|
+
return fromAls;
|
|
75
|
+
}
|
|
76
|
+
log.warn(`stream cwd fallback boot=${bootCwd} (ALS empty)`);
|
|
77
|
+
return bootCwd;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Spawn path: handle.cwd from startSession, else adapter create-time cwd. */
|
|
81
|
+
export function resolveAdapterCwd(
|
|
82
|
+
handle: { cwd?: string },
|
|
83
|
+
fallback: string,
|
|
84
|
+
): string {
|
|
85
|
+
if (typeof handle.cwd === "string" && handle.cwd.length > 0) {
|
|
86
|
+
return handle.cwd;
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** In-memory adapter maps: one entry per model+cwd so sidecar jobs stay isolated. */
|
|
92
|
+
export function adapterMemoryKey(model: string | undefined, turnCwd: string): string {
|
|
93
|
+
return `${model || "default"}${MEMORY_SEP}${turnCwd}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** True when `key` was built for this cwd via adapterMemoryKey. */
|
|
97
|
+
export function adapterMemoryKeyMatchesCwd(key: string, turnCwd: string): boolean {
|
|
98
|
+
return key.endsWith(`${MEMORY_SEP}${turnCwd}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Drop in-memory adapter entries for one session cwd (leave other jobs alone). */
|
|
102
|
+
export function deleteKeysForCwd(
|
|
103
|
+
store: { keys(): IterableIterator<string>; delete(key: string): unknown },
|
|
104
|
+
turnCwd: string,
|
|
105
|
+
): void {
|
|
106
|
+
for (const k of [...store.keys()]) {
|
|
107
|
+
if (adapterMemoryKeyMatchesCwd(k, turnCwd)) store.delete(k);
|
|
108
|
+
}
|
|
109
|
+
}
|