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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-agent-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "description": "Pi extension that uses Claude Code (via Agent SDK) as a model provider and adds an AskClaude tool.",
6
6
  "keywords": [
@@ -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
- const clean = id.replace(/[^a-zA-Z0-9_-]/g, "_");
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: { ...process.env, ...CC_CHILD_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
- // 3. Start SDK query and claim it for this context
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
- const sdkQuery = query({ prompt: promptStream.stream, options: queryOptions });
1504
- queryCtx.activeQuery = sdkQuery;
1505
- activeQueryContexts.add(queryCtx);
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
- // interrupt() asks the CLI to stop gracefully; close() kills it immediately.
1512
- // Both are needed — interrupt alone lets the current API call finish.
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
- // Background consumer — runs until query ends
1527
- consumeQuery(sdkQuery, customToolNameToPi, model, () => wasAborted, queryCtx)
1528
- .then(async ({ capturedSessionId }) => {
1529
- debug(`provider: consumeQuery completed, stopReason=${queryCtx.turnOutput?.stopReason}, error=${queryCtx.turnOutput?.errorMessage}, aborted=${wasAborted}`);
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
- // --- Abort detection in normal completion path ---
1532
- if (wasAborted || options?.signal?.aborted) {
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
- // --- Capture session ID ---
1548
- const sessionId = capturedSessionId ?? sharedSession?.sessionId;
1549
- if (syncResult.preserveSharedSession) {
1550
- if (capturedSessionId && capturedSessionId !== sharedSession?.sessionId) {
1551
- deleteSession(capturedSessionId, cwd, process.env.CLAUDE_CONFIG_DIR);
1552
- debug(`provider: query done, deleted ephemeral session ${capturedSessionId.slice(0, 8)} to preserve shared session`);
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
- if (!isReentrant && queryCtx.activeQuery === sdkQuery) {
1562
- debug("provider: clearing activeQuery before final stream completion");
1563
- queryCtx.activeQuery = null;
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
- finalizeCurrentStream(queryCtx, queryCtx.turnOutput?.stopReason);
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 (!isReentrant && queryCtx.activeQuery === sdkQuery) {
1580
+ if (queryCtx.activeQuery === authPending || (sdkQuery && queryCtx.activeQuery === sdkQuery)) {
1582
1581
  queryCtx.releasePendingToolCalls("Query ended");
1583
- debug("provider: clearing activeQuery before error stream completion");
1582
+ activeQueryContexts.delete(queryCtx);
1583
+ if (!isReentrant) debug("provider: clearing activeQuery before error stream completion");
1584
1584
  queryCtx.activeQuery = null;
1585
1585
  }
1586
- const stream = queryCtx.currentPiStream;
1587
- stream?.push({ type: "error", reason: (queryCtx.turnOutput?.stopReason ?? "error") as "aborted" | "error", error: queryCtx.turnOutput! });
1588
- markStreamComplete(stream);
1589
- stream?.end();
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.close();
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: { ...process.env, ...CC_CHILD_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
  }