@vanillagreen/pi-claude-bridge 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/bundle/index.js +7155 -6333
- package/package.json +38 -3
- package/src/config.ts +67 -1
- package/src/convert.ts +77 -20
- package/src/index.ts +398 -56
- package/src/models.ts +1 -1
- package/src/query-state.ts +187 -3
- package/src/tool-pairing-audit.ts +61 -0
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Base64ImageSource, ContentBlockParam, MessageParam } from "@anthro
|
|
|
6
6
|
import { createSession, deleteSession, openSession, repairToolPairing } from "cc-session-io";
|
|
7
7
|
import { spawn as spawnProcess } from "child_process";
|
|
8
8
|
import { createHash } from "crypto";
|
|
9
|
-
import { accessSync, appendFileSync, constants as fsConstants, mkdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
9
|
+
import { accessSync, appendFileSync, chmodSync, constants as fsConstants, mkdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
10
10
|
import { resolve as pathResolve } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { delimiter, dirname, join } from "path";
|
|
@@ -16,7 +16,8 @@ import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, extractSkillsBlock } from "./skills.j
|
|
|
16
16
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
17
17
|
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
18
18
|
import { QueryContext, ctx, stackDepth, pushContext, popContext } from "./query-state.js";
|
|
19
|
-
import {
|
|
19
|
+
import { findUnpairedToolUses, summarizeMissingToolNames, type MissingToolResult } from "./tool-pairing-audit.js";
|
|
20
|
+
import { loadConfig, normalizeEffortLevel, type Config } from "./config.js";
|
|
20
21
|
import { extractAgentsAppend } from "./agents-md.js";
|
|
21
22
|
import { buildPromptContextAppend } from "./prompt-context.js";
|
|
22
23
|
import { jsonSchemaToZodShape } from "./typebox-to-zod.js";
|
|
@@ -33,13 +34,17 @@ const newAssistantMessageEventStream: () => AssistantMessageEventStream =
|
|
|
33
34
|
|
|
34
35
|
const DEBUG = process.env.CLAUDE_BRIDGE_DEBUG === "1";
|
|
35
36
|
const DEBUG_LOG_PATH = process.env.CLAUDE_BRIDGE_DEBUG_PATH || join(homedir(), ".pi", "agent", "claude-bridge.log");
|
|
36
|
-
const
|
|
37
|
+
const DEFAULT_DIAG_LOG_PATH = join(homedir(), ".pi", "agent", "claude-bridge-diag.log");
|
|
38
|
+
|
|
39
|
+
function diagLogPath(): string {
|
|
40
|
+
return process.env.CLAUDE_BRIDGE_DIAG_PATH || DEFAULT_DIAG_LOG_PATH;
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
// Ensure log directories exist when debug is enabled
|
|
39
44
|
if (DEBUG) {
|
|
40
45
|
try {
|
|
41
46
|
mkdirSync(dirname(DEBUG_LOG_PATH), { recursive: true });
|
|
42
|
-
mkdirSync(dirname(
|
|
47
|
+
mkdirSync(dirname(diagLogPath()), { recursive: true, mode: 0o700 });
|
|
43
48
|
} catch {
|
|
44
49
|
// If directory creation fails, debug functions will throw on first use
|
|
45
50
|
}
|
|
@@ -57,7 +62,7 @@ function debug(...args: unknown[]) {
|
|
|
57
62
|
return JSON.stringify(a);
|
|
58
63
|
};
|
|
59
64
|
const msg = args.map(fmt).join(" ");
|
|
60
|
-
appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${moduleInstanceId}] ${msg}\n`);
|
|
65
|
+
try { appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${moduleInstanceId}] ${msg}\n`); } catch { /* debug is best effort */ }
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
function executableFromPath(name: string): string | undefined {
|
|
@@ -342,10 +347,109 @@ function makeCliDebugOptions(tag: string): { debug?: boolean; debugFile?: string
|
|
|
342
347
|
|
|
343
348
|
/** Unconditional diagnostic dump — for "should never happen" paths */
|
|
344
349
|
function diagDump(label: string, data: Record<string, unknown>) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
350
|
+
try {
|
|
351
|
+
const ts = new Date().toISOString();
|
|
352
|
+
const entry = { ts, moduleInstanceId, label, ...data };
|
|
353
|
+
const path = diagLogPath();
|
|
354
|
+
try { mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); } catch { /* best effort */ }
|
|
355
|
+
appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
356
|
+
try { chmodSync(path, 0o600); } catch { /* best effort */ }
|
|
357
|
+
debug(`DIAG: ${label} (see ${path})`);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
debug(`DIAG FAILED: ${label}`, error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function safeNotify(message: string, level: "info" | "warning" | "error" = "warning"): void {
|
|
364
|
+
try { piUI?.notify(message, level); }
|
|
365
|
+
catch (error) { debug("notify failed:", error); }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function argKeys(args: Record<string, unknown> | undefined): string[] {
|
|
369
|
+
return Object.keys(args ?? {}).sort();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function safeToolCallSummary(calls: Array<{ id: string; toolName: string; arguments?: Record<string, unknown> }>): Array<{ id: string; toolName: string; argKeys: string[] }> {
|
|
373
|
+
return calls.map((call) => ({ id: call.id, toolName: call.toolName, argKeys: argKeys(call.arguments) }));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function compactToolNameSummary(names: Array<{ name: string; count: number }>, limit = 12): string[] {
|
|
377
|
+
const shown = names.slice(0, limit).map(({ name, count }) => count > 1 ? `${name}×${count}` : name);
|
|
378
|
+
if (names.length > limit) shown.push(`+${names.length - limit} more`);
|
|
379
|
+
return shown;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function reportSyntheticToolResultRepair(missing: MissingToolResult[], context: Record<string, unknown>): void {
|
|
383
|
+
try {
|
|
384
|
+
if (missing.length === 0) return;
|
|
385
|
+
const toolNames = summarizeMissingToolNames(missing);
|
|
386
|
+
const toolNameSummary = compactToolNameSummary(toolNames);
|
|
387
|
+
const sampledToolCallIds = missing.slice(0, 50).map((item) => item.id);
|
|
388
|
+
diagDump("repair_tool_pairing_synthetic_results", {
|
|
389
|
+
count: missing.length,
|
|
390
|
+
toolNames,
|
|
391
|
+
sampledToolCallIds,
|
|
392
|
+
missing: missing.slice(0, 50),
|
|
393
|
+
...context,
|
|
394
|
+
});
|
|
395
|
+
safeNotify(
|
|
396
|
+
`Claude bridge: ${missing.length} missing tool result(s) repaired with "[no tool result recorded]"` +
|
|
397
|
+
`${toolNameSummary.length ? ` for ${toolNameSummary.join(", ")}` : ""}. ` +
|
|
398
|
+
`Real tool output was lost before Claude session import; see ${diagLogPath()}.`,
|
|
399
|
+
"error",
|
|
400
|
+
);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
debug("reportSyntheticToolResultRepair failed:", error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function reportToolResultMismatch(queryCtx: QueryContext, reason: string, cwd: string | undefined, opts: { forceRotate?: boolean } = {}): boolean {
|
|
407
|
+
try {
|
|
408
|
+
if (queryCtx.reportedToolResultMismatch) return false;
|
|
409
|
+
const progress = queryCtx.toolResultProgress();
|
|
410
|
+
const hasMismatch = progress.expectedCount > 0
|
|
411
|
+
? progress.unresolvedIds.length > 0 || progress.waitingCount > 0 || progress.queuedCount > 0 || progress.unmatchedResultCount > 0
|
|
412
|
+
: progress.waitingCount > 0 || progress.queuedCount > 0 || progress.unmatchedResultCount > 0;
|
|
413
|
+
if (!hasMismatch) return false;
|
|
414
|
+
queryCtx.reportedToolResultMismatch = true;
|
|
415
|
+
if (sharedSession) {
|
|
416
|
+
sharedSession = { ...sharedSession, needsRebuild: true, ...(opts.forceRotate ? { forceRotate: true } : {}) };
|
|
417
|
+
}
|
|
418
|
+
const toolNameSummary = compactToolNameSummary(progress.toolNames);
|
|
419
|
+
diagDump("tool_result_delivery_mismatch", {
|
|
420
|
+
reason,
|
|
421
|
+
cwd,
|
|
422
|
+
progress,
|
|
423
|
+
activeQueryExists: queryCtx.activeQuery !== null,
|
|
424
|
+
sharedSession: sharedSession ? {
|
|
425
|
+
sessionId: sharedSession.sessionId.slice(0, 8),
|
|
426
|
+
cursor: sharedSession.cursor,
|
|
427
|
+
needsRebuild: sharedSession.needsRebuild === true,
|
|
428
|
+
forceRotate: sharedSession.forceRotate === true,
|
|
429
|
+
} : null,
|
|
430
|
+
});
|
|
431
|
+
safeNotify(
|
|
432
|
+
`Claude bridge: tool result delivery interrupted during ${reason}; ` +
|
|
433
|
+
`delivered ${progress.deliveredCount}/${progress.expectedCount}, resolved ${progress.resolvedCount}/${progress.expectedCount}, ` +
|
|
434
|
+
`waiting=${progress.waitingCount}, queued=${progress.queuedCount}, unmatched=${progress.unmatchedResultCount}` +
|
|
435
|
+
`${toolNameSummary.length ? `, tools=${toolNameSummary.join(", ")}` : ""}. ` +
|
|
436
|
+
`Claude session will rebuild before the next turn; see ${diagLogPath()}.`,
|
|
437
|
+
"error",
|
|
438
|
+
);
|
|
439
|
+
return true;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
debug("reportToolResultMismatch failed:", error);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function __testSetBridgeIntegrityState(state: { ui?: Pick<ExtensionUIContext, "notify"> | null; sharedSession?: SessionState | null }): void {
|
|
447
|
+
if ("ui" in state) piUI = state.ui as ExtensionUIContext | undefined;
|
|
448
|
+
if ("sharedSession" in state) sharedSession = state.sharedSession ?? null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function __testGetBridgeIntegrityState(): { sharedSession: SessionState | null } {
|
|
452
|
+
return { sharedSession };
|
|
349
453
|
}
|
|
350
454
|
|
|
351
455
|
// --- Constants ---
|
|
@@ -426,6 +530,9 @@ let extensionApi: ExtensionAPI | undefined;
|
|
|
426
530
|
let piUI: ExtensionUIContext | undefined;
|
|
427
531
|
let extraUsageHelperInFlight: Promise<string> | null = null;
|
|
428
532
|
|
|
533
|
+
const RATE_LIMIT_AUTO_RESUME_EVENT = "vstack:rate-limit";
|
|
534
|
+
const RATE_LIMIT_TOKEN = "\x1b[31m[rate-limit]\x1b[39m";
|
|
535
|
+
|
|
429
536
|
export function isExtraUsageRequiredMessage(value: unknown): boolean {
|
|
430
537
|
let text: string;
|
|
431
538
|
if (typeof value === "string") text = value;
|
|
@@ -437,6 +544,64 @@ export function isExtraUsageRequiredMessage(value: unknown): boolean {
|
|
|
437
544
|
return /extra[-\s]?usage|overage|extra usage billing|extra usage credits|1M context/i.test(text);
|
|
438
545
|
}
|
|
439
546
|
|
|
547
|
+
export function uniqueNonEmptyLines(values: unknown[]): string[] {
|
|
548
|
+
const seen = new Set<string>();
|
|
549
|
+
const out: string[] = [];
|
|
550
|
+
for (const value of values) {
|
|
551
|
+
const text = typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim();
|
|
552
|
+
if (!text || seen.has(text)) continue;
|
|
553
|
+
seen.add(text);
|
|
554
|
+
out.push(text);
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function formatResetTimestamp(value: unknown): string {
|
|
560
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
561
|
+
if (!Number.isFinite(parsed)) return "unknown";
|
|
562
|
+
return new Date(parsed).toLocaleString(undefined, {
|
|
563
|
+
day: "numeric",
|
|
564
|
+
hour: "numeric",
|
|
565
|
+
minute: "2-digit",
|
|
566
|
+
month: "short",
|
|
567
|
+
second: "2-digit",
|
|
568
|
+
timeZoneName: "short",
|
|
569
|
+
year: "numeric",
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export const ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD = 80;
|
|
574
|
+
|
|
575
|
+
export function normalizeRateLimitUtilization(value: unknown): number | undefined {
|
|
576
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return undefined;
|
|
577
|
+
if (value === 0) return 0;
|
|
578
|
+
// Claude SDK payloads have appeared as both fractions and percentages.
|
|
579
|
+
// Exact 1 is unit-ambiguous (1% vs 100%), so do not use it for allowed-warning copy.
|
|
580
|
+
if (value > 0 && value < 1) return value * 100;
|
|
581
|
+
if (value > 1 && value <= 100) return value;
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function rateLimitTypeLabel(value: unknown): string {
|
|
586
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
587
|
+
return text || "unknown";
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function formatAllowedRateLimitWarning(info: { status?: unknown; utilization?: unknown; rateLimitType?: unknown } | null | undefined): string | undefined {
|
|
591
|
+
if (info?.status !== "allowed_warning") return undefined;
|
|
592
|
+
const utilization = normalizeRateLimitUtilization(info.utilization);
|
|
593
|
+
if (utilization === undefined || utilization < ALLOWED_RATE_LIMIT_WARNING_UTILIZATION_THRESHOLD) return undefined;
|
|
594
|
+
return `Claude rate limit warning: nearing ${rateLimitTypeLabel(info.rateLimitType)} limit; check Claude Code /usage for exact utilization.`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function emitRateLimitEvent(payload: Record<string, unknown>): void {
|
|
598
|
+
try {
|
|
599
|
+
extensionApi?.events?.emit?.(RATE_LIMIT_AUTO_RESUME_EVENT, payload);
|
|
600
|
+
} catch {
|
|
601
|
+
// Cross-extension broker is best-effort only.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
440
605
|
function extraUsageAllowed(config: Config): boolean {
|
|
441
606
|
return config.provider?.allowExtraUsage === true;
|
|
442
607
|
}
|
|
@@ -641,6 +806,7 @@ function convertAndImportMessages(
|
|
|
641
806
|
session: ReturnType<typeof createSession>,
|
|
642
807
|
messages: Context["messages"],
|
|
643
808
|
customToolNameToSdk?: Map<string, string>,
|
|
809
|
+
cwd?: string,
|
|
644
810
|
): void {
|
|
645
811
|
const { anthropicMessages, sanitizedIds } = convertPiMessages(messages, customToolNameToSdk);
|
|
646
812
|
|
|
@@ -656,7 +822,17 @@ function convertAndImportMessages(
|
|
|
656
822
|
[...sanitizedIds.entries()].map(([orig, clean]) => orig === clean ? orig : `${orig}→${clean}`).join(", "));
|
|
657
823
|
}
|
|
658
824
|
// Pre-repair for debug logging; importMessages also repairs internally (idempotent).
|
|
825
|
+
const missingToolResults = findUnpairedToolUses(anthropicMessages);
|
|
659
826
|
const repaired = repairToolPairing(anthropicMessages);
|
|
827
|
+
if (missingToolResults.length > 0) {
|
|
828
|
+
reportSyntheticToolResultRepair(missingToolResults, {
|
|
829
|
+
cwd,
|
|
830
|
+
messageCount: messages.length,
|
|
831
|
+
anthropicMessageCount: anthropicMessages.length,
|
|
832
|
+
sessionId: session.sessionId,
|
|
833
|
+
jsonlPath: session.jsonlPath,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
660
836
|
if (repaired.length !== anthropicMessages.length) {
|
|
661
837
|
debug(`convertAndImportMessages: repairToolPairing ${anthropicMessages.length} → ${repaired.length} msgs`);
|
|
662
838
|
}
|
|
@@ -854,7 +1030,7 @@ function syncSharedSession(
|
|
|
854
1030
|
...(preserveId ? { sessionId: previousSessionId } : {}),
|
|
855
1031
|
...(modelId ? { model: modelId } : {}),
|
|
856
1032
|
});
|
|
857
|
-
convertAndImportMessages(session, priorMessages, customToolNameToSdk);
|
|
1033
|
+
convertAndImportMessages(session, priorMessages, customToolNameToSdk, cwd);
|
|
858
1034
|
session.save();
|
|
859
1035
|
verifyWrittenSession(session.jsonlPath, session.sessionId, session.messages.length, cwd);
|
|
860
1036
|
sharedSession = { sessionId: session.sessionId, cursor: priorMessages.length, cwd };
|
|
@@ -954,28 +1130,50 @@ function resolveMcpTools(context: Context, excludeToolName?: string): {
|
|
|
954
1130
|
|
|
955
1131
|
// Creates an MCP server that bridges pi tools to the SDK. Each tool handler
|
|
956
1132
|
// blocks on a Promise until pi delivers the tool result via streamSimple.
|
|
957
|
-
// Handlers
|
|
958
|
-
//
|
|
959
|
-
// Handlers close over the captured `queryCtx`, ensuring they
|
|
960
|
-
// correct query's state even across pushContext/popContext calls.
|
|
1133
|
+
// Handlers claim their tool_call id by matching the actual MCP call
|
|
1134
|
+
// (tool name + arguments) against the recorded tool_use blocks, then results
|
|
1135
|
+
// are matched by ID. Handlers close over the captured `queryCtx`, ensuring they
|
|
1136
|
+
// operate on the correct query's state even across pushContext/popContext calls.
|
|
961
1137
|
function buildMcpServers(tools: Tool[], queryCtx: QueryContext): Record<string, ReturnType<typeof createSdkMcpServer>> | undefined {
|
|
962
1138
|
if (!tools.length) return undefined;
|
|
963
1139
|
const mcpTools = tools.map((tool) => ({
|
|
964
1140
|
name: tool.name,
|
|
965
1141
|
description: tool.description,
|
|
966
1142
|
inputSchema: jsonSchemaToZodShape(tool.parameters),
|
|
967
|
-
handler: async () => {
|
|
968
|
-
const
|
|
969
|
-
|
|
1143
|
+
handler: async (args?: Record<string, unknown>) => {
|
|
1144
|
+
const mappedArgs = mapToolArgs(tool.name, args);
|
|
1145
|
+
const claim = queryCtx.claimToolCall(tool.name, mappedArgs);
|
|
1146
|
+
const toolCallId = claim.toolCallId;
|
|
1147
|
+
if (!toolCallId) {
|
|
1148
|
+
debug(`WARNING: mcp handler ${tool.name} has no toolCallId (available=${claim.available})`);
|
|
1149
|
+
diagDump("tool_handler_unmatched", {
|
|
1150
|
+
toolName: tool.name,
|
|
1151
|
+
argKeys: argKeys(mappedArgs),
|
|
1152
|
+
available: claim.available,
|
|
1153
|
+
turnToolCallIds: queryCtx.turnToolCallIds,
|
|
1154
|
+
turnToolCalls: safeToolCallSummary(queryCtx.turnToolCalls),
|
|
1155
|
+
});
|
|
1156
|
+
return { content: [{ type: "text", text: `Claude bridge internal error: no matching tool_call id for ${tool.name}` }], isError: true } satisfies McpResult;
|
|
1157
|
+
}
|
|
1158
|
+
if (claim.match !== "tool-args" || claim.ambiguous) {
|
|
1159
|
+
debug(`mcp handler: ${tool.name} [${toolCallId}] claimed by ${claim.match}${claim.ambiguous ? " (ambiguous)" : ""}`);
|
|
1160
|
+
}
|
|
970
1161
|
if (toolCallId && queryCtx.pendingResults.has(toolCallId)) {
|
|
971
1162
|
const result = queryCtx.pendingResults.get(toolCallId)!;
|
|
972
1163
|
queryCtx.pendingResults.delete(toolCallId);
|
|
1164
|
+
queryCtx.markToolResultResolved(toolCallId);
|
|
973
1165
|
debug(`mcp handler: ${tool.name} [${toolCallId}] → resolved from queue (${queryCtx.pendingResults.size} remaining)`);
|
|
974
1166
|
return result;
|
|
975
1167
|
}
|
|
976
1168
|
debug(`mcp handler: ${tool.name} [${toolCallId}] → waiting`);
|
|
977
1169
|
return new Promise<McpResult>((resolve) => {
|
|
978
|
-
queryCtx.pendingToolCalls.set(toolCallId, {
|
|
1170
|
+
queryCtx.pendingToolCalls.set(toolCallId, {
|
|
1171
|
+
toolName: tool.name,
|
|
1172
|
+
resolve: (result) => {
|
|
1173
|
+
queryCtx.markToolResultResolved(toolCallId);
|
|
1174
|
+
resolve(result);
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
979
1177
|
});
|
|
980
1178
|
},
|
|
981
1179
|
}));
|
|
@@ -1004,6 +1202,26 @@ const REASONING_TO_EFFORT: Record<string, EffortLevel> = {
|
|
|
1004
1202
|
minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "max",
|
|
1005
1203
|
};
|
|
1006
1204
|
|
|
1205
|
+
function normalizeEffortOverrideModelKey(value: string): string {
|
|
1206
|
+
const key = value.trim().toLowerCase();
|
|
1207
|
+
return key.startsWith(`${PROVIDER_ID}/`) ? key.slice(PROVIDER_ID.length + 1) : key;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
export function resolveConfiguredEffort(
|
|
1211
|
+
modelId: string,
|
|
1212
|
+
reasoningEffort: EffortLevel | undefined,
|
|
1213
|
+
providerConfig?: Config["provider"],
|
|
1214
|
+
): EffortLevel | undefined {
|
|
1215
|
+
const target = normalizeEffortOverrideModelKey(modelId);
|
|
1216
|
+
for (const [key, rawEffort] of Object.entries(providerConfig?.modelEffortOverrides ?? {})) {
|
|
1217
|
+
const normalizedKey = normalizeEffortOverrideModelKey(key);
|
|
1218
|
+
if (normalizedKey !== "*" && normalizedKey !== target) continue;
|
|
1219
|
+
const effort = normalizeEffortLevel(rawEffort) as EffortLevel | undefined;
|
|
1220
|
+
if (effort) return effort;
|
|
1221
|
+
}
|
|
1222
|
+
return (normalizeEffortLevel(providerConfig?.forceEffort) as EffortLevel | undefined) ?? reasoningEffort;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1007
1225
|
// --- Provider helpers: misc ---
|
|
1008
1226
|
|
|
1009
1227
|
function mapStopReason(reason: string | undefined): "stop" | "length" | "toolUse" {
|
|
@@ -1054,24 +1272,28 @@ function finalizeCurrentStream(stopReason?: string): void {
|
|
|
1054
1272
|
|
|
1055
1273
|
/** Maps Anthropic stream events to pi stream events (text, thinking, toolcall).
|
|
1056
1274
|
* On message_stop with tool_use: ends currentPiStream so pi can execute the tool. */
|
|
1057
|
-
function processStreamEvent(
|
|
1275
|
+
export function processStreamEvent(
|
|
1058
1276
|
message: SDKMessage,
|
|
1059
1277
|
customToolNameToPi: Map<string, string>,
|
|
1060
1278
|
model: Model<any>,
|
|
1061
1279
|
): void {
|
|
1062
1280
|
const c = ctx();
|
|
1063
1281
|
if (!c.currentPiStream || !c.turnOutput) return;
|
|
1064
|
-
c.turnSawStreamEvent = true;
|
|
1065
1282
|
const event = (message as SDKMessage & { event: any }).event;
|
|
1283
|
+
if (event?.type === "ping") return;
|
|
1284
|
+
if (event?.type === "message_stop" && !c.turnSawToolCall) {
|
|
1285
|
+
debug("processStreamEvent: ignoring bare message_stop with no streamed content/tool call");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1066
1288
|
|
|
1067
1289
|
if (event?.type === "message_start") {
|
|
1068
|
-
c.
|
|
1069
|
-
c.nextHandlerIdx = 0;
|
|
1290
|
+
c.resetToolTracking();
|
|
1070
1291
|
if (event.message?.usage) updateUsage(c.turnOutput, event.message.usage, model);
|
|
1071
1292
|
return;
|
|
1072
1293
|
}
|
|
1073
1294
|
|
|
1074
1295
|
if (event?.type === "content_block_start") {
|
|
1296
|
+
c.turnSawStreamEvent = true;
|
|
1075
1297
|
ensureTurnStarted();
|
|
1076
1298
|
if (event.content_block?.type === "text") {
|
|
1077
1299
|
c.turnBlocks.push({ type: "text", text: "", index: event.index });
|
|
@@ -1081,10 +1303,11 @@ function processStreamEvent(
|
|
|
1081
1303
|
c.currentPiStream!.push({ type: "thinking_start", contentIndex: c.turnBlocks.length - 1, partial: c.turnOutput });
|
|
1082
1304
|
} else if (event.content_block?.type === "tool_use") {
|
|
1083
1305
|
c.turnSawToolCall = true;
|
|
1084
|
-
|
|
1306
|
+
const mappedName = mapToolName(event.content_block.name, customToolNameToPi);
|
|
1307
|
+
c.recordToolCall(event.content_block.id, mappedName, {});
|
|
1085
1308
|
c.turnBlocks.push({
|
|
1086
1309
|
type: "toolCall", id: event.content_block.id,
|
|
1087
|
-
name:
|
|
1310
|
+
name: mappedName,
|
|
1088
1311
|
arguments: (event.content_block.input as Record<string, unknown>) ?? {},
|
|
1089
1312
|
partialJson: "", index: event.index,
|
|
1090
1313
|
});
|
|
@@ -1098,7 +1321,11 @@ function processStreamEvent(
|
|
|
1098
1321
|
if (event?.type === "content_block_delta") {
|
|
1099
1322
|
const index = c.turnBlocks.findIndex((b: any) => b.index === event.index);
|
|
1100
1323
|
const block = c.turnBlocks[index];
|
|
1101
|
-
if (!block)
|
|
1324
|
+
if (!block) {
|
|
1325
|
+
debug("processStreamEvent: ignoring unmatched content_block_delta", event.index);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
c.turnSawStreamEvent = true;
|
|
1102
1329
|
if (event.delta?.type === "text_delta" && block.type === "text") {
|
|
1103
1330
|
block.text += event.delta.text;
|
|
1104
1331
|
c.currentPiStream!.push({ type: "text_delta", contentIndex: index, delta: event.delta.text, partial: c.turnOutput });
|
|
@@ -1120,7 +1347,11 @@ function processStreamEvent(
|
|
|
1120
1347
|
if (event?.type === "content_block_stop") {
|
|
1121
1348
|
const index = c.turnBlocks.findIndex((b: any) => b.index === event.index);
|
|
1122
1349
|
const block = c.turnBlocks[index];
|
|
1123
|
-
if (!block)
|
|
1350
|
+
if (!block) {
|
|
1351
|
+
debug("processStreamEvent: ignoring unmatched content_block_stop", event.index);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
c.turnSawStreamEvent = true;
|
|
1124
1355
|
delete block.index;
|
|
1125
1356
|
if (block.type === "text") {
|
|
1126
1357
|
c.currentPiStream!.push({ type: "text_end", contentIndex: index, content: block.text, partial: c.turnOutput });
|
|
@@ -1131,6 +1362,7 @@ function processStreamEvent(
|
|
|
1131
1362
|
block.arguments = mapToolArgs(
|
|
1132
1363
|
block.name, parsePartialJson(block.partialJson, block.arguments),
|
|
1133
1364
|
);
|
|
1365
|
+
c.updateToolCallArgs(block.id, block.arguments);
|
|
1134
1366
|
delete block.partialJson;
|
|
1135
1367
|
c.currentPiStream!.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: c.turnOutput });
|
|
1136
1368
|
}
|
|
@@ -1169,13 +1401,73 @@ function processStreamEvent(
|
|
|
1169
1401
|
// arrives before any stream_events, this is the primary content path. Must maintain
|
|
1170
1402
|
// the same stream lifecycle as processStreamEvent — including ending the stream on
|
|
1171
1403
|
// tool_use to prevent deadlock with the MCP handler.
|
|
1172
|
-
function
|
|
1404
|
+
function appendMissingToolUsesFromAssistant(
|
|
1405
|
+
assistantMsg: { content?: Array<any>; usage?: Record<string, number | undefined> },
|
|
1406
|
+
model: Model<any>,
|
|
1407
|
+
customToolNameToPi: Map<string, string>,
|
|
1408
|
+
): boolean {
|
|
1409
|
+
const c = ctx();
|
|
1410
|
+
if (!assistantMsg?.content) return false;
|
|
1411
|
+
let sawToolUse = false;
|
|
1412
|
+
for (const block of assistantMsg.content) {
|
|
1413
|
+
if (block.type !== "tool_use") continue;
|
|
1414
|
+
sawToolUse = true;
|
|
1415
|
+
const existingIdx = c.turnBlocks.findIndex((b: any) => b.type === "toolCall" && b.id === block.id);
|
|
1416
|
+
const name = mapToolName(block.name, customToolNameToPi);
|
|
1417
|
+
const mappedArgs = mapToolArgs(name, block.input);
|
|
1418
|
+
c.recordToolCall(block.id, name, mappedArgs);
|
|
1419
|
+
if (existingIdx >= 0) {
|
|
1420
|
+
const existing = c.turnBlocks[existingIdx] as any;
|
|
1421
|
+
existing.name = name;
|
|
1422
|
+
existing.arguments = mappedArgs;
|
|
1423
|
+
c.updateToolCallArgs(block.id, mappedArgs);
|
|
1424
|
+
if ("partialJson" in existing) {
|
|
1425
|
+
delete existing.partialJson;
|
|
1426
|
+
delete existing.index;
|
|
1427
|
+
c.currentPiStream?.push({ type: "toolcall_end", contentIndex: existingIdx, toolCall: existing, partial: c.turnOutput });
|
|
1428
|
+
}
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
ensureTurnStarted();
|
|
1433
|
+
c.turnBlocks.push({
|
|
1434
|
+
type: "toolCall", id: block.id,
|
|
1435
|
+
name,
|
|
1436
|
+
arguments: mappedArgs,
|
|
1437
|
+
});
|
|
1438
|
+
const idx = c.turnBlocks.length - 1;
|
|
1439
|
+
const toolBlock = c.turnBlocks[idx];
|
|
1440
|
+
c.currentPiStream?.push({ type: "toolcall_start", contentIndex: idx, partial: c.turnOutput });
|
|
1441
|
+
c.currentPiStream?.push({ type: "toolcall_end", contentIndex: idx, toolCall: toolBlock as any, partial: c.turnOutput });
|
|
1442
|
+
}
|
|
1443
|
+
if (assistantMsg.usage && c.turnOutput) updateUsage(c.turnOutput, assistantMsg.usage, model);
|
|
1444
|
+
return sawToolUse;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
export function processAssistantMessage(message: SDKMessage, model: Model<any>, customToolNameToPi: Map<string, string>): void {
|
|
1173
1448
|
const c = ctx();
|
|
1174
|
-
if (c.turnSawStreamEvent) return;
|
|
1175
1449
|
const assistantMsg = (message as any).message;
|
|
1176
1450
|
if (!assistantMsg?.content) return;
|
|
1177
|
-
c.
|
|
1178
|
-
|
|
1451
|
+
if (c.turnSawStreamEvent) {
|
|
1452
|
+
// Claude Agent SDK can yield the completed assistant message before (or
|
|
1453
|
+
// instead of) a stream_event message_stop for a tool-use turn. Treat that
|
|
1454
|
+
// assistant message as a hard turn boundary so Pi executes the tool calls
|
|
1455
|
+
// and the MCP handlers stay blocked until real tool results are delivered.
|
|
1456
|
+
// Without this fallback, Claude Code can continue internally with empty MCP
|
|
1457
|
+
// results and Pi only sees the real outputs one render cycle later.
|
|
1458
|
+
if (appendMissingToolUsesFromAssistant(assistantMsg, model, customToolNameToPi)) {
|
|
1459
|
+
c.turnSawToolCall = true;
|
|
1460
|
+
if (c.currentPiStream && c.turnOutput) {
|
|
1461
|
+
c.turnOutput.stopReason = "toolUse";
|
|
1462
|
+
c.currentPiStream.push({ type: "done", reason: "toolUse", message: c.turnOutput });
|
|
1463
|
+
c.currentPiStream.end();
|
|
1464
|
+
c.currentPiStream = null;
|
|
1465
|
+
debug("processAssistantMessage boundary: ended streamed tool_use turn from assistant message");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
c.resetToolTracking();
|
|
1179
1471
|
debug(`processAssistantMessage fallback: ${assistantMsg.content.length} blocks, types=${assistantMsg.content.map((b: any) => b.type).join(",")}`);
|
|
1180
1472
|
for (const block of assistantMsg.content) {
|
|
1181
1473
|
if (block.type === "text" && block.text) {
|
|
@@ -1195,11 +1487,12 @@ function processAssistantMessage(message: SDKMessage, model: Model<any>, customT
|
|
|
1195
1487
|
} else if (block.type === "tool_use") {
|
|
1196
1488
|
ensureTurnStarted();
|
|
1197
1489
|
c.turnSawToolCall = true;
|
|
1198
|
-
|
|
1199
|
-
const mappedArgs = mapToolArgs(
|
|
1490
|
+
const mappedName = mapToolName(block.name, customToolNameToPi);
|
|
1491
|
+
const mappedArgs = mapToolArgs(mappedName, block.input);
|
|
1492
|
+
c.recordToolCall(block.id, mappedName, mappedArgs);
|
|
1200
1493
|
c.turnBlocks.push({
|
|
1201
1494
|
type: "toolCall", id: block.id,
|
|
1202
|
-
name:
|
|
1495
|
+
name: mappedName,
|
|
1203
1496
|
arguments: mappedArgs,
|
|
1204
1497
|
});
|
|
1205
1498
|
const idx = c.turnBlocks.length - 1;
|
|
@@ -1238,7 +1531,9 @@ async function consumeQuery(
|
|
|
1238
1531
|
|
|
1239
1532
|
for await (const message of sdkQuery) {
|
|
1240
1533
|
if (wasAborted()) break;
|
|
1241
|
-
|
|
1534
|
+
const queryCtx = ctx();
|
|
1535
|
+
if (!queryCtx.turnOutput) continue;
|
|
1536
|
+
if (!queryCtx.currentPiStream && !(message.type === "assistant" && queryCtx.turnSawToolCall)) continue;
|
|
1242
1537
|
|
|
1243
1538
|
switch (message.type) {
|
|
1244
1539
|
case "stream_event":
|
|
@@ -1257,8 +1552,10 @@ async function consumeQuery(
|
|
|
1257
1552
|
ctx().currentPiStream?.push({ type: "text_delta", contentIndex: idx, delta: text, partial: ctx().turnOutput });
|
|
1258
1553
|
ctx().currentPiStream?.push({ type: "text_end", contentIndex: idx, content: text, partial: ctx().turnOutput });
|
|
1259
1554
|
} else if (message.subtype !== "success" && isExtraUsageRequiredMessage(message)) {
|
|
1260
|
-
const
|
|
1555
|
+
const errorLines = Array.isArray((message as any).errors) ? uniqueNonEmptyLines((message as any).errors) : [];
|
|
1556
|
+
const errors = errorLines.length > 0 ? errorLines.join("\n") : String(message.subtype ?? "Claude Code rate limit");
|
|
1261
1557
|
const openedExtraUsage = launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "result error");
|
|
1558
|
+
ctx().handledTerminalError = true;
|
|
1262
1559
|
ctx().turnOutput.stopReason = "error";
|
|
1263
1560
|
ctx().turnOutput.errorMessage = `${errors}${openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : "\n\nRun /claude-bridge:extra, or enable Allow extra usage helper in settings."}`;
|
|
1264
1561
|
ctx().currentPiStream?.push({ type: "error", reason: "error", error: ctx().turnOutput });
|
|
@@ -1277,12 +1574,25 @@ async function consumeQuery(
|
|
|
1277
1574
|
const info = (message as any).rate_limit_info;
|
|
1278
1575
|
debug("consumeQuery: rate_limit_event", JSON.stringify(info).slice(0, 300));
|
|
1279
1576
|
if (info?.status === "rejected") {
|
|
1280
|
-
const resetsAt =
|
|
1577
|
+
const resetsAt = formatResetTimestamp(info.resetsAt);
|
|
1578
|
+
const resetAtMs = typeof info.resetsAt === "string" ? Date.parse(info.resetsAt) : undefined;
|
|
1281
1579
|
const reason = `${info.rateLimitType ?? "unknown"} rate limit`;
|
|
1282
1580
|
const launchedExtraUsage = isExtraUsageRequiredMessage(info) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, reason);
|
|
1283
|
-
|
|
1581
|
+
emitRateLimitEvent({
|
|
1582
|
+
model: model.id,
|
|
1583
|
+
provider: PROVIDER_ID,
|
|
1584
|
+
rateLimitType: info.rateLimitType,
|
|
1585
|
+
reason,
|
|
1586
|
+
resetAt: info.resetsAt,
|
|
1587
|
+
...(Number.isFinite(resetAtMs) ? { resetAtMs } : {}),
|
|
1588
|
+
source: "claude-bridge",
|
|
1589
|
+
status: "rejected",
|
|
1590
|
+
});
|
|
1591
|
+
piUI?.notify(`${RATE_LIMIT_TOKEN} Claude ${reason} hit — resets ${resetsAt}${launchedExtraUsage ? "; opened /extra-usage helper" : ""}`, "warning");
|
|
1284
1592
|
} else if (info?.status === "allowed_warning") {
|
|
1285
|
-
|
|
1593
|
+
const warning = formatAllowedRateLimitWarning(info);
|
|
1594
|
+
if (warning) piUI?.notify(warning, "warning");
|
|
1595
|
+
else debug("consumeQuery: suppressed low/ambiguous allowed_warning rate_limit_event", JSON.stringify(info).slice(0, 300));
|
|
1286
1596
|
}
|
|
1287
1597
|
break;
|
|
1288
1598
|
}
|
|
@@ -1305,6 +1615,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1305
1615
|
|
|
1306
1616
|
// DEBUG: trace followUp message triggering
|
|
1307
1617
|
const lastMsgRole = context.messages[context.messages.length - 1]?.role;
|
|
1618
|
+
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1308
1619
|
debug(`provider: streamClaudeAgentSdk called, activeQuery=${!!ctx().activeQuery}, lastMsgRole=${lastMsgRole}, isReentrant=${ctx().activeQuery !== null}`);
|
|
1309
1620
|
|
|
1310
1621
|
// --- Tool result delivery ---
|
|
@@ -1312,30 +1623,48 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1312
1623
|
// (everything after the last assistant message) and match against waiting MCP
|
|
1313
1624
|
// handlers. Results that arrive before their handler get queued in pendingResults.
|
|
1314
1625
|
if (ctx().activeQuery) {
|
|
1315
|
-
ctx()
|
|
1316
|
-
|
|
1626
|
+
const queryCtx = ctx();
|
|
1627
|
+
queryCtx.currentPiStream = stream;
|
|
1628
|
+
queryCtx.resetTurnState(model);
|
|
1317
1629
|
const allResults = extractAllToolResults(context);
|
|
1318
|
-
debug(`provider: tool results, ${allResults.length} results, ${
|
|
1630
|
+
debug(`provider: tool results, ${allResults.length} results, ${queryCtx.pendingToolCalls.size} waiting handlers, ctx.msgs=${context.messages.length}`);
|
|
1631
|
+
const unmatchedResultIds: string[] = [];
|
|
1319
1632
|
for (const result of allResults) {
|
|
1320
1633
|
const id = result.toolCallId;
|
|
1321
|
-
if (id &&
|
|
1322
|
-
|
|
1323
|
-
|
|
1634
|
+
if (id && !queryCtx.hasRecordedToolCall(id)) {
|
|
1635
|
+
queryCtx.markToolResultUnmatched(id);
|
|
1636
|
+
unmatchedResultIds.push(id);
|
|
1637
|
+
debug(`ERROR: tool result [${id}] has no registered tool_call id; refusing to queue or deliver`);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
queryCtx.markToolResultDelivered(id);
|
|
1641
|
+
if (id && queryCtx.pendingToolCalls.has(id)) {
|
|
1642
|
+
const pending = queryCtx.pendingToolCalls.get(id)!;
|
|
1643
|
+
queryCtx.pendingToolCalls.delete(id);
|
|
1324
1644
|
debug(`provider: resolving ${pending.toolName} [${id}]${result.isError ? " (error)" : ""}`, JSON.stringify(result.content).slice(0, 200));
|
|
1325
1645
|
pending.resolve(result);
|
|
1326
1646
|
} else if (id) {
|
|
1327
|
-
|
|
1328
|
-
debug(`provider: queued result [${id}] (${
|
|
1647
|
+
queryCtx.pendingResults.set(id, result);
|
|
1648
|
+
debug(`provider: queued result [${id}] (${queryCtx.pendingResults.size} pending)`);
|
|
1329
1649
|
} else {
|
|
1330
1650
|
debug(`WARNING: tool result without toolCallId, cannot match`);
|
|
1331
1651
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
debug(`BUG: both maps non-empty! handlers=${
|
|
1652
|
+
if (queryCtx.pendingToolCalls.size > 0 && queryCtx.pendingResults.size > 0) {
|
|
1653
|
+
debug(`BUG: both maps non-empty! handlers=${queryCtx.pendingToolCalls.size} results=${queryCtx.pendingResults.size}`);
|
|
1334
1654
|
}
|
|
1335
1655
|
}
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1338
|
-
|
|
1656
|
+
if (unmatchedResultIds.length > 0) {
|
|
1657
|
+
const errorResult: McpResult = {
|
|
1658
|
+
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 ? ", ..." : ""}` }],
|
|
1659
|
+
isError: true,
|
|
1660
|
+
};
|
|
1661
|
+
for (const pending of queryCtx.pendingToolCalls.values()) pending.resolve(errorResult);
|
|
1662
|
+
queryCtx.pendingToolCalls.clear();
|
|
1663
|
+
reportToolResultMismatch(queryCtx, "unmatched tool result", cwd);
|
|
1664
|
+
}
|
|
1665
|
+
if (queryCtx.pendingToolCalls.size > 0) {
|
|
1666
|
+
debug(`WARNING: ${queryCtx.pendingToolCalls.size} MCP handlers still waiting after delivering ${allResults.length} results`);
|
|
1667
|
+
piUI?.notify(`Claude bridge: ${queryCtx.pendingToolCalls.size} tool handler(s) still waiting — provider may be stuck`, "warning");
|
|
1339
1668
|
}
|
|
1340
1669
|
|
|
1341
1670
|
// Detect user messages (steer/followUp) that pi injected into context
|
|
@@ -1355,7 +1684,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1355
1684
|
}
|
|
1356
1685
|
|
|
1357
1686
|
if (sharedSession) sharedSession.cursor = context.messages.length;
|
|
1358
|
-
|
|
1687
|
+
queryCtx.latestCursor = Math.max(queryCtx.latestCursor, context.messages.length);
|
|
1359
1688
|
return stream;
|
|
1360
1689
|
}
|
|
1361
1690
|
|
|
@@ -1389,10 +1718,10 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1389
1718
|
ctx().pendingResults.clear();
|
|
1390
1719
|
ctx().deferredUserMessages = [];
|
|
1391
1720
|
ctx().resetTurnState(model);
|
|
1721
|
+
ctx().resetToolTracking();
|
|
1392
1722
|
ctx().latestCursor = 0;
|
|
1393
1723
|
|
|
1394
1724
|
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context);
|
|
1395
|
-
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1396
1725
|
const promptBlocks = extractUserPromptBlocks(context.messages);
|
|
1397
1726
|
let promptText = extractUserPrompt(context.messages) ?? "";
|
|
1398
1727
|
|
|
@@ -1440,10 +1769,11 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1440
1769
|
// Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
|
|
1441
1770
|
// per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
|
|
1442
1771
|
// Fall back to our generic table for older pi-ai or unmapped levels.
|
|
1443
|
-
const
|
|
1772
|
+
const requestedEffort = options?.reasoning
|
|
1444
1773
|
? ((model as any).thinkingLevelMap?.[options.reasoning] as EffortLevel | undefined)
|
|
1445
1774
|
?? REASONING_TO_EFFORT[options.reasoning]
|
|
1446
1775
|
: undefined;
|
|
1776
|
+
const effort = resolveConfiguredEffort(model.id, requestedEffort, providerSettings);
|
|
1447
1777
|
|
|
1448
1778
|
const extraArgs: Record<string, string | null> = { model: model.id };
|
|
1449
1779
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
@@ -1468,6 +1798,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1468
1798
|
...CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
1469
1799
|
permissionMode: "bypassPermissions",
|
|
1470
1800
|
includePartialMessages: true,
|
|
1801
|
+
...(providerSettings.fastMode ? { settings: { fastMode: true } } : {}),
|
|
1471
1802
|
systemPrompt: {
|
|
1472
1803
|
type: "preset", preset: "claude_code",
|
|
1473
1804
|
append: systemPromptAppend ? systemPromptAppend : undefined,
|
|
@@ -1485,7 +1816,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1485
1816
|
debug("provider: fresh query",
|
|
1486
1817
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1487
1818
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1488
|
-
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled}`,
|
|
1819
|
+
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled} fastMode=${providerSettings.fastMode === true}`,
|
|
1489
1820
|
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
1490
1821
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`);
|
|
1491
1822
|
|
|
@@ -1507,6 +1838,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1507
1838
|
wasAborted = true;
|
|
1508
1839
|
// Prevent stale deferred messages from being replayed by parent on pop
|
|
1509
1840
|
abortCtx.deferredUserMessages = [];
|
|
1841
|
+
reportToolResultMismatch(abortCtx, "abort", cwd, { forceRotate: true });
|
|
1510
1842
|
for (const pending of abortCtx.pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Operation aborted" }] }); }
|
|
1511
1843
|
abortCtx.pendingToolCalls.clear();
|
|
1512
1844
|
abortCtx.pendingResults.clear();
|
|
@@ -1553,6 +1885,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1553
1885
|
const steerPrompt = ctx().deferredUserMessages.shift()!;
|
|
1554
1886
|
debug(`provider: replaying deferred user message: ${steerPrompt.slice(0, 60)}`);
|
|
1555
1887
|
ctx().resetTurnState(model);
|
|
1888
|
+
ctx().resetToolTracking();
|
|
1556
1889
|
|
|
1557
1890
|
const resumeId = sharedSession?.sessionId;
|
|
1558
1891
|
if (!resumeId) {
|
|
@@ -1588,13 +1921,18 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1588
1921
|
})
|
|
1589
1922
|
.catch((error) => {
|
|
1590
1923
|
debug(`provider: query error, model=${model.id}, aborted=${Boolean(options?.signal?.aborted)}, error=`, error);
|
|
1591
|
-
const
|
|
1924
|
+
const suppressDuplicateError = ctx().handledTerminalError;
|
|
1925
|
+
const openedExtraUsage = !suppressDuplicateError && isExtraUsageRequiredMessage(error) && launchExtraUsageHelperIfAllowed(cwd, bridgeConfig, "query error");
|
|
1592
1926
|
if ((wasAborted || options?.signal?.aborted) && sharedSession) {
|
|
1593
1927
|
sharedSession = { ...sharedSession, needsRebuild: true, forceRotate: true };
|
|
1594
1928
|
} else {
|
|
1595
1929
|
sharedSession = null;
|
|
1596
1930
|
}
|
|
1597
1931
|
ctx().deferredUserMessages = [];
|
|
1932
|
+
if (suppressDuplicateError) {
|
|
1933
|
+
debug("provider: suppressing duplicate query error after terminal error was already emitted");
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1598
1936
|
if (ctx().turnOutput) {
|
|
1599
1937
|
ctx().turnOutput.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1600
1938
|
ctx().turnOutput.errorMessage = `${error instanceof Error ? error.message : String(error)}${openedExtraUsage ? "\n\nOpened Claude Code /extra-usage helper. Complete billing/admin flow in the browser, then retry the prompt." : ""}`;
|
|
@@ -1606,6 +1944,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1606
1944
|
.finally(() => {
|
|
1607
1945
|
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
|
1608
1946
|
if (ctx().activeQuery === sdkQuery) {
|
|
1947
|
+
reportToolResultMismatch(ctx(), "query teardown", cwd, { forceRotate: wasAborted || options?.signal?.aborted });
|
|
1609
1948
|
// Drain pending handlers for this query
|
|
1610
1949
|
for (const pending of ctx().pendingToolCalls.values()) { pending.resolve({ content: [{ type: "text", text: "Query ended" }] }); }
|
|
1611
1950
|
ctx().pendingToolCalls.clear();
|
|
@@ -1741,6 +2080,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1741
2080
|
// triggers CC's autocompact-thrashing guard (issue #8). Force the next
|
|
1742
2081
|
// call down the REBUILD path so CC sees the current history.
|
|
1743
2082
|
const markRebuild = (event: string) => {
|
|
2083
|
+
if (ctx().activeQuery) {
|
|
2084
|
+
reportToolResultMismatch(ctx(), event, sharedSession?.cwd ?? process.cwd());
|
|
2085
|
+
}
|
|
1744
2086
|
if (sharedSession) {
|
|
1745
2087
|
debug(`${event}: marking needsRebuild on session ${sharedSession.sessionId.slice(0, 8)}`);
|
|
1746
2088
|
sharedSession = { ...sharedSession, needsRebuild: true };
|