@vanillagreen/pi-claude-bridge 2.0.0 → 3.2.2

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/bundle/index.js CHANGED
@@ -26941,12 +26941,116 @@ function splitPrefixSuffix(input, options = {}) {
26941
26941
  // src/config.ts
26942
26942
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
26943
26943
  import { homedir } from "os";
26944
- import { dirname, join as join2, resolve as resolve2 } from "path";
26944
+ import { dirname as dirname2, join as join3, resolve as resolve2, sep as sep2 } from "path";
26945
+
26946
+ // src/debug.ts
26947
+ import { appendFileSync as appendFileSync2, chmodSync, mkdirSync as mkdirSync2 } from "fs";
26948
+ import { dirname, join as join2 } from "path";
26949
+ var DEBUG = process.env.CLAUDE_BRIDGE_DEBUG === "1";
26950
+ var DEBUG_LOG_PATH = process.env.CLAUDE_BRIDGE_DEBUG_PATH || join2(piUserDir(), "claude-bridge.log");
26951
+ function diagLogPath() {
26952
+ return process.env.CLAUDE_BRIDGE_DIAG_PATH || join2(piUserDir(), "claude-bridge-diag.log");
26953
+ }
26954
+ function diagGuidance() {
26955
+ return DEBUG ? `see ${diagLogPath()}` : "re-run with CLAUDE_BRIDGE_DEBUG=1 to capture a diagnostic dump";
26956
+ }
26957
+ if (DEBUG) {
26958
+ try {
26959
+ mkdirSync2(dirname(DEBUG_LOG_PATH), { recursive: true, mode: 448 });
26960
+ mkdirSync2(dirname(diagLogPath()), { recursive: true, mode: 448 });
26961
+ chmodSync(dirname(DEBUG_LOG_PATH), 448);
26962
+ chmodSync(DEBUG_LOG_PATH, 384);
26963
+ } catch {
26964
+ }
26965
+ }
26966
+ var moduleInstanceId = Math.random().toString(36).slice(2, 8);
26967
+ function debug(...args) {
26968
+ if (!DEBUG) return;
26969
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString();
26970
+ const fmt = (a) => {
26971
+ if (typeof a === "string") return a;
26972
+ if (a instanceof Error) return `${a.name}: ${a.message}${a.stack ? "\n" + a.stack : ""}`;
26973
+ if (typeof a === "function") return fmt(a());
26974
+ return JSON.stringify(a);
26975
+ };
26976
+ const safeFmt = (a) => {
26977
+ let out;
26978
+ try {
26979
+ out = fmt(a);
26980
+ } catch (error51) {
26981
+ let reason = "formatting failed";
26982
+ try {
26983
+ reason = String(error51 instanceof Error ? error51.message : error51);
26984
+ } catch {
26985
+ }
26986
+ return `[unprintable: ${reason}]`;
26987
+ }
26988
+ if (out !== void 0) return out;
26989
+ if (typeof a === "function") return "undefined";
26990
+ try {
26991
+ return String(a);
26992
+ } catch {
26993
+ return "[unprintable: formatting failed]";
26994
+ }
26995
+ };
26996
+ const msg = args.map(safeFmt).join(" ");
26997
+ try {
26998
+ appendFileSync2(DEBUG_LOG_PATH, `[${ts2}] [${moduleInstanceId}] ${msg}
26999
+ `, { mode: 384 });
27000
+ } catch {
27001
+ }
27002
+ }
27003
+ var nextCliDebugSeq = 1;
27004
+ function makeCliDebugOptions(tag) {
27005
+ if (!DEBUG) return {};
27006
+ const seq = nextCliDebugSeq++;
27007
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
27008
+ const logDir = join2(dirname(DEBUG_LOG_PATH), "cc-cli-logs");
27009
+ try {
27010
+ mkdirSync2(logDir, { recursive: true, mode: 448 });
27011
+ chmodSync(logDir, 448);
27012
+ } catch {
27013
+ }
27014
+ const debugFile = join2(logDir, `${ts2}-${tag}-${seq}.log`);
27015
+ debug(`cli-debug: ${tag} #${seq} \u2192 ${debugFile}`);
27016
+ return {
27017
+ debug: true,
27018
+ debugFile,
27019
+ stderr: (data) => {
27020
+ for (const line of data.split(/\r?\n/)) {
27021
+ if (line) debug(`[cli-stderr ${tag}#${seq}] ${line}`);
27022
+ }
27023
+ }
27024
+ };
27025
+ }
27026
+ function diagDump(label, data) {
27027
+ if (!DEBUG) return;
27028
+ try {
27029
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString();
27030
+ const entry = { ts: ts2, moduleInstanceId, label, ...data };
27031
+ const path = diagLogPath();
27032
+ try {
27033
+ mkdirSync2(dirname(path), { recursive: true, mode: 448 });
27034
+ } catch {
27035
+ }
27036
+ appendFileSync2(path, JSON.stringify(entry) + "\n", { mode: 384 });
27037
+ try {
27038
+ chmodSync(path, 384);
27039
+ } catch {
27040
+ }
27041
+ debug(`DIAG: ${label} (see ${path})`);
27042
+ } catch (error51) {
27043
+ debug(`DIAG FAILED: ${label}`, error51);
27044
+ }
27045
+ }
27046
+
27047
+ // src/config.ts
26945
27048
  var PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
27049
+ var EXTERNAL_CONFIG_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.extension-config-resolver");
26946
27050
  var VALID_EFFORT_LEVELS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
26947
27051
  function expandHome(input) {
26948
27052
  if (input === "~") return homedir();
26949
- if (input.startsWith("~/")) return join2(homedir(), input.slice(2));
27053
+ if (input.startsWith("~/")) return join3(homedir(), input.slice(2));
26950
27054
  return input;
26951
27055
  }
26952
27056
  function piUserDir() {
@@ -26971,11 +27075,11 @@ function mergeDeep(target, source) {
26971
27075
  function projectSettingsPath(cwd) {
26972
27076
  let current = resolve2(cwd);
26973
27077
  while (true) {
26974
- const candidate = join2(current, ".pi", "settings.json");
27078
+ const candidate = join3(current, ".pi", "settings.json");
26975
27079
  if (existsSync2(candidate)) return candidate;
26976
- if (existsSync2(join2(current, ".pi")) || existsSync2(join2(current, ".git")) || existsSync2(join2(current, ".vstack-lock.json"))) return candidate;
26977
- const parent = dirname(current);
26978
- if (parent === current) return join2(resolve2(cwd), ".pi", "settings.json");
27080
+ if (existsSync2(join3(current, ".pi")) || existsSync2(join3(current, ".git")) || existsSync2(join3(current, ".vstack-lock.json"))) return candidate;
27081
+ const parent = dirname2(current);
27082
+ if (parent === current) return join3(resolve2(cwd), ".pi", "settings.json");
26979
27083
  current = parent;
26980
27084
  }
26981
27085
  }
@@ -27005,7 +27109,7 @@ function projectSettingsTrusted(settingsPath) {
27005
27109
  return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
27006
27110
  }
27007
27111
  function settingsPaths(cwd) {
27008
- const user = join2(piUserDir(), "settings.json");
27112
+ const user = join3(piUserDir(), "settings.json");
27009
27113
  if (isolatedFromEnv()) return [];
27010
27114
  const project = projectSettingsPath(cwd);
27011
27115
  return projectSettingsTrusted(project) ? [user, project] : [user];
@@ -27014,24 +27118,39 @@ function tryParseJson(path) {
27014
27118
  if (!existsSync2(path)) return {};
27015
27119
  try {
27016
27120
  return JSON.parse(readFileSync2(path, "utf-8"));
27017
- } catch {
27121
+ } catch (error51) {
27122
+ debug(`config: ignoring malformed ${path}:`, error51 instanceof Error ? error51.message : String(error51));
27018
27123
  return {};
27019
27124
  }
27020
27125
  }
27021
27126
  function readManagerConfig(cwd) {
27022
27127
  const merged = {};
27128
+ const userPath = join3(piUserDir(), "settings.json");
27023
27129
  for (const path of settingsPaths(cwd)) {
27024
27130
  if (!existsSync2(path)) continue;
27025
27131
  try {
27026
27132
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
27027
27133
  const configRoot = asRecord(asRecord(asRecord(parsed?.vstack)?.extensionManager)?.config);
27028
27134
  const config2 = asRecord(configRoot?.[PACKAGE_ID]);
27029
- if (config2) mergeDeep(merged, config2);
27030
- } catch {
27135
+ if (config2) mergeDeep(merged, path === userPath ? config2 : withoutUserScopeOnlyKeys(config2));
27136
+ } catch (error51) {
27137
+ debug(`config: ignoring malformed manager config ${path}:`, error51 instanceof Error ? error51.message : String(error51));
27031
27138
  }
27032
27139
  }
27033
27140
  return merged;
27034
27141
  }
27142
+ var USER_SCOPE_ONLY_PROVIDER_KEYS = ["enableConnectors", "connectorWriteMode"];
27143
+ function withoutUserScopeOnlyKeys(raw) {
27144
+ const out = { ...raw };
27145
+ for (const key of USER_SCOPE_ONLY_PROVIDER_KEYS) delete out[key];
27146
+ return out;
27147
+ }
27148
+ function stripUserScopeOnlyProviderKeys(config2) {
27149
+ if (!config2.provider) return config2;
27150
+ const provider = { ...config2.provider };
27151
+ for (const key of USER_SCOPE_ONLY_PROVIDER_KEYS) delete provider[key];
27152
+ return { ...config2, provider };
27153
+ }
27035
27154
  function boolFrom(raw, key) {
27036
27155
  return typeof raw[key] === "boolean" ? raw[key] : void 0;
27037
27156
  }
@@ -27095,8 +27214,6 @@ function managerToConfig(raw) {
27095
27214
  const promptContext = {};
27096
27215
  const appendSystemPrompt = boolFrom(raw, "appendSystemPrompt");
27097
27216
  if (appendSystemPrompt !== void 0) provider.appendSystemPrompt = appendSystemPrompt;
27098
- const allowExtraUsage = boolFrom(raw, "allowExtraUsage");
27099
- if (allowExtraUsage !== void 0) provider.allowExtraUsage = allowExtraUsage;
27100
27217
  const fastMode = boolFrom(raw, "fastMode");
27101
27218
  if (fastMode !== void 0) provider.fastMode = fastMode;
27102
27219
  if (hasOwn(raw, "forceEffort")) {
@@ -27127,19 +27244,84 @@ function managerToConfig(raw) {
27127
27244
  ...Object.keys(promptContext).length ? { promptContext } : {}
27128
27245
  };
27129
27246
  }
27247
+ function legacyFileConfig(path) {
27248
+ const raw = asRecord(tryParseJson(path)) ?? {};
27249
+ const flat = managerToConfig(raw);
27250
+ const provider = { ...flat.provider, ...asRecord(raw.provider) };
27251
+ const promptContext = { ...flat.promptContext, ...asRecord(raw.promptContext) };
27252
+ return {
27253
+ ...flat.enabled !== void 0 ? { enabled: flat.enabled } : {},
27254
+ ...Object.keys(provider ?? {}).length ? { provider } : {},
27255
+ ...Object.keys(promptContext ?? {}).length ? { promptContext } : {}
27256
+ };
27257
+ }
27258
+ function legacyLayers(cwd) {
27259
+ const globalPath = join3(piUserDir(), "claude-bridge.json");
27260
+ const layers = [{ path: globalPath, config: legacyFileConfig(globalPath) }];
27261
+ if (isolatedFromEnv()) return layers;
27262
+ const projectSettings = projectSettingsPath(cwd);
27263
+ if (!projectSettingsTrusted(projectSettings)) return layers;
27264
+ const projectPath = join3(dirname2(projectSettings), "claude-bridge.json");
27265
+ return [...layers, { path: projectPath, config: stripUserScopeOnlyProviderKeys(legacyFileConfig(projectPath)) }];
27266
+ }
27267
+ function mergeLayers(layers) {
27268
+ const merged = { provider: {}, promptContext: {} };
27269
+ for (const layer of layers) {
27270
+ if (layer.config.enabled !== void 0) merged.enabled = layer.config.enabled;
27271
+ merged.provider = { ...merged.provider, ...layer.config.provider };
27272
+ merged.promptContext = { ...merged.promptContext, ...layer.config.promptContext };
27273
+ }
27274
+ return merged;
27275
+ }
27130
27276
  function loadConfig(cwd) {
27131
- const global2 = tryParseJson(join2(piUserDir(), "claude-bridge.json"));
27132
- const isolated = isolatedFromEnv();
27133
- const projectSettings = isolated ? void 0 : projectSettingsPath(cwd);
27134
- const trustedProject = projectSettings !== void 0 && projectSettingsTrusted(projectSettings);
27135
- const project = trustedProject ? tryParseJson(join2(dirname(projectSettings), "claude-bridge.json")) : {};
27136
- const manager = isolated ? {} : managerToConfig(readManagerConfig(cwd));
27137
- const provider = normalizeProviderConfig({ ...global2.provider, ...project.provider, ...manager.provider });
27277
+ const legacy = mergeLayers(legacyLayers(cwd));
27278
+ const manager = isolatedFromEnv() ? {} : managerToConfig(readManagerConfig(cwd));
27279
+ const provider = normalizeProviderConfig({ ...legacy.provider, ...manager.provider });
27138
27280
  return {
27139
- enabled: manager.enabled ?? project.enabled ?? global2.enabled ?? true,
27281
+ enabled: manager.enabled ?? legacy.enabled ?? true,
27140
27282
  provider,
27141
- promptContext: { ...global2.promptContext, ...project.promptContext, ...manager.promptContext }
27142
- };
27283
+ promptContext: { ...legacy.promptContext, ...manager.promptContext }
27284
+ };
27285
+ }
27286
+ var PROVIDER_KEYS = /* @__PURE__ */ new Set([
27287
+ "appendSystemPrompt",
27288
+ "connectorWriteMode",
27289
+ "enableConnectors",
27290
+ "fastMode",
27291
+ "forceEffort",
27292
+ "modelEffortOverrides",
27293
+ "pathToClaudeCodeExecutable",
27294
+ "strictMcpConfig"
27295
+ ]);
27296
+ var PROMPT_CONTEXT_KEYS = /* @__PURE__ */ new Set([
27297
+ "includeAppendSystemPromptMd",
27298
+ "includeCavemanHook",
27299
+ "includeProjectAgentsHook",
27300
+ "includeTaskPanelHook"
27301
+ ]);
27302
+ function configValueForKey(config2, key) {
27303
+ if (key === "enabled") return config2.enabled;
27304
+ if (PROVIDER_KEYS.has(key)) return normalizeProviderConfig(config2.provider)?.[key];
27305
+ if (PROMPT_CONTEXT_KEYS.has(key)) return config2.promptContext?.[key];
27306
+ return void 0;
27307
+ }
27308
+ function displayPath(path) {
27309
+ const home = homedir();
27310
+ return home && path.startsWith(home + sep2) ? `~${path.slice(home.length)}` : path;
27311
+ }
27312
+ function resolveExternalConfigValue(key, cwd) {
27313
+ const layers = legacyLayers(cwd);
27314
+ const value = configValueForKey(mergeLayers(layers), key);
27315
+ if (value === void 0) return { explicit: false, value: void 0 };
27316
+ const source = [...layers].reverse().find((layer) => configValueForKey(layer.config, key) !== void 0)?.path;
27317
+ return { explicit: true, value, ...source ? { source: displayPath(source) } : {} };
27318
+ }
27319
+ function registerExternalConfigResolver() {
27320
+ const host = globalThis;
27321
+ const existing = asRecord(host[EXTERNAL_CONFIG_RESOLVER_SYMBOL]);
27322
+ const registry2 = existing ?? {};
27323
+ if (!existing) host[EXTERNAL_CONFIG_RESOLVER_SYMBOL] = registry2;
27324
+ registry2[PACKAGE_ID] = (key, cwd) => resolveExternalConfigValue(key, cwd);
27143
27325
  }
27144
27326
 
27145
27327
  // src/skills.ts
@@ -27179,9 +27361,12 @@ function connectorServerNamespace(connectorName) {
27179
27361
  function credentialCandidatePaths(env = process.env) {
27180
27362
  const roots = [];
27181
27363
  const configDir = env.CLAUDE_CONFIG_DIR?.trim();
27182
- if (configDir) roots.push(configDir);
27183
- const home = env.HOME?.trim();
27184
- if (home) roots.push(`${home}/.claude`, home);
27364
+ if (configDir) {
27365
+ roots.push(configDir);
27366
+ } else {
27367
+ const home = env.HOME?.trim();
27368
+ if (home) roots.push(`${home}/.claude`, home);
27369
+ }
27185
27370
  const seen = /* @__PURE__ */ new Set();
27186
27371
  const paths = [];
27187
27372
  for (const root of roots) {
@@ -27348,6 +27533,10 @@ function connectorsEnabledFromEnv() {
27348
27533
  function connectorsEnabledFor(config2) {
27349
27534
  return connectorsEnabledFromEnv() || config2?.provider?.enableConnectors === true;
27350
27535
  }
27536
+ function settingSourcesForQuery(connectorsEnabled, appendSystemPrompt, configured) {
27537
+ if (connectorsEnabled) return configured ?? ["user"];
27538
+ return appendSystemPrompt ? void 0 : configured ?? ["user", "project"];
27539
+ }
27351
27540
  var CLAUDE_AI_CONNECTOR_TOOL_PATTERNS = [
27352
27541
  "mcp__claude_ai_Gmail__*",
27353
27542
  "mcp__claude_ai_Google_Calendar__*",
@@ -27355,7 +27544,16 @@ var CLAUDE_AI_CONNECTOR_TOOL_PATTERNS = [
27355
27544
  "mcp__claude_ai_Slack__*",
27356
27545
  "mcp__claude_ai_Atlassian__*"
27357
27546
  ];
27547
+ var SDK_TOOL_ALIASES = {
27548
+ ListMcpResources: "ListMcpResourcesTool",
27549
+ ReadMcpResource: "ReadMcpResourceTool"
27550
+ };
27551
+ function deliveredSpellings(name) {
27552
+ const canonical = SDK_TOOL_ALIASES[name];
27553
+ return canonical ? [name, canonical] : [name];
27554
+ }
27358
27555
  var CONNECTOR_DISCOVERY_TOOLS = ["ToolSearch", "ListMcpResources", "ReadMcpResource"];
27556
+ var CONNECTOR_DISCOVERY_TOOL_NAMES = new Set(CONNECTOR_DISCOVERY_TOOLS.flatMap(deliveredSpellings));
27359
27557
  var CONNECTOR_NS_PREFIX2 = "mcp__claude_ai_";
27360
27558
  var CONNECTOR_NS_GMAIL = `${CONNECTOR_NS_PREFIX2}Gmail__`;
27361
27559
  var CONNECTOR_NS_CALENDAR = `${CONNECTOR_NS_PREFIX2}Google_Calendar__`;
@@ -27512,15 +27710,22 @@ var CONNECTOR_WRITE_TOOLS = [
27512
27710
  `${CONNECTOR_NS_ATLASSIAN}createCompassComponentRelationship`,
27513
27711
  `${CONNECTOR_NS_ATLASSIAN}createCompassCustomFieldDefinition`
27514
27712
  ];
27515
- function isChildExecutedTool(name) {
27713
+ function isConnectorTool(name) {
27516
27714
  return typeof name === "string" && name.startsWith(CONNECTOR_NS_PREFIX2);
27517
27715
  }
27716
+ var CHILD_INTERNAL_TOOLS = /* @__PURE__ */ new Set(["ToolSearch", "ScheduleWakeup"]);
27717
+ function isChildInternalTool(name) {
27718
+ return typeof name === "string" && CHILD_INTERNAL_TOOLS.has(name);
27719
+ }
27720
+ function isChildExecutedTool(name) {
27721
+ return isConnectorTool(name) || isChildInternalTool(name);
27722
+ }
27518
27723
  function isConnectorWriteTool(name) {
27519
- if (!name.startsWith(CONNECTOR_NS_PREFIX2)) return false;
27520
- const sep2 = name.indexOf("__", CONNECTOR_NS_PREFIX2.length);
27521
- if (sep2 <= CONNECTOR_NS_PREFIX2.length) return true;
27522
- const server = name.slice(CONNECTOR_NS_PREFIX2.length, sep2);
27523
- const words = connectorNameWords(name.slice(sep2 + "__".length));
27724
+ if (!isConnectorTool(name)) return false;
27725
+ const sep3 = name.indexOf("__", CONNECTOR_NS_PREFIX2.length);
27726
+ if (sep3 <= CONNECTOR_NS_PREFIX2.length) return true;
27727
+ const server = name.slice(CONNECTOR_NS_PREFIX2.length, sep3);
27728
+ const words = connectorNameWords(name.slice(sep3 + "__".length));
27524
27729
  const serverWords = connectorNameWords(server);
27525
27730
  let skipped = 0;
27526
27731
  while (skipped < serverWords.length && words[skipped] === serverWords[skipped] && !CONNECTOR_MUTATION_WORDS.has(words[skipped])) skipped++;
@@ -27543,11 +27748,11 @@ function connectorWriteDenyHook() {
27543
27748
  return async (input) => {
27544
27749
  try {
27545
27750
  if (input.hook_event_name !== "PreToolUse") return { continue: true };
27751
+ if (typeof input.tool_name !== "string") return connectorWriteDenyOutput("<unknown>");
27546
27752
  if (!isConnectorWriteTool(input.tool_name)) return { continue: true };
27547
- return connectorWriteDenyOutput(String(input.tool_name));
27753
+ return connectorWriteDenyOutput(input.tool_name);
27548
27754
  } catch {
27549
- const toolName = typeof input?.tool_name === "string" ? input.tool_name : "<unknown>";
27550
- return connectorWriteDenyOutput(toolName);
27755
+ return connectorWriteDenyOutput(safeToolNameFrom(input));
27551
27756
  }
27552
27757
  };
27553
27758
  }
@@ -27560,14 +27765,66 @@ function connectorWriteDenyOutput(toolName) {
27560
27765
  }
27561
27766
  };
27562
27767
  }
27768
+ function isAllowlistedConnectorSessionTool(name) {
27769
+ return name.startsWith(MCP_TOOL_PREFIX) || name.startsWith(CONNECTOR_NS_PREFIX2) || CONNECTOR_DISCOVERY_TOOL_NAMES.has(name);
27770
+ }
27771
+ function connectorBuiltinAllowlistHook() {
27772
+ return async (input) => {
27773
+ try {
27774
+ if (input.hook_event_name !== "PreToolUse") return { continue: true };
27775
+ if (typeof input.tool_name !== "string") return allowlistDenyOutput("<unknown>");
27776
+ if (isAllowlistedConnectorSessionTool(input.tool_name)) return { continue: true };
27777
+ return allowlistDenyOutput(input.tool_name);
27778
+ } catch {
27779
+ return allowlistDenyOutput(safeToolNameFrom(input));
27780
+ }
27781
+ };
27782
+ }
27783
+ function safeToolNameFrom(input) {
27784
+ try {
27785
+ const candidate = input?.tool_name;
27786
+ return typeof candidate === "string" ? candidate : "<unknown>";
27787
+ } catch {
27788
+ return "<unknown>";
27789
+ }
27790
+ }
27791
+ function allowlistDenyOutput(toolName) {
27792
+ return {
27793
+ hookSpecificOutput: {
27794
+ hookEventName: "PreToolUse",
27795
+ permissionDecision: "deny",
27796
+ permissionDecisionReason: `Tool "${toolName}" is not available in this connector session. Only bridged custom tools, claude.ai connector tools, and tool discovery are permitted here.`
27797
+ }
27798
+ };
27799
+ }
27800
+ function denyAllToolsHook() {
27801
+ return async (input) => {
27802
+ try {
27803
+ if (input.hook_event_name !== "PreToolUse") return { continue: true };
27804
+ return denyAllOutput(typeof input.tool_name === "string" ? input.tool_name : "<unknown>");
27805
+ } catch {
27806
+ return denyAllOutput("<unknown>");
27807
+ }
27808
+ };
27809
+ }
27810
+ function denyAllOutput(toolName) {
27811
+ return {
27812
+ hookSpecificOutput: {
27813
+ hookEventName: "PreToolUse",
27814
+ permissionDecision: "deny",
27815
+ permissionDecisionReason: `Tool "${toolName}" is not available: this session executes no tools.`
27816
+ }
27817
+ };
27818
+ }
27563
27819
  function connectorQueryOptions(connectorsEnabled, writeMode = "deny") {
27564
27820
  const isolation = toolIsolationForQuery(connectorsEnabled, writeMode);
27565
- if (!connectorsEnabled || writeMode === "allow") return isolation;
27566
- return { ...isolation, hooks: { PreToolUse: [{ hooks: [connectorWriteDenyHook()] }] } };
27821
+ if (!connectorsEnabled) return isolation;
27822
+ const hooks = writeMode === "allow" ? [connectorBuiltinAllowlistHook()] : [connectorBuiltinAllowlistHook(), connectorWriteDenyHook()];
27823
+ return { ...isolation, hooks: { PreToolUse: [{ hooks }] } };
27567
27824
  }
27568
27825
  function toolIsolationForQuery(connectorsEnabled, writeMode = "deny") {
27569
27826
  if (!connectorsEnabled) return CLAUDE_BRIDGE_TOOL_ISOLATION;
27570
- const disallowedTools = DISALLOWED_BUILTIN_TOOLS.filter((t) => !CONNECTOR_DISCOVERY_TOOLS.includes(t));
27827
+ const disallowedTools = DISALLOWED_BUILTIN_TOOLS.filter((t) => !CONNECTOR_DISCOVERY_TOOL_NAMES.has(t));
27571
27828
  if (writeMode !== "allow") disallowedTools.push(...CONNECTOR_WRITE_TOOLS);
27572
27829
  return {
27573
27830
  disallowedTools,
@@ -27596,7 +27853,7 @@ function connectorDeclarationsDisabled(env = process.env) {
27596
27853
  }
27597
27854
 
27598
27855
  // src/convert.ts
27599
- var PROVIDER_ID = "claude-bridge";
27856
+ var PROVIDER_ID = "pi-claude";
27600
27857
  var PI_TO_SDK_TOOL_NAME = {
27601
27858
  read: "Read",
27602
27859
  write: "Write",
@@ -27700,15 +27957,7 @@ function convertPiMessages(messages, customToolNameToSdk) {
27700
27957
  if (toolMessages.length === 0) return;
27701
27958
  anthropicMessages.push({
27702
27959
  role: "user",
27703
- content: toolMessages.map((toolMsg) => {
27704
- const content = toolResultContentToAnthropic(toolMsg.content);
27705
- return {
27706
- type: "tool_result",
27707
- tool_use_id: sanitizeToolId(toolMsg.toolCallId, sanitizedIds),
27708
- content: content || "",
27709
- is_error: toolMsg.isError
27710
- };
27711
- })
27960
+ content: toolMessages.map((toolMsg) => toolResultToAnthropicBlock(toolMsg, sanitizedIds))
27712
27961
  });
27713
27962
  };
27714
27963
  for (let i = 0; i < messages.length; i++) {
@@ -27867,7 +28116,36 @@ function extractAllToolResults(messages) {
27867
28116
  return { results, stopIdx };
27868
28117
  }
27869
28118
 
28119
+ // src/request-lane.ts
28120
+ import { AsyncLocalStorage } from "node:async_hooks";
28121
+ var REQUEST_LANE_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.request-lane.v1");
28122
+ function requestLaneStorage() {
28123
+ const host = globalThis;
28124
+ let storage = host[REQUEST_LANE_SYMBOL];
28125
+ if (!storage) {
28126
+ storage = new AsyncLocalStorage();
28127
+ host[REQUEST_LANE_SYMBOL] = storage;
28128
+ }
28129
+ return storage;
28130
+ }
28131
+ function runInRequestLane(sessionId, callback) {
28132
+ const storage = requestLaneStorage();
28133
+ if (sessionId !== void 0) return storage.run(sessionId, callback);
28134
+ return storage.getStore() === void 0 ? callback() : storage.exit(callback);
28135
+ }
28136
+ function currentRequestLaneId() {
28137
+ return requestLaneStorage().getStore();
28138
+ }
28139
+
27870
28140
  // src/query-state.ts
28141
+ function summarizeDroppedUserMessages(site, dropped) {
28142
+ return {
28143
+ site,
28144
+ count: dropped.length,
28145
+ textLengths: dropped.map((message) => message.text.length),
28146
+ imageOnlyCount: dropped.filter((message) => !message.text && message.blocks?.length).length
28147
+ };
28148
+ }
27871
28149
  var DRAIN_CAUSE_TEXT = {
27872
28150
  "abort": "the turn was aborted",
27873
28151
  "stream-idle-timeout": "the Claude Code stream went idle and the turn timed out",
@@ -27892,6 +28170,49 @@ function drainPendingToolCalls(queryCtx, cause) {
27892
28170
  queryCtx.pendingToolCalls.clear();
27893
28171
  return drained;
27894
28172
  }
28173
+ function strandedToolCallResult() {
28174
+ return {
28175
+ content: [{ type: "text", text: "Claude bridge: this tool call was never forwarded to Pi before its turn ended, so it did not execute and no result can arrive. Re-run the tool." }],
28176
+ isError: true
28177
+ };
28178
+ }
28179
+ function failStrandedToolCall(queryCtx, id) {
28180
+ if (queryCtx.forwardedToolCallIds.has(id)) return false;
28181
+ const pending = queryCtx.pendingToolCalls.get(id);
28182
+ if (!pending) return false;
28183
+ queryCtx.pendingToolCalls.delete(id);
28184
+ queryCtx.deadToolCallIds.add(id);
28185
+ pending.resolve(strandedToolCallResult());
28186
+ return true;
28187
+ }
28188
+ function drainStrandedToolCalls(queryCtx) {
28189
+ const stranded = [];
28190
+ for (const [id, pending] of queryCtx.pendingToolCalls) {
28191
+ if (pending.generation >= queryCtx.callbackGeneration) continue;
28192
+ if (queryCtx.forwardedToolCallIds.has(id)) continue;
28193
+ stranded.push({ id, toolName: pending.toolName });
28194
+ }
28195
+ for (const { id } of stranded) {
28196
+ const pending = queryCtx.pendingToolCalls.get(id);
28197
+ queryCtx.pendingToolCalls.delete(id);
28198
+ queryCtx.deadToolCallIds.add(id);
28199
+ pending.resolve(strandedToolCallResult());
28200
+ }
28201
+ return stranded;
28202
+ }
28203
+ function takeQueuedOrParkedResult(queryCtx, id) {
28204
+ const queued = queryCtx.pendingResults.get(id);
28205
+ if (queued !== void 0) {
28206
+ queryCtx.pendingResults.delete(id);
28207
+ return queued;
28208
+ }
28209
+ const parked = queryCtx.reapedResults.get(id);
28210
+ if (parked !== void 0) {
28211
+ queryCtx.reapedResults.delete(id);
28212
+ return parked;
28213
+ }
28214
+ return void 0;
28215
+ }
27895
28216
  function normalizeForCompare(value) {
27896
28217
  if (Array.isArray(value)) return value.map(normalizeForCompare);
27897
28218
  if (value && typeof value === "object") {
@@ -27930,6 +28251,33 @@ var QueryContext = class {
27930
28251
  latestCursor = 0;
27931
28252
  pendingToolCalls = /* @__PURE__ */ new Map();
27932
28253
  pendingResults = /* @__PURE__ */ new Map();
28254
+ /** Results a message-boundary reap moved OUT of pendingResults so they stop
28255
+ * poisoning mismatch reports, kept CONSUMABLE for a handler that fires later.
28256
+ * The 2026-08-17 deadlock session showed the reap's "no consumer will ever
28257
+ * come" assumption failing routinely: Pi delivers a turn's results in one
28258
+ * callback while the SDK staggers handler invocations past the next message
28259
+ * boundary. Query-scoped, bounded by the query's tool-call count. */
28260
+ reapedResults = /* @__PURE__ */ new Map();
28261
+ /** Every tool-call id this query has handed to Pi inside an ENDED turn — the
28262
+ * set endToolUseTurn stamps from the turn's content. A forwarded id is one Pi
28263
+ * will execute and answer; it must never be emitted again (a lagging stream
28264
+ * replays the same tool_use into the NEXT turn, and per-message turnBlocks
28265
+ * dedup cannot see across turns — vstack#1469's duplicate executions), and a
28266
+ * handler waiting on it must be left waiting at the stranded-handler drains.
28267
+ * Query-scoped, never reset per message. */
28268
+ forwardedToolCallIds = /* @__PURE__ */ new Set();
28269
+ /** Ids whose waiting handler was resolved with strandedToolCallResult. The
28270
+ * model has been told these calls failed; forwarding one later would execute
28271
+ * it behind the model's back, so every forward path skips them. */
28272
+ deadToolCallIds = /* @__PURE__ */ new Set();
28273
+ /** Streamed block indexes suppressed as duplicate or dead tool_use blocks —
28274
+ * their deltas and stops must be ignored the same way child-executed indexes
28275
+ * are. Per message; reset by resetToolTracking. */
28276
+ suppressedStreamIndexes = /* @__PURE__ */ new Set();
28277
+ /** Bumped at every provider callback for this query. Stamped onto handlers at
28278
+ * registration so the stranded-handler drain can tell "registered before this
28279
+ * callback, provably settled" from "racing this callback's own stream". */
28280
+ callbackGeneration = 0;
27933
28281
  turnToolCallIds = [];
27934
28282
  turnToolCalls = [];
27935
28283
  /**
@@ -27942,6 +28290,12 @@ var QueryContext = class {
27942
28290
  * showed. Bounded by the number of tool calls in one query.
27943
28291
  */
27944
28292
  queryToolNames = /* @__PURE__ */ new Map();
28293
+ /** id → last-known arguments, query-scoped like queryToolNames and for the
28294
+ * same reason: a late handler firing after resetToolTracking wiped the
28295
+ * per-message records must still be able to exact-match the parked/queued
28296
+ * result of ITS OWN call — without stored args the only fallback is
28297
+ * sole-same-name, which can hand it a LIVE sibling's id (vstack#1469). */
28298
+ queryToolArgs = /* @__PURE__ */ new Map();
27945
28299
  claimedToolCallIds = /* @__PURE__ */ new Set();
27946
28300
  deliveredToolResultIds = /* @__PURE__ */ new Set();
27947
28301
  resolvedToolResultIds = /* @__PURE__ */ new Set();
@@ -27949,18 +28303,33 @@ var QueryContext = class {
27949
28303
  reportedToolResultMismatch = false;
27950
28304
  deferredUserMessages = [];
27951
28305
  handledTerminalError = false;
28306
+ // Once visible text/thinking, a complete tool call, or a child-executed
28307
+ // CONNECTOR dispatch reaches Pi, the request must never be replayed on
28308
+ // another account (duplicate side effects). Query-scoped, not per-turn:
28309
+ // resetTurnState must not clear it.
28310
+ committedOutput = false;
28311
+ /** True when this query holds NO claim on the module-level shared session
28312
+ * record: a reentrant (subagent) query, or a foreign-conversation one-shot
28313
+ * (vstack#1001). Every shared-record mutation reachable from this context —
28314
+ * reportToolResultMismatch's needsRebuild/forceRotate mark, the cursor
28315
+ * advances on the tool-result-delivery and orphaned-result paths — must
28316
+ * no-op so the PARENT's record stays untouched. Assigned at fresh-query
28317
+ * setup; deliberately NOT cleared at query end, so a late orphaned tool
28318
+ * result arriving after this query settled is still attributed to it. */
28319
+ detachedFromSharedSession = false;
27952
28320
  /** Armed grace timer for ending a tool_use turn whose terminal stream events
27953
28321
  * (message_delta/message_stop) never arrive. The normal path ends the turn at
27954
28322
  * message_stop, AFTER message_delta delivered the real output-token count;
27955
28323
  * this is the deadlock backstop for streams that go silent instead. Managed
27956
28324
  * by schedule/cancelToolUseTurnEnd in assistant-stream.ts. */
27957
28325
  scheduledToolUseEnd = null;
27958
- // Tool calls the CHILD executes itself (claude.ai connectors — see
27959
- // isChildExecutedTool). Deliberately NOT in turnToolCalls/turnToolCallIds:
27960
- // those track calls Pi owes a result for, and Pi owes nothing here. Kept only
27961
- // so the child's real result can be recognized when it comes back on the SDK's
27962
- // `user` message, and so the streamed block's deltas can be skipped silently
27963
- // instead of logging as "unmatched" (which reads like a bug).
28326
+ // Tool calls the CHILD executes itself (see isChildExecutedTool).
28327
+ // Deliberately NOT in turnToolCalls/turnToolCallIds: those track calls Pi
28328
+ // owes a result for, and Pi owes nothing here. CONNECTORS ONLY — kept so the
28329
+ // child's real result can be recognized when it comes back on the SDK's
28330
+ // `user` message and audited. A child-internal built-in (ToolSearch et al.)
28331
+ // never enters this map: its result needs no recognition and no audit, only
28332
+ // its streamed deltas need skipping (childExecutedStreamIndexes below).
27964
28333
  /** tool_use id → raw SDK tool name. */
27965
28334
  childExecutedToolCalls = /* @__PURE__ */ new Map();
27966
28335
  /**
@@ -28072,11 +28441,19 @@ var QueryContext = class {
28072
28441
  this.reportedToolResultMismatch = false;
28073
28442
  this.childExecutedToolCalls.clear();
28074
28443
  this.childExecutedStreamIndexes.clear();
28444
+ this.suppressedStreamIndexes.clear();
28075
28445
  }
28076
28446
  /** Note a tool_use the child runs itself. `streamIndex` is present only on the
28077
- * streamed path, where later deltas/stops for that block must be skipped. */
28447
+ * streamed path, where later deltas/stops for that block must be skipped
28448
+ * that skip applies to every child-executed call. Result recognition and the
28449
+ * connector-call audit apply to CONNECTORS only: a child-internal built-in
28450
+ * (ToolSearch et al.) is tool plumbing, not account-data access, so nothing
28451
+ * about it belongs in the audit trail and no result needs matching. */
28078
28452
  noteChildExecutedToolCall(id, rawName, streamIndex) {
28079
- if (id) {
28453
+ if (isConnectorTool(rawName)) {
28454
+ this.markOutputCommitted();
28455
+ }
28456
+ if (id && isConnectorTool(rawName)) {
28080
28457
  this.childExecutedToolCalls.set(id, rawName);
28081
28458
  if (!this.connectorCallAudit.has(id)) {
28082
28459
  this.connectorCallAudit.set(id, {
@@ -28091,6 +28468,7 @@ var QueryContext = class {
28091
28468
  recordToolCall(id, toolName, args = {}) {
28092
28469
  if (!id) return;
28093
28470
  this.queryToolNames.set(id, toolName);
28471
+ this.queryToolArgs.set(id, args);
28094
28472
  if (!this.turnToolCallIds.includes(id)) this.turnToolCallIds.push(id);
28095
28473
  const existing = this.turnToolCalls.find((call) => call.id === id);
28096
28474
  if (existing) {
@@ -28102,16 +28480,32 @@ var QueryContext = class {
28102
28480
  }
28103
28481
  updateToolCallArgs(id, args) {
28104
28482
  if (!id) return;
28483
+ this.queryToolArgs.set(id, args);
28105
28484
  const existing = this.turnToolCalls.find((call) => call.id === id);
28106
28485
  if (existing) existing.arguments = args;
28107
28486
  }
28108
28487
  hasRecordedToolCall(id) {
28109
28488
  return Boolean(id && (this.turnToolCallIds.includes(id) || this.turnToolCalls.some((call) => call.id === id)));
28110
28489
  }
28490
+ markOutputCommitted() {
28491
+ this.committedOutput = true;
28492
+ }
28111
28493
  claimToolCall(toolName, args = {}) {
28112
28494
  const unclaimed = this.turnToolCalls.filter((call) => !this.claimedToolCallIds.has(call.id));
28113
28495
  const byName = unclaimed.filter((call) => call.toolName === toolName);
28114
28496
  const exact = byName.filter((call) => sameArgs(call.arguments, args));
28497
+ const resultBacked = [.../* @__PURE__ */ new Set([...this.pendingResults.keys(), ...this.reapedResults.keys()])].filter((id) => !this.claimedToolCallIds.has(id) && this.queryToolNames.get(id) === toolName);
28498
+ const backedExact = resultBacked.filter((id) => sameArgs(this.queryToolArgs.get(id), args));
28499
+ const claimBacked = (id, viaExact) => {
28500
+ this.claimedToolCallIds.add(id);
28501
+ return {
28502
+ toolCallId: id,
28503
+ match: viaExact ? "tool-args" : "tool-name",
28504
+ ambiguous: viaExact && backedExact.length > 1,
28505
+ available: unclaimed.length,
28506
+ ...!viaExact && hasRecordedArgs(this.queryToolArgs.get(id)) ? { argsMismatch: true } : {}
28507
+ };
28508
+ };
28115
28509
  let chosen;
28116
28510
  let match = "none";
28117
28511
  let ambiguous = false;
@@ -28120,33 +28514,38 @@ var QueryContext = class {
28120
28514
  chosen = exact[0];
28121
28515
  match = "tool-args";
28122
28516
  ambiguous = exact.length > 1;
28517
+ } else if (backedExact.length > 0) {
28518
+ return claimBacked(backedExact[0], true);
28123
28519
  } else if (byName.length === 1) {
28124
28520
  chosen = byName[0];
28125
28521
  match = "tool-name";
28126
28522
  argsMismatch = hasRecordedArgs(byName[0].arguments);
28127
28523
  }
28524
+ if (!chosen && resultBacked.length === 1) return claimBacked(resultBacked[0], false);
28128
28525
  if (!chosen) return { match: "none", ambiguous: false, available: unclaimed.length };
28129
28526
  this.claimedToolCallIds.add(chosen.id);
28130
28527
  return { toolCallId: chosen.id, match, ambiguous, available: unclaimed.length, ...argsMismatch ? { argsMismatch } : {} };
28131
28528
  }
28132
28529
  /**
28133
- * Drain results still queued in `pendingResults` and report what was dropped.
28530
+ * Move results still queued in `pendingResults` into the parked store and
28531
+ * report what moved.
28134
28532
  *
28135
28533
  * Called at a child MESSAGE boundary (message_start / the no-stream-events
28136
- * assistant fallback): by then the child has necessarily received every tool
28137
- * result for the previous message a handler that matched resolved its result
28138
- * directly or from this queue, and one that never matched already returned an
28139
- * error. Whatever is still queued therefore belongs to a call whose handler
28140
- * gave up, and no consumer will ever come for it. Left in place, each entry
28141
- * poisons every later mismatch report for the whole query (queued>0 with 0/0
28142
- * counters and no tool names) and forces a session rebuild per turn.
28534
+ * assistant fallback). Left in pendingResults, each entry poisons every later
28535
+ * mismatch report for the whole query (queued>0 with 0/0 counters and no tool
28536
+ * names) and forces a session rebuild per turn. But the boundary does NOT
28537
+ * prove the handler gave up the SDK staggers handler invocations, and the
28538
+ * 2026-08-17 deadlock session (vstack#1469) had three of five parallel
28539
+ * handlers fire after this reap destroyed their results. So the reap parks
28540
+ * instead of dropping: reports stay clean, and a late handler still gets its
28541
+ * real result through takeQueuedOrParkedResult.
28143
28542
  */
28144
28543
  takeStaleQueuedResults() {
28145
28544
  if (this.pendingResults.size === 0) return [];
28146
- const stale = [...this.pendingResults.keys()].map((id) => ({
28147
- id,
28148
- toolName: this.queryToolNames.get(id) ?? "unknown"
28149
- }));
28545
+ const stale = [...this.pendingResults.entries()].map(([id, result]) => {
28546
+ this.reapedResults.set(id, result);
28547
+ return { id, toolName: this.queryToolNames.get(id) ?? "unknown" };
28548
+ });
28150
28549
  this.pendingResults.clear();
28151
28550
  return stale;
28152
28551
  }
@@ -28202,109 +28601,68 @@ var QueryContext = class {
28202
28601
  };
28203
28602
  }
28204
28603
  };
28205
- var _ctx = new QueryContext();
28206
- var contextStack = [];
28604
+ var QUERY_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.query-lanes.v1");
28605
+ function queryLaneStore() {
28606
+ const host = globalThis;
28607
+ let store = host[QUERY_LANES_SYMBOL];
28608
+ if (!store) {
28609
+ store = {
28610
+ defaultLane: { current: new QueryContext(), stack: [] },
28611
+ sessionLanes: /* @__PURE__ */ new Map()
28612
+ };
28613
+ host[QUERY_LANES_SYMBOL] = store;
28614
+ }
28615
+ return store;
28616
+ }
28617
+ function lane() {
28618
+ const store = queryLaneStore();
28619
+ const sessionId = currentRequestLaneId();
28620
+ if (sessionId === void 0) return store.defaultLane;
28621
+ let state = store.sessionLanes.get(sessionId);
28622
+ if (!state) {
28623
+ state = { current: new QueryContext(), stack: [] };
28624
+ store.sessionLanes.set(sessionId, state);
28625
+ }
28626
+ return state;
28627
+ }
28207
28628
  function ctx() {
28208
- return _ctx;
28629
+ return lane().current;
28209
28630
  }
28210
28631
  function stackDepth() {
28211
- return contextStack.length;
28632
+ return lane().stack.length;
28212
28633
  }
28213
28634
  function pushContext() {
28214
- if (!_ctx.activeQuery) throw new Error("pushContext() called with no active query");
28215
- contextStack.push(_ctx);
28216
- _ctx = new QueryContext();
28635
+ const state = lane();
28636
+ if (!state.current.activeQuery) throw new Error("pushContext() called with no active query");
28637
+ state.stack.push(state.current);
28638
+ state.current = new QueryContext();
28217
28639
  }
28218
28640
  function popContext() {
28219
- if (contextStack.length === 0) throw new Error("popContext() called with empty stack");
28220
- const parent = contextStack[contextStack.length - 1];
28221
- parent.deferredUserMessages.push(..._ctx.deferredUserMessages);
28222
- _ctx = contextStack.pop();
28641
+ const state = lane();
28642
+ if (state.stack.length === 0) throw new Error("popContext() called with empty stack");
28643
+ const parent = state.stack[state.stack.length - 1];
28644
+ parent.deferredUserMessages.push(...state.current.deferredUserMessages);
28645
+ state.current = state.stack.pop();
28223
28646
  }
28224
28647
  function popContextFor(target) {
28225
- if (_ctx === target) {
28648
+ const state = lane();
28649
+ if (state.current === target) {
28226
28650
  popContext();
28227
28651
  return true;
28228
28652
  }
28229
- const idx = contextStack.indexOf(target);
28653
+ const idx = state.stack.indexOf(target);
28230
28654
  if (idx < 0) return false;
28231
- const parent = idx > 0 ? contextStack[idx - 1] : void 0;
28655
+ const parent = idx > 0 ? state.stack[idx - 1] : void 0;
28232
28656
  parent?.deferredUserMessages.push(...target.deferredUserMessages);
28233
- contextStack.splice(idx, 1);
28657
+ state.stack.splice(idx, 1);
28234
28658
  return true;
28235
28659
  }
28236
-
28237
- // src/debug.ts
28238
- import { appendFileSync as appendFileSync2, chmodSync, mkdirSync as mkdirSync2 } from "fs";
28239
- import { dirname as dirname2, join as join3 } from "path";
28240
- var DEBUG = process.env.CLAUDE_BRIDGE_DEBUG === "1";
28241
- var DEBUG_LOG_PATH = process.env.CLAUDE_BRIDGE_DEBUG_PATH || join3(piUserDir(), "claude-bridge.log");
28242
- function diagLogPath() {
28243
- return process.env.CLAUDE_BRIDGE_DIAG_PATH || join3(piUserDir(), "claude-bridge-diag.log");
28244
- }
28245
- if (DEBUG) {
28246
- try {
28247
- mkdirSync2(dirname2(DEBUG_LOG_PATH), { recursive: true });
28248
- mkdirSync2(dirname2(diagLogPath()), { recursive: true, mode: 448 });
28249
- } catch {
28250
- }
28251
- }
28252
- var moduleInstanceId = Math.random().toString(36).slice(2, 8);
28253
- function debug(...args) {
28254
- if (!DEBUG) return;
28255
- const ts2 = (/* @__PURE__ */ new Date()).toISOString();
28256
- const fmt = (a) => {
28257
- if (typeof a === "string") return a;
28258
- if (a instanceof Error) return `${a.name}: ${a.message}${a.stack ? "\n" + a.stack : ""}`;
28259
- return JSON.stringify(a);
28260
- };
28261
- const msg = args.map(fmt).join(" ");
28262
- try {
28263
- appendFileSync2(DEBUG_LOG_PATH, `[${ts2}] [${moduleInstanceId}] ${msg}
28264
- `);
28265
- } catch {
28266
- }
28267
- }
28268
- var nextCliDebugSeq = 1;
28269
- function makeCliDebugOptions(tag) {
28270
- if (!DEBUG) return {};
28271
- const seq = nextCliDebugSeq++;
28272
- const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28273
- const logDir = join3(dirname2(DEBUG_LOG_PATH), "cc-cli-logs");
28274
- try {
28275
- mkdirSync2(logDir, { recursive: true });
28276
- } catch {
28277
- }
28278
- const debugFile = join3(logDir, `${ts2}-${tag}-${seq}.log`);
28279
- debug(`cli-debug: ${tag} #${seq} \u2192 ${debugFile}`);
28280
- return {
28281
- debug: true,
28282
- debugFile,
28283
- stderr: (data) => {
28284
- for (const line of data.split(/\r?\n/)) {
28285
- if (line) debug(`[cli-stderr ${tag}#${seq}] ${line}`);
28286
- }
28287
- }
28288
- };
28289
- }
28290
- function diagDump(label, data) {
28291
- try {
28292
- const ts2 = (/* @__PURE__ */ new Date()).toISOString();
28293
- const entry = { ts: ts2, moduleInstanceId, label, ...data };
28294
- const path = diagLogPath();
28295
- try {
28296
- mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
28297
- } catch {
28298
- }
28299
- appendFileSync2(path, JSON.stringify(entry) + "\n", { mode: 384 });
28300
- try {
28301
- chmodSync(path, 384);
28302
- } catch {
28303
- }
28304
- debug(`DIAG: ${label} (see ${path})`);
28305
- } catch (error51) {
28306
- debug(`DIAG FAILED: ${label}`, error51);
28307
- }
28660
+ function deleteQueryLane(sessionId) {
28661
+ const store = queryLaneStore();
28662
+ if (sessionId === void 0) {
28663
+ store.defaultLane.current = new QueryContext();
28664
+ store.defaultLane.stack.length = 0;
28665
+ } else store.sessionLanes.delete(sessionId);
28308
28666
  }
28309
28667
 
28310
28668
  // src/tool-pairing-audit.ts
@@ -28321,6 +28679,44 @@ function toolResultIds(content) {
28321
28679
  }
28322
28680
  return ids;
28323
28681
  }
28682
+ function recoverLaterToolResults(messages) {
28683
+ const recovered = [];
28684
+ for (let i = 0; i < messages.length; i++) {
28685
+ const assistant = messages[i];
28686
+ if (assistant?.role !== "assistant") continue;
28687
+ const uses = toolUses(assistant.content);
28688
+ if (uses.length === 0) continue;
28689
+ const target = messages[i + 1];
28690
+ if (target?.role !== "user") continue;
28691
+ const present = toolResultIds(target.content);
28692
+ const missing = uses.filter((use2) => !present.has(use2.id));
28693
+ if (missing.length === 0) continue;
28694
+ for (const use2 of missing) {
28695
+ let sourceBlock;
28696
+ let sourceUserIndex = -1;
28697
+ for (let j2 = i + 2; j2 < messages.length; j2++) {
28698
+ const candidate = messages[j2];
28699
+ if (candidate?.role !== "user") continue;
28700
+ sourceBlock = contentBlocks(candidate.content).find(
28701
+ (block) => block.type === "tool_result" && block.tool_use_id === use2.id
28702
+ );
28703
+ if (sourceBlock) {
28704
+ sourceUserIndex = j2;
28705
+ break;
28706
+ }
28707
+ }
28708
+ if (!sourceBlock) continue;
28709
+ const targetBlocks = Array.isArray(target.content) ? target.content : typeof target.content === "string" && target.content ? [{ type: "text", text: target.content }] : [];
28710
+ let insertAt = 0;
28711
+ while (insertAt < targetBlocks.length && targetBlocks[insertAt]?.type === "tool_result") insertAt++;
28712
+ targetBlocks.splice(insertAt, 0, { ...sourceBlock });
28713
+ target.content = targetBlocks;
28714
+ present.add(use2.id);
28715
+ recovered.push({ id: use2.id, assistantIndex: i, sourceUserIndex, targetUserIndex: i + 1 });
28716
+ }
28717
+ }
28718
+ return recovered;
28719
+ }
28324
28720
  function findUnpairedToolUses(messages) {
28325
28721
  const missing = [];
28326
28722
  for (let i = 0; i < messages.length; i++) {
@@ -28368,11 +28764,62 @@ function summarizeMissingToolNames(missing) {
28368
28764
  }
28369
28765
 
28370
28766
  // src/bridge-state.ts
28371
- var sharedSession = null;
28767
+ var SHARED_SESSION_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.shared-session-lanes.v1");
28768
+ function sharedSessionLaneStore() {
28769
+ const host = globalThis;
28770
+ let store = host[SHARED_SESSION_LANES_SYMBOL];
28771
+ if (!store) {
28772
+ store = { defaultSession: null, sessions: /* @__PURE__ */ new Map() };
28773
+ host[SHARED_SESSION_LANES_SYMBOL] = store;
28774
+ }
28775
+ return store;
28776
+ }
28372
28777
  var extensionApi;
28373
28778
  var piUI;
28779
+ function getSharedSession() {
28780
+ const store = sharedSessionLaneStore();
28781
+ const sessionId = currentRequestLaneId();
28782
+ return sessionId === void 0 ? store.defaultSession : store.sessions.get(sessionId) ?? null;
28783
+ }
28374
28784
  function setSharedSession(next) {
28375
- sharedSession = next;
28785
+ const store = sharedSessionLaneStore();
28786
+ const sessionId = currentRequestLaneId();
28787
+ if (sessionId === void 0) store.defaultSession = next;
28788
+ else store.sessions.set(sessionId, next);
28789
+ }
28790
+ function deleteSharedSessionLane(sessionId) {
28791
+ const store = sharedSessionLaneStore();
28792
+ if (sessionId === void 0) store.defaultSession = null;
28793
+ else store.sessions.delete(sessionId);
28794
+ }
28795
+ function clearSharedSessionLanes() {
28796
+ const store = sharedSessionLaneStore();
28797
+ store.sessions.clear();
28798
+ store.defaultSession = null;
28799
+ }
28800
+ var STARTED_LANES_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.started-lanes.v1");
28801
+ function startedLaneStore() {
28802
+ const host = globalThis;
28803
+ let store = host[STARTED_LANES_SYMBOL];
28804
+ if (!store) {
28805
+ store = /* @__PURE__ */ new WeakMap();
28806
+ host[STARTED_LANES_SYMBOL] = store;
28807
+ }
28808
+ return store;
28809
+ }
28810
+ function recordStartedLane(sessionManager, sessionId) {
28811
+ startedLaneStore().set(sessionManager, sessionId);
28812
+ }
28813
+ function takeStartedLane(sessionManager) {
28814
+ const store = startedLaneStore();
28815
+ const sessionId = store.get(sessionManager);
28816
+ store.delete(sessionManager);
28817
+ return sessionId;
28818
+ }
28819
+ function markSessionForRebuild(opts = {}) {
28820
+ const sharedSession = getSharedSession();
28821
+ if (!sharedSession) return;
28822
+ setSharedSession({ ...sharedSession, needsRebuild: true, ...opts.forceRotate ? { forceRotate: true } : {} });
28376
28823
  }
28377
28824
  function setExtensionApi(next) {
28378
28825
  extensionApi = next;
@@ -28428,7 +28875,7 @@ function reportSyntheticToolResultRepair(missing, context) {
28428
28875
  sampledToolCallIds: sampledToolCallIds.slice(0, 12)
28429
28876
  });
28430
28877
  safeNotify(
28431
- `Claude bridge: ${missing.length} missing tool result(s) repaired with an explicit error placeholder${toolNameSummary.length ? ` for ${toolNameSummary.join(", ")}` : ""}. Real tool output was lost before Claude session import; see ${diagLogPath()}.`,
28878
+ `Claude bridge: ${missing.length} missing tool result(s) repaired with an explicit error placeholder${toolNameSummary.length ? ` for ${toolNameSummary.join(", ")}` : ""}. Real tool output was lost before Claude session import; ${diagGuidance()}.`,
28432
28879
  "error"
28433
28880
  );
28434
28881
  } catch (error51) {
@@ -28442,15 +28889,21 @@ function reportToolResultMismatch(queryCtx, reason, cwd, opts = {}) {
28442
28889
  const hasMismatch = progress.expectedCount > 0 ? progress.unresolvedIds.length > 0 || progress.waitingCount > 0 || progress.queuedCount > 0 || progress.unmatchedResultCount > 0 : progress.waitingCount > 0 || progress.queuedCount > 0 || progress.unmatchedResultCount > 0;
28443
28890
  if (!hasMismatch) return false;
28444
28891
  queryCtx.reportedToolResultMismatch = true;
28445
- if (sharedSession) {
28446
- sharedSession = { ...sharedSession, needsRebuild: true, ...opts.forceRotate ? { forceRotate: true } : {} };
28447
- }
28892
+ if (!queryCtx.detachedFromSharedSession) markSessionForRebuild(opts);
28893
+ if (opts.expectedInterruption) {
28894
+ debug(
28895
+ `tool result delivery interrupted as expected during ${reason}; delivered=${progress.deliveredCount}/${progress.expectedCount} resolved=${progress.resolvedCount}/${progress.expectedCount} waiting=${progress.waitingCount} queued=${progress.queuedCount}`
28896
+ );
28897
+ return true;
28898
+ }
28448
28899
  const toolNameSummary = compactToolNameSummary(progress.toolNames);
28900
+ const sharedSession = getSharedSession();
28449
28901
  diagDump("tool_result_delivery_mismatch", {
28450
28902
  reason,
28451
28903
  cwd,
28452
28904
  progress,
28453
28905
  activeQueryExists: queryCtx.activeQuery !== null,
28906
+ detachedFromSharedSession: queryCtx.detachedFromSharedSession,
28454
28907
  sharedSession: sharedSession ? {
28455
28908
  sessionId: sharedSession.sessionId.slice(0, 8),
28456
28909
  cursor: sharedSession.cursor,
@@ -28469,7 +28922,7 @@ function reportToolResultMismatch(queryCtx, reason, cwd, opts = {}) {
28469
28922
  unmatchedResultIds: progress.unmatchedResultIds
28470
28923
  });
28471
28924
  safeNotify(
28472
- `Claude bridge: tool result delivery interrupted during ${reason}; delivered ${progress.deliveredCount}/${progress.expectedCount}, resolved ${progress.resolvedCount}/${progress.expectedCount}, waiting=${progress.waitingCount}, queued=${progress.queuedCount}, unmatched=${progress.unmatchedResultCount}${toolNameSummary.length ? `, tools=${toolNameSummary.join(", ")}` : ""}. Claude session will rebuild before the next turn; see ${diagLogPath()}.`,
28925
+ `Claude bridge: tool result delivery interrupted during ${reason}; delivered ${progress.deliveredCount}/${progress.expectedCount}, resolved ${progress.resolvedCount}/${progress.expectedCount}, waiting=${progress.waitingCount}, queued=${progress.queuedCount}, unmatched=${progress.unmatchedResultCount}${toolNameSummary.length ? `, tools=${toolNameSummary.join(", ")}` : ""}. ` + (queryCtx.detachedFromSharedSession ? `Detached one-shot query \u2014 shared Claude session record left untouched; ${diagGuidance()}.` : `Claude session will rebuild before the next turn; ${diagGuidance()}.`),
28473
28926
  "error"
28474
28927
  );
28475
28928
  return true;
@@ -28480,10 +28933,16 @@ function reportToolResultMismatch(queryCtx, reason, cwd, opts = {}) {
28480
28933
  }
28481
28934
  function __testSetBridgeIntegrityState(state) {
28482
28935
  if ("ui" in state) piUI = state.ui;
28483
- if ("sharedSession" in state) sharedSession = state.sharedSession ?? null;
28936
+ if ("sharedSession" in state) {
28937
+ if (currentRequestLaneId() !== void 0) setSharedSession(state.sharedSession ?? null);
28938
+ else {
28939
+ clearSharedSessionLanes();
28940
+ sharedSessionLaneStore().defaultSession = state.sharedSession ?? null;
28941
+ }
28942
+ }
28484
28943
  }
28485
28944
  function __testGetBridgeIntegrityState() {
28486
- return { sharedSession };
28945
+ return { sharedSession: getSharedSession() };
28487
28946
  }
28488
28947
 
28489
28948
  // src/connector-audit.ts
@@ -28622,16 +29081,16 @@ function claudeAuthSourceLabel(env = process.env) {
28622
29081
  if (env.ANTHROPIC_AUTH_TOKEN?.trim()) return "ANTHROPIC_AUTH_TOKEN";
28623
29082
  return "Claude Code login";
28624
29083
  }
28625
- function buildNativeProvider(piAi2, models, streamSimple, env = process.env) {
29084
+ function buildNativeProvider(piAi2, models, streamSimple, env = process.env, hasCredentials = () => hasClaudeCredentials(env)) {
28626
29085
  if (!supportsNativeProvider(piAi2)) throw new Error(NATIVE_PROVIDER_UNSUPPORTED_MESSAGE);
28627
- const stamped = models.map((model) => ({ api: "claude-bridge", baseUrl: "claude-bridge", provider: PROVIDER_ID, ...model }));
29086
+ const stamped = models.map((model) => ({ ...model, api: "claude-bridge", baseUrl: "claude-bridge", provider: PROVIDER_ID }));
28628
29087
  const streams = {
28629
29088
  stream: streamSimple,
28630
29089
  streamSimple
28631
29090
  };
28632
29091
  return piAi2.createProvider({
28633
29092
  id: PROVIDER_ID,
28634
- name: "Claude (Claude Code)",
29093
+ name: "Pi Claude",
28635
29094
  baseUrl: "claude-bridge",
28636
29095
  auth: {
28637
29096
  apiKey: {
@@ -28639,8 +29098,8 @@ function buildNativeProvider(piAi2, models, streamSimple, env = process.env) {
28639
29098
  // check() exists so pi's availability pass never has to call
28640
29099
  // resolve(): both are existence-only, but check is the documented
28641
29100
  // side-effect-free probe.
28642
- check: async () => hasClaudeCredentials(env) ? { type: "api_key", source: claudeAuthSourceLabel(env) } : void 0,
28643
- resolve: async () => hasClaudeCredentials(env) ? { auth: { apiKey: "not-used" }, source: claudeAuthSourceLabel(env) } : void 0
29101
+ check: async () => hasCredentials() ? { type: "api_key", source: claudeAuthSourceLabel(env) } : void 0,
29102
+ resolve: async () => hasCredentials() ? { auth: { apiKey: "not-used" }, source: claudeAuthSourceLabel(env) } : void 0
28644
29103
  }
28645
29104
  },
28646
29105
  models: stamped,
@@ -28648,177 +29107,6 @@ function buildNativeProvider(piAi2, models, streamSimple, env = process.env) {
28648
29107
  });
28649
29108
  }
28650
29109
 
28651
- // src/agents-md.ts
28652
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
28653
- import { dirname as dirname3, join as join5, resolve as resolve3 } from "path";
28654
- function globalAgentsPath() {
28655
- return join5(piUserDir(), "AGENTS.md");
28656
- }
28657
- function resolveAgentsMdPath() {
28658
- if (isolatedFromEnv()) return void 0;
28659
- const fromCwd = findAgentsMdInParents(process.cwd());
28660
- if (fromCwd) return fromCwd;
28661
- const globalPath = globalAgentsPath();
28662
- if (existsSync4(globalPath)) return globalPath;
28663
- return void 0;
28664
- }
28665
- function findAgentsMdInParents(startDir) {
28666
- let current = resolve3(startDir);
28667
- while (true) {
28668
- const candidate = join5(current, "AGENTS.md");
28669
- if (existsSync4(candidate)) return candidate;
28670
- const parent = dirname3(current);
28671
- if (parent === current) break;
28672
- current = parent;
28673
- }
28674
- return void 0;
28675
- }
28676
- function extractAgentsAppend() {
28677
- const agentsPath = resolveAgentsMdPath();
28678
- if (!agentsPath) return void 0;
28679
- try {
28680
- const content = readFileSync4(agentsPath, "utf-8").trim();
28681
- if (!content) return void 0;
28682
- const sanitized = sanitizeAgentsContent(content);
28683
- return sanitized.length > 0 ? `# CLAUDE.md
28684
-
28685
- ${sanitized}` : void 0;
28686
- } catch {
28687
- return void 0;
28688
- }
28689
- }
28690
- function sanitizeAgentsContent(content) {
28691
- let sanitized = content;
28692
- sanitized = sanitized.replace(/~\/\.pi\b/gi, "~/.claude");
28693
- sanitized = sanitized.replace(/(^|[\s'"`])\.pi\//g, "$1.claude/");
28694
- sanitized = sanitized.replace(/\b\.pi\b/gi, ".claude");
28695
- sanitized = sanitized.replace(/\bpi\b/gi, "environment");
28696
- return sanitized;
28697
- }
28698
-
28699
- // src/prompt-context.ts
28700
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
28701
- import { dirname as dirname4, join as join6, resolve as resolve4 } from "path";
28702
- function readTrimmed(path) {
28703
- try {
28704
- if (!existsSync5(path)) return void 0;
28705
- const content = readFileSync5(path, "utf8").trim();
28706
- return content.length > 0 ? content : void 0;
28707
- } catch {
28708
- return void 0;
28709
- }
28710
- }
28711
- function findProjectAppendSystem(startDir) {
28712
- let current = resolve4(startDir);
28713
- while (true) {
28714
- const candidate = join6(current, ".pi", "APPEND_SYSTEM.md");
28715
- if (existsSync5(candidate)) return candidate;
28716
- const parent = dirname4(current);
28717
- if (parent === current) break;
28718
- current = parent;
28719
- }
28720
- return void 0;
28721
- }
28722
- function readAppendSystemPromptFiles(cwd) {
28723
- const files = [
28724
- { label: "global APPEND_SYSTEM.md", path: join6(piUserDir(), "APPEND_SYSTEM.md") }
28725
- ];
28726
- const projectPath = isolatedFromEnv() ? void 0 : findProjectAppendSystem(cwd);
28727
- if (projectPath) files.push({ label: "project .pi/APPEND_SYSTEM.md", path: projectPath });
28728
- const seen = /* @__PURE__ */ new Set();
28729
- const output = [];
28730
- for (const file2 of files) {
28731
- if (seen.has(file2.path)) continue;
28732
- seen.add(file2.path);
28733
- const content = readTrimmed(file2.path);
28734
- if (content) output.push({ label: file2.label, content });
28735
- }
28736
- return output;
28737
- }
28738
- function splitPromptBlocks(systemPrompt) {
28739
- return (systemPrompt ?? "").split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
28740
- }
28741
- function extractHeadingSection(systemPrompt, headings) {
28742
- if (!systemPrompt) return void 0;
28743
- let start = -1;
28744
- for (const heading of headings) {
28745
- const index = systemPrompt.indexOf(heading);
28746
- if (index >= 0 && (start < 0 || index < start)) start = index;
28747
- }
28748
- if (start < 0) return void 0;
28749
- const rest = systemPrompt.slice(start).trim();
28750
- const endCandidates = [
28751
- rest.slice(1).search(/\n##\s+/),
28752
- rest.search(/\n<\/project_instructions>/),
28753
- rest.search(/\n<\/project_context>/)
28754
- ].map((index, offset) => index >= 0 && offset === 0 ? index + 1 : index).filter((index) => index >= 0);
28755
- const end = endCandidates.length > 0 ? Math.min(...endCandidates) : -1;
28756
- return (end >= 0 ? rest.slice(0, end) : rest).trim();
28757
- }
28758
- function extractBlockByMarkers(systemPrompt, markers) {
28759
- for (const block of splitPromptBlocks(systemPrompt)) {
28760
- if (markers.some((marker) => marker.test(block))) return block;
28761
- }
28762
- return void 0;
28763
- }
28764
- function buildPromptContextAppend(systemPrompt, cwd, settings) {
28765
- const parts = [];
28766
- const labels = [];
28767
- if (settings.includeAppendSystemPromptMd) {
28768
- for (const file2 of readAppendSystemPromptFiles(cwd)) {
28769
- parts.push(xmlBlock("append_system_prompt", { label: file2.label }, file2.content));
28770
- labels.push(file2.label);
28771
- }
28772
- }
28773
- if (settings.includeProjectAgentsHook) {
28774
- const projectAgents = extractHeadingSection(systemPrompt, ["## Project Agents", "## Project Subagents"]);
28775
- if (projectAgents) {
28776
- parts.push(xmlBlock("before_agent_start", { source: "project-agents" }, projectAgents));
28777
- labels.push("project agents hook");
28778
- }
28779
- }
28780
- if (settings.includeTaskPanelHook) {
28781
- const taskReminder = extractBlockByMarkers(systemPrompt, [/^Task workflow reminder:/]);
28782
- if (taskReminder) {
28783
- parts.push(xmlBlock("before_agent_start", { source: "task-panel" }, taskReminder));
28784
- labels.push("task panel hook");
28785
- }
28786
- }
28787
- if (settings.includeCavemanHook) {
28788
- const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
28789
- if (caveman) {
28790
- parts.push(xmlBlock("before_agent_start", { source: "caveman" }, caveman));
28791
- labels.push("caveman hook");
28792
- }
28793
- }
28794
- if (parts.length === 0) return { labels };
28795
- return {
28796
- labels,
28797
- text: xmlBlock(
28798
- "forwarded_pi_context",
28799
- {},
28800
- [
28801
- "The following content was explicitly enabled in pi-claude-bridge settings and comes from Pi prompt files or before_agent_start prompt hooks.",
28802
- ...parts
28803
- ].join("\n\n"),
28804
- false
28805
- )
28806
- };
28807
- }
28808
- function escapeXmlAttr(value) {
28809
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28810
- }
28811
- function escapeXmlText(value) {
28812
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
28813
- }
28814
- function xmlBlock(tag, attrs, content, escapeContent = true) {
28815
- const attrText = Object.entries(attrs).map(([key, value]) => ` ${key}="${escapeXmlAttr(value)}"`).join("");
28816
- const body = escapeContent ? escapeXmlText(content.trim()) : content.trim();
28817
- return `<${tag}${attrText}>
28818
- ${body}
28819
- </${tag}>`;
28820
- }
28821
-
28822
29110
  // node_modules/zod/v4/classic/external.js
28823
29111
  var external_exports = {};
28824
29112
  __export(external_exports, {
@@ -30254,8 +30542,8 @@ function prettifyError(error51) {
30254
30542
  }
30255
30543
 
30256
30544
  // node_modules/zod/v4/core/parse.js
30257
- var _parse = (_Err) => (schema, value, _ctx2, _params) => {
30258
- const ctx2 = _ctx2 ? { ..._ctx2, async: false } : { async: false };
30545
+ var _parse = (_Err) => (schema, value, _ctx, _params) => {
30546
+ const ctx2 = _ctx ? { ..._ctx, async: false } : { async: false };
30259
30547
  const result = schema._zod.run({ value, issues: [] }, ctx2);
30260
30548
  if (result instanceof Promise) {
30261
30549
  throw new $ZodAsyncError();
@@ -30268,8 +30556,8 @@ var _parse = (_Err) => (schema, value, _ctx2, _params) => {
30268
30556
  return result.value;
30269
30557
  };
30270
30558
  var parse2 = /* @__PURE__ */ _parse($ZodRealError);
30271
- var _parseAsync = (_Err) => async (schema, value, _ctx2, params) => {
30272
- const ctx2 = _ctx2 ? { ..._ctx2, async: true } : { async: true };
30559
+ var _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
30560
+ const ctx2 = _ctx ? { ..._ctx, async: true } : { async: true };
30273
30561
  let result = schema._zod.run({ value, issues: [] }, ctx2);
30274
30562
  if (result instanceof Promise)
30275
30563
  result = await result;
@@ -30281,8 +30569,8 @@ var _parseAsync = (_Err) => async (schema, value, _ctx2, params) => {
30281
30569
  return result.value;
30282
30570
  };
30283
30571
  var parseAsync = /* @__PURE__ */ _parseAsync($ZodRealError);
30284
- var _safeParse = (_Err) => (schema, value, _ctx2) => {
30285
- const ctx2 = _ctx2 ? { ..._ctx2, async: false } : { async: false };
30572
+ var _safeParse = (_Err) => (schema, value, _ctx) => {
30573
+ const ctx2 = _ctx ? { ..._ctx, async: false } : { async: false };
30286
30574
  const result = schema._zod.run({ value, issues: [] }, ctx2);
30287
30575
  if (result instanceof Promise) {
30288
30576
  throw new $ZodAsyncError();
@@ -30293,8 +30581,8 @@ var _safeParse = (_Err) => (schema, value, _ctx2) => {
30293
30581
  } : { success: true, data: result.value };
30294
30582
  };
30295
30583
  var safeParse = /* @__PURE__ */ _safeParse($ZodRealError);
30296
- var _safeParseAsync = (_Err) => async (schema, value, _ctx2) => {
30297
- const ctx2 = _ctx2 ? { ..._ctx2, async: true } : { async: true };
30584
+ var _safeParseAsync = (_Err) => async (schema, value, _ctx) => {
30585
+ const ctx2 = _ctx ? { ..._ctx, async: true } : { async: true };
30298
30586
  let result = schema._zod.run({ value, issues: [] }, ctx2);
30299
30587
  if (result instanceof Promise)
30300
30588
  result = await result;
@@ -30304,40 +30592,40 @@ var _safeParseAsync = (_Err) => async (schema, value, _ctx2) => {
30304
30592
  } : { success: true, data: result.value };
30305
30593
  };
30306
30594
  var safeParseAsync = /* @__PURE__ */ _safeParseAsync($ZodRealError);
30307
- var _encode = (_Err) => (schema, value, _ctx2) => {
30308
- const ctx2 = _ctx2 ? { ..._ctx2, direction: "backward" } : { direction: "backward" };
30595
+ var _encode = (_Err) => (schema, value, _ctx) => {
30596
+ const ctx2 = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
30309
30597
  return _parse(_Err)(schema, value, ctx2);
30310
30598
  };
30311
30599
  var encode = /* @__PURE__ */ _encode($ZodRealError);
30312
- var _decode = (_Err) => (schema, value, _ctx2) => {
30313
- return _parse(_Err)(schema, value, _ctx2);
30600
+ var _decode = (_Err) => (schema, value, _ctx) => {
30601
+ return _parse(_Err)(schema, value, _ctx);
30314
30602
  };
30315
30603
  var decode = /* @__PURE__ */ _decode($ZodRealError);
30316
- var _encodeAsync = (_Err) => async (schema, value, _ctx2) => {
30317
- const ctx2 = _ctx2 ? { ..._ctx2, direction: "backward" } : { direction: "backward" };
30604
+ var _encodeAsync = (_Err) => async (schema, value, _ctx) => {
30605
+ const ctx2 = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
30318
30606
  return _parseAsync(_Err)(schema, value, ctx2);
30319
30607
  };
30320
30608
  var encodeAsync = /* @__PURE__ */ _encodeAsync($ZodRealError);
30321
- var _decodeAsync = (_Err) => async (schema, value, _ctx2) => {
30322
- return _parseAsync(_Err)(schema, value, _ctx2);
30609
+ var _decodeAsync = (_Err) => async (schema, value, _ctx) => {
30610
+ return _parseAsync(_Err)(schema, value, _ctx);
30323
30611
  };
30324
30612
  var decodeAsync = /* @__PURE__ */ _decodeAsync($ZodRealError);
30325
- var _safeEncode = (_Err) => (schema, value, _ctx2) => {
30326
- const ctx2 = _ctx2 ? { ..._ctx2, direction: "backward" } : { direction: "backward" };
30613
+ var _safeEncode = (_Err) => (schema, value, _ctx) => {
30614
+ const ctx2 = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
30327
30615
  return _safeParse(_Err)(schema, value, ctx2);
30328
30616
  };
30329
30617
  var safeEncode = /* @__PURE__ */ _safeEncode($ZodRealError);
30330
- var _safeDecode = (_Err) => (schema, value, _ctx2) => {
30331
- return _safeParse(_Err)(schema, value, _ctx2);
30618
+ var _safeDecode = (_Err) => (schema, value, _ctx) => {
30619
+ return _safeParse(_Err)(schema, value, _ctx);
30332
30620
  };
30333
30621
  var safeDecode = /* @__PURE__ */ _safeDecode($ZodRealError);
30334
- var _safeEncodeAsync = (_Err) => async (schema, value, _ctx2) => {
30335
- const ctx2 = _ctx2 ? { ..._ctx2, direction: "backward" } : { direction: "backward" };
30622
+ var _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
30623
+ const ctx2 = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
30336
30624
  return _safeParseAsync(_Err)(schema, value, ctx2);
30337
30625
  };
30338
30626
  var safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync($ZodRealError);
30339
- var _safeDecodeAsync = (_Err) => async (schema, value, _ctx2) => {
30340
- return _safeParseAsync(_Err)(schema, value, _ctx2);
30627
+ var _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
30628
+ return _safeParseAsync(_Err)(schema, value, _ctx);
30341
30629
  };
30342
30630
  var safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync($ZodRealError);
30343
30631
 
@@ -31531,7 +31819,7 @@ var $ZodCustomStringFormat = /* @__PURE__ */ $constructor("$ZodCustomStringForma
31531
31819
  var $ZodNumber = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => {
31532
31820
  $ZodType.init(inst, def);
31533
31821
  inst._zod.pattern = inst._zod.bag.pattern ?? number;
31534
- inst._zod.parse = (payload, _ctx2) => {
31822
+ inst._zod.parse = (payload, _ctx) => {
31535
31823
  if (def.coerce)
31536
31824
  try {
31537
31825
  payload.value = Number(payload.value);
@@ -31559,7 +31847,7 @@ var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, d
31559
31847
  var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
31560
31848
  $ZodType.init(inst, def);
31561
31849
  inst._zod.pattern = boolean;
31562
- inst._zod.parse = (payload, _ctx2) => {
31850
+ inst._zod.parse = (payload, _ctx) => {
31563
31851
  if (def.coerce)
31564
31852
  try {
31565
31853
  payload.value = Boolean(payload.value);
@@ -31580,7 +31868,7 @@ var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
31580
31868
  var $ZodBigInt = /* @__PURE__ */ $constructor("$ZodBigInt", (inst, def) => {
31581
31869
  $ZodType.init(inst, def);
31582
31870
  inst._zod.pattern = bigint;
31583
- inst._zod.parse = (payload, _ctx2) => {
31871
+ inst._zod.parse = (payload, _ctx) => {
31584
31872
  if (def.coerce)
31585
31873
  try {
31586
31874
  payload.value = BigInt(payload.value);
@@ -31603,7 +31891,7 @@ var $ZodBigIntFormat = /* @__PURE__ */ $constructor("$ZodBigIntFormat", (inst, d
31603
31891
  });
31604
31892
  var $ZodSymbol = /* @__PURE__ */ $constructor("$ZodSymbol", (inst, def) => {
31605
31893
  $ZodType.init(inst, def);
31606
- inst._zod.parse = (payload, _ctx2) => {
31894
+ inst._zod.parse = (payload, _ctx) => {
31607
31895
  const input = payload.value;
31608
31896
  if (typeof input === "symbol")
31609
31897
  return payload;
@@ -31620,7 +31908,7 @@ var $ZodUndefined = /* @__PURE__ */ $constructor("$ZodUndefined", (inst, def) =>
31620
31908
  $ZodType.init(inst, def);
31621
31909
  inst._zod.pattern = _undefined;
31622
31910
  inst._zod.values = /* @__PURE__ */ new Set([void 0]);
31623
- inst._zod.parse = (payload, _ctx2) => {
31911
+ inst._zod.parse = (payload, _ctx) => {
31624
31912
  const input = payload.value;
31625
31913
  if (typeof input === "undefined")
31626
31914
  return payload;
@@ -31637,7 +31925,7 @@ var $ZodNull = /* @__PURE__ */ $constructor("$ZodNull", (inst, def) => {
31637
31925
  $ZodType.init(inst, def);
31638
31926
  inst._zod.pattern = _null;
31639
31927
  inst._zod.values = /* @__PURE__ */ new Set([null]);
31640
- inst._zod.parse = (payload, _ctx2) => {
31928
+ inst._zod.parse = (payload, _ctx) => {
31641
31929
  const input = payload.value;
31642
31930
  if (input === null)
31643
31931
  return payload;
@@ -31660,7 +31948,7 @@ var $ZodUnknown = /* @__PURE__ */ $constructor("$ZodUnknown", (inst, def) => {
31660
31948
  });
31661
31949
  var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
31662
31950
  $ZodType.init(inst, def);
31663
- inst._zod.parse = (payload, _ctx2) => {
31951
+ inst._zod.parse = (payload, _ctx) => {
31664
31952
  payload.issues.push({
31665
31953
  expected: "never",
31666
31954
  code: "invalid_type",
@@ -31672,7 +31960,7 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
31672
31960
  });
31673
31961
  var $ZodVoid = /* @__PURE__ */ $constructor("$ZodVoid", (inst, def) => {
31674
31962
  $ZodType.init(inst, def);
31675
- inst._zod.parse = (payload, _ctx2) => {
31963
+ inst._zod.parse = (payload, _ctx) => {
31676
31964
  const input = payload.value;
31677
31965
  if (typeof input === "undefined")
31678
31966
  return payload;
@@ -31687,7 +31975,7 @@ var $ZodVoid = /* @__PURE__ */ $constructor("$ZodVoid", (inst, def) => {
31687
31975
  });
31688
31976
  var $ZodDate = /* @__PURE__ */ $constructor("$ZodDate", (inst, def) => {
31689
31977
  $ZodType.init(inst, def);
31690
- inst._zod.parse = (payload, _ctx2) => {
31978
+ inst._zod.parse = (payload, _ctx) => {
31691
31979
  if (def.coerce) {
31692
31980
  try {
31693
31981
  payload.value = new Date(payload.value);
@@ -32634,7 +32922,7 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
32634
32922
  const valuesSet = new Set(values);
32635
32923
  inst._zod.values = valuesSet;
32636
32924
  inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
32637
- inst._zod.parse = (payload, _ctx2) => {
32925
+ inst._zod.parse = (payload, _ctx) => {
32638
32926
  const input = payload.value;
32639
32927
  if (valuesSet.has(input)) {
32640
32928
  return payload;
@@ -32656,7 +32944,7 @@ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
32656
32944
  const values = new Set(def.values);
32657
32945
  inst._zod.values = values;
32658
32946
  inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
32659
- inst._zod.parse = (payload, _ctx2) => {
32947
+ inst._zod.parse = (payload, _ctx) => {
32660
32948
  const input = payload.value;
32661
32949
  if (values.has(input)) {
32662
32950
  return payload;
@@ -32672,7 +32960,7 @@ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
32672
32960
  });
32673
32961
  var $ZodFile = /* @__PURE__ */ $constructor("$ZodFile", (inst, def) => {
32674
32962
  $ZodType.init(inst, def);
32675
- inst._zod.parse = (payload, _ctx2) => {
32963
+ inst._zod.parse = (payload, _ctx) => {
32676
32964
  const input = payload.value;
32677
32965
  if (input instanceof File)
32678
32966
  return payload;
@@ -32890,7 +33178,7 @@ var $ZodCatch = /* @__PURE__ */ $constructor("$ZodCatch", (inst, def) => {
32890
33178
  });
32891
33179
  var $ZodNaN = /* @__PURE__ */ $constructor("$ZodNaN", (inst, def) => {
32892
33180
  $ZodType.init(inst, def);
32893
- inst._zod.parse = (payload, _ctx2) => {
33181
+ inst._zod.parse = (payload, _ctx) => {
32894
33182
  if (typeof payload.value !== "number" || !Number.isNaN(payload.value)) {
32895
33183
  payload.issues.push({
32896
33184
  input: payload.value,
@@ -33026,7 +33314,7 @@ var $ZodTemplateLiteral = /* @__PURE__ */ $constructor("$ZodTemplateLiteral", (i
33026
33314
  }
33027
33315
  }
33028
33316
  inst._zod.pattern = new RegExp(`^${regexParts.join("")}$`);
33029
- inst._zod.parse = (payload, _ctx2) => {
33317
+ inst._zod.parse = (payload, _ctx) => {
33030
33318
  if (typeof payload.value !== "string") {
33031
33319
  payload.issues.push({
33032
33320
  input: payload.value,
@@ -33080,7 +33368,7 @@ var $ZodFunction = /* @__PURE__ */ $constructor("$ZodFunction", (inst, def) => {
33080
33368
  return result;
33081
33369
  };
33082
33370
  };
33083
- inst._zod.parse = (payload, _ctx2) => {
33371
+ inst._zod.parse = (payload, _ctx) => {
33084
33372
  if (typeof payload.value !== "function") {
33085
33373
  payload.issues.push({
33086
33374
  code: "invalid_type",
@@ -40520,8 +40808,8 @@ function finalize(ctx2, schema) {
40520
40808
  throw new Error("Error converting schema to JSON.");
40521
40809
  }
40522
40810
  }
40523
- function isTransforming(_schema, _ctx2) {
40524
- const ctx2 = _ctx2 ?? { seen: /* @__PURE__ */ new Set() };
40811
+ function isTransforming(_schema, _ctx) {
40812
+ const ctx2 = _ctx ?? { seen: /* @__PURE__ */ new Set() };
40525
40813
  if (ctx2.seen.has(_schema))
40526
40814
  return false;
40527
40815
  ctx2.seen.add(_schema);
@@ -40661,7 +40949,7 @@ var numberProcessor = (schema, ctx2, _json, _params) => {
40661
40949
  if (typeof multipleOf === "number")
40662
40950
  json2.multipleOf = multipleOf;
40663
40951
  };
40664
- var booleanProcessor = (_schema, _ctx2, json2, _params) => {
40952
+ var booleanProcessor = (_schema, _ctx, json2, _params) => {
40665
40953
  json2.type = "boolean";
40666
40954
  };
40667
40955
  var bigintProcessor = (_schema, ctx2, _json, _params) => {
@@ -40693,19 +40981,19 @@ var voidProcessor = (_schema, ctx2, _json, _params) => {
40693
40981
  throw new Error("Void cannot be represented in JSON Schema");
40694
40982
  }
40695
40983
  };
40696
- var neverProcessor = (_schema, _ctx2, json2, _params) => {
40984
+ var neverProcessor = (_schema, _ctx, json2, _params) => {
40697
40985
  json2.not = {};
40698
40986
  };
40699
- var anyProcessor = (_schema, _ctx2, _json, _params) => {
40987
+ var anyProcessor = (_schema, _ctx, _json, _params) => {
40700
40988
  };
40701
- var unknownProcessor = (_schema, _ctx2, _json, _params) => {
40989
+ var unknownProcessor = (_schema, _ctx, _json, _params) => {
40702
40990
  };
40703
40991
  var dateProcessor = (_schema, ctx2, _json, _params) => {
40704
40992
  if (ctx2.unrepresentable === "throw") {
40705
40993
  throw new Error("Date cannot be represented in JSON Schema");
40706
40994
  }
40707
40995
  };
40708
- var enumProcessor = (schema, _ctx2, json2, _params) => {
40996
+ var enumProcessor = (schema, _ctx, json2, _params) => {
40709
40997
  const def = schema._zod.def;
40710
40998
  const values = getEnumValues(def.entries);
40711
40999
  if (values.every((v2) => typeof v2 === "number"))
@@ -40759,7 +41047,7 @@ var nanProcessor = (_schema, ctx2, _json, _params) => {
40759
41047
  throw new Error("NaN cannot be represented in JSON Schema");
40760
41048
  }
40761
41049
  };
40762
- var templateLiteralProcessor = (schema, _ctx2, json2, _params) => {
41050
+ var templateLiteralProcessor = (schema, _ctx, json2, _params) => {
40763
41051
  const _json = json2;
40764
41052
  const pattern = schema._zod.pattern;
40765
41053
  if (!pattern)
@@ -40767,7 +41055,7 @@ var templateLiteralProcessor = (schema, _ctx2, json2, _params) => {
40767
41055
  _json.type = "string";
40768
41056
  _json.pattern = pattern.source;
40769
41057
  };
40770
- var fileProcessor = (schema, _ctx2, json2, _params) => {
41058
+ var fileProcessor = (schema, _ctx, json2, _params) => {
40771
41059
  const _json = json2;
40772
41060
  const file2 = {
40773
41061
  type: "string",
@@ -40791,7 +41079,7 @@ var fileProcessor = (schema, _ctx2, json2, _params) => {
40791
41079
  Object.assign(_json, file2);
40792
41080
  }
40793
41081
  };
40794
- var successProcessor = (_schema, _ctx2, json2, _params) => {
41082
+ var successProcessor = (_schema, _ctx, json2, _params) => {
40795
41083
  json2.type = "boolean";
40796
41084
  };
40797
41085
  var customProcessor = (_schema, ctx2, _json, _params) => {
@@ -42479,8 +42767,8 @@ var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
42479
42767
  $ZodTransform.init(inst, def);
42480
42768
  ZodType.init(inst, def);
42481
42769
  inst._zod.processJSONSchema = (ctx2, json2, params) => transformProcessor(inst, ctx2, json2, params);
42482
- inst._zod.parse = (payload, _ctx2) => {
42483
- if (_ctx2.direction === "backward") {
42770
+ inst._zod.parse = (payload, _ctx) => {
42771
+ if (_ctx.direction === "backward") {
42484
42772
  throw new $ZodEncodeError(inst.constructor.name);
42485
42773
  }
42486
42774
  payload.addIssue = (issue2) => {
@@ -43382,9 +43670,6 @@ function jsonSchemaToZodShape(schema) {
43382
43670
  return shape;
43383
43671
  }
43384
43672
 
43385
- // src/index.ts
43386
- import { readFileSync as nodeReadFileSync } from "node:fs";
43387
-
43388
43673
  // src/pi-ai-compat.ts
43389
43674
  var dynamicImport = (specifier) => import(specifier);
43390
43675
  async function resolveGetModels(root, loadCompat = () => dynamicImport("@earendil-works/pi-ai/compat")) {
@@ -43396,32 +43681,39 @@ async function resolveGetModels(root, loadCompat = () => dynamicImport("@earendi
43396
43681
 
43397
43682
  // src/connector-cache.ts
43398
43683
  import { createHash } from "node:crypto";
43399
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync } from "node:fs";
43400
- import { dirname as dirname5, join as join7 } from "node:path";
43401
- var CACHE_VERSION = 1;
43684
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "node:fs";
43685
+ import { dirname as dirname3, join as join5 } from "node:path";
43686
+ var CACHE_VERSION = 2;
43402
43687
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
43688
+ function scopeKeyFor(claudeConfigDir) {
43689
+ return claudeConfigDir?.trim() || "<default>";
43690
+ }
43403
43691
  function connectorCacheScopeKey(env = process.env) {
43404
- return env.CLAUDE_CONFIG_DIR?.trim() || "<default>";
43692
+ return scopeKeyFor(env.CLAUDE_CONFIG_DIR);
43693
+ }
43694
+ function connectorCacheScopeDigest(scopeKey) {
43695
+ return createHash("sha256").update(scopeKey).digest("hex");
43405
43696
  }
43406
43697
  function connectorCachePath(scopeKey = connectorCacheScopeKey()) {
43407
- const digest = createHash("sha256").update(scopeKey).digest("hex").slice(0, 16);
43408
- return join7(piUserDir(), "connector-cache", `${digest}.json`);
43698
+ const digest = connectorCacheScopeDigest(scopeKey).slice(0, 16);
43699
+ return join5(piUserDir(), "connector-cache", `${digest}.json`);
43409
43700
  }
43410
43701
  function readCachedConnectors(scopeKey = connectorCacheScopeKey(), now = Date.now()) {
43411
43702
  let raw;
43412
43703
  try {
43413
- raw = readFileSync6(connectorCachePath(scopeKey), "utf8");
43704
+ raw = readFileSync4(connectorCachePath(scopeKey), "utf8");
43414
43705
  } catch {
43415
43706
  return void 0;
43416
43707
  }
43417
43708
  let parsed;
43418
43709
  try {
43419
43710
  parsed = JSON.parse(raw);
43420
- } catch {
43711
+ } catch (error51) {
43712
+ debug(`connector-cache: corrupt cache ${connectorCachePath(scopeKey)}:`, error51 instanceof Error ? error51.message : String(error51));
43421
43713
  return void 0;
43422
43714
  }
43423
43715
  if (parsed?.version !== CACHE_VERSION) return void 0;
43424
- if (parsed?.scope !== scopeKey) return void 0;
43716
+ if (parsed?.scope !== connectorCacheScopeDigest(scopeKey)) return void 0;
43425
43717
  const savedAt = typeof parsed?.savedAt === "number" ? parsed.savedAt : 0;
43426
43718
  if (!savedAt || now - savedAt > MAX_AGE_MS || savedAt > now) return void 0;
43427
43719
  if (!Array.isArray(parsed?.connectors)) return void 0;
@@ -43434,26 +43726,112 @@ function writeCachedConnectors(connectors, scopeKey = connectorCacheScopeKey(),
43434
43726
  if (!Array.isArray(connectors) || connectors.length === 0) return false;
43435
43727
  const path = connectorCachePath(scopeKey);
43436
43728
  try {
43437
- mkdirSync3(dirname5(path), { recursive: true, mode: 448 });
43729
+ mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
43438
43730
  writeFileSync(
43439
43731
  path,
43440
- JSON.stringify({ version: CACHE_VERSION, scope: scopeKey, savedAt: now, connectors }),
43732
+ JSON.stringify({ version: CACHE_VERSION, scope: connectorCacheScopeDigest(scopeKey), savedAt: now, connectors }),
43441
43733
  { mode: 384 }
43442
43734
  );
43443
43735
  return true;
43444
- } catch {
43736
+ } catch (error51) {
43737
+ debug(`connector-cache: write failed ${path}:`, error51 instanceof Error ? error51.message : String(error51));
43445
43738
  return false;
43446
43739
  }
43447
43740
  }
43448
43741
 
43742
+ // src/connector-runtime.ts
43743
+ import { readFileSync as nodeReadFileSync } from "node:fs";
43744
+ function readCredentialFile(path) {
43745
+ try {
43746
+ return nodeReadFileSync(path, "utf8");
43747
+ } catch {
43748
+ return void 0;
43749
+ }
43750
+ }
43751
+ var connectorServerCache = /* @__PURE__ */ new Map();
43752
+ var connectorServerPending = /* @__PURE__ */ new Set();
43753
+ var connectorServerFailureAt = /* @__PURE__ */ new Map();
43754
+ var CONNECTOR_PRIME_TIMEOUT_MS = 1e4;
43755
+ var CONNECTOR_PRIME_FAILURE_COOLDOWN_MS = 6e4;
43756
+ function connectorScopeKey(claudeConfigDir = process.env.CLAUDE_CONFIG_DIR) {
43757
+ return scopeKeyFor(claudeConfigDir);
43758
+ }
43759
+ function connectorCredentialEnv(claudeConfigDir = process.env.CLAUDE_CONFIG_DIR) {
43760
+ const env = { ...process.env };
43761
+ if (claudeConfigDir?.trim()) env.CLAUDE_CONFIG_DIR = claudeConfigDir.trim();
43762
+ else delete env.CLAUDE_CONFIG_DIR;
43763
+ return env;
43764
+ }
43765
+ function primeConnectorServers(claudeConfigDir, overrides = {}) {
43766
+ const key = connectorScopeKey(claudeConfigDir);
43767
+ if (connectorServerCache.has(key) || connectorServerPending.has(key)) return;
43768
+ const now = overrides.now ?? Date.now;
43769
+ const cooldownMs = overrides.failureCooldownMs ?? CONNECTOR_PRIME_FAILURE_COOLDOWN_MS;
43770
+ const failedAt = connectorServerFailureAt.get(key);
43771
+ if (failedAt !== void 0 && now() - failedAt < cooldownMs) {
43772
+ debug("connectors: prime cooling down after failure; declaring none until retry window opens");
43773
+ return;
43774
+ }
43775
+ connectorServerPending.add(key);
43776
+ void (async () => {
43777
+ let failed = false;
43778
+ try {
43779
+ const credentials = resolveClaudeOAuth(readCredentialFile, connectorCredentialEnv(claudeConfigDir));
43780
+ if (!credentials) {
43781
+ debug("connectors: no OAuth credentials; declaring none (will retry)");
43782
+ return;
43783
+ }
43784
+ const inventory = await listAccountConnectors({
43785
+ credentials,
43786
+ // A hung inventory request must not outlive the deadline: the abort
43787
+ // surfaces as `ok: false` and takes the cooldown path below.
43788
+ signal: AbortSignal.timeout(overrides.timeoutMs ?? CONNECTOR_PRIME_TIMEOUT_MS)
43789
+ });
43790
+ if (!inventory.ok) {
43791
+ failed = true;
43792
+ debug(`connectors: inventory failed (${inventory.reason}); declaring none (will retry after cooldown)`);
43793
+ return;
43794
+ }
43795
+ const servers = connectorMcpServers(inventory);
43796
+ debug(
43797
+ `connectors: declaring ${Object.keys(servers).length} of ${inventory.connectors.length} installed`,
43798
+ Object.keys(servers).join(", ") || "none"
43799
+ );
43800
+ connectorServerCache.set(key, servers);
43801
+ connectorServerFailureAt.delete(key);
43802
+ if (writeCachedConnectors(inventory.connectors, key)) {
43803
+ debug(`connectors: cached ${inventory.connectors.length} entries`);
43804
+ }
43805
+ } catch (error51) {
43806
+ failed = true;
43807
+ debug("connectors: declaration lookup threw; declaring none (will retry after cooldown)", error51);
43808
+ } finally {
43809
+ if (failed) connectorServerFailureAt.set(key, now());
43810
+ connectorServerPending.delete(key);
43811
+ }
43812
+ })();
43813
+ }
43814
+ function connectorServersSnapshot(claudeConfigDir) {
43815
+ const key = connectorScopeKey(claudeConfigDir);
43816
+ const ready = connectorServerCache.get(key);
43817
+ if (ready) return ready;
43818
+ primeConnectorServers(claudeConfigDir);
43819
+ const cached2 = readCachedConnectors(key);
43820
+ if (!cached2) return {};
43821
+ const servers = connectorMcpServers({ ok: true, complete: true, connectors: cached2 });
43822
+ if (Object.keys(servers).length === 0) return {};
43823
+ debug(`connectors: turn-1 declarations from cache \u2014 ${Object.keys(servers).join(", ")}`);
43824
+ return servers;
43825
+ }
43826
+
43449
43827
  // src/claude-executable.ts
43450
43828
  import { spawn as spawnProcess } from "child_process";
43451
- import { accessSync, constants as fsConstants, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
43452
- import { delimiter, join as join8 } from "path";
43829
+ import { accessSync, constants as fsConstants, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
43830
+ import { delimiter, join as join6 } from "path";
43453
43831
  function executableFromPath(name) {
43454
43832
  const paths = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
43455
43833
  for (const dir of paths) {
43456
- const candidate = join8(dir, name);
43834
+ const candidate = join6(dir, name);
43457
43835
  try {
43458
43836
  accessSync(candidate, fsConstants.X_OK);
43459
43837
  return candidate;
@@ -43569,7 +43947,7 @@ function preflightClaudeExecutable(path, cwd) {
43569
43947
  }
43570
43948
  let fileType;
43571
43949
  try {
43572
- fileType = classifyClaudeExecutableBytes(readFileSync7(realPath).subarray(0, 16));
43950
+ fileType = classifyClaudeExecutableBytes(readFileSync5(realPath).subarray(0, 16));
43573
43951
  } catch (err) {
43574
43952
  throw makeClaudePreflightError("Claude Code executable preflight failed: cannot read executable header before spawning Claude Code.", {
43575
43953
  code: codeValue(err, "EACCES"),
@@ -43662,19 +44040,19 @@ function spawnClaudeCodeWithDiagnostics(options) {
43662
44040
  };
43663
44041
  }
43664
44042
 
43665
- // node_modules/cc-session-io/dist/chunk-D6EZBJOC.js
44043
+ // node_modules/cc-session-io/dist/chunk-7RWUSC7F.js
43666
44044
  import { randomUUID } from "crypto";
43667
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync3, existsSync as existsSync6, rmSync as rmSync2 } from "fs";
43668
- import { dirname as dirname6 } from "path";
43669
- import { readFileSync as readFileSync8 } from "fs";
44045
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync3, existsSync as existsSync4, rmSync as rmSync2 } from "fs";
44046
+ import { dirname as dirname4 } from "path";
44047
+ import { readFileSync as readFileSync6 } from "fs";
43670
44048
  import { realpathSync as realpathSync3 } from "fs";
43671
44049
  import { homedir as homedir3 } from "os";
43672
- import { join as join9 } from "path";
44050
+ import { join as join7 } from "path";
43673
44051
  function parseJsonl(content) {
43674
44052
  return content.split("\n").filter((line) => line.trim()).map(parseRecord);
43675
44053
  }
43676
44054
  function parseJsonlFile(path) {
43677
- return parseJsonl(readFileSync8(path, "utf-8"));
44055
+ return parseJsonl(readFileSync6(path, "utf-8"));
43678
44056
  }
43679
44057
  function parseRecord(line) {
43680
44058
  const raw = JSON.parse(line);
@@ -43687,7 +44065,7 @@ function serializeRecord(record2) {
43687
44065
  }
43688
44066
  var MAX_SANITIZED_LENGTH = 200;
43689
44067
  function getClaudeDir(claudeDir) {
43690
- return claudeDir ?? process.env.CLAUDE_CONFIG_DIR ?? join9(homedir3(), ".claude");
44068
+ return claudeDir ?? process.env.CLAUDE_CONFIG_DIR ?? join7(homedir3(), ".claude");
43691
44069
  }
43692
44070
  function normalizeProjectPath(projectPath) {
43693
44071
  try {
@@ -43705,10 +44083,10 @@ function projectPathToHash(projectPath) {
43705
44083
  return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${Math.abs(h).toString(36)}`;
43706
44084
  }
43707
44085
  function getProjectDir(projectPath, claudeDir) {
43708
- return join9(getClaudeDir(claudeDir), "projects", projectPathToHash(normalizeProjectPath(projectPath)));
44086
+ return join7(getClaudeDir(claudeDir), "projects", projectPathToHash(normalizeProjectPath(projectPath)));
43709
44087
  }
43710
44088
  function getSessionPath(sessionId, projectPath, claudeDir) {
43711
- return join9(getProjectDir(projectPath, claudeDir), `${sessionId}.jsonl`);
44089
+ return join7(getProjectDir(projectPath, claudeDir), `${sessionId}.jsonl`);
43712
44090
  }
43713
44091
  function repairToolPairing(messages) {
43714
44092
  const result = [];
@@ -43920,13 +44298,16 @@ var Session = class {
43920
44298
  this._lastUuid = uuid3;
43921
44299
  return record2;
43922
44300
  }
43923
- /** Add a user text message. Returns its uuid. */
43924
- addUserMessage(text) {
44301
+ /** Add a user message, as plain text or content blocks. Returns its uuid. */
44302
+ addUserMessage(content) {
44303
+ if (Array.isArray(content) && content.length === 0) {
44304
+ throw new Error("addUserMessage: content array is empty; Anthropic rejects empty message content");
44305
+ }
43925
44306
  const base = this.baseFields();
43926
44307
  const record2 = {
43927
44308
  type: "user",
43928
44309
  ...base,
43929
- message: { role: "user", content: text }
44310
+ message: { role: "user", content }
43930
44311
  };
43931
44312
  this._pendingRecords.push(record2);
43932
44313
  return base.uuid;
@@ -44014,16 +44395,17 @@ var Session = class {
44014
44395
  const toolResults = msg.content.filter(
44015
44396
  (b) => b.type === "tool_result"
44016
44397
  );
44398
+ const rest = msg.content.filter(
44399
+ (b) => b.type !== "tool_result"
44400
+ );
44017
44401
  if (toolResults.length > 0) {
44018
44402
  this.addToolResults(toolResults.map((r) => ({
44019
44403
  toolUseId: r.tool_use_id,
44020
44404
  content: r.content,
44021
44405
  isError: r.is_error
44022
44406
  })));
44023
- } else {
44024
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
44025
- this.addUserMessage(text || JSON.stringify(msg.content));
44026
44407
  }
44408
+ if (rest.length > 0) this.addUserMessage(rest);
44027
44409
  }
44028
44410
  }
44029
44411
  }
@@ -44043,8 +44425,8 @@ var Session = class {
44043
44425
  /** Write pending records to disk. Creates the file/directory if needed. */
44044
44426
  save() {
44045
44427
  if (this._pendingRecords.length === 0) return;
44046
- const dir = dirname6(this.jsonlPath);
44047
- if (!existsSync6(dir)) {
44428
+ const dir = dirname4(this.jsonlPath);
44429
+ if (!existsSync4(dir)) {
44048
44430
  mkdirSync4(dir, { recursive: true });
44049
44431
  }
44050
44432
  const data = this._pendingRecords.map((r) => serializeRecord(r) + "\n").join("");
@@ -44183,25 +44565,286 @@ function verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount)
44183
44565
  return warnings;
44184
44566
  }
44185
44567
 
44186
- // src/session-persistence.ts
44187
- var BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
44188
- function fingerprintMessages(messages) {
44189
- const normalized = messages.map((message) => {
44190
- if (message.role === "assistant") {
44191
- return {
44192
- role: message.role,
44193
- provider: message.provider,
44194
- model: message.model,
44195
- content: message.content
44196
- };
44197
- }
44198
- return message;
44199
- });
44200
- return createHash2("sha256").update(JSON.stringify(normalized)).digest("hex");
44568
+ // src/account-router.ts
44569
+ import { homedir as homedir4 } from "node:os";
44570
+ import { join as join8 } from "node:path";
44571
+
44572
+ // src/rate-limit.ts
44573
+ var RATE_LIMIT_AUTO_RESUME_EVENT = "vstack:rate-limit";
44574
+ var RATE_LIMIT_TOKEN = "\x1B[31m[rate-limit]\x1B[39m";
44575
+ var USAGE_LIMIT_PREFIXES = Array.isArray(qO) ? qO : [];
44576
+ function coerceMessageText(value) {
44577
+ if (typeof value === "string") return value;
44578
+ if (value instanceof Error) return value.message;
44579
+ try {
44580
+ return JSON.stringify(value ?? "");
44581
+ } catch {
44582
+ return String(value);
44583
+ }
44201
44584
  }
44202
- function readBuiltSessionContext(sessionManager) {
44203
- const built = typeof sessionManager?.buildSessionContext === "function" ? sessionManager.buildSessionContext() : void 0;
44204
- return Array.isArray(built?.messages) ? built : void 0;
44585
+ function isUsageLimitMessage(value) {
44586
+ const text = coerceMessageText(value);
44587
+ return USAGE_LIMIT_PREFIXES.some((prefix) => text.includes(prefix));
44588
+ }
44589
+ function uniqueNonEmptyLines(values) {
44590
+ const seen = /* @__PURE__ */ new Set();
44591
+ const out = [];
44592
+ for (const value of values) {
44593
+ const text = typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim();
44594
+ if (!text || seen.has(text)) continue;
44595
+ seen.add(text);
44596
+ out.push(text);
44597
+ }
44598
+ return out;
44599
+ }
44600
+ function resetTimestampMs(value) {
44601
+ if (typeof value === "number" && Number.isFinite(value)) {
44602
+ return Math.abs(value) < 1e12 ? value * 1e3 : value;
44603
+ }
44604
+ if (typeof value !== "string" || !value.trim()) return void 0;
44605
+ const numeric = Number(value);
44606
+ if (Number.isFinite(numeric)) return Math.abs(numeric) < 1e12 ? numeric * 1e3 : numeric;
44607
+ const parsed = Date.parse(value);
44608
+ return Number.isFinite(parsed) ? parsed : void 0;
44609
+ }
44610
+ function formatResetTimestamp(value) {
44611
+ const parsed = resetTimestampMs(value);
44612
+ if (parsed === void 0) return "unknown";
44613
+ return new Date(parsed).toLocaleString(void 0, {
44614
+ day: "numeric",
44615
+ hour: "numeric",
44616
+ minute: "2-digit",
44617
+ month: "short",
44618
+ second: "2-digit",
44619
+ timeZoneName: "short",
44620
+ year: "numeric"
44621
+ });
44622
+ }
44623
+ var ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD = 80;
44624
+ function normalizeRateLimitUtilization(value) {
44625
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
44626
+ if (value === 0) return 0;
44627
+ if (value > 0 && value <= 1) return value * 100;
44628
+ if (value > 1 && value <= 100) return value;
44629
+ return void 0;
44630
+ }
44631
+ function rateLimitTypeLabel(value) {
44632
+ const text = typeof value === "string" ? value.trim() : "";
44633
+ return text || "unknown";
44634
+ }
44635
+ function formatAllowedRateLimitWarning(info) {
44636
+ if (info?.status !== "allowed_warning") return void 0;
44637
+ const utilization = normalizeRateLimitUtilization(info.utilization);
44638
+ if (utilization === void 0 || utilization < ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD) return void 0;
44639
+ return `Claude rate limit warning: nearing ${rateLimitTypeLabel(info.rateLimitType)} limit; check Claude Code /usage for exact utilization.`;
44640
+ }
44641
+
44642
+ // src/account-router.ts
44643
+ var CLAUDE_ACCOUNT_ROUTER_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-account-router.v1");
44644
+ var CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.account-host.v1");
44645
+ function resolveClaudeAccountRouter() {
44646
+ const host = globalThis;
44647
+ const candidate = host[CLAUDE_ACCOUNT_ROUTER_SYMBOL];
44648
+ return candidate?.version === 1 ? candidate : void 0;
44649
+ }
44650
+ function safeRouterCall(label, call) {
44651
+ try {
44652
+ call();
44653
+ } catch (error51) {
44654
+ debug(`router callback ${label} threw:`, error51);
44655
+ }
44656
+ }
44657
+ function subscriberProfileEnv(profile, base = process.env) {
44658
+ const env = { ...base };
44659
+ const directOverrides = /* @__PURE__ */ new Set([
44660
+ "ANTHROPIC_API_KEY",
44661
+ "ANTHROPIC_AUTH_TOKEN",
44662
+ "ANTHROPIC_OAUTH_TOKEN",
44663
+ "CLAUDE_CODE_OAUTH_TOKEN",
44664
+ "ANTHROPIC_BASE_URL",
44665
+ "ANTHROPIC_CUSTOM_HEADERS",
44666
+ "ANTHROPIC_AWS_API_KEY",
44667
+ "ANTHROPIC_FOUNDRY_AUTH_TOKEN",
44668
+ "ANTHROPIC_BEDROCK_BASE_URL",
44669
+ "ANTHROPIC_VERTEX_BASE_URL",
44670
+ "ANTHROPIC_FOUNDRY_BASE_URL",
44671
+ "AWS_BEARER_TOKEN_BEDROCK"
44672
+ ]);
44673
+ for (const key of Object.keys(env)) {
44674
+ if (directOverrides.has(key) || key.startsWith("CLAUDE_CODE_USE_")) delete env[key];
44675
+ }
44676
+ if (profile.configDir) env.CLAUDE_CONFIG_DIR = profile.configDir;
44677
+ else delete env.CLAUDE_CONFIG_DIR;
44678
+ return env;
44679
+ }
44680
+ function claudeDirForProfile(profile) {
44681
+ return profile.configDir?.trim() || join8(homedir4(), ".claude");
44682
+ }
44683
+ function accountSessionScope(profile) {
44684
+ return profile ? { accountProfileId: profile.profileId, claudeConfigDir: claudeDirForProfile(profile) } : {};
44685
+ }
44686
+ function commitsVisibleOutput(event) {
44687
+ if (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") {
44688
+ return event.delta.length > 0;
44689
+ }
44690
+ if (event.type === "text_end" || event.type === "thinking_end") return event.content.length > 0;
44691
+ return event.type === "toolcall_end";
44692
+ }
44693
+ var RetryEventBuffer = class {
44694
+ pending = [];
44695
+ committed = false;
44696
+ ended = false;
44697
+ discarded = false;
44698
+ target;
44699
+ onCommit;
44700
+ constructor(target, onCommit) {
44701
+ this.target = target;
44702
+ this.onCommit = onCommit;
44703
+ }
44704
+ push(event) {
44705
+ if (this.discarded) return;
44706
+ if (this.committed) {
44707
+ this.target.push(event);
44708
+ return;
44709
+ }
44710
+ this.pending.push(event);
44711
+ if (commitsVisibleOutput(event) || event.type === "done" || event.type === "error") this.commit();
44712
+ }
44713
+ end() {
44714
+ if (this.discarded) return;
44715
+ this.ended = true;
44716
+ if (this.committed) this.target.end();
44717
+ }
44718
+ commit() {
44719
+ if (this.discarded || this.committed) return;
44720
+ this.committed = true;
44721
+ this.onCommit?.();
44722
+ for (const event of this.pending) this.target.push(event);
44723
+ this.pending.length = 0;
44724
+ if (this.ended) this.target.end();
44725
+ }
44726
+ discard() {
44727
+ if (this.committed) return;
44728
+ this.discarded = true;
44729
+ this.pending.length = 0;
44730
+ }
44731
+ get hasCommittedOutput() {
44732
+ return this.committed;
44733
+ }
44734
+ };
44735
+ function rateLimitTypeFromInfo(info) {
44736
+ return info?.rateLimitType ?? info?.rate_limit_type ?? info?.type;
44737
+ }
44738
+ function rateLimitResetFromInfo(info) {
44739
+ return info?.resetsAt ?? info?.resets_at ?? info?.resetAt ?? info?.reset_at;
44740
+ }
44741
+ function rateLimitResetMs(info) {
44742
+ return resetTimestampMs(rateLimitResetFromInfo(info));
44743
+ }
44744
+ function httpStatusInText(normalized) {
44745
+ const match = /\b(?:http|https|status(?: code)?|error|code)\b[^a-z0-9]{0,4}([45]\d\d)\b/.exec(normalized);
44746
+ return match ? Number(match[1]) : void 0;
44747
+ }
44748
+ function classifyStatusCode(status) {
44749
+ if (status === 401) return "auth";
44750
+ if (status === 403) return "auth";
44751
+ if (status === 402) return "billing";
44752
+ if (status === 429) return "rate-limit";
44753
+ if (status === 529) return "overloaded";
44754
+ if (status >= 500 && status <= 599) return "server";
44755
+ return void 0;
44756
+ }
44757
+ function classifyClaudeFailure(value) {
44758
+ const details = [value];
44759
+ let numericStatus;
44760
+ if (value && typeof value === "object") {
44761
+ const record2 = value;
44762
+ details.push(record2.name, record2.type, record2.message, record2.code, record2.status, record2.statusCode, record2.body, record2.error);
44763
+ for (const field of [record2.status, record2.statusCode]) {
44764
+ if (typeof field === "number" && Number.isInteger(field)) {
44765
+ numericStatus = field;
44766
+ break;
44767
+ }
44768
+ }
44769
+ }
44770
+ const text = details.map((detail) => {
44771
+ if (typeof detail === "string" || typeof detail === "number") return String(detail);
44772
+ try {
44773
+ return JSON.stringify(detail ?? "");
44774
+ } catch {
44775
+ return String(detail);
44776
+ }
44777
+ }).join(" ");
44778
+ const normalized = text.toLowerCase().replace(/[_-]+/g, " ");
44779
+ const statusKind = numericStatus !== void 0 ? classifyStatusCode(numericStatus) : void 0;
44780
+ if (statusKind) return statusKind;
44781
+ if (/authentication (?:failed|error)|permission error|oauth org not allowed|oauth token.*expired|token.*expired|unauthorized|invalid token|login required|please run .*login|not logged in/.test(normalized) || httpStatusInText(normalized) === 401 || httpStatusInText(normalized) === 403) return "auth";
44782
+ if (/extra usage|overage/.test(normalized)) return "rate-limit";
44783
+ if (/billing error|payment|required.*billing|credit balance.*(?:low|insufficient|empty)|insufficient credits/.test(normalized)) return "billing";
44784
+ const quotaInUsageContext = /\bquota\b/.test(normalized) && /\b(?:rate|usage|limits?|requests?|tokens?|messages?|api)\b/.test(normalized);
44785
+ if (/\brate limit|usage limit|session limit|weekly limit|monthly limit|limit reached|you(?:'|’)ve hit your .* limit|too many requests|resets? (?:at )?\d/.test(normalized) || quotaInUsageContext || httpStatusInText(normalized) === 429) return "rate-limit";
44786
+ if (/overloaded|capacity/.test(normalized) || httpStatusInText(normalized) === 529) return "overloaded";
44787
+ const statusInText = httpStatusInText(normalized);
44788
+ if (/server error|internal server/.test(normalized) || statusInText !== void 0 && statusInText >= 500) return "server";
44789
+ if (/network|timeout|timed out|socket|econn|connection closed|fetch failed|unexpected end|\beof\b/.test(normalized)) return "network";
44790
+ return void 0;
44791
+ }
44792
+
44793
+ // src/session-persistence.ts
44794
+ var BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
44795
+ function normalizedMessageText(message) {
44796
+ const content = message.content;
44797
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((block) => block.type === "text" ? block.text ?? "" : "").join("\n") : "";
44798
+ return text.trim();
44799
+ }
44800
+ function shortHash(text) {
44801
+ return createHash2("sha256").update(text).digest("hex").slice(0, 12);
44802
+ }
44803
+ function conversationFingerprint(messages) {
44804
+ const firstUser = messages.find((message) => message.role === "user");
44805
+ if (!firstUser) return void 0;
44806
+ const userText = normalizedMessageText(firstUser);
44807
+ if (!userText) return void 0;
44808
+ const firstAssistant = messages.find((message) => message.role === "assistant");
44809
+ const assistantText = firstAssistant ? normalizedMessageText(firstAssistant) : "";
44810
+ return assistantText ? `u:${shortHash(userText)}|a:${shortHash(assistantText)}` : `u:${shortHash(userText)}`;
44811
+ }
44812
+ function parseConversationFingerprint(fp2) {
44813
+ const match = /^u:([0-9a-f]+)(?:\|a:([0-9a-f]+))?$/.exec(fp2);
44814
+ if (!match) return void 0;
44815
+ return { user: match[1], ...match[2] ? { assistant: match[2] } : {} };
44816
+ }
44817
+ function conversationFingerprintsMatch(recorded, incoming) {
44818
+ const rec = parseConversationFingerprint(recorded);
44819
+ const inc = parseConversationFingerprint(incoming);
44820
+ if (!rec || !inc) return true;
44821
+ if (rec.user !== inc.user) return false;
44822
+ return !(rec.assistant && inc.assistant && rec.assistant !== inc.assistant);
44823
+ }
44824
+ function conversationFingerprintUpgrade(recorded, incoming) {
44825
+ if (!incoming) return void 0;
44826
+ if (!recorded) return incoming;
44827
+ const rec = parseConversationFingerprint(recorded);
44828
+ const inc = parseConversationFingerprint(incoming);
44829
+ return rec && inc && !rec.assistant && inc.assistant && rec.user === inc.user ? incoming : void 0;
44830
+ }
44831
+ function fingerprintMessages(messages) {
44832
+ const normalized = messages.map((message) => {
44833
+ if (message.role === "assistant") {
44834
+ return {
44835
+ role: message.role,
44836
+ provider: message.provider,
44837
+ model: message.model,
44838
+ content: message.content
44839
+ };
44840
+ }
44841
+ return message;
44842
+ });
44843
+ return createHash2("sha256").update(JSON.stringify(normalized)).digest("hex");
44844
+ }
44845
+ function readBuiltSessionContext(sessionManager) {
44846
+ const built = typeof sessionManager?.buildSessionContext === "function" ? sessionManager.buildSessionContext() : void 0;
44847
+ return Array.isArray(built?.messages) ? built : void 0;
44205
44848
  }
44206
44849
  function latestPersistedBridgeSession(sessionManager) {
44207
44850
  const entries = typeof sessionManager?.getEntries === "function" ? sessionManager.getEntries() : [];
@@ -44215,9 +44858,9 @@ function latestPersistedBridgeSession(sessionManager) {
44215
44858
  }
44216
44859
  return void 0;
44217
44860
  }
44218
- function claudeSessionExists(sessionId, cwd) {
44861
+ function claudeSessionExists(sessionId, cwd, claudeDir) {
44219
44862
  try {
44220
- const session = openSession({ sessionId, projectPath: cwd, claudeDir: process.env.CLAUDE_CONFIG_DIR });
44863
+ const session = openSession({ sessionId, projectPath: cwd, claudeDir });
44221
44864
  statSync4(session.jsonlPath);
44222
44865
  return true;
44223
44866
  } catch {
@@ -44260,34 +44903,72 @@ function restoreSharedSessionFromPi(ctx2) {
44260
44903
  debug(`restoreSharedSession: fingerprint mismatch for ${persisted.sessionId.slice(0, 8)}`);
44261
44904
  return;
44262
44905
  }
44263
- if (!claudeSessionExists(persisted.sessionId, persisted.cwd)) {
44906
+ const accountProfileId = typeof persisted.accountProfileId === "string" ? persisted.accountProfileId : void 0;
44907
+ const claudeConfigDir = accountProfileId ? claudeDirForProfile(resolveClaudeAccountRouter()?.resolveProfile?.(accountProfileId) ?? {}) : void 0;
44908
+ if (!claudeSessionExists(persisted.sessionId, persisted.cwd, claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR)) {
44264
44909
  debug(`restoreSharedSession: Claude session missing for ${persisted.sessionId.slice(0, 8)}`);
44265
44910
  return;
44266
44911
  }
44267
- setSharedSession({ sessionId: persisted.sessionId, cursor, cwd: persisted.cwd });
44268
- debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}`);
44912
+ setSharedSession({
44913
+ sessionId: persisted.sessionId,
44914
+ cursor,
44915
+ cwd: persisted.cwd,
44916
+ // Absent on pre-3.1.1 markers: restore as identity-unknown (the foreign
44917
+ // guard fails open) rather than rejecting the entry.
44918
+ ...typeof persisted.conversationFingerprint === "string" ? { conversationFingerprint: persisted.conversationFingerprint } : {},
44919
+ ...accountProfileId ? { accountProfileId, claudeConfigDir } : {}
44920
+ });
44921
+ debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}, account=${accountProfileId ?? "default"}`);
44922
+ }
44923
+ var SCHEDULED_PERSISTENCE_SYMBOL = /* @__PURE__ */ Symbol.for("vstack.pi.claude-bridge.scheduled-persistence.v1");
44924
+ function scheduledPersistenceTimers() {
44925
+ const host = globalThis;
44926
+ let store = host[SCHEDULED_PERSISTENCE_SYMBOL];
44927
+ if (!store) {
44928
+ store = /* @__PURE__ */ new Map();
44929
+ host[SCHEDULED_PERSISTENCE_SYMBOL] = store;
44930
+ }
44931
+ return store;
44932
+ }
44933
+ function cancelScheduledSessionPersistence(sessionManager) {
44934
+ const timers = scheduledPersistenceTimers();
44935
+ const timer = timers.get(sessionManager);
44936
+ if (timer === void 0) return;
44937
+ clearTimeout(timer);
44938
+ timers.delete(sessionManager);
44269
44939
  }
44270
44940
  function schedulePersistSharedSession(ctxLike) {
44941
+ const sharedSession = getSharedSession();
44271
44942
  if (!extensionApi || !sharedSession || !ctxLike?.sessionManager) return;
44272
- const snapshot = { ...sharedSession };
44943
+ const sessionManager = ctxLike.sessionManager;
44944
+ const { claudeConfigDir: _omitted, ...snapshot } = sharedSession;
44945
+ const timers = scheduledPersistenceTimers();
44946
+ const superseded = timers.get(sessionManager);
44947
+ if (superseded !== void 0) clearTimeout(superseded);
44273
44948
  const timer = setTimeout(() => {
44949
+ if (timers.get(sessionManager) === timer) timers.delete(sessionManager);
44274
44950
  try {
44275
- const built = readBuiltSessionContext(ctxLike.sessionManager);
44951
+ const built = readBuiltSessionContext(sessionManager);
44276
44952
  if (!built) return;
44277
44953
  const cursor = Math.max(0, Math.min(snapshot.cursor, built.messages.length));
44278
44954
  const data = {
44279
44955
  ...snapshot,
44280
44956
  cursor,
44281
44957
  fingerprint: fingerprintMessages(built.messages.slice(0, cursor)),
44282
- piSessionId: typeof ctxLike.sessionManager?.getSessionId === "function" ? ctxLike.sessionManager.getSessionId() : void 0,
44958
+ piSessionId: typeof sessionManager?.getSessionId === "function" ? sessionManager.getSessionId() : void 0,
44283
44959
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
44284
44960
  };
44285
44961
  extensionApi?.appendEntry(BRIDGE_SESSION_CUSTOM_TYPE, data);
44286
44962
  debug(`persistSharedSession: saved ${data.sessionId.slice(0, 8)}, cursor=${data.cursor}`);
44287
44963
  } catch (error51) {
44288
- debug("persistSharedSession failed:", error51);
44964
+ diagDump("persist_shared_session_failed", {
44965
+ sessionId: snapshot.sessionId.slice(0, 8),
44966
+ cursor: snapshot.cursor,
44967
+ error: error51 instanceof Error ? `${error51.name}: ${error51.message}` : String(error51)
44968
+ });
44289
44969
  }
44290
44970
  }, 0);
44971
+ timers.set(sessionManager, timer);
44291
44972
  timer.unref?.();
44292
44973
  }
44293
44974
  function convertAndImportMessages(session, messages, customToolNameToSdk, cwd) {
@@ -44305,6 +44986,13 @@ function convertAndImportMessages(session, messages, customToolNameToSdk, cwd) {
44305
44986
  [...sanitizedIds.entries()].map(([orig, clean]) => orig === clean ? orig : `${orig}\u2192${clean}`).join(", ")
44306
44987
  );
44307
44988
  }
44989
+ const recoveredToolResults = recoverLaterToolResults(anthropicMessages);
44990
+ if (recoveredToolResults.length > 0) {
44991
+ debug(
44992
+ `convertAndImportMessages: recovered ${recoveredToolResults.length} later tool result(s) for original parallel batch`,
44993
+ recoveredToolResults.map((item) => item.id).join(", ")
44994
+ );
44995
+ }
44308
44996
  const missingToolResults = findUnpairedToolUses(anthropicMessages);
44309
44997
  if (missingToolResults.length > 0) insertLostToolResultPlaceholders(anthropicMessages, missingToolResults);
44310
44998
  const repaired = repairToolPairing(anthropicMessages);
@@ -44322,17 +45010,37 @@ function convertAndImportMessages(session, messages, customToolNameToSdk, cwd) {
44322
45010
  }
44323
45011
  if (repaired.length) session.importMessages(repaired);
44324
45012
  }
44325
- function verifyWrittenSession2(jsonlPath, expectedSessionId, expectedRecordCount, cwd) {
45013
+ function planIncrementalPromptBatch(messages, cursor) {
45014
+ const lastIndex = messages.length - 1;
45015
+ if (lastIndex < 0 || messages[lastIndex].role !== "user") return void 0;
45016
+ if (cursor > lastIndex) {
45017
+ debug(`planIncrementalPromptBatch: rejected \u2014 cursor=${cursor} beyond last index ${lastIndex}; messages are not the conversation this cursor describes`);
45018
+ return void 0;
45019
+ }
45020
+ const boundedCursor = Math.max(0, cursor);
45021
+ let promptStart = boundedCursor;
45022
+ if (messages[promptStart]?.role === "assistant") promptStart++;
45023
+ const pendingPrompts = messages.slice(promptStart);
45024
+ if (pendingPrompts.length === 0 || pendingPrompts.some((message) => message.role !== "user")) {
45025
+ debug(`planIncrementalPromptBatch: rejected \u2014 cursor=${cursor} promptStart=${promptStart} tail roles=[${messages.slice(boundedCursor).map((m) => m.role).join(", ")}]`);
45026
+ return void 0;
45027
+ }
45028
+ return {
45029
+ promptStart,
45030
+ userMessageCount: pendingPrompts.length
45031
+ };
45032
+ }
45033
+ function verifyWrittenSession2(jsonlPath, expectedSessionId, expectedRecordCount, cwd, claudeDir) {
44326
45034
  const warnings = verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount);
44327
45035
  for (const msg of warnings) {
44328
45036
  debug(`WARNING session verify: ${msg}`);
44329
- piUI?.notify(
45037
+ safeNotify(
44330
45038
  `Session file issue: ${msg}
44331
- cwd=${cwd} realpath=${safeRealpath(cwd)} CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR ?? "(unset)"}
44332
- Please copy and paste this message into a new issue at https://github.com/elidickinson/pi-claude-bridge/issues/new` + (DEBUG ? ` and attach ${DEBUG_LOG_PATH}` : ` (rerun with CLAUDE_BRIDGE_DEBUG=1 to capture a debug log)`),
45039
+ cwd=${displayPath(cwd)} realpath=${displayPath(safeRealpath(cwd))}
45040
+ Please copy and paste this message into a new issue at https://github.com/vanillagreencom/vstack/issues/new` + (DEBUG ? ` and attach ${DEBUG_LOG_PATH}` : ` (rerun with CLAUDE_BRIDGE_DEBUG=1 to capture a debug log)`),
44333
45041
  "warning"
44334
45042
  );
44335
- diagDump("session_verify_fail", { msg, jsonlPath, cwd, realpath: safeRealpath(cwd), claudeConfigDir: process.env.CLAUDE_CONFIG_DIR ?? null });
45043
+ diagDump("session_verify_fail", { msg, jsonlPath, cwd, realpath: safeRealpath(cwd), claudeConfigDir: claudeDir ?? null });
44336
45044
  }
44337
45045
  }
44338
45046
  function safeRealpath(p2) {
@@ -44342,7 +45050,7 @@ function safeRealpath(p2) {
44342
45050
  return `<failed: ${e.message}>`;
44343
45051
  }
44344
45052
  }
44345
- function debugSessionPaths(label, cwd, jsonlPath) {
45053
+ function debugSessionPaths(label, cwd, jsonlPath, claudeDir) {
44346
45054
  const realCwd = safeRealpath(cwd);
44347
45055
  let fileSize = null;
44348
45056
  let fileExists = false;
@@ -44356,54 +45064,89 @@ function debugSessionPaths(label, cwd, jsonlPath) {
44356
45064
  if (realCwd !== cwd) debug(`${label}: realpath(cwd)=${realCwd} (DIFFERS \u2014 symlink-resolved path is what CC SDK uses)`);
44357
45065
  debug(`${label}: jsonlPath=${jsonlPath}`);
44358
45066
  debug(`${label}: fileExists=${fileExists}${fileSize != null ? ` size=${fileSize}` : ""}`);
44359
- debug(`${label}: env.CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR ?? "(unset)"} HOME=${process.env.HOME ?? "(unset)"}`);
45067
+ debug(`${label}: selected.CLAUDE_CONFIG_DIR=${claudeDir ?? "(unset)"} HOME=${process.env.HOME ?? "(unset)"}`);
44360
45068
  }
44361
- function syncSharedSession(messages, cwd, customToolNameToSdk, modelId) {
45069
+ function syncSharedSession(messages, cwd, customToolNameToSdk, modelId, account) {
45070
+ const sharedSession = getSharedSession();
44362
45071
  const priorMessages = messages.slice(0, -1);
44363
- if (sharedSession && !sharedSession.needsRebuild) {
44364
- const missed = priorMessages.slice(sharedSession.cursor);
44365
- const trailingAssistantOnly = missed.length === 1 && missed[0].role === "assistant";
44366
- if (missed.length === 0 || trailingAssistantOnly) {
44367
- if (trailingAssistantOnly) {
44368
- setSharedSession({ ...sharedSession, cursor: priorMessages.length, cwd });
44369
- }
44370
- debug(`Case 3: ${trailingAssistantOnly ? "advanced cursor past trailing assistant, " : ""}resuming session ${sharedSession.sessionId.slice(0, 8)}, cursor=${sharedSession.cursor}`);
44371
- debug(`syncResult: path=reuse sessionId=${sharedSession.sessionId} cursor=${sharedSession.cursor}`);
44372
- return { sessionId: sharedSession.sessionId };
45072
+ const accountProfileId = account?.accountProfileId;
45073
+ const scopeConfigDir = account?.claudeConfigDir;
45074
+ const claudeDir = scopeConfigDir ?? process.env.CLAUDE_CONFIG_DIR;
45075
+ const sameAccount = Boolean(
45076
+ sharedSession && sharedSession.accountProfileId === accountProfileId && sharedSession.claudeConfigDir === scopeConfigDir
45077
+ );
45078
+ const incomingFingerprint = conversationFingerprint(messages);
45079
+ if (sharedSession && !sharedSession.needsRebuild && sharedSession.conversationFingerprint && incomingFingerprint && !conversationFingerprintsMatch(sharedSession.conversationFingerprint, incomingFingerprint) && priorMessages.length <= sharedSession.cursor) {
45080
+ debug(
45081
+ `Case 6 foreign-conversation: fingerprint ${incomingFingerprint.slice(0, 8)} != record ${sharedSession.conversationFingerprint.slice(0, 8)} (cursor=${sharedSession.cursor}, priors=${priorMessages.length}) \u2014 clean one-shot, record untouched`
45082
+ );
45083
+ debug(`syncResult: path=foreign-one-shot`);
45084
+ return { sessionId: null, promptStart: messages.length - 1, foreignContext: true };
45085
+ }
45086
+ if (sharedSession && sameAccount && !sharedSession.needsRebuild) {
45087
+ const batch = planIncrementalPromptBatch(messages, sharedSession.cursor);
45088
+ if (batch) {
45089
+ const cursorBeforeUpdate = sharedSession.cursor;
45090
+ const upgradedFingerprint = conversationFingerprintUpgrade(sharedSession.conversationFingerprint, incomingFingerprint);
45091
+ setSharedSession({
45092
+ ...sharedSession,
45093
+ cursor: batch.promptStart,
45094
+ cwd,
45095
+ ...upgradedFingerprint ? { conversationFingerprint: upgradedFingerprint } : {}
45096
+ });
45097
+ const batching = batch.userMessageCount > 1 ? `batched ${batch.userMessageCount} consecutive user messages, ` : batch.promptStart > cursorBeforeUpdate ? "advanced cursor past trailing assistant, " : "";
45098
+ debug(`Case 3: ${batching}resuming session ${sharedSession.sessionId.slice(0, 8)}, cursor=${batch.promptStart}, account=${accountProfileId ?? "default"}`);
45099
+ debug(`syncResult: path=reuse sessionId=${sharedSession.sessionId} cursor=${batch.promptStart} promptUsers=${batch.userMessageCount}`);
45100
+ return {
45101
+ sessionId: sharedSession.sessionId,
45102
+ promptStart: batch.promptStart
45103
+ };
44373
45104
  }
44374
45105
  }
44375
45106
  if (priorMessages.length === 0) {
44376
- debug(`Case 1: clean start, ${messages.length} total messages`);
45107
+ debug(`Case 1: clean start, ${messages.length} total messages, account=${accountProfileId ?? "default"}`);
44377
45108
  debug(`syncResult: path=clean-start`);
44378
- return { sessionId: null };
45109
+ return { sessionId: null, promptStart: messages.length - 1 };
44379
45110
  }
44380
- const previousSessionId = sharedSession?.sessionId;
44381
- const previousCursor = sharedSession?.cursor ?? 0;
45111
+ const replacedSessionId = sharedSession?.sessionId;
45112
+ const previousSessionId = sameAccount ? sharedSession?.sessionId : void 0;
45113
+ const previousCursor = sameAccount ? sharedSession?.cursor ?? 0 : 0;
44382
45114
  const preserveId = previousSessionId !== void 0 && !sharedSession?.forceRotate;
44383
45115
  if (preserveId) {
44384
- deleteSession(previousSessionId, cwd, process.env.CLAUDE_CONFIG_DIR);
45116
+ deleteSession(previousSessionId, cwd, claudeDir);
44385
45117
  }
44386
45118
  const session = createSession({
44387
45119
  projectPath: cwd,
44388
- claudeDir: process.env.CLAUDE_CONFIG_DIR,
45120
+ claudeDir,
44389
45121
  ...preserveId ? { sessionId: previousSessionId } : {},
44390
45122
  ...modelId ? { model: modelId } : {}
44391
45123
  });
44392
45124
  convertAndImportMessages(session, priorMessages, customToolNameToSdk, cwd);
44393
45125
  session.save();
44394
- verifyWrittenSession2(session.jsonlPath, session.sessionId, session.messages.length, cwd);
44395
- setSharedSession({ sessionId: session.sessionId, cursor: priorMessages.length, cwd });
44396
- if (previousSessionId === void 0) {
45126
+ verifyWrittenSession2(session.jsonlPath, session.sessionId, session.messages.length, cwd, claudeDir);
45127
+ setSharedSession({
45128
+ sessionId: session.sessionId,
45129
+ cursor: priorMessages.length,
45130
+ cwd,
45131
+ // The rebuilt file's content IS this context, so its anchor is the
45132
+ // record's identity — including after a compact/tree-nav that moved it.
45133
+ ...incomingFingerprint ? { conversationFingerprint: incomingFingerprint } : {},
45134
+ ...accountProfileId ? { accountProfileId } : {},
45135
+ ...scopeConfigDir ? { claudeConfigDir: scopeConfigDir } : {}
45136
+ });
45137
+ if (replacedSessionId === void 0) {
44397
45138
  debug(`Case 2: first turn with ${priorMessages.length} prior messages \u2192 session ${session.sessionId.slice(0, 8)}, ${session.messages.length} records`);
45139
+ } else if (!sameAccount) {
45140
+ debug(`Case 5 account-rotation: ${priorMessages.length} prior messages \u2192 new session ${session.sessionId.slice(0, 8)} for account ${accountProfileId ?? "default"} (replaced ${replacedSessionId.slice(0, 8)})`);
44398
45141
  } else if (preserveId) {
44399
45142
  const missedCount = priorMessages.length - previousCursor;
44400
45143
  debug(`Case 4: ${missedCount} missed messages, ${priorMessages.length} total \u2192 rewrote session ${session.sessionId.slice(0, 8)} (same id), ${session.messages.length} records`);
44401
45144
  } else {
44402
45145
  debug(`Case 4 post-abort: ${priorMessages.length} total \u2192 new session ${session.sessionId.slice(0, 8)} (was ${previousSessionId.slice(0, 8)}, rotated to avoid race with orphan writer), ${session.messages.length} records`);
44403
45146
  }
44404
- debugSessionPaths(`${session.sessionId.slice(0, 8)}`, cwd, session.jsonlPath);
44405
- debug(`syncResult: path=rebuild sessionId=${session.sessionId} priors=${priorMessages.length} ${previousSessionId === void 0 ? "first" : preserveId ? "preserved" : "rotated-post-abort"}`);
44406
- return { sessionId: session.sessionId };
45147
+ debugSessionPaths(`${session.sessionId.slice(0, 8)}`, cwd, session.jsonlPath, claudeDir);
45148
+ debug(`syncResult: path=rebuild sessionId=${session.sessionId} priors=${priorMessages.length} ${replacedSessionId === void 0 ? "first" : !sameAccount ? "account-rotated" : preserveId ? "preserved" : "rotated-post-abort"}`);
45149
+ return { sessionId: session.sessionId, promptStart: messages.length - 1 };
44407
45150
  }
44408
45151
 
44409
45152
  // src/stream-idle-watchdog.ts
@@ -44492,75 +45235,6 @@ function createStreamIdleWatchdog({
44492
45235
  };
44493
45236
  }
44494
45237
 
44495
- // src/rate-limit.ts
44496
- var RATE_LIMIT_AUTO_RESUME_EVENT = "vstack:rate-limit";
44497
- var RATE_LIMIT_TOKEN = "\x1B[31m[rate-limit]\x1B[39m";
44498
- var USAGE_LIMIT_PREFIXES = Array.isArray(qO) ? qO : [];
44499
- function coerceMessageText(value) {
44500
- if (typeof value === "string") return value;
44501
- if (value instanceof Error) return value.message;
44502
- try {
44503
- return JSON.stringify(value ?? "");
44504
- } catch {
44505
- return String(value);
44506
- }
44507
- }
44508
- function isExtraUsageRequiredMessage(value) {
44509
- return /extra[-\s]?usage|overage|extra usage billing|extra usage credits|1M context/i.test(coerceMessageText(value));
44510
- }
44511
- function isUsageLimitMessage(value) {
44512
- const text = coerceMessageText(value);
44513
- return USAGE_LIMIT_PREFIXES.some((prefix) => text.includes(prefix));
44514
- }
44515
- function uniqueNonEmptyLines(values) {
44516
- const seen = /* @__PURE__ */ new Set();
44517
- const out = [];
44518
- for (const value of values) {
44519
- const text = typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim();
44520
- if (!text || seen.has(text)) continue;
44521
- seen.add(text);
44522
- out.push(text);
44523
- }
44524
- return out;
44525
- }
44526
- function resetTimestampMs(value) {
44527
- let parsed = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : Number.NaN;
44528
- if (!Number.isFinite(parsed)) return void 0;
44529
- if (typeof value === "number" && Math.abs(parsed) < 1e12) parsed *= 1e3;
44530
- return parsed;
44531
- }
44532
- function formatResetTimestamp(value) {
44533
- const parsed = resetTimestampMs(value);
44534
- if (parsed === void 0) return "unknown";
44535
- return new Date(parsed).toLocaleString(void 0, {
44536
- day: "numeric",
44537
- hour: "numeric",
44538
- minute: "2-digit",
44539
- month: "short",
44540
- second: "2-digit",
44541
- timeZoneName: "short",
44542
- year: "numeric"
44543
- });
44544
- }
44545
- var ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD = 80;
44546
- function normalizeRateLimitUtilization(value) {
44547
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
44548
- if (value === 0) return 0;
44549
- if (value > 0 && value < 1) return value * 100;
44550
- if (value > 1 && value <= 100) return value;
44551
- return void 0;
44552
- }
44553
- function rateLimitTypeLabel(value) {
44554
- const text = typeof value === "string" ? value.trim() : "";
44555
- return text || "unknown";
44556
- }
44557
- function formatAllowedRateLimitWarning(info) {
44558
- if (info?.status !== "allowed_warning") return void 0;
44559
- const utilization = normalizeRateLimitUtilization(info.utilization);
44560
- if (utilization === void 0 || utilization < ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD) return void 0;
44561
- return `Claude rate limit warning: nearing ${rateLimitTypeLabel(info.rateLimitType)} limit; check Claude Code /usage for exact utilization.`;
44562
- }
44563
-
44564
45238
  // src/tool-mapping.ts
44565
45239
  var SDK_TO_PI_TOOL_NAME = {
44566
45240
  read: "read",
@@ -44607,8 +45281,7 @@ function mapToolArgs(toolName, args) {
44607
45281
 
44608
45282
  // src/assistant-stream.ts
44609
45283
  import { calculateCost } from "@earendil-works/pi-ai";
44610
- function updateUsage(output, usage, model) {
44611
- const c = ctx();
45284
+ function updateUsage(output, usage, model, c) {
44612
45285
  const current = c.currentMessageUsage;
44613
45286
  const carry = c.turnUsageCarry;
44614
45287
  if (usage.input_tokens != null) current.input = usage.input_tokens;
@@ -44663,6 +45336,17 @@ var TOOL_USE_END_GRACE_MS = 1500;
44663
45336
  function endToolUseTurn(c) {
44664
45337
  if (!c.currentPiStream || !c.turnOutput) return;
44665
45338
  cancelScheduledToolUseEnd(c);
45339
+ const partial2 = c.turnOutput.content.filter((b) => b?.type === "toolCall" && "partialJson" in b);
45340
+ if (partial2.length > 0) {
45341
+ const calls = partial2.map((b) => ({ id: b.id, name: b.name }));
45342
+ debug(`endToolUseTurn: pruning ${partial2.length} still-partial tool call(s) \u2014 truncated arguments never execute:`, calls.map((entry) => `${entry.name} [${entry.id}]`).join(", "));
45343
+ diagDump("partial_tool_calls_pruned", { count: partial2.length, calls });
45344
+ appendIntegrityEntry("partial_tool_calls_pruned", { count: partial2.length, calls });
45345
+ c.turnOutput.content = c.turnOutput.content.filter((b) => !(b?.type === "toolCall" && "partialJson" in b));
45346
+ }
45347
+ for (const block of c.turnOutput.content) {
45348
+ if (block?.type === "toolCall" && typeof block.id === "string") c.forwardedToolCallIds.add(block.id);
45349
+ }
44666
45350
  c.turnOutput.stopReason = "toolUse";
44667
45351
  c.currentPiStream.push({ type: "done", reason: "toolUse", message: c.turnOutput });
44668
45352
  c.currentPiStream.end();
@@ -44691,33 +45375,42 @@ function reapStaleQueuedResults(c) {
44691
45375
  const stale = c.takeStaleQueuedResults();
44692
45376
  if (stale.length === 0) return;
44693
45377
  const names = stale.map((entry) => entry.toolName);
44694
- debug(`reapStaleQueuedResults: dropping ${stale.length} queued tool result(s) with no possible consumer:`, names.join(", "));
44695
- diagDump("stale_queued_tool_results_dropped", { count: stale.length, stale });
44696
- appendIntegrityEntry("stale_queued_tool_results_dropped", { count: stale.length, stale });
45378
+ debug(`reapStaleQueuedResults: parked ${stale.length} early tool result(s) awaiting a late handler:`, names.join(", "));
45379
+ diagDump("stale_queued_tool_results_parked", { count: stale.length, stale });
45380
+ appendIntegrityEntry("stale_queued_tool_results_parked", { count: stale.length, stale });
44697
45381
  safeNotify(
44698
- `Claude bridge: dropped ${stale.length} tool result(s) whose handler never matched (${names.slice(0, 6).join(", ")}${names.length > 6 ? ", \u2026" : ""}). The model saw an error for these calls and may retry them.`,
45382
+ `Claude bridge: parked ${stale.length} early tool result(s) whose handler has not arrived (${names.slice(0, 6).join(", ")}${names.length > 6 ? ", \u2026" : ""}). A late handler can still consume them.`,
44699
45383
  "warning"
44700
45384
  );
44701
45385
  }
44702
- function updateTurnOutputModel(modelId) {
44703
- const c = ctx();
45386
+ function updateTurnOutputModel(modelId, c = ctx()) {
44704
45387
  if (typeof modelId !== "string" || !modelId || !c.turnOutput) return;
44705
45388
  if (c.turnOutput.model === modelId) return;
44706
45389
  debug(`provider: active Claude model changed ${c.turnOutput.model} -> ${modelId}`);
44707
45390
  c.turnOutput.model = modelId;
44708
45391
  }
44709
- function finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, toolName, mappedArgs) {
44710
- if (!queryCtx.currentPiStream || !queryCtx.turnOutput) return;
45392
+ var FINALIZE_MAX_REARMS = 3;
45393
+ function finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, toolName, mappedArgs, rearmCount = 0) {
45394
+ if (!queryCtx.currentPiStream || !queryCtx.turnOutput) {
45395
+ if (failStrandedToolCall(queryCtx, toolCallId)) {
45396
+ debug(`mcp handler: ${toolName} [${toolCallId}] stranded \u2014 turn ended before its call reached Pi; resolved with error`);
45397
+ diagDump("tool_handler_stranded", { toolCallId, toolName, site: "finalize-no-stream" });
45398
+ appendIntegrityEntry("tool_handler_stranded", { toolCallId, toolName, site: "finalize-no-stream" });
45399
+ }
45400
+ return;
45401
+ }
44711
45402
  let idx = queryCtx.turnBlocks.findIndex((b) => b.type === "toolCall" && b.id === toolCallId);
44712
45403
  if (idx >= 0) {
44713
45404
  const block = queryCtx.turnBlocks[idx];
44714
45405
  if ("partialJson" in block) {
44715
- block.arguments = mapToolArgs(block.name, parsePartialJson(block.partialJson, block.arguments));
45406
+ block.arguments = mappedArgs;
44716
45407
  queryCtx.updateToolCallArgs(block.id, block.arguments);
44717
45408
  delete block.partialJson;
44718
45409
  delete block.index;
44719
45410
  queryCtx.currentPiStream.push({ type: "toolcall_end", contentIndex: idx, toolCall: block, partial: queryCtx.turnOutput });
44720
45411
  }
45412
+ } else if (queryCtx.forwardedToolCallIds.has(toolCallId) || queryCtx.deadToolCallIds.has(toolCallId)) {
45413
+ debug(`mcp handler: ${toolName} [${toolCallId}] already ${queryCtx.forwardedToolCallIds.has(toolCallId) ? "forwarded" : "dead"} \u2014 not re-emitting`);
44721
45414
  } else {
44722
45415
  queryCtx.turnBlocks.push({ type: "toolCall", id: toolCallId, name: toolName, arguments: mappedArgs });
44723
45416
  idx = queryCtx.turnBlocks.length - 1;
@@ -44725,12 +45418,37 @@ function finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, toolName, ma
44725
45418
  queryCtx.currentPiStream.push({ type: "toolcall_start", contentIndex: idx, partial: queryCtx.turnOutput });
44726
45419
  queryCtx.currentPiStream.push({ type: "toolcall_end", contentIndex: idx, toolCall: block, partial: queryCtx.turnOutput });
44727
45420
  }
45421
+ for (let i = 0; i < queryCtx.turnBlocks.length; i++) {
45422
+ const sibling = queryCtx.turnBlocks[i];
45423
+ if (sibling.type !== "toolCall" || !("partialJson" in sibling)) continue;
45424
+ const waiting = queryCtx.pendingToolCalls.get(sibling.id);
45425
+ if (!waiting) continue;
45426
+ sibling.arguments = waiting.args;
45427
+ queryCtx.updateToolCallArgs(sibling.id, sibling.arguments);
45428
+ delete sibling.partialJson;
45429
+ delete sibling.index;
45430
+ queryCtx.currentPiStream.push({ type: "toolcall_end", contentIndex: i, toolCall: sibling, partial: queryCtx.turnOutput });
45431
+ }
45432
+ const unsettled = queryCtx.turnBlocks.filter((b) => b.type === "toolCall" && "partialJson" in b);
45433
+ if (unsettled.length > 0 && rearmCount < FINALIZE_MAX_REARMS) {
45434
+ debug(`mcp handler: ${unsettled.length} sibling tool call(s) still streaming \u2014 re-arming grace (${rearmCount + 1}/${FINALIZE_MAX_REARMS})`);
45435
+ scheduleToolUseTurnEnd(
45436
+ queryCtx,
45437
+ () => finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, toolName, mappedArgs, rearmCount + 1),
45438
+ `finalize-rearm:${toolName}`
45439
+ );
45440
+ return;
45441
+ }
45442
+ const executable = queryCtx.turnBlocks.some((b) => b.type === "toolCall" && !("partialJson" in b));
45443
+ if (!executable) {
45444
+ debug(`mcp handler: nothing executable in this turn after suppression \u2014 leaving the stream to its own terminal events (${toolName} [${toolCallId}])`);
45445
+ return;
45446
+ }
44728
45447
  queryCtx.turnSawToolCall = true;
44729
45448
  debug(`mcp handler: finalizing tool_use turn from MCP invocation [${toolCallId}] (${toolName}) \u2014 terminal stream events never arrived`);
44730
45449
  endToolUseTurn(queryCtx);
44731
45450
  }
44732
- function processStreamEvent(message, customToolNameToPi, model) {
44733
- const c = ctx();
45451
+ function processStreamEvent(message, customToolNameToPi, model, c = ctx()) {
44734
45452
  if (!c.currentPiStream || !c.turnOutput) return;
44735
45453
  const event = message.event;
44736
45454
  if (event?.type === "ping") return;
@@ -44742,14 +45460,15 @@ function processStreamEvent(message, customToolNameToPi, model) {
44742
45460
  reapStaleQueuedResults(c);
44743
45461
  c.resetToolTracking();
44744
45462
  c.beginChildMessage(event.message?.id);
44745
- updateTurnOutputModel(event.message?.model);
44746
- if (event.message?.usage) updateUsage(c.turnOutput, event.message.usage, model);
45463
+ updateTurnOutputModel(event.message?.model, c);
45464
+ if (event.message?.usage) updateUsage(c.turnOutput, event.message.usage, model, c);
44747
45465
  return;
44748
45466
  }
44749
45467
  if (event?.type === "content_block_start") {
44750
45468
  c.turnSawStreamEvent = true;
44751
- ensureTurnStarted();
45469
+ ensureTurnStarted(c);
44752
45470
  c.childExecutedStreamIndexes.delete(event.index);
45471
+ c.suppressedStreamIndexes.delete(event.index);
44753
45472
  if (event.content_block?.type === "tool_use" && isChildExecutedTool(event.content_block.name)) {
44754
45473
  c.noteChildExecutedToolCall(event.content_block.id, event.content_block.name, event.index);
44755
45474
  debug(`processStreamEvent: child-executed tool ${event.content_block.name} [${event.content_block.id}] \u2014 not mirrored as a Pi tool call`);
@@ -44762,6 +45481,17 @@ function processStreamEvent(message, customToolNameToPi, model) {
44762
45481
  c.turnBlocks.push({ type: "thinking", thinking: "", thinkingSignature: "", index: event.index });
44763
45482
  c.currentPiStream.push({ type: "thinking_start", contentIndex: c.turnBlocks.length - 1, partial: c.turnOutput });
44764
45483
  } else if (event.content_block?.type === "tool_use") {
45484
+ const streamedId = event.content_block.id;
45485
+ if (typeof streamedId === "string" && (c.forwardedToolCallIds.has(streamedId) || c.deadToolCallIds.has(streamedId))) {
45486
+ c.suppressedStreamIndexes.add(event.index);
45487
+ debug(`processStreamEvent: tool_use ${streamedId} already ${c.forwardedToolCallIds.has(streamedId) ? "forwarded" : "dead"} \u2014 suppressing duplicate stream block`);
45488
+ return;
45489
+ }
45490
+ if (typeof streamedId === "string" && c.turnBlocks.some((b) => b.type === "toolCall" && b.id === streamedId)) {
45491
+ c.suppressedStreamIndexes.add(event.index);
45492
+ debug(`processStreamEvent: tool_use ${streamedId} already recorded in this turn \u2014 suppressing duplicate stream block`);
45493
+ return;
45494
+ }
44765
45495
  c.turnSawToolCall = true;
44766
45496
  const mappedName = mapToolName(event.content_block.name, customToolNameToPi);
44767
45497
  c.recordToolCall(event.content_block.id, mappedName, {});
@@ -44780,7 +45510,7 @@ function processStreamEvent(message, customToolNameToPi, model) {
44780
45510
  return;
44781
45511
  }
44782
45512
  if (event?.type === "content_block_delta") {
44783
- if (c.childExecutedStreamIndexes.has(event.index)) {
45513
+ if (c.childExecutedStreamIndexes.has(event.index) || c.suppressedStreamIndexes.has(event.index)) {
44784
45514
  c.turnSawStreamEvent = true;
44785
45515
  return;
44786
45516
  }
@@ -44809,7 +45539,7 @@ function processStreamEvent(message, customToolNameToPi, model) {
44809
45539
  return;
44810
45540
  }
44811
45541
  if (event?.type === "content_block_stop") {
44812
- if (c.childExecutedStreamIndexes.has(event.index)) {
45542
+ if (c.childExecutedStreamIndexes.has(event.index) || c.suppressedStreamIndexes.has(event.index)) {
44813
45543
  c.turnSawStreamEvent = true;
44814
45544
  return;
44815
45545
  }
@@ -44839,7 +45569,7 @@ function processStreamEvent(message, customToolNameToPi, model) {
44839
45569
  }
44840
45570
  if (event?.type === "message_delta") {
44841
45571
  c.turnOutput.stopReason = mapStopReason(event.delta?.stop_reason);
44842
- if (event.usage) updateUsage(c.turnOutput, event.usage, model);
45572
+ if (event.usage) updateUsage(c.turnOutput, event.usage, model, c);
44843
45573
  return;
44844
45574
  }
44845
45575
  if (event?.type === "message_stop" && c.turnSawToolCall) {
@@ -44850,9 +45580,9 @@ function processStreamEvent(message, customToolNameToPi, model) {
44850
45580
  debug("processStreamEvent: unhandled event type", event?.type);
44851
45581
  }
44852
45582
  }
44853
- function appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi) {
44854
- const c = ctx();
45583
+ function appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi, c) {
44855
45584
  if (!assistantMsg?.content) return false;
45585
+ const streamLive = Boolean(c.currentPiStream && c.turnOutput);
44856
45586
  let sawToolUse = false;
44857
45587
  for (const block of assistantMsg.content) {
44858
45588
  if (block.type !== "tool_use") continue;
@@ -44861,11 +45591,16 @@ function appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameT
44861
45591
  debug(`assistant message: child-executed tool ${block.name} [${block.id}] \u2014 not mirrored as a Pi tool call`);
44862
45592
  continue;
44863
45593
  }
44864
- sawToolUse = true;
44865
45594
  const existingIdx = c.turnBlocks.findIndex((b) => b.type === "toolCall" && b.id === block.id);
45595
+ if (existingIdx < 0 && (c.forwardedToolCallIds.has(block.id) || c.deadToolCallIds.has(block.id))) {
45596
+ debug(`assistant message: tool_use ${block.id} already ${c.forwardedToolCallIds.has(block.id) ? "forwarded" : "dead"} \u2014 skipping duplicate`);
45597
+ continue;
45598
+ }
45599
+ sawToolUse = true;
44866
45600
  const name = mapToolName(block.name, customToolNameToPi);
44867
45601
  const mappedArgs = mapToolArgs(name, block.input);
44868
45602
  c.recordToolCall(block.id, name, mappedArgs);
45603
+ if (!streamLive) continue;
44869
45604
  if (existingIdx >= 0) {
44870
45605
  const existing = c.turnBlocks[existingIdx];
44871
45606
  existing.name = name;
@@ -44878,7 +45613,7 @@ function appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameT
44878
45613
  }
44879
45614
  continue;
44880
45615
  }
44881
- ensureTurnStarted();
45616
+ ensureTurnStarted(c);
44882
45617
  c.turnBlocks.push({
44883
45618
  type: "toolCall",
44884
45619
  id: block.id,
@@ -44890,11 +45625,10 @@ function appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameT
44890
45625
  c.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c.turnOutput });
44891
45626
  c.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock, partial: c.turnOutput });
44892
45627
  }
44893
- if (assistantMsg.usage && c.turnOutput && c.currentPiStream) updateUsage(c.turnOutput, assistantMsg.usage, model);
45628
+ if (assistantMsg.usage && c.turnOutput && c.currentPiStream) updateUsage(c.turnOutput, assistantMsg.usage, model, c);
44894
45629
  return sawToolUse;
44895
45630
  }
44896
- function noteChildExecutedToolResults(message) {
44897
- const c = ctx();
45631
+ function noteChildExecutedToolResults(message, c = ctx()) {
44898
45632
  if (c.childExecutedToolCalls.size === 0) return;
44899
45633
  const content = message.message?.content;
44900
45634
  if (!Array.isArray(content)) return;
@@ -44908,13 +45642,12 @@ function noteChildExecutedToolResults(message) {
44908
45642
  debug(`child-executed tool result: ${name} [${block.tool_use_id}] isError=${isError} byteSize=${byteSize ?? "unknown"} audited=${audited}`);
44909
45643
  }
44910
45644
  }
44911
- function processAssistantMessage(message, model, customToolNameToPi) {
44912
- const c = ctx();
45645
+ function processAssistantMessage(message, model, customToolNameToPi, c = ctx()) {
44913
45646
  const assistantMsg = message.message;
44914
45647
  if (!assistantMsg?.content) return;
44915
- updateTurnOutputModel(assistantMsg.model);
45648
+ updateTurnOutputModel(assistantMsg.model, c);
44916
45649
  if (c.turnSawStreamEvent) {
44917
- if (appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi)) {
45650
+ if (appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi, c)) {
44918
45651
  c.turnSawToolCall = true;
44919
45652
  scheduleToolUseTurnEnd(c, () => endToolUseTurn(c), "assistant-boundary");
44920
45653
  }
@@ -44931,7 +45664,7 @@ function processAssistantMessage(message, model, customToolNameToPi) {
44931
45664
  for (const block of assistantMsg.content) {
44932
45665
  if (block.type === "text" && block.text) {
44933
45666
  if (alreadyRendered("text", block.text)) continue;
44934
- ensureTurnStarted();
45667
+ ensureTurnStarted(c);
44935
45668
  c.turnBlocks.push({ type: "text", text: block.text });
44936
45669
  const idx = c.turnBlocks.length - 1;
44937
45670
  c.currentPiStream?.push({ type: "text_start", contentIndex: idx, partial: c.turnOutput });
@@ -44939,7 +45672,7 @@ function processAssistantMessage(message, model, customToolNameToPi) {
44939
45672
  c.currentPiStream?.push({ type: "text_end", contentIndex: idx, content: block.text, partial: c.turnOutput });
44940
45673
  } else if (block.type === "thinking") {
44941
45674
  if (alreadyRendered("thinking", block.thinking ?? "")) continue;
44942
- ensureTurnStarted();
45675
+ ensureTurnStarted(c);
44943
45676
  c.turnBlocks.push({ type: "thinking", thinking: block.thinking ?? "", thinkingSignature: block.signature ?? "" });
44944
45677
  const idx = c.turnBlocks.length - 1;
44945
45678
  c.currentPiStream?.push({ type: "thinking_start", contentIndex: idx, partial: c.turnOutput });
@@ -44951,7 +45684,11 @@ function processAssistantMessage(message, model, customToolNameToPi) {
44951
45684
  debug(`processAssistantMessage fallback: child-executed tool ${block.name} [${block.id}] \u2014 not mirrored as a Pi tool call`);
44952
45685
  continue;
44953
45686
  }
44954
- ensureTurnStarted();
45687
+ if (!c.turnBlocks.some((b) => b.type === "toolCall" && b.id === block.id) && (c.forwardedToolCallIds.has(block.id) || c.deadToolCallIds.has(block.id))) {
45688
+ debug(`processAssistantMessage fallback: tool_use ${block.id} already ${c.forwardedToolCallIds.has(block.id) ? "forwarded" : "dead"} \u2014 skipping duplicate`);
45689
+ continue;
45690
+ }
45691
+ ensureTurnStarted(c);
44955
45692
  c.turnSawToolCall = true;
44956
45693
  const mappedName = mapToolName(block.name, customToolNameToPi);
44957
45694
  const mappedArgs = mapToolArgs(mappedName, block.input);
@@ -44975,108 +45712,641 @@ function processAssistantMessage(message, model, customToolNameToPi) {
44975
45712
  c.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c.turnOutput });
44976
45713
  c.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock, partial: c.turnOutput });
44977
45714
  } else if (block.type === "fallback") {
44978
- updateTurnOutputModel(block.to?.model);
45715
+ updateTurnOutputModel(block.to?.model, c);
44979
45716
  } else {
44980
45717
  debug("processAssistantMessage: unhandled block type", block.type);
44981
45718
  }
44982
45719
  }
44983
- if (assistantMsg.usage && c.turnOutput) updateUsage(c.turnOutput, assistantMsg.usage, model);
45720
+ if (assistantMsg.usage && c.turnOutput) updateUsage(c.turnOutput, assistantMsg.usage, model, c);
44984
45721
  if (c.turnSawToolCall && c.currentPiStream && c.turnOutput) {
44985
45722
  endToolUseTurn(c);
44986
45723
  }
44987
45724
  }
44988
45725
 
44989
- // src/index.ts
44990
- var _piAi = piAi;
44991
- var getModels = await resolveGetModels(_piAi);
44992
- var newAssistantMessageEventStream = typeof _piAi.createAssistantMessageEventStream === "function" ? _piAi.createAssistantMessageEventStream : () => new _piAi.AssistantMessageEventStream();
44993
- var PRIMARY_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:primaryInstance");
44994
- var ACTIVE_STREAM_SIMPLE_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:activeStreamSimple");
44995
- var COMMANDS_REGISTERED_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:commandsRegistered");
44996
- var MODELS = buildModels(getModels("anthropic"));
44997
- var extraUsageHelperInFlight = null;
44998
- function emitRateLimitEvent(payload) {
44999
- try {
45000
- extensionApi?.events?.emit?.(RATE_LIMIT_AUTO_RESUME_EVENT, payload);
45001
- } catch {
45002
- }
45003
- }
45004
- function extraUsageAllowed(config2) {
45005
- return config2.provider?.allowExtraUsage === true;
45006
- }
45007
- var lastFastModeDisabledNoticeReason = null;
45008
- var FAST_MODE_DISABLED_REASON_TEXT = {
45009
- disabled_by_env: "disabled by an environment variable",
45010
- extra_usage_disabled: "extra usage is disabled for this account",
45011
- free: "not available on the free plan",
45012
- model_not_allowed: "not available for this model",
45013
- network_error: "the eligibility check hit a network error",
45014
- not_first_party: "not available for this account type",
45015
- preference: "disabled by a Claude Code preference",
45016
- sdk_opt_in_required: "the SDK opt-in is missing",
45017
- unknown: "unavailable for an unknown reason"
45018
- };
45019
- function noteFastModeDisabledReason(message, bridgeConfig) {
45020
- if (bridgeConfig.provider?.fastMode !== true) return;
45021
- const reason = message.fast_mode_disabled_reason;
45022
- if (typeof reason !== "string" || reason === "pending") return;
45023
- if (reason === lastFastModeDisabledNoticeReason) return;
45024
- lastFastModeDisabledNoticeReason = reason;
45025
- const text = FAST_MODE_DISABLED_REASON_TEXT[reason] ?? `unavailable (${reason})`;
45026
- safeNotify(`Claude bridge: fast mode is enabled in settings but Claude Code declined it \u2014 ${text}.`, "warning");
45027
- }
45028
- function sdkTextFromMessage(message) {
45029
- if (message.type === "result") return message.result;
45030
- if (message.type === "assistant") {
45031
- const content = message.message?.content;
45032
- if (!Array.isArray(content)) return void 0;
45033
- return content.map((block) => block?.type === "text" && typeof block.text === "string" ? block.text : "").filter(Boolean).join("\n");
45034
- }
45035
- return void 0;
45726
+ // src/sdk-query.ts
45727
+ var sdkQueryFactory = Okt;
45728
+ function __testSetSdkQueryFactory(factory) {
45729
+ sdkQueryFactory = factory ?? Okt;
45036
45730
  }
45037
- async function runExtraUsageHelper(cwd, config2 = loadConfig(cwd)) {
45038
- const providerSettings = config2.provider ?? {};
45039
- const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
45040
- if (claudeExecutable) preflightClaudeExecutable(claudeExecutable, cwd);
45041
- const helperQuery = Okt({
45042
- prompt: "/extra-usage",
45731
+
45732
+ // src/account-host.ts
45733
+ var ACCOUNT_PROBE_DEADLINE_MS = 1e4;
45734
+ async function probeClaudeAccountProfile(input) {
45735
+ const config2 = loadConfig(input.cwd);
45736
+ const claudeExecutable = resolveClaudeExecutable(config2.provider?.pathToClaudeCodeExecutable);
45737
+ if (claudeExecutable) preflightClaudeExecutable(claudeExecutable, input.cwd);
45738
+ const probe = sdkQueryFactory({
45739
+ prompt: "/usage",
45043
45740
  options: {
45044
- cwd,
45045
- env: { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: "0", DISABLE_AUTO_COMPACT: "1" },
45741
+ cwd: input.cwd,
45742
+ env: {
45743
+ ...subscriberProfileEnv(input.profile),
45744
+ ENABLE_CLAUDEAI_MCP_SERVERS: "0",
45745
+ DISABLE_AUTO_COMPACT: "1"
45746
+ },
45046
45747
  maxTurns: 1,
45748
+ // bypassPermissions makes tool containment the only gate, so the probe
45749
+ // gets BOTH layers: the standard bridge isolation lists AND a deny-all
45750
+ // PreToolUse hook — a /usage probe has no business executing anything.
45751
+ permissionMode: "bypassPermissions",
45752
+ ...CLAUDE_BRIDGE_TOOL_ISOLATION,
45753
+ hooks: { PreToolUse: [{ hooks: [denyAllToolsHook()] }] },
45047
45754
  ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
45048
45755
  spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
45049
- ...makeCliDebugOptions("extra-usage")
45756
+ ...makeCliDebugOptions("account-probe")
45050
45757
  }
45051
45758
  });
45052
- const outputs = [];
45053
- try {
45054
- for await (const message of helperQuery) {
45055
- const text = sdkTextFromMessage(message)?.trim();
45056
- if (text && outputs[outputs.length - 1] !== text) outputs.push(text);
45759
+ const onAbort = () => {
45760
+ void probe.interrupt().catch(() => {
45761
+ });
45762
+ try {
45763
+ probe.close();
45764
+ } catch {
45765
+ }
45766
+ };
45767
+ if (input.signal?.aborted) onAbort();
45768
+ else input.signal?.addEventListener("abort", onAbort, { once: true });
45769
+ let controls;
45770
+ let deadlineTimer;
45771
+ const deadline = new Promise((resolveDeadline) => {
45772
+ deadlineTimer = setTimeout(() => {
45773
+ debug(`account-probe: deadline expired for ${input.profile.label}; killing probe child`);
45774
+ onAbort();
45775
+ resolveDeadline("deadline");
45776
+ }, input.deadlineMs ?? ACCOUNT_PROBE_DEADLINE_MS);
45777
+ deadlineTimer.unref?.();
45778
+ });
45779
+ const consume = (async () => {
45780
+ for await (const message of probe) {
45781
+ if (message.type === "system" && message.subtype === "init" && !controls) {
45782
+ controls = Promise.allSettled([
45783
+ probe.accountInfo(),
45784
+ probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET()
45785
+ ]).then(([identityResult, usageResult]) => ({
45786
+ ...identityResult.status === "fulfilled" ? { identity: {
45787
+ email: identityResult.value.email,
45788
+ organization: identityResult.value.organization,
45789
+ subscriptionType: identityResult.value.subscriptionType
45790
+ } } : {},
45791
+ ...usageResult.status === "fulfilled" ? { usage: usageResult.value } : {}
45792
+ }));
45793
+ }
45057
45794
  }
45795
+ return controls ? await controls : {};
45796
+ })();
45797
+ try {
45798
+ const result = await Promise.race([consume, deadline]);
45799
+ return result === "deadline" ? {} : result;
45058
45800
  } finally {
45059
- helperQuery.close();
45801
+ clearTimeout(deadlineTimer);
45802
+ input.signal?.removeEventListener("abort", onAbort);
45803
+ probe.close();
45804
+ void consume.catch(() => {
45805
+ });
45060
45806
  }
45061
- return outputs.join("\n").trim() || "Claude Code /extra-usage completed.";
45062
45807
  }
45063
- function launchExtraUsageHelperIfAllowed(cwd, config2, reason) {
45064
- if (!extraUsageAllowed(config2)) return false;
45065
- if (extraUsageHelperInFlight) return true;
45066
- extraUsageHelperInFlight = runExtraUsageHelper(cwd, config2).then((message) => {
45067
- piUI?.notify(`Claude extra usage helper: ${message}`, "info");
45068
- return message;
45069
- }).catch((error51) => {
45070
- const message = error51 instanceof Error ? error51.message : String(error51);
45071
- piUI?.notify(`Claude extra usage helper failed after ${reason}: ${message}`, "error");
45072
- throw error51;
45073
- }).finally(() => {
45074
- extraUsageHelperInFlight = null;
45808
+ var BRIDGE_ACCOUNT_HOST = {
45809
+ version: 1,
45810
+ probeProfile: probeClaudeAccountProfile
45811
+ };
45812
+
45813
+ // src/bridge-commands.ts
45814
+ var COMMANDS_REGISTERED_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:commandsRegistered");
45815
+ function commandCwd(ctx2) {
45816
+ const value = ctx2?.cwd;
45817
+ return typeof value === "string" && value.length > 0 ? value : process.cwd();
45818
+ }
45819
+ async function tryOpenExtensionManagerSettings(ctx2) {
45820
+ const host = globalThis;
45821
+ const openQuickSettings = host[/* @__PURE__ */ Symbol.for("vstack.pi.extension-manager.open-quick-settings")];
45822
+ if (typeof openQuickSettings !== "function") return false;
45823
+ try {
45824
+ await openQuickSettings(ctx2, "@vanillagreen/pi-claude-bridge");
45825
+ return true;
45826
+ } catch {
45827
+ return false;
45828
+ }
45829
+ }
45830
+ function showBridgeStatus(ctx2) {
45831
+ const config2 = loadConfig(commandCwd(ctx2));
45832
+ ctx2.ui.notify([
45833
+ `Pi Claude: ${config2.enabled === false ? "disabled" : "enabled"}`,
45834
+ "Claude account billing settings (including Extra Usage) are managed in Claude."
45835
+ ].join("\n"), "info");
45836
+ }
45837
+ async function reportConnectorInventory(ctx2) {
45838
+ const account = ctx2.model ? resolveClaudeAccountRouter()?.current(ctx2.model.id, ctx2.sessionManager?.getSessionId?.()) : void 0;
45839
+ const credentials = resolveClaudeOAuth(readCredentialFile, connectorCredentialEnv(account ? accountSessionScope(account).claudeConfigDir : void 0));
45840
+ if (!credentials) {
45841
+ ctx2.ui.notify("Pi Claude: no Claude OAuth credentials found \u2014 cannot enumerate connectors.", "error");
45842
+ return;
45843
+ }
45844
+ const inventory = await listAccountConnectors({ credentials });
45845
+ if (!inventory.ok) {
45846
+ ctx2.ui.notify(`Pi Claude: connector enumeration failed \u2014 ${inventory.reason}`, "error");
45847
+ return;
45848
+ }
45849
+ if (inventory.connectors.length === 0) {
45850
+ ctx2.ui.notify("Pi Claude: this account has no connectors installed.", "info");
45851
+ return;
45852
+ }
45853
+ const names = inventory.connectors.map((c) => c.name).join(", ");
45854
+ ctx2.ui.notify(`Pi Claude: ${inventory.connectors.length} connector(s) installed \u2014 ${names}`, "info");
45855
+ }
45856
+ function registerBridgeCommands(pi) {
45857
+ const guard = pi;
45858
+ if (guard[COMMANDS_REGISTERED_KEY]) return;
45859
+ guard[COMMANDS_REGISTERED_KEY] = true;
45860
+ pi.registerCommand("pi-claude", {
45861
+ description: "Open Pi Claude settings/status",
45862
+ handler: async (args, ctx2) => {
45863
+ if (args.trim()) ctx2.ui.notify("Unknown /pi-claude argument.", "warning");
45864
+ if (await tryOpenExtensionManagerSettings(ctx2)) return;
45865
+ showBridgeStatus(ctx2);
45866
+ }
45075
45867
  });
45076
- void extraUsageHelperInFlight.catch(() => {
45868
+ pi.registerCommand("pi-claude:connectors", {
45869
+ description: "List the Claude account's installed claude.ai connectors",
45870
+ handler: async (_args, ctx2) => reportConnectorInventory(ctx2)
45077
45871
  });
45078
- return true;
45079
45872
  }
45873
+
45874
+ // src/consume-query.ts
45875
+ function emitRateLimitEvent(payload) {
45876
+ try {
45877
+ extensionApi?.events?.emit?.(RATE_LIMIT_AUTO_RESUME_EVENT, payload);
45878
+ } catch {
45879
+ }
45880
+ }
45881
+ var lastFastModeDisabledNoticeReason = null;
45882
+ var FAST_MODE_DISABLED_REASON_TEXT = {
45883
+ disabled_by_env: "disabled by an environment variable",
45884
+ extra_usage_disabled: "extra usage is disabled for this account",
45885
+ free: "not available on the free plan",
45886
+ model_not_allowed: "not available for this model",
45887
+ network_error: "the eligibility check hit a network error",
45888
+ not_first_party: "not available for this account type",
45889
+ preference: "disabled by a Claude Code preference",
45890
+ sdk_opt_in_required: "the SDK opt-in is missing",
45891
+ unknown: "unavailable for an unknown reason"
45892
+ };
45893
+ function noteFastModeDisabledReason(message, bridgeConfig) {
45894
+ if (bridgeConfig.provider?.fastMode !== true) return;
45895
+ const reason = message.fast_mode_disabled_reason;
45896
+ if (typeof reason !== "string" || reason === "pending") return;
45897
+ if (reason === lastFastModeDisabledNoticeReason) return;
45898
+ lastFastModeDisabledNoticeReason = reason;
45899
+ const text = FAST_MODE_DISABLED_REASON_TEXT[reason] ?? `unavailable (${reason})`;
45900
+ safeNotify(`Pi Claude: fast mode is enabled in settings but Claude Code declined it \u2014 ${text}.`, "warning");
45901
+ }
45902
+ async function consumeQuery(sdkQuery, queryCtx, customToolNameToPi, model, bridgeConfig, wasAborted, account, router, attemptFailureBox) {
45903
+ let capturedSessionId;
45904
+ let failure;
45905
+ let accountProbe;
45906
+ const holdFailure = (next) => {
45907
+ failure = next;
45908
+ if (attemptFailureBox) attemptFailureBox.failure = next;
45909
+ };
45910
+ for await (const message of sdkQuery) {
45911
+ if (wasAborted()) break;
45912
+ activeStreamIdleWatchdogs.get(queryCtx)?.noteChunk();
45913
+ if (account) {
45914
+ debug("consumeQuery: managed message", () => JSON.stringify({
45915
+ type: message.type,
45916
+ subtype: message.subtype,
45917
+ error: message.error,
45918
+ eventType: message.event?.type,
45919
+ deltaType: message.event?.delta?.type,
45920
+ contentType: message.event?.content_block?.type
45921
+ }));
45922
+ }
45923
+ if (!queryCtx.turnOutput) continue;
45924
+ const streamLive = Boolean(queryCtx.currentPiStream);
45925
+ switch (message.type) {
45926
+ case "stream_event":
45927
+ if (!streamLive) break;
45928
+ processStreamEvent(message, customToolNameToPi, model, queryCtx);
45929
+ break;
45930
+ case "assistant": {
45931
+ const sdkError = message.error;
45932
+ if (sdkError && account) {
45933
+ if (!failure) holdFailure({ kind: classifyClaudeFailure(sdkError), message: String(sdkError) });
45934
+ break;
45935
+ }
45936
+ if (!streamLive && !queryCtx.turnSawToolCall) break;
45937
+ processAssistantMessage(message, model, customToolNameToPi, queryCtx);
45938
+ break;
45939
+ }
45940
+ case "result":
45941
+ if (failure && message.subtype === "success" && queryCtx.committedOutput) {
45942
+ debug(`consumeQuery: clearing informational ${failure.kind ?? "unclassified"} failure \u2014 query recovered with committed output`);
45943
+ holdFailure(void 0);
45944
+ }
45945
+ if (account && failure) break;
45946
+ if (!queryCtx.turnSawStreamEvent && message.subtype === "success") {
45947
+ if (!streamLive) break;
45948
+ const text = message.result || "";
45949
+ if (queryCtx.turnBlocks.some((b) => b.type === "text" && b.text === text)) {
45950
+ debug("consumeQuery: result text already rendered by assistant fallback; skipping duplicate");
45951
+ break;
45952
+ }
45953
+ ensureTurnStarted(queryCtx);
45954
+ queryCtx.turnBlocks.push({ type: "text", text });
45955
+ const idx = queryCtx.turnBlocks.length - 1;
45956
+ queryCtx.currentPiStream?.push({ type: "text_start", contentIndex: idx, partial: queryCtx.turnOutput });
45957
+ queryCtx.currentPiStream?.push({ type: "text_delta", contentIndex: idx, delta: text, partial: queryCtx.turnOutput });
45958
+ queryCtx.currentPiStream?.push({ type: "text_end", contentIndex: idx, content: text, partial: queryCtx.turnOutput });
45959
+ } else if (message.subtype !== "success") {
45960
+ const errorLines = Array.isArray(message.errors) ? uniqueNonEmptyLines(message.errors) : [];
45961
+ const errors = errorLines.length > 0 ? errorLines.join("\n") : String(message.result || message.subtype || "Claude Code request failed");
45962
+ const usageLimit = isUsageLimitMessage(message);
45963
+ if (!failure || !failure.rateLimitInfo) {
45964
+ holdFailure({ kind: usageLimit ? "rate-limit" : classifyClaudeFailure(errors), message: errors });
45965
+ }
45966
+ if (account) break;
45967
+ if (usageLimit) {
45968
+ queryCtx.handledTerminalError = true;
45969
+ queryCtx.turnOutput.stopReason = "error";
45970
+ queryCtx.turnOutput.errorMessage = errors;
45971
+ queryCtx.currentPiStream?.push({ type: "error", reason: "error", error: queryCtx.turnOutput });
45972
+ queryCtx.currentPiStream?.end();
45973
+ queryCtx.currentPiStream = null;
45974
+ }
45975
+ }
45976
+ break;
45977
+ case "system":
45978
+ if (!streamLive) break;
45979
+ if (message.subtype === "init" && message.session_id) {
45980
+ capturedSessionId = message.session_id;
45981
+ queryCtx.childSessionId = capturedSessionId;
45982
+ noteFastModeDisabledReason(message, bridgeConfig);
45983
+ if (account && router && !accountProbe) {
45984
+ accountProbe = Promise.allSettled([
45985
+ sdkQuery.accountInfo().then((info) => router.recordIdentity(account.profileId, {
45986
+ email: info.email,
45987
+ organization: info.organization,
45988
+ subscriptionType: info.subscriptionType
45989
+ })),
45990
+ sdkQuery.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET().then((usage) => router.recordUsage(account.profileId, usage))
45991
+ ]).then((results) => {
45992
+ const labels = ["recordIdentity", "recordUsage"];
45993
+ results.forEach((result, i) => {
45994
+ if (result.status === "rejected") debug(`consumeQuery: account probe ${labels[i]} rejected:`, result.reason);
45995
+ });
45996
+ });
45997
+ }
45998
+ } else if (message.subtype === "model_refusal_fallback") {
45999
+ const originalModel = message.original_model;
46000
+ const fallbackModel = message.fallback_model;
46001
+ updateTurnOutputModel(fallbackModel, queryCtx);
46002
+ debug("consumeQuery: model_refusal_fallback", JSON.stringify({ originalModel, fallbackModel }));
46003
+ if (typeof fallbackModel === "string" && typeof originalModel === "string" && fallbackModelForPrimaryModel(originalModel) === fallbackModel) {
46004
+ safeNotify(
46005
+ `Pi Claude switched ${modelDisplayName(originalModel)} to ${modelDisplayName(fallbackModel)} after Claude Code safety fallback.`,
46006
+ "info"
46007
+ );
46008
+ }
46009
+ }
46010
+ break;
46011
+ case "user":
46012
+ noteChildExecutedToolResults(message, queryCtx);
46013
+ break;
46014
+ case "rate_limit_event": {
46015
+ if (!streamLive) break;
46016
+ const info = message.rate_limit_info;
46017
+ debug("consumeQuery: rate_limit_event", JSON.stringify(info).slice(0, 300));
46018
+ if (info?.status === "rejected") {
46019
+ const rateLimitType = rateLimitTypeFromInfo(info);
46020
+ const resetAt = rateLimitResetFromInfo(info);
46021
+ const resetAtMs = rateLimitResetMs(info);
46022
+ const reason = `${rateLimitType ?? "unknown"} rate limit`;
46023
+ if (account && router) {
46024
+ holdFailure({ kind: "rate-limit", message: reason, rateLimitInfo: info });
46025
+ safeRouterCall("recordRateLimit", () => router.recordRateLimit(account.profileId, info, model.id));
46026
+ } else {
46027
+ const resetsAt = formatResetTimestamp(resetAtMs ?? resetAt);
46028
+ emitRateLimitEvent({
46029
+ model: model.id,
46030
+ provider: model.provider,
46031
+ rateLimitType,
46032
+ reason,
46033
+ resetAt,
46034
+ ...Number.isFinite(resetAtMs) ? { resetAtMs } : {},
46035
+ source: "claude-bridge",
46036
+ status: "rejected"
46037
+ });
46038
+ safeNotify(`${RATE_LIMIT_TOKEN} Claude ${reason} hit \u2014 resets ${resetsAt}`, "warning");
46039
+ }
46040
+ } else if (info?.status === "allowed_warning") {
46041
+ const warning = formatAllowedRateLimitWarning(info);
46042
+ if (warning) safeNotify(warning, "warning");
46043
+ else debug("consumeQuery: suppressed low/ambiguous allowed_warning rate_limit_event", JSON.stringify(info).slice(0, 300));
46044
+ }
46045
+ break;
46046
+ }
46047
+ default:
46048
+ debug("consumeQuery: unhandled SDK message type", message.type);
46049
+ break;
46050
+ }
46051
+ }
46052
+ if (accountProbe) {
46053
+ await Promise.race([
46054
+ accountProbe,
46055
+ new Promise((resolve5) => setTimeout(resolve5, 1500))
46056
+ ]);
46057
+ }
46058
+ debug(`consumeQuery: for-await loop exited, wasAborted=${wasAborted()}, capturedSessionId=${capturedSessionId?.slice(0, 8) ?? "none"}, failure=${failure?.kind ?? "none"}`);
46059
+ return { capturedSessionId, failure };
46060
+ }
46061
+
46062
+ // src/agents-md.ts
46063
+ import { lstatSync as lstatSync2, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
46064
+ import { dirname as dirname5, join as join9, resolve as resolve3 } from "path";
46065
+ var CONTEXT_FILE_CANDIDATES = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD"];
46066
+ function contextFileInDir(dir) {
46067
+ for (const filename of CONTEXT_FILE_CANDIDATES) {
46068
+ const candidate = join9(dir, filename);
46069
+ try {
46070
+ if (statSync5(candidate).isFile()) return candidate;
46071
+ } catch (error51) {
46072
+ let lstatCode;
46073
+ let entryExists = false;
46074
+ try {
46075
+ lstatSync2(candidate);
46076
+ entryExists = true;
46077
+ } catch (lstatError) {
46078
+ lstatCode = lstatError.code;
46079
+ }
46080
+ if (entryExists || lstatCode !== "ENOENT") {
46081
+ const detail = error51.code ?? String(error51);
46082
+ const suffix = entryExists ? "" : ` (lstat: ${lstatCode ?? "unknown"})`;
46083
+ debug(`agents-md: skipping unusable ${candidate}: ${detail}${suffix}`);
46084
+ }
46085
+ }
46086
+ }
46087
+ return void 0;
46088
+ }
46089
+ function resolveAgentsMdPath() {
46090
+ if (isolatedFromEnv()) return void 0;
46091
+ const fromCwd = findAgentsMdInParents(process.cwd());
46092
+ if (fromCwd) return fromCwd;
46093
+ return contextFileInDir(piUserDir());
46094
+ }
46095
+ function findAgentsMdInParents(startDir) {
46096
+ let current = resolve3(startDir);
46097
+ while (true) {
46098
+ const candidate = contextFileInDir(current);
46099
+ if (candidate) return candidate;
46100
+ const parent = dirname5(current);
46101
+ if (parent === current) break;
46102
+ current = parent;
46103
+ }
46104
+ return void 0;
46105
+ }
46106
+ function extractAgentsAppend() {
46107
+ const agentsPath = resolveAgentsMdPath();
46108
+ if (!agentsPath) return void 0;
46109
+ try {
46110
+ const content = readFileSync7(agentsPath, "utf-8").trim();
46111
+ if (!content) return void 0;
46112
+ const sanitized = sanitizeAgentsContent(content);
46113
+ return sanitized.length > 0 ? `# CLAUDE.md
46114
+
46115
+ ${sanitized}` : void 0;
46116
+ } catch (error51) {
46117
+ debug(`agents-md: failed to read ${agentsPath}:`, error51 instanceof Error ? error51.message : String(error51));
46118
+ return void 0;
46119
+ }
46120
+ }
46121
+ function sanitizeAgentsContent(content) {
46122
+ let sanitized = content;
46123
+ sanitized = sanitized.replace(/~\/\.pi\b/gi, "~/.claude");
46124
+ sanitized = sanitized.replace(/(^|[\s'"`])\.pi\//g, "$1.claude/");
46125
+ sanitized = sanitized.replace(/\b\.pi\b/gi, ".claude");
46126
+ sanitized = sanitized.replace(/\bpi\b/gi, "environment");
46127
+ return sanitized;
46128
+ }
46129
+
46130
+ // src/prompt-context.ts
46131
+ import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
46132
+ import { dirname as dirname6, join as join10, resolve as resolve4 } from "path";
46133
+ function readTrimmed(path) {
46134
+ try {
46135
+ if (!existsSync5(path)) return void 0;
46136
+ const content = readFileSync8(path, "utf8").trim();
46137
+ return content.length > 0 ? content : void 0;
46138
+ } catch (error51) {
46139
+ debug(`prompt-context: failed to read ${path}:`, error51 instanceof Error ? error51.message : String(error51));
46140
+ return void 0;
46141
+ }
46142
+ }
46143
+ function findProjectAppendSystem(startDir) {
46144
+ let current = resolve4(startDir);
46145
+ while (true) {
46146
+ const candidate = join10(current, ".pi", "APPEND_SYSTEM.md");
46147
+ if (existsSync5(candidate)) return candidate;
46148
+ const parent = dirname6(current);
46149
+ if (parent === current) break;
46150
+ current = parent;
46151
+ }
46152
+ return void 0;
46153
+ }
46154
+ function readAppendSystemPromptFiles(cwd) {
46155
+ const files = [
46156
+ { label: "global APPEND_SYSTEM.md", path: join10(piUserDir(), "APPEND_SYSTEM.md") }
46157
+ ];
46158
+ const projectPath = isolatedFromEnv() ? void 0 : findProjectAppendSystem(cwd);
46159
+ if (projectPath) files.push({ label: "project .pi/APPEND_SYSTEM.md", path: projectPath });
46160
+ const seen = /* @__PURE__ */ new Set();
46161
+ const output = [];
46162
+ for (const file2 of files) {
46163
+ if (seen.has(file2.path)) continue;
46164
+ seen.add(file2.path);
46165
+ const content = readTrimmed(file2.path);
46166
+ if (content) output.push({ label: file2.label, content });
46167
+ }
46168
+ return output;
46169
+ }
46170
+ function splitPromptBlocks(systemPrompt) {
46171
+ return (systemPrompt ?? "").split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
46172
+ }
46173
+ function extractHeadingSection(systemPrompt, headings) {
46174
+ if (!systemPrompt) return void 0;
46175
+ let start = -1;
46176
+ for (const heading of headings) {
46177
+ const index = systemPrompt.indexOf(heading);
46178
+ if (index >= 0 && (start < 0 || index < start)) start = index;
46179
+ }
46180
+ if (start < 0) return void 0;
46181
+ const rest = systemPrompt.slice(start).trim();
46182
+ const endCandidates = [
46183
+ rest.slice(1).search(/\n##\s+/),
46184
+ rest.search(/\n<\/project_instructions>/),
46185
+ rest.search(/\n<\/project_context>/)
46186
+ ].map((index, offset) => index >= 0 && offset === 0 ? index + 1 : index).filter((index) => index >= 0);
46187
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : -1;
46188
+ return (end >= 0 ? rest.slice(0, end) : rest).trim();
46189
+ }
46190
+ function extractBlockByMarkers(systemPrompt, markers) {
46191
+ for (const block of splitPromptBlocks(systemPrompt)) {
46192
+ if (markers.some((marker) => marker.test(block))) return block;
46193
+ }
46194
+ return void 0;
46195
+ }
46196
+ function buildPromptContextAppend(systemPrompt, cwd, settings) {
46197
+ const parts = [];
46198
+ const labels = [];
46199
+ if (settings.includeAppendSystemPromptMd) {
46200
+ for (const file2 of readAppendSystemPromptFiles(cwd)) {
46201
+ parts.push(xmlBlock("append_system_prompt", { label: file2.label }, file2.content));
46202
+ labels.push(file2.label);
46203
+ }
46204
+ }
46205
+ if (settings.includeProjectAgentsHook) {
46206
+ const projectAgents = extractHeadingSection(systemPrompt, ["## Project Agents", "## Project Subagents"]);
46207
+ if (projectAgents) {
46208
+ parts.push(xmlBlock("before_agent_start", { source: "project-agents" }, projectAgents));
46209
+ labels.push("project agents hook");
46210
+ }
46211
+ }
46212
+ if (settings.includeTaskPanelHook) {
46213
+ const taskReminder = extractBlockByMarkers(systemPrompt, [/^Task workflow reminder:/]);
46214
+ if (taskReminder) {
46215
+ parts.push(xmlBlock("before_agent_start", { source: "task-panel" }, taskReminder));
46216
+ labels.push("task panel hook");
46217
+ }
46218
+ }
46219
+ if (settings.includeCavemanHook) {
46220
+ const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
46221
+ if (caveman) {
46222
+ parts.push(xmlBlock("before_agent_start", { source: "caveman" }, caveman));
46223
+ labels.push("caveman hook");
46224
+ }
46225
+ }
46226
+ if (parts.length === 0) return { labels };
46227
+ return {
46228
+ labels,
46229
+ text: xmlBlock(
46230
+ "forwarded_pi_context",
46231
+ {},
46232
+ [
46233
+ "The following content was explicitly enabled in pi-claude-bridge settings and comes from Pi prompt files or before_agent_start prompt hooks.",
46234
+ ...parts
46235
+ ].join("\n\n"),
46236
+ false
46237
+ )
46238
+ };
46239
+ }
46240
+ function escapeXmlAttr(value) {
46241
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
46242
+ }
46243
+ function escapeXmlText(value) {
46244
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
46245
+ }
46246
+ function xmlBlock(tag, attrs, content, escapeContent = true) {
46247
+ const attrText = Object.entries(attrs).map(([key, value]) => ` ${key}="${escapeXmlAttr(value)}"`).join("");
46248
+ const body = escapeContent ? escapeXmlText(content.trim()) : content.trim();
46249
+ return `<${tag}${attrText}>
46250
+ ${body}
46251
+ </${tag}>`;
46252
+ }
46253
+
46254
+ // src/query-options.ts
46255
+ var REASONING_TO_EFFORT = {
46256
+ minimal: "low",
46257
+ low: "low",
46258
+ medium: "medium",
46259
+ high: "high",
46260
+ xhigh: "max",
46261
+ max: "max"
46262
+ };
46263
+ function normalizeEffortOverrideModelKey(value) {
46264
+ const key = value.trim().toLowerCase();
46265
+ return key.startsWith(`${PROVIDER_ID}/`) ? key.slice(PROVIDER_ID.length + 1) : key;
46266
+ }
46267
+ function resolveConfiguredEffort(modelId, reasoningEffort, providerConfig) {
46268
+ const target = normalizeEffortOverrideModelKey(modelId);
46269
+ for (const [key, rawEffort] of Object.entries(providerConfig?.modelEffortOverrides ?? {})) {
46270
+ const normalizedKey = normalizeEffortOverrideModelKey(key);
46271
+ if (normalizedKey !== "*" && normalizedKey !== target) continue;
46272
+ const effort = normalizeEffortLevel(rawEffort);
46273
+ if (effort) return effort;
46274
+ }
46275
+ return normalizeEffortLevel(providerConfig?.forceEffort) ?? reasoningEffort;
46276
+ }
46277
+ function buildClaudeQueryOptions(input) {
46278
+ const { cwd, requestedModel, queryModel, account, bridgeConfig, systemPrompt, reasoning, resumeSessionId, mcpServers, claudeExecutable } = input;
46279
+ const providerSettings = bridgeConfig.provider ?? {};
46280
+ const accountScope = accountSessionScope(account);
46281
+ const enableCloudMcp = connectorsEnabledFor(bridgeConfig);
46282
+ const connectorWriteMode = connectorWriteModeFor(bridgeConfig);
46283
+ const connectorServers = enableCloudMcp ? connectorServersSnapshot(accountScope.claudeConfigDir) : {};
46284
+ const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
46285
+ const agentsAppend = appendSystemPrompt ? extractAgentsAppend() : void 0;
46286
+ const skillsAppend = appendSystemPrompt ? extractSkillsBlock(systemPrompt) : void 0;
46287
+ const promptContextAppend = buildPromptContextAppend(systemPrompt, cwd, bridgeConfig.promptContext ?? {});
46288
+ const appendParts = [agentsAppend, skillsAppend, promptContextAppend.text].filter((part) => Boolean(part));
46289
+ const systemPromptAppend = appendParts.length > 0 ? appendParts.join("\n\n") : void 0;
46290
+ const settingSources = settingSourcesForQuery(
46291
+ enableCloudMcp,
46292
+ appendSystemPrompt,
46293
+ providerSettings.settingSources
46294
+ );
46295
+ const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
46296
+ const requestedEffort = reasoning ? queryModel.thinkingLevelMap?.[reasoning] ?? REASONING_TO_EFFORT[reasoning] : void 0;
46297
+ const effort = resolveConfiguredEffort(queryModel.id, requestedEffort, providerSettings);
46298
+ const extraArgs = {};
46299
+ if (effort) extraArgs["thinking-display"] = "summarized";
46300
+ const fallbackModel = account && requestedModel.id === FABLE_MODEL_ID && queryModel.id === requestedModel.id ? void 0 : fallbackModelForPrimaryModel(queryModel.id);
46301
+ const childEnv = {
46302
+ ...account ? subscriberProfileEnv(account) : process.env,
46303
+ ENABLE_CLAUDEAI_MCP_SERVERS: enableCloudMcp ? "1" : "0",
46304
+ DISABLE_AUTO_COMPACT: "1"
46305
+ };
46306
+ const queryOptions = {
46307
+ cwd,
46308
+ model: queryModel.id,
46309
+ env: childEnv,
46310
+ ...connectorQueryOptions(enableCloudMcp, connectorWriteMode),
46311
+ permissionMode: "bypassPermissions",
46312
+ includePartialMessages: true,
46313
+ ...fallbackModel ? { fallbackModel } : {},
46314
+ ...providerSettings.fastMode ? { settings: { fastMode: true } } : {},
46315
+ systemPrompt: {
46316
+ type: "preset",
46317
+ preset: "claude_code",
46318
+ append: systemPromptAppend ? systemPromptAppend : void 0
46319
+ },
46320
+ extraArgs,
46321
+ ...strictMcpConfigEnabled ? { strictMcpConfig: true } : {},
46322
+ ...effort ? { effort } : {},
46323
+ ...settingSources ? { settingSources } : {},
46324
+ ...mcpServers || Object.keys(connectorServers).length > 0 ? { mcpServers: { ...mcpServers ?? {}, ...connectorServers } } : {},
46325
+ ...resumeSessionId ? { resume: resumeSessionId } : {},
46326
+ ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
46327
+ spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
46328
+ ...makeCliDebugOptions("provider")
46329
+ };
46330
+ return {
46331
+ queryOptions,
46332
+ enableCloudMcp,
46333
+ appendSystemPrompt,
46334
+ promptContextLabels: promptContextAppend.labels,
46335
+ strictMcpConfigEnabled,
46336
+ ...effort ? { effort } : {},
46337
+ ...fallbackModel ? { fallbackModel } : {}
46338
+ };
46339
+ }
46340
+
46341
+ // src/index.ts
46342
+ var _piAi = piAi;
46343
+ var getModels = await resolveGetModels(_piAi);
46344
+ var newAssistantMessageEventStream = typeof _piAi.createAssistantMessageEventStream === "function" ? _piAi.createAssistantMessageEventStream : () => new _piAi.AssistantMessageEventStream();
46345
+ var PRIMARY_INSTANCE_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:primaryInstance");
46346
+ var ACTIVE_STREAM_SIMPLE_KEY = /* @__PURE__ */ Symbol.for("claude-bridge:activeStreamSimple");
46347
+ var ROTATION_STATE_KEY = /* @__PURE__ */ Symbol("claude-bridge:rotationState");
46348
+ var MAX_ROTATION_ATTEMPTS = 16;
46349
+ var MODELS = buildModels(getModels("anthropic"));
45080
46350
  function extractAllToolResults2(context) {
45081
46351
  const { results, stopIdx } = extractAllToolResults(context.messages);
45082
46352
  debug(`extractAllToolResults: ${results.length} results from ${context.messages.length} msgs, stopped at index ${stopIdx}`);
@@ -45087,43 +46357,59 @@ function extractAllToolResults2(context) {
45087
46357
  return results;
45088
46358
  }
45089
46359
  function extractUserPrompt(messages) {
45090
- const last = messages[messages.length - 1];
45091
- if (!last || last.role !== "user") return null;
45092
- if (typeof last.content === "string") return last.content;
45093
- return messageContentToText(last.content) || "";
46360
+ if (messages.length === 0 || messages.some((message) => message.role !== "user")) return null;
46361
+ return messages.map(
46362
+ (message) => typeof message.content === "string" ? message.content : messageContentToText(message.content) || ""
46363
+ ).join("\n\n");
45094
46364
  }
45095
46365
  function extractUserPromptBlocks(messages) {
45096
- const last = messages[messages.length - 1];
45097
- if (!last || last.role !== "user") return null;
45098
- if (typeof last.content === "string") {
45099
- debug(`extractUserPromptBlocks: content is string (length=${last.content.length})`);
45100
- return null;
45101
- }
45102
- if (!Array.isArray(last.content)) {
45103
- debug(`extractUserPromptBlocks: content is ${typeof last.content}`);
45104
- return null;
45105
- }
45106
- debug(`extractUserPromptBlocks: ${last.content.length} blocks, types=${last.content.map((b) => b.type).join(",")}`);
46366
+ if (messages.length === 0 || messages.some((message) => message.role !== "user")) return null;
45107
46367
  let hasImage = false;
45108
46368
  const blocks = [];
45109
- for (const block of last.content) {
45110
- if (block.type === "text" && block.text) {
45111
- blocks.push({ type: "text", text: block.text });
45112
- } else if (block.type === "image") {
45113
- debug(`image block: mimeType=${block.mimeType}, data length=${(block.data ?? "").length}, keys=${Object.keys(block).join(",")}`);
45114
- if (!block.data || !block.mimeType) {
45115
- debug(`image block missing data or mimeType, skipping`);
45116
- continue;
46369
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
46370
+ const content = messages[messageIndex].content;
46371
+ if (messageIndex > 0) blocks.push({ type: "text", text: "\n\n" });
46372
+ if (typeof content === "string") {
46373
+ if (content) blocks.push({ type: "text", text: content });
46374
+ continue;
46375
+ }
46376
+ if (!Array.isArray(content)) {
46377
+ debug(`extractUserPromptBlocks: content is ${typeof content}`);
46378
+ continue;
46379
+ }
46380
+ debug(`extractUserPromptBlocks: ${content.length} blocks, types=${content.map((b) => b.type).join(",")}`);
46381
+ for (const block of content) {
46382
+ if (block.type === "text" && block.text) {
46383
+ blocks.push({ type: "text", text: block.text });
46384
+ } else if (block.type === "image") {
46385
+ debug(`image block: mimeType=${block.mimeType}, data length=${(block.data ?? "").length}, keys=${Object.keys(block).join(",")}`);
46386
+ if (!block.data || !block.mimeType) {
46387
+ debug(`image block missing data or mimeType, skipping`);
46388
+ continue;
46389
+ }
46390
+ hasImage = true;
46391
+ blocks.push({
46392
+ type: "image",
46393
+ source: { type: "base64", media_type: block.mimeType, data: block.data }
46394
+ });
45117
46395
  }
45118
- hasImage = true;
45119
- blocks.push({
45120
- type: "image",
45121
- source: { type: "base64", media_type: block.mimeType, data: block.data }
45122
- });
45123
46396
  }
45124
46397
  }
45125
46398
  return hasImage ? blocks : null;
45126
46399
  }
46400
+ function planDeferredUserReplay(messages, capturedThrough = 0) {
46401
+ let runStart = messages.length;
46402
+ while (runStart > capturedThrough && messages[runStart - 1]?.role === "user") runStart--;
46403
+ const trailingUsers = messages.slice(runStart);
46404
+ const prompt = trailingUsers.length > 0 ? extractUserPrompt(trailingUsers) : null;
46405
+ const blocks = trailingUsers.length > 0 ? extractUserPromptBlocks(trailingUsers) : null;
46406
+ return {
46407
+ runStart,
46408
+ userMessageCount: trailingUsers.length,
46409
+ prompt: prompt?.trim() ? prompt : null,
46410
+ blocks
46411
+ };
46412
+ }
45127
46413
  async function* wrapPromptStream(blocks) {
45128
46414
  yield {
45129
46415
  type: "user",
@@ -45183,162 +46469,44 @@ function buildMcpServers(tools, queryCtx) {
45183
46469
  });
45184
46470
  return { content: [{ type: "text", text: `Claude bridge internal error: no matching tool_call id for ${tool.name}` }], isError: true };
45185
46471
  }
45186
- if (claim.argsMismatch) {
45187
- debug(`mcp handler: ${tool.name} [${toolCallId}] claimed sole same-name call despite args mismatch`);
45188
- diagDump("tool_claim_args_mismatch", {
45189
- toolName: tool.name,
45190
- toolCallId,
45191
- handlerArgKeys: argKeys(mappedArgs),
45192
- recordedArgKeys: argKeys(queryCtx.turnToolCalls.find((call) => call.id === toolCallId)?.arguments)
45193
- });
45194
- } else if (claim.match !== "tool-args" || claim.ambiguous) {
45195
- debug(`mcp handler: ${tool.name} [${toolCallId}] claimed by ${claim.match}${claim.ambiguous ? " (ambiguous)" : ""}`);
45196
- }
45197
- if (toolCallId && queryCtx.pendingResults.has(toolCallId)) {
45198
- const result = queryCtx.pendingResults.get(toolCallId);
45199
- queryCtx.pendingResults.delete(toolCallId);
45200
- queryCtx.markToolResultResolved(toolCallId);
45201
- debug(`mcp handler: ${tool.name} [${toolCallId}] \u2192 resolved from queue (${queryCtx.pendingResults.size} remaining)`);
45202
- return result;
45203
- }
45204
- debug(`mcp handler: ${tool.name} [${toolCallId}] \u2192 waiting`);
45205
- scheduleToolUseTurnEnd(
45206
- queryCtx,
45207
- () => finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, tool.name, mappedArgs),
45208
- `mcp-invocation:${tool.name}`
45209
- );
45210
- return new Promise((resolve5) => {
45211
- queryCtx.pendingToolCalls.set(toolCallId, {
45212
- toolName: tool.name,
45213
- resolve: (result) => {
45214
- queryCtx.markToolResultResolved(toolCallId);
45215
- resolve5(result);
45216
- }
45217
- });
45218
- });
45219
- }
45220
- }));
45221
- const server = f0e({ name: MCP_SERVER_NAME, version: "1.0.0", tools: mcpTools });
45222
- return { [MCP_SERVER_NAME]: server };
45223
- }
45224
- var REASONING_TO_EFFORT = {
45225
- minimal: "low",
45226
- low: "low",
45227
- medium: "medium",
45228
- high: "high",
45229
- xhigh: "max",
45230
- max: "max"
45231
- };
45232
- function normalizeEffortOverrideModelKey(value) {
45233
- const key = value.trim().toLowerCase();
45234
- return key.startsWith(`${PROVIDER_ID}/`) ? key.slice(PROVIDER_ID.length + 1) : key;
45235
- }
45236
- function resolveConfiguredEffort(modelId, reasoningEffort, providerConfig) {
45237
- const target = normalizeEffortOverrideModelKey(modelId);
45238
- for (const [key, rawEffort] of Object.entries(providerConfig?.modelEffortOverrides ?? {})) {
45239
- const normalizedKey = normalizeEffortOverrideModelKey(key);
45240
- if (normalizedKey !== "*" && normalizedKey !== target) continue;
45241
- const effort = normalizeEffortLevel(rawEffort);
45242
- if (effort) return effort;
45243
- }
45244
- return normalizeEffortLevel(providerConfig?.forceEffort) ?? reasoningEffort;
45245
- }
45246
- async function consumeQuery(sdkQuery, customToolNameToPi, model, cwd, bridgeConfig, wasAborted) {
45247
- let capturedSessionId;
45248
- for await (const message of sdkQuery) {
45249
- if (wasAborted()) break;
45250
- const queryCtx = ctx();
45251
- activeStreamIdleWatchdogs.get(queryCtx)?.noteChunk();
45252
- if (!queryCtx.turnOutput) continue;
45253
- if (!queryCtx.currentPiStream && !(message.type === "assistant" && queryCtx.turnSawToolCall)) continue;
45254
- switch (message.type) {
45255
- case "stream_event":
45256
- processStreamEvent(message, customToolNameToPi, model);
45257
- break;
45258
- case "assistant":
45259
- processAssistantMessage(message, model, customToolNameToPi);
45260
- break;
45261
- case "result":
45262
- if (!ctx().turnSawStreamEvent && message.subtype === "success") {
45263
- const text = message.result || "";
45264
- if (ctx().turnBlocks.some((b) => b.type === "text" && b.text === text)) {
45265
- debug("consumeQuery: result text already rendered by assistant fallback; skipping duplicate");
45266
- break;
45267
- }
45268
- ensureTurnStarted();
45269
- ctx().turnBlocks.push({ type: "text", text });
45270
- const idx = ctx().turnBlocks.length - 1;
45271
- ctx().currentPiStream?.push({ type: "text_start", contentIndex: idx, partial: ctx().turnOutput });
45272
- ctx().currentPiStream?.push({ type: "text_delta", contentIndex: idx, delta: text, partial: ctx().turnOutput });
45273
- ctx().currentPiStream?.push({ type: "text_end", contentIndex: idx, content: text, partial: ctx().turnOutput });
45274
- } else if (message.subtype !== "success" && (isExtraUsageRequiredMessage(message) || isUsageLimitMessage(message))) {
45275
- const errorLines = Array.isArray(message.errors) ? uniqueNonEmptyLines(message.errors) : [];
45276
- const errors = errorLines.length > 0 ? errorLines.join("\n") : String(message.subtype ?? "Claude Code rate limit");
45277
- const extraUsage = isExtraUsageRequiredMessage(message);
45278
- const openedExtraUsage = extraUsage && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "result error");
45279
- ctx().handledTerminalError = true;
45280
- ctx().turnOutput.stopReason = "error";
45281
- const extraUsageHint = openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : extraUsage ? "\n\nRun /claude-bridge:extra, or enable Allow extra usage helper in settings." : "";
45282
- ctx().turnOutput.errorMessage = `${errors}${extraUsageHint}`;
45283
- ctx().currentPiStream?.push({ type: "error", reason: "error", error: ctx().turnOutput });
45284
- ctx().currentPiStream?.end();
45285
- ctx().currentPiStream = null;
45286
- }
45287
- break;
45288
- case "system":
45289
- if (message.subtype === "init" && message.session_id) {
45290
- capturedSessionId = message.session_id;
45291
- queryCtx.childSessionId = capturedSessionId;
45292
- noteFastModeDisabledReason(message, bridgeConfig);
45293
- } else if (message.subtype === "model_refusal_fallback") {
45294
- const originalModel = message.original_model;
45295
- const fallbackModel = message.fallback_model;
45296
- updateTurnOutputModel(fallbackModel);
45297
- debug("consumeQuery: model_refusal_fallback", JSON.stringify({ originalModel, fallbackModel }));
45298
- if (typeof fallbackModel === "string" && typeof originalModel === "string" && fallbackModelForPrimaryModel(originalModel) === fallbackModel) {
45299
- safeNotify(
45300
- `Claude bridge switched ${modelDisplayName(originalModel)} to ${modelDisplayName(fallbackModel)} after Claude Code safety fallback.`,
45301
- "info"
45302
- );
45303
- }
45304
- }
45305
- break;
45306
- case "user":
45307
- noteChildExecutedToolResults(message);
45308
- break;
45309
- case "rate_limit_event": {
45310
- const info = message.rate_limit_info;
45311
- debug("consumeQuery: rate_limit_event", JSON.stringify(info).slice(0, 300));
45312
- if (info?.status === "rejected") {
45313
- const resetsAt = formatResetTimestamp(info.resetsAt);
45314
- const resetAtMs = resetTimestampMs(info.resetsAt);
45315
- const reason = `${info.rateLimitType ?? "unknown"} rate limit`;
45316
- const launchedExtraUsage = isExtraUsageRequiredMessage(info) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, reason);
45317
- emitRateLimitEvent({
45318
- model: model.id,
45319
- provider: PROVIDER_ID,
45320
- rateLimitType: info.rateLimitType,
45321
- reason,
45322
- resetAt: info.resetsAt,
45323
- ...Number.isFinite(resetAtMs) ? { resetAtMs } : {},
45324
- source: "claude-bridge",
45325
- status: "rejected"
45326
- });
45327
- piUI?.notify(`${RATE_LIMIT_TOKEN} Claude ${reason} hit \u2014 resets ${resetsAt}${launchedExtraUsage ? "; opened /extra-usage helper" : ""}`, "warning");
45328
- } else if (info?.status === "allowed_warning") {
45329
- const warning = formatAllowedRateLimitWarning(info);
45330
- if (warning) piUI?.notify(warning, "warning");
45331
- else debug("consumeQuery: suppressed low/ambiguous allowed_warning rate_limit_event", JSON.stringify(info).slice(0, 300));
45332
- }
45333
- break;
45334
- }
45335
- default:
45336
- debug("consumeQuery: unhandled SDK message type", message.type);
45337
- break;
46472
+ if (claim.argsMismatch) {
46473
+ debug(`mcp handler: ${tool.name} [${toolCallId}] claimed sole same-name call despite args mismatch`);
46474
+ diagDump("tool_claim_args_mismatch", {
46475
+ toolName: tool.name,
46476
+ toolCallId,
46477
+ handlerArgKeys: argKeys(mappedArgs),
46478
+ recordedArgKeys: argKeys(queryCtx.turnToolCalls.find((call) => call.id === toolCallId)?.arguments)
46479
+ });
46480
+ } else if (claim.match !== "tool-args" || claim.ambiguous) {
46481
+ debug(`mcp handler: ${tool.name} [${toolCallId}] claimed by ${claim.match}${claim.ambiguous ? " (ambiguous)" : ""}`);
46482
+ }
46483
+ const earlyResult = toolCallId ? takeQueuedOrParkedResult(queryCtx, toolCallId) : void 0;
46484
+ if (earlyResult !== void 0) {
46485
+ queryCtx.markToolResultResolved(toolCallId);
46486
+ debug(`mcp handler: ${tool.name} [${toolCallId}] \u2192 resolved from queue/parked (${queryCtx.pendingResults.size} queued, ${queryCtx.reapedResults.size} parked remaining)`);
46487
+ return earlyResult;
46488
+ }
46489
+ debug(`mcp handler: ${tool.name} [${toolCallId}] \u2192 waiting`);
46490
+ scheduleToolUseTurnEnd(
46491
+ queryCtx,
46492
+ () => finalizeToolUseTurnFromMcpInvocation(queryCtx, toolCallId, tool.name, mappedArgs),
46493
+ `mcp-invocation:${tool.name}`
46494
+ );
46495
+ return new Promise((resolve5) => {
46496
+ queryCtx.pendingToolCalls.set(toolCallId, {
46497
+ toolName: tool.name,
46498
+ args: mappedArgs,
46499
+ generation: queryCtx.callbackGeneration,
46500
+ resolve: (result) => {
46501
+ queryCtx.markToolResultResolved(toolCallId);
46502
+ resolve5(result);
46503
+ }
46504
+ });
46505
+ });
45338
46506
  }
45339
- }
45340
- debug(`consumeQuery: for-await loop exited, wasAborted=${wasAborted()}, capturedSessionId=${capturedSessionId?.slice(0, 8) ?? "none"}`);
45341
- return { capturedSessionId };
46507
+ }));
46508
+ const server = f0e({ name: MCP_SERVER_NAME, version: "1.0.0", tools: mcpTools });
46509
+ return { [MCP_SERVER_NAME]: server };
45342
46510
  }
45343
46511
  function claimPrimaryInstance() {
45344
46512
  const g = globalThis;
@@ -45347,6 +46515,9 @@ function claimPrimaryInstance() {
45347
46515
  }
45348
46516
  function releaseProviderTokens(event) {
45349
46517
  const g = globalThis;
46518
+ if (g[CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL] === BRIDGE_ACCOUNT_HOST) {
46519
+ g[CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL] = void 0;
46520
+ }
45350
46521
  if (g[ACTIVE_STREAM_SIMPLE_KEY] === streamClaudeAgentSdk) {
45351
46522
  debug(`${event}: clearing ACTIVE_STREAM_SIMPLE_KEY`);
45352
46523
  g[ACTIVE_STREAM_SIMPLE_KEY] = void 0;
@@ -45378,12 +46549,20 @@ function applyProviderRegistration(trigger) {
45378
46549
  }
45379
46550
  return;
45380
46551
  }
45381
- const credentialed = hasClaudeCredentials();
46552
+ const credentialed = hasClaudeCredentials() || Boolean(resolveClaudeAccountRouter());
45382
46553
  debug(`${trigger}: native registration upsert, credentialed=${credentialed} (module=${moduleInstanceId})`);
45383
- if (credentialed && connectorsEnabledFor(loadConfig(process.cwd()))) primeConnectorServers();
46554
+ if (hasClaudeCredentials() && connectorsEnabledFor(loadConfig(process.cwd()))) primeConnectorServers();
45384
46555
  g[ACTIVE_STREAM_SIMPLE_KEY] = streamClaudeAgentSdk;
45385
46556
  try {
45386
- nativeProviderInstance ??= buildNativeProvider(_piAi, MODELS, streamClaudeAgentSdk);
46557
+ nativeProviderInstance ??= buildNativeProvider(
46558
+ _piAi,
46559
+ MODELS,
46560
+ streamClaudeAgentSdk,
46561
+ process.env,
46562
+ // Availability includes a companion account pool: the router owns
46563
+ // credentials the direct existence probes cannot see.
46564
+ () => hasClaudeCredentials() || Boolean(resolveClaudeAccountRouter())
46565
+ );
45387
46566
  pi.registerProvider(nativeProviderInstance);
45388
46567
  } catch (err) {
45389
46568
  if (g[ACTIVE_STREAM_SIMPLE_KEY] === streamClaudeAgentSdk) g[ACTIVE_STREAM_SIMPLE_KEY] = void 0;
@@ -45391,7 +46570,17 @@ function applyProviderRegistration(trigger) {
45391
46570
  }
45392
46571
  }
45393
46572
  function streamClaudeAgentSdk(model, context, options) {
46573
+ return runInRequestLane(options?.sessionId, () => streamClaudeAgentSdkInLane(model, context, options));
46574
+ }
46575
+ function streamClaudeAgentSdkInLane(model, context, options) {
45394
46576
  const stream = newAssistantMessageEventStream();
46577
+ const laneId = currentRequestLaneId();
46578
+ const ephemeralLane = laneId !== void 0 && options?.cacheRetention === "none";
46579
+ const releaseEphemeralLane = () => {
46580
+ if (!ephemeralLane) return;
46581
+ deleteSharedSessionLane(laneId);
46582
+ deleteQueryLane(laneId);
46583
+ };
45395
46584
  const lastMsgRole = context.messages[context.messages.length - 1]?.role;
45396
46585
  const cwd = options?.cwd ?? process.cwd();
45397
46586
  debug(`provider: streamClaudeAgentSdk called, activeQuery=${!!ctx().activeQuery}, lastMsgRole=${lastMsgRole}, isReentrant=${ctx().activeQuery !== null}`);
@@ -45399,13 +46588,14 @@ function streamClaudeAgentSdk(model, context, options) {
45399
46588
  const queryCtx = ctx();
45400
46589
  queryCtx.currentPiStream = stream;
45401
46590
  queryCtx.resetTurnState(model);
46591
+ queryCtx.callbackGeneration += 1;
45402
46592
  activeStreamIdleWatchdogs.get(queryCtx)?.refresh();
45403
46593
  const allResults = extractAllToolResults2(context);
45404
46594
  debug(`provider: tool results, ${allResults.length} results, ${queryCtx.pendingToolCalls.size} waiting handlers, ctx.msgs=${context.messages.length}`);
45405
46595
  const unmatchedResultIds = [];
45406
46596
  for (const result of allResults) {
45407
46597
  const id = result.toolCallId;
45408
- if (id && !queryCtx.hasRecordedToolCall(id)) {
46598
+ if (id && !queryCtx.hasRecordedToolCall(id) && !queryCtx.forwardedToolCallIds.has(id)) {
45409
46599
  queryCtx.markToolResultUnmatched(id);
45410
46600
  unmatchedResultIds.push(id);
45411
46601
  debug(`ERROR: tool result [${id}] has no registered tool_call id; refusing to queue or deliver`);
@@ -45424,7 +46614,7 @@ function streamClaudeAgentSdk(model, context, options) {
45424
46614
  debug(`WARNING: tool result without toolCallId, cannot match`);
45425
46615
  }
45426
46616
  if (queryCtx.pendingToolCalls.size > 0 && queryCtx.pendingResults.size > 0) {
45427
- debug(`BUG: both maps non-empty! handlers=${queryCtx.pendingToolCalls.size} results=${queryCtx.pendingResults.size}`);
46617
+ debug(`note: handlers and queued results coexist: handlers=${queryCtx.pendingToolCalls.size} results=${queryCtx.pendingResults.size}`);
45428
46618
  }
45429
46619
  }
45430
46620
  if (unmatchedResultIds.length > 0) {
@@ -45432,38 +46622,65 @@ function streamClaudeAgentSdk(model, context, options) {
45432
46622
  content: [{ type: "text", text: `Claude bridge internal error: ${unmatchedResultIds.length} tool result(s) did not match any registered tool_call id. The turn was stopped to avoid delivering tool output to the wrong call. Unmatched ids: ${unmatchedResultIds.slice(0, 8).join(", ")}${unmatchedResultIds.length > 8 ? ", ..." : ""}` }],
45433
46623
  isError: true
45434
46624
  };
45435
- for (const pending of queryCtx.pendingToolCalls.values()) pending.resolve(errorResult);
46625
+ for (const [pendingId, pending] of queryCtx.pendingToolCalls) {
46626
+ if (!queryCtx.forwardedToolCallIds.has(pendingId)) queryCtx.deadToolCallIds.add(pendingId);
46627
+ pending.resolve(errorResult);
46628
+ }
45436
46629
  queryCtx.pendingToolCalls.clear();
45437
46630
  reportToolResultMismatch(queryCtx, "unmatched tool result", cwd);
45438
46631
  }
45439
46632
  if (queryCtx.pendingToolCalls.size > 0) {
45440
- debug(`WARNING: ${queryCtx.pendingToolCalls.size} MCP handlers still waiting after delivering ${allResults.length} results`);
45441
- piUI?.notify(`Claude bridge: ${queryCtx.pendingToolCalls.size} tool handler(s) still waiting \u2014 provider may be stuck`, "warning");
46633
+ const stranded = drainStrandedToolCalls(queryCtx);
46634
+ if (stranded.length > 0) {
46635
+ const names = stranded.map((entry) => entry.toolName).join(", ");
46636
+ debug(`provider: failed ${stranded.length} stranded MCP handler(s) never forwarded to Pi: ${names}`);
46637
+ diagDump("tool_handlers_stranded", { count: stranded.length, stranded });
46638
+ appendIntegrityEntry("tool_handlers_stranded", { count: stranded.length, stranded });
46639
+ safeNotify(`Claude bridge: failed ${stranded.length} tool call(s) that never reached Pi before their turn ended (${names}). The model saw a retryable error.`, "warning");
46640
+ }
46641
+ if (queryCtx.pendingToolCalls.size > 0) {
46642
+ debug(`WARNING: ${queryCtx.pendingToolCalls.size} MCP handlers still waiting after delivering ${allResults.length} results`);
46643
+ safeNotify(`Claude bridge: ${queryCtx.pendingToolCalls.size} tool handler(s) still waiting \u2014 provider may be stuck`, "warning");
46644
+ }
45442
46645
  }
46646
+ let capturedThrough = context.messages.length;
45443
46647
  if (lastMsgRole === "user") {
45444
- const userPrompt = extractUserPrompt(context.messages);
45445
- if (userPrompt) {
45446
- ctx().deferredUserMessages.push(userPrompt);
45447
- debug(`provider: deferred user message for replay after query: ${userPrompt.slice(0, 60)}`);
46648
+ const replay = planDeferredUserReplay(context.messages, queryCtx.latestCursor);
46649
+ if (replay.prompt || replay.blocks) {
46650
+ ctx().deferredUserMessages.push({ text: replay.prompt ?? "", blocks: replay.blocks ?? void 0 });
46651
+ debug(`provider: deferred ${replay.userMessageCount} user message(s) for replay after query${replay.blocks ? ` (${replay.blocks.length} blocks incl. images)` : ""}: ${(replay.prompt ?? "[image-only]").slice(0, 60)}`);
46652
+ } else {
46653
+ capturedThrough = replay.runStart;
46654
+ diagDump("deferred_user_replay_skipped", {
46655
+ contextLength: context.messages.length,
46656
+ runStart: replay.runStart,
46657
+ userMessageCount: replay.userMessageCount,
46658
+ messageRoles: context.messages.map((m, i) => `[${i}]${m.role}`).join(" ")
46659
+ });
45448
46660
  }
45449
46661
  }
45450
- if (sharedSession) sharedSession.cursor = context.messages.length;
45451
- queryCtx.latestCursor = Math.max(queryCtx.latestCursor, context.messages.length);
46662
+ const activeSession = getSharedSession();
46663
+ if (activeSession && stackDepth() === 0 && !queryCtx.detachedFromSharedSession) {
46664
+ setSharedSession({ ...activeSession, cursor: Math.max(activeSession.cursor, capturedThrough) });
46665
+ }
46666
+ queryCtx.latestCursor = Math.max(queryCtx.latestCursor, capturedThrough);
45452
46667
  return stream;
45453
46668
  }
45454
46669
  const lastMsg = context.messages[context.messages.length - 1];
45455
46670
  if (lastMsg?.role === "toolResult") {
45456
46671
  debug(`provider: orphaned tool result after abort, emitting end_turn`);
45457
- if (sharedSession) sharedSession.cursor = context.messages.length;
46672
+ const activeSession = getSharedSession();
46673
+ if (activeSession && stackDepth() === 0 && !ctx().detachedFromSharedSession) setSharedSession({ ...activeSession, cursor: context.messages.length });
45458
46674
  const c = ctx();
45459
46675
  queueMicrotask(() => {
45460
46676
  c.resetTurnState(model);
45461
46677
  stream.push({ type: "done", reason: "stop", message: c.turnOutput });
45462
46678
  stream.end();
46679
+ releaseEphemeralLane();
45463
46680
  });
45464
46681
  return stream;
45465
46682
  }
45466
- if (!hasClaudeCredentials()) {
46683
+ if (!hasClaudeCredentials() && !resolveClaudeAccountRouter()) {
45467
46684
  try {
45468
46685
  applyProviderRegistration("pre-spawn");
45469
46686
  } catch {
@@ -45491,6 +46708,7 @@ function streamClaudeAgentSdk(model, context, options) {
45491
46708
  queueMicrotask(() => {
45492
46709
  stream.push({ type: "error", reason: "error", error: errorOutput });
45493
46710
  stream.end();
46711
+ releaseEphemeralLane();
45494
46712
  });
45495
46713
  return stream;
45496
46714
  }
@@ -45500,87 +46718,167 @@ function streamClaudeAgentSdk(model, context, options) {
45500
46718
  ctx().currentPiStream = stream;
45501
46719
  ctx().pendingToolCalls.clear();
45502
46720
  ctx().pendingResults.clear();
46721
+ ctx().reapedResults.clear();
46722
+ ctx().forwardedToolCallIds.clear();
46723
+ ctx().deadToolCallIds.clear();
46724
+ ctx().callbackGeneration = 0;
45503
46725
  ctx().deferredUserMessages = [];
45504
46726
  ctx().resetTurnState(model);
45505
46727
  ctx().resetToolTracking();
45506
46728
  ctx().latestCursor = 0;
46729
+ ctx().committedOutput = false;
46730
+ ctx().detachedFromSharedSession = isReentrant;
46731
+ const router = resolveClaudeAccountRouter();
46732
+ const rotationOptions = options;
46733
+ const rotationState = rotationOptions?.[ROTATION_STATE_KEY] ?? {
46734
+ excludedProfileIds: /* @__PURE__ */ new Set(),
46735
+ attempts: 0
46736
+ };
46737
+ let account;
46738
+ if (router) {
46739
+ try {
46740
+ account = router.acquire({
46741
+ modelId: model.id,
46742
+ sessionId: options?.sessionId,
46743
+ excludedProfileIds: [...rotationState.excludedProfileIds],
46744
+ forceRerank: rotationState.attempts > 0,
46745
+ reason: rotationState.attempts > 0 ? "automatic-failover" : void 0
46746
+ });
46747
+ rotationState.attempts += 1;
46748
+ } catch (error51) {
46749
+ const message = error51 instanceof Error ? error51.message : String(error51);
46750
+ const resetAtMs = Number(error51?.resetAtMs);
46751
+ const rateLimitType = error51?.rateLimitType;
46752
+ if (ctx().turnOutput) {
46753
+ ctx().turnOutput.stopReason = "error";
46754
+ ctx().turnOutput.errorMessage = message;
46755
+ if (Number.isFinite(resetAtMs)) {
46756
+ Object.assign(ctx().turnOutput, { resetAtMs, rateLimitType });
46757
+ }
46758
+ }
46759
+ if (Number.isFinite(resetAtMs)) {
46760
+ emitRateLimitEvent({
46761
+ model: model.id,
46762
+ provider: model.provider,
46763
+ rateLimitType: rateLimitType ?? "all_accounts",
46764
+ reason: message,
46765
+ resetAt: new Date(resetAtMs).toISOString(),
46766
+ resetAtMs,
46767
+ source: "claude-bridge",
46768
+ status: "rejected"
46769
+ });
46770
+ }
46771
+ const errorOutput = ctx().turnOutput;
46772
+ if (isReentrant) popContext();
46773
+ queueMicrotask(() => {
46774
+ stream.push({ type: "error", reason: "error", error: errorOutput });
46775
+ stream.end();
46776
+ releaseEphemeralLane();
46777
+ });
46778
+ return stream;
46779
+ }
46780
+ }
46781
+ const queryModel = account?.modelId && account.modelId !== model.id ? { ...model, id: account.modelId, name: modelDisplayName(account.modelId) } : model;
46782
+ if (queryModel.id !== model.id) {
46783
+ updateTurnOutputModel(queryModel.id);
46784
+ if (rotationState.announcedModelId !== queryModel.id) {
46785
+ rotationState.announcedModelId = queryModel.id;
46786
+ safeNotify(
46787
+ account?.fallbackReason === "fable-quota" ? `Every ready account rejected Claude Fable; using ${modelDisplayName(queryModel.id)}.` : `Pi Claude switched to ${modelDisplayName(queryModel.id)}.`,
46788
+ "info"
46789
+ );
46790
+ }
46791
+ }
46792
+ const attemptCtx = ctx();
46793
+ const attemptBuffer = account ? new RetryEventBuffer(stream, () => attemptCtx.markOutputCommitted()) : void 0;
46794
+ if (attemptBuffer) attemptCtx.currentPiStream = attemptBuffer;
45507
46795
  const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context);
45508
- const promptBlocks = extractUserPromptBlocks(context.messages);
45509
- let promptText = extractUserPrompt(context.messages) ?? "";
45510
- if (!promptText && !promptBlocks) {
46796
+ const bridgeConfig = loadConfig(cwd);
46797
+ const providerSettings = bridgeConfig.provider ?? {};
46798
+ const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
46799
+ const claudeExecutablePreflight = claudeExecutable ? preflightClaudeExecutable(claudeExecutable, cwd) : void 0;
46800
+ const accountScope = accountSessionScope(account);
46801
+ const cursorBeforeSync = getSharedSession()?.cursor ?? null;
46802
+ const syncResult = isReentrant ? { sessionId: null, promptStart: context.messages.length - 1 } : syncSharedSession(context.messages, cwd, customToolNameToSdk, queryModel.id, accountScope);
46803
+ const { sessionId: resumeSessionId, promptStart } = syncResult;
46804
+ const foreignContext = syncResult.foreignContext === true;
46805
+ if (foreignContext) ctx().detachedFromSharedSession = true;
46806
+ const conversationFp = isReentrant || foreignContext ? void 0 : conversationFingerprint(context.messages);
46807
+ const promptMessages = context.messages.slice(promptStart);
46808
+ const promptBlocks = extractUserPromptBlocks(promptMessages);
46809
+ let promptText = extractUserPrompt(promptMessages) ?? "";
46810
+ if (!promptText.trim() && !promptBlocks) {
45511
46811
  diagDump("empty_prompt", {
45512
46812
  contextLength: context.messages.length,
45513
46813
  lastMsgRole: lastMsg?.role,
45514
46814
  isReentrant,
45515
46815
  stackDepth: stackDepth(),
45516
46816
  activeQueryExists: ctx().activeQuery !== null,
45517
- sharedSession: sharedSession ? { sessionId: sharedSession.sessionId.slice(0, 8), cursor: sharedSession.cursor } : null,
46817
+ cursorBeforeSync,
46818
+ promptStart,
46819
+ promptRoles: promptMessages.map((m) => m.role).join(" "),
46820
+ sharedSession: (() => {
46821
+ const activeSession = getSharedSession();
46822
+ return activeSession ? { sessionId: activeSession.sessionId.slice(0, 8), cursor: activeSession.cursor } : null;
46823
+ })(),
45518
46824
  messageRoles: context.messages.map((m, i) => `[${i}]${m.role}`).join(" ")
45519
46825
  });
45520
46826
  promptText = "[continue]";
45521
46827
  }
45522
46828
  const prompt = promptBlocks ? wrapPromptStream(promptBlocks) : promptText;
45523
46829
  const mcpServers = buildMcpServers(mcpTools, ctx());
45524
- const bridgeConfig = loadConfig(cwd);
45525
- const providerSettings = bridgeConfig.provider ?? {};
45526
- const enableCloudMcp = connectorsEnabledFor(bridgeConfig);
45527
- const connectorWriteMode = connectorWriteModeFor(bridgeConfig);
45528
- const connectorServers = enableCloudMcp ? connectorServersSnapshot() : {};
45529
- const appendSystemPrompt = providerSettings.appendSystemPrompt !== false;
45530
- const agentsAppend = appendSystemPrompt ? extractAgentsAppend() : void 0;
45531
- const skillsAppend = appendSystemPrompt ? extractSkillsBlock(context.systemPrompt) : void 0;
45532
- const promptContextAppend = buildPromptContextAppend(context.systemPrompt, cwd, bridgeConfig.promptContext ?? {});
45533
- const appendParts = [agentsAppend, skillsAppend, promptContextAppend.text].filter((part) => Boolean(part));
45534
- const systemPromptAppend = appendParts.length > 0 ? appendParts.join("\n\n") : void 0;
45535
- const settingSources = enableCloudMcp ? providerSettings.settingSources ?? ["user", "project", "local"] : appendSystemPrompt ? void 0 : providerSettings.settingSources ?? ["user", "project"];
45536
- const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
45537
- const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
45538
- const claudeExecutablePreflight = claudeExecutable ? preflightClaudeExecutable(claudeExecutable, cwd) : void 0;
45539
- const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
45540
- const requestedEffort = options?.reasoning ? model.thinkingLevelMap?.[options.reasoning] ?? REASONING_TO_EFFORT[options.reasoning] : void 0;
45541
- const effort = resolveConfiguredEffort(model.id, requestedEffort, providerSettings);
45542
- const extraArgs = {};
45543
- if (effort) extraArgs["thinking-display"] = "summarized";
45544
- const fallbackModel = fallbackModelForPrimaryModel(model.id);
45545
- const childEnv = { ...process.env, ENABLE_CLAUDEAI_MCP_SERVERS: enableCloudMcp ? "1" : "0", DISABLE_AUTO_COMPACT: "1" };
45546
- const queryOptions = {
46830
+ const built = buildClaudeQueryOptions({
45547
46831
  cwd,
45548
- model: model.id,
45549
- env: childEnv,
45550
- ...connectorQueryOptions(enableCloudMcp, connectorWriteMode),
45551
- permissionMode: "bypassPermissions",
45552
- includePartialMessages: true,
45553
- ...fallbackModel ? { fallbackModel } : {},
45554
- ...providerSettings.fastMode ? { settings: { fastMode: true } } : {},
45555
- systemPrompt: {
45556
- type: "preset",
45557
- preset: "claude_code",
45558
- append: systemPromptAppend ? systemPromptAppend : void 0
45559
- },
45560
- extraArgs,
45561
- ...strictMcpConfigEnabled ? { strictMcpConfig: true } : {},
45562
- ...effort ? { effort } : {},
45563
- ...settingSources ? { settingSources } : {},
45564
- ...mcpServers || Object.keys(connectorServers).length > 0 ? { mcpServers: { ...mcpServers ?? {}, ...connectorServers } } : {},
45565
- ...resumeSessionId ? { resume: resumeSessionId } : {},
45566
- ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
45567
- spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
45568
- ...makeCliDebugOptions("provider")
45569
- };
46832
+ requestedModel: model,
46833
+ queryModel,
46834
+ account,
46835
+ bridgeConfig,
46836
+ systemPrompt: context.systemPrompt,
46837
+ reasoning: options?.reasoning,
46838
+ resumeSessionId,
46839
+ mcpServers,
46840
+ claudeExecutable
46841
+ });
46842
+ const { queryOptions } = built;
45570
46843
  debug(
45571
46844
  "provider: fresh query",
45572
- `model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
45573
- `resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
45574
- `fallback=${fallbackModel ?? "none"}`,
45575
- `appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true} connectors=${enableCloudMcp}`,
46845
+ `model=${queryModel.id} requested=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
46846
+ `resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${built.effort ?? "default"} account=${account?.label ?? "legacy"}`,
46847
+ `fallback=${built.fallbackModel ?? "none"}`,
46848
+ `appendSys=${built.appendSystemPrompt} promptCtx=${built.promptContextLabels.join(",") || "none"} strictMcp=${built.strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true} connectors=${built.enableCloudMcp}`,
45576
46849
  `claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
45577
46850
  `prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`
45578
46851
  );
45579
46852
  let wasAborted = false;
45580
46853
  let streamIdleTimedOut = false;
45581
- const sdkQuery = Okt({ prompt, options: queryOptions });
46854
+ let retryRequested = false;
46855
+ let retryFailure;
46856
+ const sdkQuery = sdkQueryFactory({ prompt, options: queryOptions });
45582
46857
  ctx().activeQuery = sdkQuery;
45583
46858
  const abortCtx = ctx();
46859
+ const attemptFailure = {};
46860
+ const persistSession = (next) => {
46861
+ if (isReentrant || foreignContext) return;
46862
+ setSharedSession(next && conversationFp ? { conversationFingerprint: conversationFp, ...next } : next);
46863
+ };
46864
+ const markRebuildForThisQuery = (opts = {}) => {
46865
+ if (isReentrant || foreignContext) return;
46866
+ markSessionForRebuild(opts);
46867
+ };
46868
+ const dropDeferredUserMessages = (site, undelivered) => {
46869
+ const dropped = [...undelivered !== void 0 ? [undelivered] : [], ...abortCtx.deferredUserMessages];
46870
+ abortCtx.deferredUserMessages = [];
46871
+ if (dropped.length > 0) {
46872
+ diagDump("deferred_user_messages_dropped", summarizeDroppedUserMessages(site, dropped));
46873
+ }
46874
+ return dropped;
46875
+ };
46876
+ let accountFailureRecorded = false;
46877
+ const recordAttemptFailure = (failure) => {
46878
+ if (accountFailureRecorded || !account || !router || !failure.kind || failure.rateLimitInfo || wasAborted || options?.signal?.aborted) return;
46879
+ safeRouterCall("recordFailure", () => router.recordFailure(account.profileId, failure.kind, queryModel.id));
46880
+ accountFailureRecorded = true;
46881
+ };
45584
46882
  const requestAbort = () => {
45585
46883
  void sdkQuery.interrupt().catch(() => {
45586
46884
  });
@@ -45589,6 +46887,28 @@ function streamClaudeAgentSdk(model, context, options) {
45589
46887
  } catch {
45590
46888
  }
45591
46889
  };
46890
+ const requestRotation = (failure) => {
46891
+ recordAttemptFailure(failure);
46892
+ const committed = abortCtx.committedOutput || attemptBuffer?.hasCommittedOutput === true;
46893
+ const eligible = Boolean(!isReentrant && account && router && failure.kind && !committed && !wasAborted && !options?.signal?.aborted && rotationState.attempts < MAX_ROTATION_ATTEMPTS);
46894
+ debug("provider: account rotation decision", JSON.stringify({
46895
+ eligible,
46896
+ account: account?.label,
46897
+ kind: failure.kind,
46898
+ committedOutput: committed,
46899
+ wasAborted,
46900
+ signalAborted: options?.signal?.aborted === true,
46901
+ attempts: rotationState.attempts
46902
+ }));
46903
+ if (!eligible || !account || !router || !failure.kind) return false;
46904
+ rotationState.excludedProfileIds.add(account.profileId);
46905
+ retryRequested = true;
46906
+ retryFailure = failure;
46907
+ attemptBuffer?.discard();
46908
+ abortCtx.currentPiStream = null;
46909
+ debug(`provider: rotating account after ${failure.kind}, from=${account.label}, attempt=${rotationState.attempts}`);
46910
+ return true;
46911
+ };
45592
46912
  const streamIdleTimeoutMs = streamIdleTimeoutMsFromEnv();
45593
46913
  const streamIdleWatchdog = streamIdleTimeoutMs > 0 ? createStreamIdleWatchdog({
45594
46914
  getState: () => ({
@@ -45601,15 +46921,20 @@ function streamClaudeAgentSdk(model, context, options) {
45601
46921
  onTimeout: ({ idleMs, timeoutMs }) => {
45602
46922
  if (streamIdleTimedOut || wasAborted || options?.signal?.aborted || abortCtx.activeQuery !== sdkQuery) return;
45603
46923
  streamIdleTimedOut = true;
45604
- abortCtx.deferredUserMessages = [];
45605
- abortCtx.handledTerminalError = true;
45606
- if (sharedSession) setSharedSession({ ...sharedSession, needsRebuild: true, forceRotate: true });
46924
+ dropDeferredUserMessages("stream-idle-timeout");
46925
+ markRebuildForThisQuery({ forceRotate: true });
45607
46926
  const errorMessage = buildStreamIdleTimeoutErrorMessage(timeoutMs);
45608
- debug("provider: stream idle timeout", `model=${model.id}`, `timeout=${timeoutMs}`, `idle=${idleMs}`);
46927
+ debug("provider: stream idle timeout", `model=${queryModel.id}`, `timeout=${timeoutMs}`, `idle=${idleMs}`);
46928
+ const idleFailure = { kind: "network", message: errorMessage };
46929
+ if (requestRotation(idleFailure)) {
46930
+ requestAbort();
46931
+ return;
46932
+ }
46933
+ abortCtx.handledTerminalError = true;
45609
46934
  emitRateLimitEvent({
45610
46935
  idleMs,
45611
- model: model.id,
45612
- provider: PROVIDER_ID,
46936
+ model: queryModel.id,
46937
+ provider: queryModel.provider,
45613
46938
  rateLimitType: "stream_idle",
45614
46939
  reason: "Claude Code stream idle timeout",
45615
46940
  retryAfterMs: STREAM_IDLE_BACKOFF_HINT_MS,
@@ -45617,7 +46942,7 @@ function streamClaudeAgentSdk(model, context, options) {
45617
46942
  status: "rejected",
45618
46943
  timeoutMs
45619
46944
  });
45620
- piUI?.notify(`${RATE_LIMIT_TOKEN} Claude stream idle timeout after ${formatDurationShort(timeoutMs)} \u2014 retrying via rate-limit backoff`, "warning");
46945
+ safeNotify(`${RATE_LIMIT_TOKEN} Claude stream idle timeout after ${formatDurationShort(timeoutMs)} \u2014 retrying via rate-limit backoff`, "warning");
45621
46946
  if (abortCtx.turnOutput) {
45622
46947
  abortCtx.turnOutput.stopReason = "error";
45623
46948
  abortCtx.turnOutput.errorMessage = errorMessage;
@@ -45638,68 +46963,126 @@ function streamClaudeAgentSdk(model, context, options) {
45638
46963
  activeStreamIdleWatchdogs.set(abortCtx, streamIdleWatchdog);
45639
46964
  streamIdleWatchdog.refresh();
45640
46965
  }
45641
- const onAbort = () => {
46966
+ const onAbort = () => runInRequestLane(laneId, () => {
45642
46967
  wasAborted = true;
45643
- abortCtx.deferredUserMessages = [];
45644
- reportToolResultMismatch(abortCtx, "abort", cwd, { forceRotate: true });
46968
+ dropDeferredUserMessages("abort");
46969
+ reportToolResultMismatch(abortCtx, "abort", cwd, {
46970
+ expectedInterruption: true,
46971
+ forceRotate: true
46972
+ });
45645
46973
  const drained = drainPendingToolCalls(abortCtx, "abort");
45646
46974
  if (drained > 0) debug(`provider: abort drained ${drained} waiting MCP handler(s) as errors`);
45647
46975
  abortCtx.pendingResults.clear();
45648
46976
  requestAbort();
45649
- };
46977
+ });
45650
46978
  if (options?.signal) {
45651
46979
  if (options.signal.aborted) onAbort();
45652
46980
  else options.signal.addEventListener("abort", onAbort, { once: true });
45653
46981
  }
45654
- consumeQuery(sdkQuery, customToolNameToPi, model, cwd, bridgeConfig, () => wasAborted).then(async ({ capturedSessionId }) => {
45655
- debug(`provider: consumeQuery completed, stopReason=${abortCtx.turnOutput?.stopReason}, error=${abortCtx.turnOutput?.errorMessage}, aborted=${wasAborted}`);
46982
+ const surfaceFailure = (failure, aborted2 = false) => {
46983
+ attemptBuffer?.commit();
46984
+ if (failure.rateLimitInfo) {
46985
+ const info = failure.rateLimitInfo;
46986
+ const resetAt = rateLimitResetFromInfo(info);
46987
+ const resetAtMs = rateLimitResetMs(info);
46988
+ emitRateLimitEvent({
46989
+ model: queryModel.id,
46990
+ provider: queryModel.provider,
46991
+ rateLimitType: rateLimitTypeFromInfo(info),
46992
+ reason: failure.message,
46993
+ resetAt,
46994
+ ...Number.isFinite(resetAtMs) ? { resetAtMs } : {},
46995
+ source: "claude-bridge",
46996
+ status: "rejected"
46997
+ });
46998
+ safeNotify(`${RATE_LIMIT_TOKEN} Claude ${failure.message} \u2014 resets ${formatResetTimestamp(resetAtMs ?? resetAt)}`, "warning");
46999
+ }
47000
+ if (abortCtx.turnOutput) {
47001
+ abortCtx.turnOutput.stopReason = aborted2 ? "aborted" : "error";
47002
+ abortCtx.turnOutput.errorMessage = failure.message;
47003
+ }
47004
+ abortCtx.currentPiStream?.push({ type: "error", reason: aborted2 ? "aborted" : "error", error: abortCtx.turnOutput });
47005
+ abortCtx.currentPiStream?.end();
47006
+ abortCtx.currentPiStream = null;
47007
+ };
47008
+ consumeQuery(sdkQuery, abortCtx, customToolNameToPi, queryModel, bridgeConfig, () => wasAborted, account, router, attemptFailure).then(async ({ capturedSessionId, failure }) => {
47009
+ debug(`provider: consumeQuery completed, stopReason=${abortCtx.turnOutput?.stopReason}, failure=${failure?.kind ?? "none"}, aborted=${wasAborted}`);
45656
47010
  if (streamIdleTimedOut) {
45657
- abortCtx.deferredUserMessages = [];
45658
- debug("provider: stream idle timeout already surfaced; skipping normal completion");
47011
+ dropDeferredUserMessages("stream-idle-timeout-completion");
47012
+ debug(`provider: stream idle timeout ${retryRequested ? "queued account rotation" : "already surfaced"}; skipping normal completion`);
45659
47013
  return;
45660
47014
  }
45661
47015
  if (wasAborted || options?.signal?.aborted) {
45662
- if (sharedSession) setSharedSession({ ...sharedSession, needsRebuild: true, forceRotate: true });
45663
- abortCtx.deferredUserMessages = [];
47016
+ markRebuildForThisQuery({ forceRotate: true });
47017
+ dropDeferredUserMessages("abort-completion");
45664
47018
  debug(`provider: abort detected, marked sharedSession needsRebuild + forceRotate`);
45665
- if (abortCtx.turnOutput) {
45666
- abortCtx.turnOutput.stopReason = "aborted";
45667
- abortCtx.turnOutput.errorMessage = "Operation aborted";
47019
+ surfaceFailure({ message: "Operation aborted" }, true);
47020
+ return;
47021
+ }
47022
+ if (failure) {
47023
+ if (requestRotation(failure)) return;
47024
+ if (!abortCtx.handledTerminalError) surfaceFailure(failure);
47025
+ const droppedSteers = dropDeferredUserMessages("terminal-failure");
47026
+ const activeSession2 = getSharedSession();
47027
+ const failedSessionId = capturedSessionId ?? activeSession2?.sessionId;
47028
+ if (failedSessionId) {
47029
+ const cursor = Math.max(context.messages.length, abortCtx.latestCursor, activeSession2?.cursor ?? 0);
47030
+ debug(`provider: terminal failure, persisting session=${failedSessionId.slice(0, 8)}, cursor=${cursor}, account=${account?.label ?? "legacy"}, droppedSteers=${droppedSteers.length}`);
47031
+ persistSession({ sessionId: failedSessionId, cursor, cwd, ...accountScope, ...droppedSteers.length > 0 ? { needsRebuild: true } : {} });
45668
47032
  }
45669
- abortCtx.currentPiStream?.push({ type: "error", reason: "aborted", error: abortCtx.turnOutput });
45670
- abortCtx.currentPiStream?.end();
45671
- abortCtx.currentPiStream = null;
45672
47033
  return;
45673
47034
  }
45674
- const sessionId = capturedSessionId ?? sharedSession?.sessionId;
47035
+ const activeSession = getSharedSession();
47036
+ const sessionId = capturedSessionId ?? activeSession?.sessionId;
45675
47037
  if (sessionId) {
45676
- const cursor = Math.max(context.messages.length, abortCtx.latestCursor, sharedSession?.cursor ?? 0);
45677
- debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}`);
45678
- setSharedSession({ sessionId, cursor, cwd });
47038
+ const cursor = Math.max(context.messages.length, abortCtx.latestCursor, activeSession?.cursor ?? 0);
47039
+ debug(`provider: query done, session=${sessionId.slice(0, 8)}, cursor=${cursor}, account=${account?.label ?? "legacy"}`);
47040
+ persistSession({ sessionId, cursor, cwd, ...accountScope });
45679
47041
  }
47042
+ if (account && router) safeRouterCall("recordSuccess", () => router.recordSuccess(account.profileId, options?.sessionId));
45680
47043
  try {
45681
47044
  while (abortCtx.deferredUserMessages.length > 0 && !isReentrant && !wasAborted) {
45682
- const steerPrompt = abortCtx.deferredUserMessages.shift();
45683
- debug(`provider: replaying deferred user message: ${steerPrompt.slice(0, 60)}`);
45684
- abortCtx.resetTurnState(model);
47045
+ const steer = abortCtx.deferredUserMessages.shift();
47046
+ const steerPreview = (steer.text || "[image-only]").slice(0, 60);
47047
+ debug(`provider: replaying deferred user message: ${steerPreview}`);
47048
+ abortCtx.resetTurnState(queryModel);
45685
47049
  abortCtx.resetToolTracking();
45686
- const resumeId = sharedSession?.sessionId;
47050
+ const resumeId = foreignContext ? capturedSessionId : getSharedSession()?.sessionId;
45687
47051
  if (!resumeId) {
45688
47052
  debug(`WARNING: no session to resume for deferred message, dropping`);
47053
+ dropDeferredUserMessages("continuation-no-resume-id", steer);
45689
47054
  break;
45690
47055
  }
45691
47056
  const contOptions = { ...queryOptions, resume: resumeId, ...makeCliDebugOptions("continuation") };
45692
- const contQuery = Okt({ prompt: steerPrompt, options: contOptions });
47057
+ const contQuery = sdkQueryFactory({ prompt: steer.blocks ? wrapPromptStream(steer.blocks) : steer.text, options: contOptions });
45693
47058
  abortCtx.activeQuery = contQuery;
45694
- debug(`provider: continuation query, model=${model.id}, resume=${resumeId.slice(0, 8)}, prompt=${steerPrompt.slice(0, 60)}`);
47059
+ debug(`provider: continuation query, model=${queryModel.id}, resume=${resumeId.slice(0, 8)}, account=${account?.label ?? "legacy"}, prompt=${steerPreview}`);
45695
47060
  try {
45696
- const { capturedSessionId: contSid } = await consumeQuery(contQuery, customToolNameToPi, model, cwd, bridgeConfig, () => wasAborted);
45697
- const sid = contSid ?? sharedSession?.sessionId;
47061
+ const continuation = await consumeQuery(contQuery, abortCtx, customToolNameToPi, queryModel, bridgeConfig, () => wasAborted, account, router);
47062
+ if (continuation.failure) {
47063
+ recordAttemptFailure(continuation.failure);
47064
+ if (!abortCtx.handledTerminalError) surfaceFailure(continuation.failure);
47065
+ if (dropDeferredUserMessages("continuation-failure", steer).length > 0) {
47066
+ markRebuildForThisQuery();
47067
+ }
47068
+ break;
47069
+ }
47070
+ const activeSession2 = getSharedSession();
47071
+ const sid = continuation.capturedSessionId ?? activeSession2?.sessionId;
45698
47072
  if (sid) {
45699
- setSharedSession({ sessionId: sid, cursor: sharedSession?.cursor ?? 0, cwd });
47073
+ persistSession({ sessionId: sid, cursor: activeSession2?.cursor ?? 0, cwd, ...accountScope });
45700
47074
  }
45701
47075
  } catch (contError) {
45702
47076
  debug(`provider: continuation query error:`, contError);
47077
+ const continuationFailure = {
47078
+ kind: classifyClaudeFailure(contError),
47079
+ message: contError instanceof Error ? contError.message : String(contError)
47080
+ };
47081
+ recordAttemptFailure(continuationFailure);
47082
+ if (!abortCtx.handledTerminalError) surfaceFailure(continuationFailure);
47083
+ if (dropDeferredUserMessages("continuation-error", steer).length > 0) {
47084
+ markRebuildForThisQuery();
47085
+ }
45703
47086
  break;
45704
47087
  } finally {
45705
47088
  contQuery.close();
@@ -45710,26 +47093,25 @@ function streamClaudeAgentSdk(model, context, options) {
45710
47093
  }
45711
47094
  finalizeCurrentStream(abortCtx.turnOutput?.stopReason, abortCtx);
45712
47095
  }).catch((error51) => {
45713
- debug(`provider: query error, model=${model.id}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error51);
45714
- const suppressDuplicateError = abortCtx.handledTerminalError || streamIdleTimedOut;
45715
- const openedExtraUsage = !suppressDuplicateError && isExtraUsageRequiredMessage(error51) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "query error");
45716
- if ((wasAborted || options?.signal?.aborted) && sharedSession) {
45717
- setSharedSession({ ...sharedSession, needsRebuild: true, forceRotate: true });
45718
- } else {
45719
- setSharedSession(null);
47096
+ debug(`provider: query error, model=${queryModel.id}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error51);
47097
+ const suppressDuplicateError = abortCtx.handledTerminalError || streamIdleTimedOut && !retryRequested;
47098
+ if (wasAborted || options?.signal?.aborted) {
47099
+ markRebuildForThisQuery({ forceRotate: true });
45720
47100
  }
45721
- abortCtx.deferredUserMessages = [];
45722
- if (suppressDuplicateError) {
45723
- debug("provider: suppressing duplicate query error after terminal error was already emitted");
45724
- return;
47101
+ if (dropDeferredUserMessages("query-error").length > 0) {
47102
+ markRebuildForThisQuery();
45725
47103
  }
45726
- if (abortCtx.turnOutput) {
45727
- abortCtx.turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
45728
- abortCtx.turnOutput.errorMessage = `${error51 instanceof Error ? error51.message : String(error51)}${openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : ""}`;
47104
+ if (suppressDuplicateError || retryRequested) {
47105
+ debug("provider: suppressing duplicate query error after terminal handling");
47106
+ return;
45729
47107
  }
45730
- abortCtx.currentPiStream?.push({ type: "error", reason: abortCtx.turnOutput?.stopReason ?? "error", error: abortCtx.turnOutput });
45731
- abortCtx.currentPiStream?.end();
45732
- abortCtx.currentPiStream = null;
47108
+ const failure = attemptFailure.failure?.rateLimitInfo ? attemptFailure.failure : {
47109
+ kind: classifyClaudeFailure(error51),
47110
+ message: error51 instanceof Error ? error51.message : String(error51)
47111
+ };
47112
+ if (requestRotation(failure)) return;
47113
+ if (!wasAborted && !options?.signal?.aborted) persistSession(null);
47114
+ surfaceFailure(failure, Boolean(options?.signal?.aborted));
45733
47115
  }).finally(() => {
45734
47116
  streamIdleWatchdog?.dispose();
45735
47117
  activeStreamIdleWatchdogs.delete(abortCtx);
@@ -45737,164 +47119,58 @@ function streamClaudeAgentSdk(model, context, options) {
45737
47119
  const cause = toolCallDrainCause({ wasAborted, signalAborted: options?.signal?.aborted, streamIdleTimedOut });
45738
47120
  teardownQuery(abortCtx, sdkQuery, cause, cwd, isReentrant);
45739
47121
  sdkQuery.close();
45740
- });
45741
- return stream;
45742
- }
45743
- function commandCwd(ctx2) {
45744
- const value = ctx2?.cwd;
45745
- return typeof value === "string" && value.length > 0 ? value : process.cwd();
45746
- }
45747
- async function tryOpenExtensionManagerSettings(ctx2) {
45748
- const host = globalThis;
45749
- const openQuickSettings = host[/* @__PURE__ */ Symbol.for("vstack.pi.extension-manager.open-quick-settings")];
45750
- if (typeof openQuickSettings !== "function") return false;
45751
- try {
45752
- await openQuickSettings(ctx2, "@vanillagreen/pi-claude-bridge");
45753
- return true;
45754
- } catch {
45755
- return false;
45756
- }
45757
- }
45758
- function showBridgeStatus(ctx2) {
45759
- const config2 = loadConfig(commandCwd(ctx2));
45760
- ctx2.ui.notify([
45761
- `Claude bridge: ${config2.enabled === false ? "disabled" : "enabled"}`,
45762
- `Extra usage auto-helper: ${extraUsageAllowed(config2) ? "on" : "off"} (settings)`,
45763
- `Use /claude-bridge:extra to run Claude Code /extra-usage now.`
45764
- ].join("\n"), "info");
45765
- }
45766
- function readCredentialFile(path) {
45767
- try {
45768
- return nodeReadFileSync(path, "utf8");
45769
- } catch {
45770
- return void 0;
45771
- }
45772
- }
45773
- var connectorServerCache = /* @__PURE__ */ new Map();
45774
- var connectorServerPending = /* @__PURE__ */ new Set();
45775
- function connectorScopeKey() {
45776
- return process.env.CLAUDE_CONFIG_DIR?.trim() || "<default>";
45777
- }
45778
- function primeConnectorServers() {
45779
- const key = connectorScopeKey();
45780
- if (connectorServerCache.has(key) || connectorServerPending.has(key)) return;
45781
- connectorServerPending.add(key);
45782
- void (async () => {
45783
- try {
45784
- const credentials = resolveClaudeOAuth(readCredentialFile);
45785
- if (!credentials) {
45786
- debug("connectors: no OAuth credentials; declaring none");
45787
- connectorServerCache.set(key, {});
45788
- return;
45789
- }
45790
- const inventory = await listAccountConnectors({ credentials });
45791
- if (!inventory.ok) {
45792
- debug(`connectors: inventory failed (${inventory.reason}); declaring none`);
45793
- connectorServerCache.set(key, {});
45794
- return;
45795
- }
45796
- const servers = connectorMcpServers(inventory);
45797
- debug(
45798
- `connectors: declaring ${Object.keys(servers).length} of ${inventory.connectors.length} installed`,
45799
- Object.keys(servers).join(", ") || "none"
45800
- );
45801
- connectorServerCache.set(key, servers);
45802
- if (writeCachedConnectors(inventory.connectors, key)) {
45803
- debug(`connectors: cached ${inventory.connectors.length} entries`);
47122
+ }).then(async () => {
47123
+ if (!retryRequested) return;
47124
+ if (wasAborted || options?.signal?.aborted) {
47125
+ debug("provider: abort after queued account retry \u2014 terminating stream without retrying");
47126
+ if (abortCtx.turnOutput) {
47127
+ abortCtx.turnOutput.stopReason = "aborted";
47128
+ abortCtx.turnOutput.errorMessage = "Operation aborted";
45804
47129
  }
45805
- } catch (error51) {
45806
- debug("connectors: declaration lookup threw; declaring none", error51);
45807
- connectorServerCache.set(key, {});
45808
- } finally {
45809
- connectorServerPending.delete(key);
45810
- }
45811
- })();
45812
- }
45813
- function connectorServersSnapshot() {
45814
- const key = connectorScopeKey();
45815
- const ready = connectorServerCache.get(key);
45816
- if (ready) return ready;
45817
- primeConnectorServers();
45818
- const cached2 = readCachedConnectors(key);
45819
- if (!cached2) return {};
45820
- const servers = connectorMcpServers({ ok: true, complete: true, connectors: cached2 });
45821
- if (Object.keys(servers).length === 0) return {};
45822
- debug(`connectors: turn-1 declarations from cache \u2014 ${Object.keys(servers).join(", ")}`);
45823
- return servers;
45824
- }
45825
- async function reportConnectorInventory(ctx2) {
45826
- const credentials = resolveClaudeOAuth(readCredentialFile);
45827
- if (!credentials) {
45828
- ctx2.ui.notify("Claude bridge: no Claude OAuth credentials found \u2014 cannot enumerate connectors.", "error");
45829
- return;
45830
- }
45831
- const inventory = await listAccountConnectors({ credentials });
45832
- if (!inventory.ok) {
45833
- ctx2.ui.notify(`Claude bridge: connector enumeration failed \u2014 ${inventory.reason}`, "error");
45834
- return;
45835
- }
45836
- if (inventory.connectors.length === 0) {
45837
- ctx2.ui.notify("Claude bridge: this account has no connectors installed.", "info");
45838
- return;
45839
- }
45840
- const names = inventory.connectors.map((c) => c.name).join(", ");
45841
- ctx2.ui.notify(`Claude bridge: ${inventory.connectors.length} connector(s) installed \u2014 ${names}`, "info");
45842
- }
45843
- function registerBridgeCommands(pi) {
45844
- const guard = pi;
45845
- if (guard[COMMANDS_REGISTERED_KEY]) return;
45846
- guard[COMMANDS_REGISTERED_KEY] = true;
45847
- const runExtraUsage = async (ctx2) => {
45848
- const cwd = commandCwd(ctx2);
45849
- if (extraUsageHelperInFlight) {
45850
- ctx2.ui.notify("Claude extra usage helper already running.", "info");
45851
- await extraUsageHelperInFlight.catch(() => void 0);
47130
+ stream.push({ type: "error", reason: "aborted", error: abortCtx.turnOutput });
47131
+ stream.end();
45852
47132
  return;
45853
47133
  }
45854
- try {
45855
- ctx2.ui.notify("Claude extra usage helper starting\u2026", "info");
45856
- extraUsageHelperInFlight = runExtraUsageHelper(cwd).finally(() => {
45857
- extraUsageHelperInFlight = null;
45858
- });
45859
- const message = await extraUsageHelperInFlight;
45860
- ctx2.ui.notify(`Claude extra usage helper: ${message}`, "info");
45861
- } catch (error51) {
45862
- const message = error51 instanceof Error ? error51.message : String(error51);
45863
- ctx2.ui.notify(`Claude extra usage helper failed: ${message}`, "error");
45864
- }
45865
- };
45866
- pi.registerCommand("claude-bridge", {
45867
- description: "Open Claude bridge settings/status",
45868
- handler: async (args, ctx2) => {
45869
- if (args.trim()) ctx2.ui.notify("Unknown /claude-bridge argument. Use /claude-bridge:extra to run Claude Code /extra-usage.", "warning");
45870
- if (await tryOpenExtensionManagerSettings(ctx2)) return;
45871
- showBridgeStatus(ctx2);
47134
+ debug(`provider: starting account retry after ${retryFailure?.kind ?? "failure"}; excluded=${[...rotationState.excludedProfileIds].join(",")}`);
47135
+ const retryStream = streamClaudeAgentSdk(model, context, {
47136
+ ...options ?? {},
47137
+ [ROTATION_STATE_KEY]: rotationState
47138
+ });
47139
+ for await (const event of retryStream) stream.push(event);
47140
+ stream.end();
47141
+ }).catch((error51) => {
47142
+ debug("provider: account retry pipeline failed:", error51);
47143
+ if (abortCtx.turnOutput) {
47144
+ abortCtx.turnOutput.stopReason = "error";
47145
+ abortCtx.turnOutput.errorMessage = error51 instanceof Error ? error51.message : String(error51);
45872
47146
  }
45873
- });
45874
- pi.registerCommand("claude-bridge:extra", {
45875
- description: "Run Claude Code /extra-usage through claude-bridge",
45876
- handler: async (_args, ctx2) => runExtraUsage(ctx2)
45877
- });
45878
- pi.registerCommand("claude-bridge:connectors", {
45879
- description: "List the Claude account's installed claude.ai connectors",
45880
- handler: async (_args, ctx2) => reportConnectorInventory(ctx2)
45881
- });
47147
+ stream.push({ type: "error", reason: "error", error: abortCtx.turnOutput });
47148
+ stream.end();
47149
+ }).finally(releaseEphemeralLane);
47150
+ return stream;
45882
47151
  }
45883
47152
  function index_default(pi) {
45884
47153
  setExtensionApi(pi);
45885
47154
  process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
45886
47155
  const config2 = loadConfig(process.cwd());
45887
47156
  debug("loadConfig:", JSON.stringify(config2));
47157
+ registerExternalConfigResolver();
45888
47158
  registerBridgeCommands(pi);
45889
47159
  if (config2.enabled === false) {
45890
47160
  debug("provider: disabled by configuration");
45891
47161
  return;
45892
47162
  }
47163
+ if (claimPrimaryInstance()) {
47164
+ const host = globalThis;
47165
+ host[CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL] = BRIDGE_ACCOUNT_HOST;
47166
+ }
45893
47167
  const clearSession = (event) => {
45894
- debug(`${event}: clearing session ${sharedSession?.sessionId?.slice(0, 8) ?? "none"}`);
47168
+ const activeSession = getSharedSession();
47169
+ debug(`${event}: clearing session ${activeSession?.sessionId?.slice(0, 8) ?? "none"}`);
45895
47170
  setSharedSession(null);
45896
47171
  };
45897
- pi.on("session_start", (event, ctx2) => {
47172
+ pi.on("session_start", (event, ctx2) => runInRequestLane(ctx2.sessionManager.getSessionId(), () => {
47173
+ recordStartedLane(ctx2.sessionManager, ctx2.sessionManager.getSessionId());
45898
47174
  recordProjectTrust(ctx2);
45899
47175
  setPiUI(ctx2.ui);
45900
47176
  if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
@@ -45902,31 +47178,40 @@ function index_default(pi) {
45902
47178
  }
45903
47179
  if (event.reason === "startup" || event.reason === "resume") restoreSharedSessionFromPi(ctx2);
45904
47180
  applyProviderRegistration(`session_start:${event.reason}`);
47181
+ }));
47182
+ pi.on("session_shutdown", (_event, ctx2) => {
47183
+ const sessionId = takeStartedLane(ctx2.sessionManager) ?? ctx2.sessionManager.getSessionId();
47184
+ runInRequestLane(sessionId, () => {
47185
+ cancelScheduledSessionPersistence(ctx2.sessionManager);
47186
+ clearSession("session_shutdown");
47187
+ releaseProviderTokens("session_shutdown");
47188
+ });
47189
+ deleteSharedSessionLane(sessionId);
47190
+ deleteQueryLane(sessionId);
45905
47191
  });
45906
- pi.on("session_shutdown", () => {
45907
- clearSession("session_shutdown");
45908
- releaseProviderTokens("session_shutdown");
45909
- });
45910
- pi.on("message_end", (event, ctx2) => {
47192
+ pi.on("message_end", (event, ctx2) => runInRequestLane(ctx2.sessionManager.getSessionId(), () => {
45911
47193
  const message = event.message;
45912
47194
  if (message?.role === "assistant" && message.provider === PROVIDER_ID) schedulePersistSharedSession(ctx2);
45913
- });
47195
+ }));
45914
47196
  const markRebuild = (event) => {
47197
+ const activeSession = getSharedSession();
45915
47198
  if (ctx().activeQuery) {
45916
- reportToolResultMismatch(ctx(), event, sharedSession?.cwd ?? process.cwd());
47199
+ reportToolResultMismatch(ctx(), event, activeSession?.cwd ?? process.cwd());
45917
47200
  }
45918
- if (sharedSession) {
45919
- debug(`${event}: marking needsRebuild on session ${sharedSession.sessionId.slice(0, 8)}`);
45920
- setSharedSession({ ...sharedSession, needsRebuild: true });
47201
+ if (activeSession) {
47202
+ debug(`${event}: marking needsRebuild on session ${activeSession.sessionId.slice(0, 8)}`);
47203
+ markSessionForRebuild();
45921
47204
  }
45922
47205
  };
45923
- pi.on("session_compact", () => markRebuild("session_compact"));
45924
- pi.on("session_tree", () => markRebuild("session_tree"));
47206
+ pi.on("session_compact", (_event, ctx2) => runInRequestLane(ctx2.sessionManager.getSessionId(), () => markRebuild("session_compact")));
47207
+ pi.on("session_tree", (_event, ctx2) => runInRequestLane(ctx2.sessionManager.getSessionId(), () => markRebuild("session_tree")));
45925
47208
  applyProviderRegistration("load");
45926
47209
  }
45927
47210
  export {
45928
47211
  ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD,
47212
+ CLAUDE_ACCOUNT_ROUTER_SYMBOL,
45929
47213
  CLAUDE_AI_CONNECTOR_TOOL_PATTERNS,
47214
+ CLAUDE_BRIDGE_ACCOUNT_HOST_SYMBOL,
45930
47215
  CLAUDE_BRIDGE_TOOL_ISOLATION,
45931
47216
  CONNECTOR_CALL_CUSTOM_TYPE,
45932
47217
  CONNECTOR_DISCOVERY_TOOLS,
@@ -45935,16 +47220,24 @@ export {
45935
47220
  DISALLOWED_BUILTIN_TOOLS,
45936
47221
  INTEGRITY_CUSTOM_TYPE,
45937
47222
  NATIVE_PROVIDER_UNSUPPORTED_MESSAGE,
47223
+ RetryEventBuffer,
45938
47224
  STREAM_IDLE_BACKOFF_HINT_MS,
45939
47225
  STREAM_IDLE_TIMEOUT_ENV,
45940
47226
  __testGetBridgeIntegrityState,
45941
47227
  __testSetBridgeIntegrityState,
47228
+ __testSetSdkQueryFactory,
47229
+ accountSessionScope,
45942
47230
  appendIntegrityEntry,
45943
47231
  buildNativeProvider,
45944
47232
  buildStreamIdleTimeoutErrorMessage,
47233
+ cancelScheduledSessionPersistence,
45945
47234
  cancelScheduledToolUseEnd,
45946
47235
  classifyClaudeExecutableBytes,
47236
+ classifyClaudeFailure,
45947
47237
  claudeAuthSourceLabel,
47238
+ claudeDirForProfile,
47239
+ commitsVisibleOutput,
47240
+ connectorBuiltinAllowlistHook,
45948
47241
  connectorCachePath,
45949
47242
  connectorCacheScopeKey,
45950
47243
  connectorDeclarationsDisabled,
@@ -45954,32 +47247,44 @@ export {
45954
47247
  connectorResultByteSize,
45955
47248
  connectorServerName,
45956
47249
  connectorServerNamespace,
47250
+ connectorServersSnapshot,
45957
47251
  connectorWriteDenyHook,
45958
47252
  connectorWriteModeFor,
45959
47253
  connectorWriteModeFromEnv,
45960
47254
  connectorsEnabledFor,
45961
47255
  connectorsEnabledFromEnv,
45962
47256
  connectorsListUrl,
47257
+ conversationFingerprint,
47258
+ conversationFingerprintsMatch,
45963
47259
  createStreamIdleWatchdog,
45964
47260
  credentialCandidatePaths,
45965
47261
  index_default as default,
47262
+ denyAllToolsHook,
45966
47263
  endToolUseTurn,
45967
47264
  finalizeToolUseTurnFromMcpInvocation,
45968
47265
  flushConnectorCallAudit,
45969
47266
  formatAllowedRateLimitWarning,
45970
47267
  formatResetTimestamp,
47268
+ isAllowlistedConnectorSessionTool,
45971
47269
  isChildExecutedTool,
47270
+ isChildInternalTool,
47271
+ isConnectorTool,
45972
47272
  isConnectorWriteTool,
45973
- isExtraUsageRequiredMessage,
45974
47273
  isUsageLimitMessage,
45975
47274
  listAccountConnectors,
45976
47275
  mapToolName,
45977
47276
  normalizeRateLimitUtilization,
45978
47277
  noteChildExecutedToolResults,
47278
+ planDeferredUserReplay,
47279
+ planIncrementalPromptBatch,
45979
47280
  preflightClaudeExecutable,
45980
47281
  primeConnectorServers,
47282
+ probeClaudeAccountProfile,
45981
47283
  processAssistantMessage,
45982
47284
  processStreamEvent,
47285
+ rateLimitResetFromInfo,
47286
+ rateLimitResetMs,
47287
+ rateLimitTypeFromInfo,
45983
47288
  readCachedConnectors,
45984
47289
  reapStaleQueuedResults,
45985
47290
  recordConnectorCallResult,
@@ -45991,10 +47296,14 @@ export {
45991
47296
  resolveMcpTools,
45992
47297
  restoreSharedSessionFromPi,
45993
47298
  scheduleToolUseTurnEnd,
47299
+ scopeKeyFor,
45994
47300
  setConnectorCallAuditSink,
47301
+ settingSourcesForQuery,
45995
47302
  shouldRestorePersistedBridgeEntry,
45996
47303
  spawnClaudeCodeWithDiagnostics,
47304
+ streamClaudeAgentSdk,
45997
47305
  streamIdleTimeoutMsFromEnv,
47306
+ subscriberProfileEnv,
45998
47307
  supportsNativeProvider,
45999
47308
  toolIsolationForQuery,
46000
47309
  uniqueNonEmptyLines,