pi-claude-agent-sdk 0.7.0 → 0.7.1
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 +2 -0
- package/package.json +1 -1
- package/src/child-env.ts +74 -0
- package/src/convert.ts +19 -1
- package/src/index.ts +78 -82
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ Use `/model` to select `claude-bridge/claude-fable-5`, `claude-bridge/claude-opu
|
|
|
27
27
|
|
|
28
28
|
Behind the scenes, pi's tools are bridged to Claude Code but it should all work like normal in pi. Bash commands get a 120-second default timeout (matching Claude Code's default) since pi's bash has no timeout by default. Skills in pi are copied over to Claude Code's system prompt so should work as they would with any other pi provider. Steering works mid-turn: a message sent while Claude is running a tool reaches it at that tool boundary, not after the whole turn finishes.
|
|
29
29
|
|
|
30
|
+
**Authentication:** the bridge requires an Anthropic OAuth credential (or API key) configured in Pi and uses Pi's normal token refresh. Claude Code login and inherited Claude/Anthropic authentication settings are deliberately ignored, so configure Anthropic authentication in Pi before using the provider or AskClaude.
|
|
31
|
+
|
|
30
32
|
**1M Context:** Opus 5, Opus 4.8, and Opus 4.7 get 1M context by default. Opus 4.6 only gets 1M if you're on a Max plan or pay for Extra Usage. Sonnet 4.6 only gets 1M if you pay for Extra Usage. You will need to set `provider.plan` and/or `provider.longContextExtraUsage` for 1M context in Opus 4.6/Sonnet 4.6 as described in [Configuration](#configuration).
|
|
31
33
|
|
|
32
34
|
## AskClaude Tool
|
package/package.json
CHANGED
package/src/child-env.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AuthResult } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
// Applied to every Claude Code subprocess the bridge spawns. Pi owns both its
|
|
4
|
+
// tool surface and context compaction, so Claude Code must not add either one.
|
|
5
|
+
export const CC_CHILD_ENV = {
|
|
6
|
+
ENABLE_CLAUDEAI_MCP_SERVERS: "0",
|
|
7
|
+
DISABLE_AUTO_COMPACT: "1",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export interface AnthropicAuthRegistry {
|
|
11
|
+
getProviderAuth(provider: string): Promise<AuthResult | undefined>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Build an isolated child environment from Pi's resolved Anthropic auth.
|
|
15
|
+
*
|
|
16
|
+
* Pi refreshes stored OAuth before getProviderAuth() resolves. Claude Code's
|
|
17
|
+
* supported OAuth injection point is CLAUDE_CODE_OAUTH_TOKEN; API keys keep
|
|
18
|
+
* using ANTHROPIC_API_KEY. All inherited Claude/Anthropic auth and alternate
|
|
19
|
+
* backend settings are removed so every child relies exclusively on the
|
|
20
|
+
* credential and endpoint resolved by Pi.
|
|
21
|
+
*/
|
|
22
|
+
export function buildClaudeChildEnv(
|
|
23
|
+
base: NodeJS.ProcessEnv,
|
|
24
|
+
resolved?: AuthResult,
|
|
25
|
+
): NodeJS.ProcessEnv {
|
|
26
|
+
const env: NodeJS.ProcessEnv = { ...base, ...(resolved?.env ?? {}), ...CC_CHILD_ENV };
|
|
27
|
+
for (const key of [
|
|
28
|
+
"ANTHROPIC_API_KEY",
|
|
29
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
30
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
31
|
+
"ANTHROPIC_IDENTITY_TOKEN",
|
|
32
|
+
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
33
|
+
"ANTHROPIC_BASE_URL",
|
|
34
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
35
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
36
|
+
"CLAUDE_CODE_CUSTOM_OAUTH_URL",
|
|
37
|
+
"CLAUDE_CODE_OAUTH_CLIENT_ID",
|
|
38
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
39
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
40
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
41
|
+
] as const) delete env[key];
|
|
42
|
+
|
|
43
|
+
const headers = Object.entries(resolved?.auth.headers ?? {}).filter((entry): entry is [string, string] => entry[1] != null);
|
|
44
|
+
const authorization = headers.find(([name]) => name.toLowerCase() === "authorization")?.[1];
|
|
45
|
+
const bearerToken = authorization?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
46
|
+
const headerApiKey = headers.find(([name]) => name.toLowerCase() === "x-api-key")?.[1];
|
|
47
|
+
const credential = resolved?.auth.apiKey ?? headerApiKey;
|
|
48
|
+
if (!credential && !bearerToken) {
|
|
49
|
+
throw new Error("No Anthropic credential is configured in Pi. Configure Anthropic authentication in Pi before using claude-bridge or AskClaude.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (resolved?.auth.baseUrl) env.ANTHROPIC_BASE_URL = resolved.auth.baseUrl;
|
|
53
|
+
const customHeaders = headers.filter(([name]) => !["authorization", "x-api-key"].includes(name.toLowerCase()));
|
|
54
|
+
if (customHeaders.length > 0) {
|
|
55
|
+
env.ANTHROPIC_CUSTOM_HEADERS = customHeaders.map(([name, value]) => `${name}: ${value}`).join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (bearerToken) {
|
|
59
|
+
env.ANTHROPIC_AUTH_TOKEN = bearerToken;
|
|
60
|
+
} else if (resolved?.source?.toLowerCase().includes("oauth")) {
|
|
61
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = credential;
|
|
62
|
+
} else {
|
|
63
|
+
env.ANTHROPIC_API_KEY = credential;
|
|
64
|
+
}
|
|
65
|
+
return env;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function resolveClaudeChildEnv(
|
|
69
|
+
registry: AnthropicAuthRegistry | null | undefined,
|
|
70
|
+
base: NodeJS.ProcessEnv = process.env,
|
|
71
|
+
): Promise<NodeJS.ProcessEnv> {
|
|
72
|
+
const resolved = registry ? await registry.getProviderAuth("anthropic") : undefined;
|
|
73
|
+
return buildClaudeChildEnv(base, resolved);
|
|
74
|
+
}
|
package/src/convert.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { Message as PiMessage } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { Message as SessionMessage } from "cc-session-io";
|
|
6
6
|
import { pascalCase } from "change-case";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
7
8
|
import { MCP_TOOL_PREFIX } from "./skills.js";
|
|
8
9
|
|
|
9
10
|
export const PROVIDER_ID = "claude-bridge";
|
|
@@ -14,10 +15,27 @@ export const PI_TO_SDK_TOOL_NAME: Record<string, string> = {
|
|
|
14
15
|
read: "Read", write: "Write", edit: "Edit", bash: "Bash",
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
const MAX_ANTHROPIC_TOOL_ID_LENGTH = 64;
|
|
19
|
+
|
|
17
20
|
export function sanitizeToolId(id: string, cache: Map<string, string>): string {
|
|
18
21
|
const existing = cache.get(id);
|
|
19
22
|
if (existing) return existing;
|
|
20
|
-
|
|
23
|
+
|
|
24
|
+
const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "_") || "tool";
|
|
25
|
+
let clean = sanitized.slice(0, MAX_ANTHROPIC_TOOL_ID_LENGTH);
|
|
26
|
+
const occupied = new Set(cache.values());
|
|
27
|
+
if (occupied.has(clean)) {
|
|
28
|
+
const suffix = `_${createHash("sha256").update(id).digest("hex").slice(0, 12)}`;
|
|
29
|
+
clean = `${sanitized.slice(0, MAX_ANTHROPIC_TOOL_ID_LENGTH - suffix.length)}${suffix}`;
|
|
30
|
+
// A truncated hash collision is extraordinarily unlikely, but the converter
|
|
31
|
+
// must never emit duplicate IDs into a parallel tool turn.
|
|
32
|
+
let attempt = 2;
|
|
33
|
+
while (occupied.has(clean)) {
|
|
34
|
+
const numberedSuffix = `${suffix}_${attempt++}`;
|
|
35
|
+
clean = `${sanitized.slice(0, MAX_ANTHROPIC_TOOL_ID_LENGTH - numberedSuffix.length)}${numberedSuffix}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
cache.set(id, clean);
|
|
22
40
|
return clean;
|
|
23
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { claudeCodeSettings, loadConfig, markStartupNoticeShown, type Config } f
|
|
|
21
21
|
import { extractAgentsAppend } from "./agents-md.js";
|
|
22
22
|
import { createToolServer } from "./mcp-server.js";
|
|
23
23
|
import { buildActionSummary, type ToolCallState } from "./askclaude-ui.js";
|
|
24
|
+
import { CC_CHILD_ENV, resolveClaudeChildEnv, type AnthropicAuthRegistry } from "./child-env.js";
|
|
24
25
|
|
|
25
26
|
// Compat (#2): use factory if available (pi-ai ≥0.66), else fall back to constructor (gsd-pi etc.)
|
|
26
27
|
const _piAi = piAi as any;
|
|
@@ -42,18 +43,6 @@ const DIAG_LOG_PATH = join(homedir(), ".pi", "agent", "claude-bridge-diag.log");
|
|
|
42
43
|
// emitted rather than ones we imagined.
|
|
43
44
|
const RECORD_STREAM_PATH = process.env.CLAUDE_BRIDGE_RECORD_STREAM;
|
|
44
45
|
|
|
45
|
-
// Applied to every Claude Code subprocess the bridge spawns — provider, AskClaude
|
|
46
|
-
// and the compact summary. One place, so a guard is added once rather than three
|
|
47
|
-
// times, and so a missing one is visible.
|
|
48
|
-
//
|
|
49
|
-
// - ENABLE_CLAUDEAI_MCP_SERVERS=0: keep the user's claude.ai-connected MCP servers
|
|
50
|
-
// out of a pi session, which serves its own tools.
|
|
51
|
-
// - DISABLE_AUTO_COMPACT=1: pi owns compaction; CC compacting its own copy would
|
|
52
|
-
// diverge from pi's history, which is the source of truth for every rebuild.
|
|
53
|
-
const CC_CHILD_ENV = {
|
|
54
|
-
ENABLE_CLAUDEAI_MCP_SERVERS: "0",
|
|
55
|
-
DISABLE_AUTO_COMPACT: "1",
|
|
56
|
-
} as const;
|
|
57
46
|
|
|
58
47
|
// Ensure log directories exist when debug is enabled
|
|
59
48
|
if (DEBUG) {
|
|
@@ -394,12 +383,13 @@ async function runIsolatedSummary(
|
|
|
394
383
|
const claudeExecutable = compactProviderSettings?.pathToClaudeCodeExecutable;
|
|
395
384
|
const cliModel = claudeCodeModelId(model, longContextSettings);
|
|
396
385
|
debug(`compact summary: spawn model=${cliModel} registeredModel=${model.id} promptLen=${promptText.length}`);
|
|
386
|
+
const childEnv = await resolveClaudeChildEnv(piModelRegistry);
|
|
397
387
|
|
|
398
388
|
sdkQuery = query({
|
|
399
389
|
prompt: promptText,
|
|
400
390
|
options: {
|
|
401
391
|
cwd,
|
|
402
|
-
env:
|
|
392
|
+
env: childEnv,
|
|
403
393
|
settings: { autoMemoryEnabled: false },
|
|
404
394
|
tools: [],
|
|
405
395
|
strictMcpConfig: true,
|
|
@@ -733,6 +723,7 @@ function mapToolArgs(
|
|
|
733
723
|
// Global (not query state):
|
|
734
724
|
let piUI: ExtensionUIContext | null = null;
|
|
735
725
|
let piMode: ExtensionContext["mode"] | null = null;
|
|
726
|
+
let piModelRegistry: AnthropicAuthRegistry | null = null;
|
|
736
727
|
const activeQueryContexts = new Set<QueryContext>();
|
|
737
728
|
|
|
738
729
|
// `plan` is the one setting whose default silently costs the user something (no
|
|
@@ -1471,10 +1462,8 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1471
1462
|
// also autocompact would double-flush the prompt cache and races pi's
|
|
1472
1463
|
// threshold with CC's, including CC's anti-thrashing guard (issue #8).
|
|
1473
1464
|
// Manual /compact in CC still works (we never invoke it).
|
|
1474
|
-
const childEnv = { ...process.env, ...CC_CHILD_ENV };
|
|
1475
1465
|
const queryOptions: NonNullable<Parameters<typeof query>[0]["options"]> = {
|
|
1476
1466
|
cwd,
|
|
1477
|
-
env: childEnv,
|
|
1478
1467
|
tools: [],
|
|
1479
1468
|
permissionMode: "bypassPermissions",
|
|
1480
1469
|
includePartialMessages: true,
|
|
@@ -1498,20 +1487,17 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1498
1487
|
`appendSys=${appendSystemPrompt} strictMcp=${strictMcpConfigEnabled}`,
|
|
1499
1488
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`);
|
|
1500
1489
|
|
|
1501
|
-
//
|
|
1490
|
+
// Resolve Pi's Anthropic credential before every fresh child. OAuth refresh is
|
|
1491
|
+
// asynchronous, while pi's provider API must return its stream synchronously,
|
|
1492
|
+
// so query startup runs in the background and forwards into the claimed stream.
|
|
1502
1493
|
let wasAborted = false;
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
// 4. Capture context for abort handling
|
|
1494
|
+
let sdkQuery: ReturnType<typeof query> | null = null;
|
|
1495
|
+
const authPending = { kind: "claude-auth-pending" };
|
|
1496
|
+
queryCtx.activeQuery = authPending;
|
|
1508
1497
|
const abortCtx = queryCtx;
|
|
1509
|
-
|
|
1510
1498
|
const requestAbort = () => {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
void sdkQuery.interrupt().catch(() => {});
|
|
1514
|
-
try { sdkQuery.close(); } catch {}
|
|
1499
|
+
void sdkQuery?.interrupt().catch(() => {});
|
|
1500
|
+
try { sdkQuery?.close(); } catch {}
|
|
1515
1501
|
};
|
|
1516
1502
|
const onAbort = () => {
|
|
1517
1503
|
wasAborted = true;
|
|
@@ -1523,88 +1509,96 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1523
1509
|
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
1524
1510
|
}
|
|
1525
1511
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1512
|
+
void (async () => {
|
|
1513
|
+
queryOptions.env = await resolveClaudeChildEnv(piModelRegistry);
|
|
1514
|
+
if (wasAborted || options?.signal?.aborted) throw new Error("Operation aborted");
|
|
1515
|
+
|
|
1516
|
+
const startedQuery = query({ prompt: promptStream.stream, options: queryOptions });
|
|
1517
|
+
sdkQuery = startedQuery;
|
|
1518
|
+
queryCtx.activeQuery = startedQuery;
|
|
1519
|
+
activeQueryContexts.add(queryCtx);
|
|
1520
|
+
// query() may synchronously trigger an abort before sdkQuery is assigned.
|
|
1521
|
+
// Re-check now so the just-created child cannot escape requestAbort().
|
|
1522
|
+
if (wasAborted || options?.signal?.aborted) {
|
|
1523
|
+
requestAbort();
|
|
1524
|
+
throw new Error("Operation aborted");
|
|
1525
|
+
}
|
|
1530
1526
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
if (sharedSession) sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1534
|
-
debug(`provider: abort detected, marked sharedSession needsRebuild + forceRotate`);
|
|
1535
|
-
if (queryCtx.turnOutput) {
|
|
1536
|
-
queryCtx.turnOutput.stopReason = "aborted";
|
|
1537
|
-
queryCtx.turnOutput.errorMessage = "Operation aborted";
|
|
1538
|
-
}
|
|
1539
|
-
const stream = queryCtx.currentPiStream;
|
|
1540
|
-
stream?.push({ type: "error", reason: "aborted", error: queryCtx.turnOutput! });
|
|
1541
|
-
markStreamComplete(stream);
|
|
1542
|
-
stream?.end();
|
|
1543
|
-
queryCtx.currentPiStream = null;
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1527
|
+
const { capturedSessionId } = await consumeQuery(startedQuery, customToolNameToPi, model, () => wasAborted, queryCtx);
|
|
1528
|
+
debug(`provider: consumeQuery completed, stopReason=${queryCtx.turnOutput?.stopReason}, error=${queryCtx.turnOutput?.errorMessage}, aborted=${wasAborted}`);
|
|
1546
1529
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
}
|
|
1554
|
-
debug(`provider: query done, ignoring captured session ${capturedSessionId?.slice(0, 8) ?? "none"} to preserve shared session`);
|
|
1555
|
-
} else if (sessionId) {
|
|
1556
|
-
const cursor = Math.max(context.messages.length, queryCtx.latestCursor, sharedSession?.cursor ?? 0);
|
|
1557
|
-
debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
1558
|
-
sharedSession = { sessionId, cursor, cwd };
|
|
1530
|
+
if (wasAborted || options?.signal?.aborted) {
|
|
1531
|
+
if (sharedSession) sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1532
|
+
debug(`provider: abort detected, marked sharedSession needsRebuild + forceRotate`);
|
|
1533
|
+
if (queryCtx.turnOutput) {
|
|
1534
|
+
queryCtx.turnOutput.stopReason = "aborted";
|
|
1535
|
+
queryCtx.turnOutput.errorMessage = "Operation aborted";
|
|
1559
1536
|
}
|
|
1537
|
+
const currentStream = queryCtx.currentPiStream;
|
|
1538
|
+
currentStream?.push({ type: "error", reason: "aborted", error: queryCtx.turnOutput! });
|
|
1539
|
+
markStreamComplete(currentStream);
|
|
1540
|
+
currentStream?.end();
|
|
1541
|
+
queryCtx.currentPiStream = null;
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1560
1544
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1545
|
+
const sessionId = capturedSessionId ?? sharedSession?.sessionId;
|
|
1546
|
+
if (syncResult.preserveSharedSession) {
|
|
1547
|
+
if (capturedSessionId && capturedSessionId !== sharedSession?.sessionId) {
|
|
1548
|
+
deleteSession(capturedSessionId, cwd, process.env.CLAUDE_CONFIG_DIR);
|
|
1549
|
+
debug(`provider: query done, deleted ephemeral session ${capturedSessionId.slice(0, 8)} to preserve shared session`);
|
|
1564
1550
|
}
|
|
1565
|
-
|
|
1566
|
-
})
|
|
1551
|
+
debug(`provider: query done, ignoring captured session ${capturedSessionId?.slice(0, 8) ?? "none"} to preserve shared session`);
|
|
1552
|
+
} else if (sessionId) {
|
|
1553
|
+
const cursor = Math.max(context.messages.length, queryCtx.latestCursor, sharedSession?.cursor ?? 0);
|
|
1554
|
+
debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
1555
|
+
sharedSession = { sessionId, cursor, cwd };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
if (queryCtx.activeQuery === startedQuery) {
|
|
1559
|
+
activeQueryContexts.delete(queryCtx);
|
|
1560
|
+
if (!isReentrant) debug("provider: clearing activeQuery before final stream completion");
|
|
1561
|
+
queryCtx.activeQuery = null;
|
|
1562
|
+
}
|
|
1563
|
+
finalizeCurrentStream(queryCtx, queryCtx.turnOutput?.stopReason);
|
|
1564
|
+
})()
|
|
1567
1565
|
.catch((error) => {
|
|
1568
|
-
debug(`provider: query error, model=${cliModel}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error);
|
|
1566
|
+
debug(`provider: query error, model=${cliModel}, aborted=${Boolean(wasAborted || options?.signal?.aborted)}, error=`, error);
|
|
1569
1567
|
if ((wasAborted || options?.signal?.aborted) && sharedSession) {
|
|
1570
1568
|
sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1571
|
-
} else {
|
|
1569
|
+
} else if (sdkQuery) {
|
|
1570
|
+
// Once Claude Code starts, a failed turn can leave its transcript
|
|
1571
|
+
// incomplete. Auth-resolution failures happen before that and must not
|
|
1572
|
+
// discard an otherwise resumable shared session.
|
|
1572
1573
|
sharedSession = null;
|
|
1573
1574
|
}
|
|
1574
1575
|
promptStream.fail(error instanceof Error ? error : new Error(String(error)));
|
|
1575
1576
|
if (queryCtx.turnOutput) {
|
|
1576
|
-
queryCtx.turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1577
|
-
// The SDK drops its copy of the result text if any message follows the error
|
|
1578
|
-
// result, so prefer the cause consumeQuery recorded off the result itself.
|
|
1577
|
+
queryCtx.turnOutput.stopReason = wasAborted || options?.signal?.aborted ? "aborted" : "error";
|
|
1579
1578
|
queryCtx.turnOutput.errorMessage ??= error instanceof Error ? error.message : String(error);
|
|
1580
1579
|
}
|
|
1581
|
-
if (
|
|
1580
|
+
if (queryCtx.activeQuery === authPending || (sdkQuery && queryCtx.activeQuery === sdkQuery)) {
|
|
1582
1581
|
queryCtx.releasePendingToolCalls("Query ended");
|
|
1583
|
-
|
|
1582
|
+
activeQueryContexts.delete(queryCtx);
|
|
1583
|
+
if (!isReentrant) debug("provider: clearing activeQuery before error stream completion");
|
|
1584
1584
|
queryCtx.activeQuery = null;
|
|
1585
1585
|
}
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
markStreamComplete(
|
|
1589
|
-
|
|
1586
|
+
const currentStream = queryCtx.currentPiStream;
|
|
1587
|
+
currentStream?.push({ type: "error", reason: (queryCtx.turnOutput?.stopReason ?? "error") as "aborted" | "error", error: queryCtx.turnOutput! });
|
|
1588
|
+
markStreamComplete(currentStream);
|
|
1589
|
+
currentStream?.end();
|
|
1590
1590
|
queryCtx.currentPiStream = null;
|
|
1591
1591
|
})
|
|
1592
1592
|
.finally(() => {
|
|
1593
1593
|
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1594
|
-
// Settle any ack still parked in the generator — the CLI is gone, so
|
|
1595
|
-
// nothing will resume it. Clear the handle only if a later query
|
|
1596
|
-
// hasn't already claimed the shared context.
|
|
1597
1594
|
promptStream.fail(new Error("query ended"));
|
|
1598
1595
|
if (queryCtx.promptStream === promptStream) queryCtx.promptStream = null;
|
|
1599
|
-
if (queryCtx.activeQuery === sdkQuery) {
|
|
1596
|
+
if (queryCtx.activeQuery === authPending || (sdkQuery && queryCtx.activeQuery === sdkQuery)) {
|
|
1600
1597
|
queryCtx.releasePendingToolCalls("Query ended");
|
|
1601
1598
|
queryCtx.activeQuery = null;
|
|
1602
|
-
// Guarded like the two cleanups above: if a later query has already
|
|
1603
|
-
// claimed this context, removing it from the routing set would send
|
|
1604
|
-
// that query's tool results down the orphan path and strand its handler.
|
|
1605
1599
|
activeQueryContexts.delete(queryCtx);
|
|
1606
1600
|
}
|
|
1607
|
-
sdkQuery
|
|
1601
|
+
try { sdkQuery?.close(); } catch {}
|
|
1608
1602
|
});
|
|
1609
1603
|
|
|
1610
1604
|
return stream;
|
|
@@ -1674,12 +1668,13 @@ async function promptAndWait(
|
|
|
1674
1668
|
`mode=${mode} model=${modelId} cliModel=${cliModel} effort=${effort ?? "default"}`,
|
|
1675
1669
|
`isolated=${options?.isolated ?? false} resume=${resumeSessionId?.slice(0, 8) ?? "none"}`,
|
|
1676
1670
|
`skills=${Boolean(skillsBlock)} promptLen=${prompt.length}`);
|
|
1671
|
+
const childEnv = await resolveClaudeChildEnv(piModelRegistry);
|
|
1677
1672
|
|
|
1678
1673
|
const sdkQuery = query({
|
|
1679
1674
|
prompt,
|
|
1680
1675
|
options: {
|
|
1681
1676
|
cwd,
|
|
1682
|
-
env:
|
|
1677
|
+
env: childEnv,
|
|
1683
1678
|
permissionMode: "bypassPermissions",
|
|
1684
1679
|
settings: claudeCodeSettings(providerSettings),
|
|
1685
1680
|
...(disallowedTools.length ? { disallowedTools } : {}),
|
|
@@ -1816,6 +1811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1816
1811
|
pi.on("session_start", (event, ctx) => {
|
|
1817
1812
|
piUI = ctx.ui;
|
|
1818
1813
|
piMode = ctx.mode;
|
|
1814
|
+
piModelRegistry = ctx.modelRegistry;
|
|
1819
1815
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
1820
1816
|
clearSession(`session_start:${event.reason}`);
|
|
1821
1817
|
}
|