vigthoria-cli 1.10.51 → 1.10.55

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/dist/utils/api.js CHANGED
@@ -9,6 +9,8 @@ import https from 'https';
9
9
  import net from 'net';
10
10
  import path from 'path';
11
11
  import WebSocket from 'ws';
12
+ import { buildSemanticContext } from './context-ranker.js';
13
+ import { getChangedFiles } from './workspace-cache.js';
12
14
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
13
15
  export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
14
16
  export class CLIError extends Error {
@@ -282,6 +284,11 @@ export class APIClient {
282
284
  vigFlowTokens = new Map();
283
285
  _httpsAgent = null;
284
286
  lastChatTransportErrors = [];
287
+ clientToolErrors = [];
288
+ pendingV3ClientToolTasks = new Set();
289
+ /** Tracks files mutated via client_tool_request during an active V3 SSE stream. */
290
+ activeV3StreamedFiles = null;
291
+ activeV3StreamContext = null;
285
292
  constructor(config, logger) {
286
293
  this.config = config;
287
294
  this.logger = logger;
@@ -631,6 +638,13 @@ export class APIClient {
631
638
  }
632
639
  return `${baseUrl}/api/v3-agent/continue`;
633
640
  }
641
+ getV3AgentBackgroundUrl(baseUrl, suffix = '') {
642
+ const cleanSuffix = suffix ? `/${suffix.replace(/^\/+/, '')}` : '';
643
+ if (/127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)) {
644
+ return `${baseUrl}/api/agent/background${cleanSuffix}`;
645
+ }
646
+ return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
647
+ }
634
648
  getOperatorBaseUrls() {
635
649
  const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
636
650
  const urls = [
@@ -720,7 +734,7 @@ export class APIClient {
720
734
  continue;
721
735
  }
722
736
  for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
723
- if (entry.name === '.git' || entry.name === 'node_modules') {
737
+ if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vigthoria') {
724
738
  continue;
725
739
  }
726
740
  const fullPath = path.join(current, entry.name);
@@ -1410,11 +1424,18 @@ export class APIClient {
1410
1424
  const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
1411
1425
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
1412
1426
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
1413
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
1427
+ const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
1428
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
1414
1429
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
1415
1430
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
1416
1431
  const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
1417
1432
  const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
1433
+ const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
1434
+ const localMachineCapable = resolvedContext.localMachineCapable !== false;
1435
+ const clientToolExecution = resolvedContext.clientToolExecution === false
1436
+ ? false
1437
+ : (resolvedContext.clientToolExecution === true
1438
+ || (localMachineCapable && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
1418
1439
  const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
1419
1440
  localWorkspacePath,
1420
1441
  serverWorkspacePath,
@@ -1448,7 +1469,9 @@ export class APIClient {
1448
1469
  legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
1449
1470
  executionSurface: resolvedContext.executionSurface || 'cli',
1450
1471
  clientSurface: resolvedContext.clientSurface || 'cli',
1451
- localMachineCapable: resolvedContext.localMachineCapable !== false,
1472
+ localMachineCapable,
1473
+ clientToolExecution,
1474
+ client_tool_execution: clientToolExecution,
1452
1475
  runtimeEnvironment: publicRuntimeEnvironment,
1453
1476
  workspacePath: effectiveWorkspacePath,
1454
1477
  projectPath: effectiveWorkspacePath,
@@ -1471,6 +1494,11 @@ export class APIClient {
1471
1494
  requestStartedAt: resolvedContext.requestStartedAt,
1472
1495
  subscriptionPlan: this.config.getNormalizedPlan() || null,
1473
1496
  email: this.config.get('email') || null,
1497
+ vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
1498
+ vigthoria_brain: resolvedContext.vigthoriaBrain || null,
1499
+ codebaseContext: resolvedContext.codebaseContext || '',
1500
+ accountBrainContext: resolvedContext.accountBrainContext || '',
1501
+ projectMemory: resolvedContext.projectMemory || '',
1474
1502
  };
1475
1503
  return this.compactV3Context(payload);
1476
1504
  }
@@ -1602,6 +1630,8 @@ export class APIClient {
1602
1630
  contextId: resolvedContext.contextId,
1603
1631
  traceId: resolvedContext.traceId,
1604
1632
  requestStartedAt: resolvedContext.requestStartedAt,
1633
+ vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
1634
+ vigthoria_brain: resolvedContext.vigthoriaBrain || null,
1605
1635
  });
1606
1636
  }
1607
1637
  extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
@@ -2075,7 +2105,7 @@ menu {
2075
2105
  const headers = await this.getMcpHeaders();
2076
2106
  const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
2077
2107
  const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
2078
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
2108
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
2079
2109
  const metadata = {
2080
2110
  source: 'vigthoria-cli',
2081
2111
  sharedContextId: executionContext.contextId,
@@ -2236,7 +2266,7 @@ menu {
2236
2266
  cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
2237
2267
  };
2238
2268
  }
2239
- buildLocalWorkspaceSummary(rootPath) {
2269
+ buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
2240
2270
  if (!rootPath || !fs.existsSync(rootPath)) {
2241
2271
  return null;
2242
2272
  }
@@ -2247,8 +2277,31 @@ menu {
2247
2277
  files: [],
2248
2278
  };
2249
2279
  const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
2280
+ const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
2281
+ let orderedPaths = candidatePaths;
2282
+ try {
2283
+ const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
2284
+ const changes = getChangedFiles(rootPath, candidatePaths);
2285
+ const ordered = new Set();
2286
+ for (const filePath of changes.changed)
2287
+ ordered.add(filePath);
2288
+ for (const filePath of ranked)
2289
+ ordered.add(filePath);
2290
+ for (const filePath of changes.unchanged)
2291
+ ordered.add(filePath);
2292
+ orderedPaths = Array.from(ordered);
2293
+ summary.semanticRank = ranked.slice(0, 20);
2294
+ summary.changeSummary = {
2295
+ changed: changes.changed.length,
2296
+ unchanged: changes.unchanged.length,
2297
+ total: changes.total,
2298
+ };
2299
+ }
2300
+ catch {
2301
+ orderedPaths = candidatePaths;
2302
+ }
2250
2303
  summary.fileCount = snapshot.fileCount;
2251
- summary.files = snapshot.paths.slice(0, 40);
2304
+ summary.files = orderedPaths.slice(0, 40);
2252
2305
  const packageJsonPath = path.join(rootPath, 'package.json');
2253
2306
  if (fs.existsSync(packageJsonPath)) {
2254
2307
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -2266,7 +2319,7 @@ menu {
2266
2319
  }
2267
2320
  // Hydrate workspace: include actual file contents so the V3 server
2268
2321
  // can populate the remote workspace before the agent starts.
2269
- summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, snapshot.paths);
2322
+ summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
2270
2323
  return summary;
2271
2324
  }
2272
2325
  catch (error) {
@@ -2302,6 +2355,8 @@ menu {
2302
2355
  continue;
2303
2356
  if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
2304
2357
  continue;
2358
+ if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
2359
+ continue;
2305
2360
  const absolutePath = path.join(rootPath, relativePath);
2306
2361
  try {
2307
2362
  const stat = fs.statSync(absolutePath);
@@ -2481,6 +2536,19 @@ menu {
2481
2536
  return;
2482
2537
  }
2483
2538
  if (event.type !== 'tool_call') {
2539
+ if (event.type === 'tool_result' && event.success) {
2540
+ const name = String(event.name || event.tool || '').trim();
2541
+ const args = event.arguments || {};
2542
+ const targetPath = typeof event.target === 'string'
2543
+ ? event.target
2544
+ : (typeof args.path === 'string' ? args.path : '');
2545
+ if ((name === 'write_file' || name === 'edit_file') && targetPath) {
2546
+ const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
2547
+ if (filePath && typeof args.content === 'string') {
2548
+ streamedFiles[filePath] = args.content;
2549
+ }
2550
+ }
2551
+ }
2484
2552
  return;
2485
2553
  }
2486
2554
  const args = event.arguments || {};
@@ -2533,6 +2601,178 @@ menu {
2533
2601
  }
2534
2602
  return false;
2535
2603
  }
2604
+ resolveV3ClientToolPath(rootPath, rawPath) {
2605
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(String(rawPath || '.'), rootPath);
2606
+ if (!relativePath && String(rawPath || '.').trim() !== '.') {
2607
+ return null;
2608
+ }
2609
+ const absolutePath = path.resolve(rootPath, relativePath || '.');
2610
+ const resolvedRoot = path.resolve(rootPath);
2611
+ if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
2612
+ return null;
2613
+ }
2614
+ return { relativePath: relativePath || '.', absolutePath };
2615
+ }
2616
+ recordV3ClientToolMutation(event, context, result) {
2617
+ if (!result.success) {
2618
+ return;
2619
+ }
2620
+ const name = String(event.name || event.tool || '').trim();
2621
+ if (name !== 'write_file' && name !== 'edit_file') {
2622
+ return;
2623
+ }
2624
+ const args = event.arguments || {};
2625
+ const rootPath = this.resolveAgentTargetPath(context);
2626
+ if (!rootPath) {
2627
+ return;
2628
+ }
2629
+ const target = this.resolveV3ClientToolPath(rootPath, args.path || '.');
2630
+ if (!target) {
2631
+ return;
2632
+ }
2633
+ const bucket = this.activeV3StreamedFiles || context.__v3StreamedFiles;
2634
+ if (!bucket) {
2635
+ return;
2636
+ }
2637
+ if (name === 'write_file' && typeof args.content === 'string') {
2638
+ bucket[target.relativePath] = args.content;
2639
+ return;
2640
+ }
2641
+ if (name === 'edit_file') {
2642
+ try {
2643
+ bucket[target.relativePath] = fs.readFileSync(target.absolutePath, 'utf8');
2644
+ }
2645
+ catch {
2646
+ // ignore read failures; success was already reported by the client tool
2647
+ }
2648
+ }
2649
+ }
2650
+ async executeV3ClientToolRequest(event, context = {}) {
2651
+ const rootPath = this.resolveAgentTargetPath(context);
2652
+ if (!rootPath) {
2653
+ return { success: false, output: '', error: 'No local workspace path is available for client tool execution.' };
2654
+ }
2655
+ const name = String(event.name || event.tool || '').trim();
2656
+ const args = event.arguments || {};
2657
+ const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.');
2658
+ if (!target) {
2659
+ return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2660
+ }
2661
+ try {
2662
+ if (name === 'write_file') {
2663
+ if (typeof args.content !== 'string')
2664
+ return { success: false, output: '', error: 'write_file requires string content.' };
2665
+ fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
2666
+ fs.writeFileSync(target.absolutePath, args.content, 'utf8');
2667
+ const result = { success: true, output: `Updated ${target.relativePath}` };
2668
+ this.recordV3ClientToolMutation(event, context, result);
2669
+ return result;
2670
+ }
2671
+ if (name === 'edit_file') {
2672
+ const oldString = typeof args.old_string === 'string' ? args.old_string : String(args.old_text || '');
2673
+ const newString = typeof args.new_string === 'string' ? args.new_string : String(args.new_text || '');
2674
+ if (!oldString)
2675
+ return { success: false, output: '', error: 'edit_file requires old_string.' };
2676
+ const existing = fs.readFileSync(target.absolutePath, 'utf8');
2677
+ const count = existing.split(oldString).length - 1;
2678
+ if (count !== 1)
2679
+ return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
2680
+ fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
2681
+ const result = { success: true, output: `Edited ${target.relativePath}` };
2682
+ this.recordV3ClientToolMutation(event, context, result);
2683
+ return result;
2684
+ }
2685
+ if (name === 'read_file') {
2686
+ return { success: true, output: fs.readFileSync(target.absolutePath, 'utf8') };
2687
+ }
2688
+ if (name === 'list_directory') {
2689
+ const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
2690
+ .filter((entry) => !['node_modules', '.git'].includes(entry.name))
2691
+ .map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
2692
+ return { success: true, output: entries.join('\n') || '(empty)' };
2693
+ }
2694
+ if (name === 'glob' || name === 'search_files') {
2695
+ const pattern = String(args.pattern || args.query || '').toLowerCase();
2696
+ const files = [];
2697
+ const walk = (dir) => {
2698
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2699
+ if (['node_modules', '.git'].includes(entry.name))
2700
+ continue;
2701
+ const absolute = path.join(dir, entry.name);
2702
+ if (entry.isDirectory()) {
2703
+ walk(absolute);
2704
+ continue;
2705
+ }
2706
+ const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
2707
+ if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
2708
+ files.push(relative);
2709
+ }
2710
+ else if (name === 'search_files') {
2711
+ const content = fs.readFileSync(absolute, 'utf8');
2712
+ if (content.toLowerCase().includes(pattern))
2713
+ files.push(relative);
2714
+ }
2715
+ }
2716
+ };
2717
+ walk(fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath);
2718
+ return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
2719
+ }
2720
+ if (name === 'syntax_check') {
2721
+ const content = fs.readFileSync(target.absolutePath, 'utf8');
2722
+ if (/\.json$/i.test(target.absolutePath))
2723
+ JSON.parse(content);
2724
+ return { success: true, output: `Syntax check passed: ${target.relativePath}` };
2725
+ }
2726
+ if (name === 'run_command') {
2727
+ const command = String(args.command || '').trim();
2728
+ if (!command)
2729
+ return { success: false, output: '', error: 'run_command requires command.' };
2730
+ const { exec } = await import('child_process');
2731
+ return await new Promise((resolve) => {
2732
+ exec(command, { cwd: target.absolutePath, timeout: Number(args.timeout || 30000) }, (error, stdout, stderr) => {
2733
+ resolve({
2734
+ success: !error,
2735
+ output: String(stdout || stderr || '').slice(0, 12000),
2736
+ error: error ? String(stderr || error.message) : '',
2737
+ });
2738
+ });
2739
+ });
2740
+ }
2741
+ return { success: false, output: '', error: `Unsupported client tool: ${name}` };
2742
+ }
2743
+ catch (error) {
2744
+ return { success: false, output: '', error: error.message };
2745
+ }
2746
+ }
2747
+ async handleV3ClientToolRequest(event, context = {}, responseUrl) {
2748
+ const contextId = String(event.context_id || context.contextId || '').trim();
2749
+ const callId = String(event.call_id || '').trim();
2750
+ const toolName = String(event.tool || event.name || 'client_tool').trim();
2751
+ if (!contextId || !callId)
2752
+ return;
2753
+ const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
2754
+ try {
2755
+ const result = await this.executeV3ClientToolRequest(event, context);
2756
+ try {
2757
+ await this.submitClientToolResult(contextId, callId, result, backendUrl);
2758
+ }
2759
+ catch (submitError) {
2760
+ const submitMessage = submitError.message || String(submitError);
2761
+ if (result.success) {
2762
+ this.recordClientToolError(`Local ${toolName} succeeded on your machine, but the Vigthoria agent could not accept the result (${submitMessage}). Your workspace may not have synced to the server.`);
2763
+ return;
2764
+ }
2765
+ throw submitError;
2766
+ }
2767
+ if (!result.success && result.error) {
2768
+ this.recordClientToolError(`Local ${toolName} failed: ${result.error}`);
2769
+ }
2770
+ }
2771
+ catch (error) {
2772
+ this.recordClientToolError(`Local tool bridge failed for ${toolName}: ${error.message}`);
2773
+ throw error;
2774
+ }
2775
+ }
2536
2776
  writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
2537
2777
  const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
2538
2778
  if (!relativePath) {
@@ -3054,6 +3294,7 @@ document.addEventListener('DOMContentLoaded', () => {
3054
3294
  .replace(/<\/think>/gi, '</thinking>')
3055
3295
  .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
3056
3296
  .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
3297
+ .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
3057
3298
  .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
3058
3299
  .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
3059
3300
  .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
@@ -3146,10 +3387,27 @@ document.addEventListener('DOMContentLoaded', () => {
3146
3387
  };
3147
3388
  return sanitizeValue(event);
3148
3389
  }
3390
+ resolveAgentChangedFiles(data) {
3391
+ const streamed = data?.files && typeof data.files === 'object' ? data.files : {};
3392
+ const clientMutations = this.activeV3StreamedFiles && typeof this.activeV3StreamedFiles === 'object'
3393
+ ? this.activeV3StreamedFiles
3394
+ : {};
3395
+ return { ...streamed, ...clientMutations };
3396
+ }
3397
+ finalizeV3AgentWorkflowResponse(data, response) {
3398
+ const changedFiles = this.resolveAgentChangedFiles(data);
3399
+ this.activeV3StreamedFiles = null;
3400
+ this.activeV3StreamContext = null;
3401
+ return {
3402
+ ...response,
3403
+ changedFiles,
3404
+ };
3405
+ }
3149
3406
  async collectV3AgentStream(response, context = {}) {
3150
3407
  if (!response.body || typeof response.body.getReader !== 'function') {
3151
3408
  return response.json();
3152
3409
  }
3410
+ this.clearAgentRunDiagnostics();
3153
3411
  const reader = response.body.getReader();
3154
3412
  const decoder = new TextDecoder();
3155
3413
  let buffer = '';
@@ -3158,183 +3416,202 @@ document.addEventListener('DOMContentLoaded', () => {
3158
3416
  let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
3159
3417
  let serverWorkspaceRoot = null;
3160
3418
  const streamedFiles = {};
3419
+ this.activeV3StreamedFiles = streamedFiles;
3420
+ this.activeV3StreamContext = context;
3421
+ context.__v3StreamedFiles = streamedFiles;
3161
3422
  const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
3162
- while (true) {
3163
- let chunk;
3164
- try {
3165
- const readPromise = reader.read();
3166
- while (true) {
3167
- const timeoutSentinel = Symbol('v3-agent-idle-timeout');
3168
- const result = idleTimeoutMs > 0
3169
- ? await Promise.race([
3170
- readPromise,
3171
- new Promise((resolve) => {
3172
- setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
3173
- }),
3174
- ])
3175
- : await readPromise;
3176
- if (result !== timeoutSentinel) {
3177
- chunk = result;
3178
- break;
3423
+ try {
3424
+ while (true) {
3425
+ let chunk;
3426
+ try {
3427
+ const readPromise = reader.read();
3428
+ while (true) {
3429
+ const timeoutSentinel = Symbol('v3-agent-idle-timeout');
3430
+ const result = idleTimeoutMs > 0
3431
+ ? await Promise.race([
3432
+ readPromise,
3433
+ new Promise((resolve) => {
3434
+ setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
3435
+ }),
3436
+ ])
3437
+ : await readPromise;
3438
+ if (result !== timeoutSentinel) {
3439
+ chunk = result;
3440
+ break;
3441
+ }
3442
+ if (this.hasAgentWorkspaceOutput(context)) {
3443
+ const stalledError = new Error('V3 agent stream stalled after writing workspace output');
3444
+ stalledError.name = 'AbortError';
3445
+ stalledError.partialData = {
3446
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
3447
+ context_id: contextId,
3448
+ result: final,
3449
+ events,
3450
+ files: streamedFiles,
3451
+ };
3452
+ throw stalledError;
3453
+ }
3179
3454
  }
3180
- if (this.hasAgentWorkspaceOutput(context)) {
3181
- const stalledError = new Error('V3 agent stream stalled after writing workspace output');
3182
- stalledError.name = 'AbortError';
3183
- stalledError.partialData = {
3455
+ }
3456
+ catch (error) {
3457
+ if (error && error.name === 'AbortError') {
3458
+ error.partialData = {
3184
3459
  task_id: events.find((event) => event && event.task_id)?.task_id || null,
3185
3460
  context_id: contextId,
3186
3461
  result: final,
3187
3462
  events,
3188
3463
  files: streamedFiles,
3189
3464
  };
3190
- throw stalledError;
3191
3465
  }
3466
+ throw error;
3192
3467
  }
3193
- }
3194
- catch (error) {
3195
- if (error && error.name === 'AbortError') {
3196
- error.partialData = {
3197
- task_id: events.find((event) => event && event.task_id)?.task_id || null,
3198
- context_id: contextId,
3199
- result: final,
3200
- events,
3201
- files: streamedFiles,
3202
- };
3468
+ const { done, value } = chunk;
3469
+ if (done) {
3470
+ break;
3203
3471
  }
3204
- throw error;
3205
- }
3206
- const { done, value } = chunk;
3207
- if (done) {
3208
- break;
3209
- }
3210
- buffer += decoder.decode(value, { stream: true });
3211
- const frames = buffer.split(/\r?\n\r?\n/);
3212
- buffer = frames.pop() || '';
3213
- for (const frame of frames) {
3214
- const lines = frame.split(/\r?\n/);
3215
- const dataLines = [];
3216
- let explicitEventType = null;
3217
- for (const rawLine of lines) {
3218
- const line = rawLine.trimEnd();
3219
- if (!line || line.startsWith(':')) {
3220
- continue;
3472
+ buffer += decoder.decode(value, { stream: true });
3473
+ const frames = buffer.split(/\r?\n\r?\n/);
3474
+ buffer = frames.pop() || '';
3475
+ for (const frame of frames) {
3476
+ const lines = frame.split(/\r?\n/);
3477
+ const dataLines = [];
3478
+ let explicitEventType = null;
3479
+ for (const rawLine of lines) {
3480
+ const line = rawLine.trimEnd();
3481
+ if (!line || line.startsWith(':')) {
3482
+ continue;
3483
+ }
3484
+ if (line.startsWith('event:')) {
3485
+ explicitEventType = line.slice(6).trim() || null;
3486
+ continue;
3487
+ }
3488
+ if (line.startsWith('data:')) {
3489
+ dataLines.push(line.slice(5).trimStart());
3490
+ }
3221
3491
  }
3222
- if (line.startsWith('event:')) {
3223
- explicitEventType = line.slice(6).trim() || null;
3492
+ const payload = dataLines.join('\n').trim();
3493
+ if (!payload || payload === '[DONE]') {
3224
3494
  continue;
3225
3495
  }
3226
- if (line.startsWith('data:')) {
3227
- dataLines.push(line.slice(5).trimStart());
3496
+ let event;
3497
+ try {
3498
+ event = JSON.parse(payload);
3228
3499
  }
3229
- }
3230
- const payload = dataLines.join('\n').trim();
3231
- if (!payload || payload === '[DONE]') {
3232
- continue;
3233
- }
3234
- let event;
3235
- try {
3236
- event = JSON.parse(payload);
3237
- }
3238
- catch {
3239
- event = {
3240
- type: explicitEventType || 'message',
3241
- content: payload,
3242
- };
3243
- }
3244
- if (explicitEventType && (!event.type || event.type === 'message')) {
3245
- event.type = explicitEventType;
3246
- }
3247
- const userEvent = this.sanitizeV3AgentEventForUser(event);
3248
- events.push(userEvent);
3249
- if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
3250
- contextId = event.context_id.trim();
3251
- }
3252
- if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
3253
- serverWorkspaceRoot = event.workspace_root.trim();
3254
- }
3255
- this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
3256
- this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
3257
- // Empty workspace soft guard: the first list_directory result can be
3258
- // transiently empty before remote workspace hydration catches up.
3259
- // Avoid hard-failing here; let the stream continue.
3260
- if (event.type === 'tool_result'
3261
- && event.name === 'list_directory'
3262
- && event.success === true
3263
- && serverWorkspaceRoot
3264
- && typeof event.output === 'string') {
3265
- const listOutput = event.output.trim();
3266
- const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
3267
- || listOutput === `[${serverWorkspaceRoot}/]`
3268
- || listOutput === `[${serverWorkspaceRoot}]\\n`
3269
- || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
3270
- if (looksEmpty) {
3271
- const localPath = this.resolveAgentTargetPath(context);
3272
- const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
3273
- if (localHasFiles) {
3274
- this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
3500
+ catch {
3501
+ event = {
3502
+ type: explicitEventType || 'message',
3503
+ content: payload,
3504
+ };
3505
+ }
3506
+ if (explicitEventType && (!event.type || event.type === 'message')) {
3507
+ event.type = explicitEventType;
3508
+ }
3509
+ const userEvent = this.sanitizeV3AgentEventForUser(event);
3510
+ events.push(userEvent);
3511
+ if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
3512
+ contextId = event.context_id.trim();
3513
+ }
3514
+ if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
3515
+ serverWorkspaceRoot = event.workspace_root.trim();
3516
+ }
3517
+ if (event.type === 'client_tool_request') {
3518
+ const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
3519
+ this.logger.warn(`Failed to handle V3 client tool request: ${error.message}`);
3520
+ });
3521
+ this.pendingV3ClientToolTasks.add(toolTask);
3522
+ void toolTask.finally(() => {
3523
+ this.pendingV3ClientToolTasks.delete(toolTask);
3524
+ });
3525
+ }
3526
+ this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
3527
+ this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
3528
+ // Empty workspace soft guard: the first list_directory result can be
3529
+ // transiently empty before remote workspace hydration catches up.
3530
+ // Avoid hard-failing here; let the stream continue.
3531
+ if (event.type === 'tool_result'
3532
+ && event.name === 'list_directory'
3533
+ && event.success === true
3534
+ && serverWorkspaceRoot
3535
+ && typeof event.output === 'string') {
3536
+ const listOutput = event.output.trim();
3537
+ const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
3538
+ || listOutput === `[${serverWorkspaceRoot}/]`
3539
+ || listOutput === `[${serverWorkspaceRoot}]\\n`
3540
+ || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
3541
+ if (looksEmpty) {
3542
+ const localPath = this.resolveAgentTargetPath(context);
3543
+ const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
3544
+ if (localHasFiles) {
3545
+ this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
3546
+ }
3275
3547
  }
3276
3548
  }
3277
- }
3278
- if (typeof context.onStreamEvent === 'function') {
3279
- try {
3280
- context.onStreamEvent(userEvent);
3549
+ if (typeof context.onStreamEvent === 'function') {
3550
+ try {
3551
+ context.onStreamEvent(userEvent);
3552
+ }
3553
+ catch {
3554
+ // Ignore UI callback failures; never break the agent stream for them.
3555
+ }
3281
3556
  }
3282
- catch {
3283
- // Ignore UI callback failures; never break the agent stream for them.
3557
+ if (event.type === 'error') {
3558
+ if (event.checkpointed && event.task_id) {
3559
+ // Agent checkpointed — return data so caller can auto-continue
3560
+ return {
3561
+ task_id: event.task_id,
3562
+ context_id: contextId,
3563
+ result: final || event,
3564
+ events,
3565
+ files: streamedFiles,
3566
+ partial: true,
3567
+ checkpointed: true,
3568
+ checkpointed_task_id: event.task_id,
3569
+ };
3570
+ }
3571
+ if (this.hasAgentWorkspaceOutput(context)) {
3572
+ return {
3573
+ task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3574
+ context_id: contextId,
3575
+ result: final || event,
3576
+ events,
3577
+ files: streamedFiles,
3578
+ partial: true,
3579
+ };
3580
+ }
3581
+ throw new Error(event.message || 'V3 agent returned an error');
3284
3582
  }
3285
- }
3286
- if (event.type === 'error') {
3287
- if (event.checkpointed && event.task_id) {
3288
- // Agent checkpointed — return data so caller can auto-continue
3289
- return {
3290
- task_id: event.task_id,
3291
- context_id: contextId,
3292
- result: final || event,
3293
- events,
3294
- files: streamedFiles,
3295
- partial: true,
3296
- checkpointed: true,
3297
- checkpointed_task_id: event.task_id,
3298
- };
3583
+ if (event.type === 'complete' || event.type === 'message') {
3584
+ final = event;
3299
3585
  }
3300
- if (this.hasAgentWorkspaceOutput(context)) {
3586
+ // Exit stream early on complete — agent is done; server-side teardown
3587
+ // can hold the connection open for many seconds otherwise.
3588
+ if (event.type === 'complete') {
3589
+ reader.cancel().catch(() => { });
3301
3590
  return {
3302
3591
  task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3303
3592
  context_id: contextId,
3304
- result: final || event,
3593
+ result: final,
3305
3594
  events,
3306
3595
  files: streamedFiles,
3307
- partial: true,
3596
+ serverWorkspaceRoot: serverWorkspaceRoot || null,
3308
3597
  };
3309
3598
  }
3310
- throw new Error(event.message || 'V3 agent returned an error');
3311
- }
3312
- if (event.type === 'complete' || event.type === 'message') {
3313
- final = event;
3314
- }
3315
- // Exit stream early on complete — agent is done; server-side teardown
3316
- // can hold the connection open for many seconds otherwise.
3317
- if (event.type === 'complete') {
3318
- reader.cancel().catch(() => { });
3319
- return {
3320
- task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3321
- context_id: contextId,
3322
- result: final,
3323
- events,
3324
- files: streamedFiles,
3325
- serverWorkspaceRoot: serverWorkspaceRoot || null,
3326
- };
3327
3599
  }
3328
3600
  }
3601
+ await Promise.allSettled([...this.pendingV3ClientToolTasks]);
3602
+ return {
3603
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
3604
+ context_id: contextId,
3605
+ result: final,
3606
+ events,
3607
+ files: streamedFiles,
3608
+ serverWorkspaceRoot: serverWorkspaceRoot || null,
3609
+ clientToolErrors: this.getClientToolErrors(),
3610
+ };
3611
+ }
3612
+ finally {
3613
+ await Promise.allSettled([...this.pendingV3ClientToolTasks]);
3329
3614
  }
3330
- return {
3331
- task_id: events.find((event) => event && event.task_id)?.task_id || null,
3332
- context_id: contextId,
3333
- result: final,
3334
- events,
3335
- files: streamedFiles,
3336
- serverWorkspaceRoot: serverWorkspaceRoot || null,
3337
- };
3338
3615
  }
3339
3616
  async runV3AgentWorkflow(message, context = {}) {
3340
3617
  const executionContext = await this.bindExecutionContext(context);
@@ -3457,14 +3734,14 @@ document.addEventListener('DOMContentLoaded', () => {
3457
3734
  await this.ensureAgentFrontendPolish(message, executionContext);
3458
3735
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3459
3736
  const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3460
- return {
3737
+ return this.finalizeV3AgentWorkflowResponse(continuationData, {
3461
3738
  content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
3462
3739
  taskId: continuationData.task_id || data.task_id || null,
3463
3740
  contextId: finalContextId,
3464
3741
  backendUrl: baseUrl,
3465
3742
  partial: continuationData.checkpointed === true,
3466
3743
  metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
3467
- };
3744
+ });
3468
3745
  }
3469
3746
  const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3470
3747
  const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
@@ -3476,13 +3753,13 @@ document.addEventListener('DOMContentLoaded', () => {
3476
3753
  errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
3477
3754
  continue;
3478
3755
  }
3479
- return {
3756
+ return this.finalizeV3AgentWorkflowResponse(data, {
3480
3757
  content: this.formatV3AgentResponse(data),
3481
3758
  taskId: data.task_id || null,
3482
3759
  contextId,
3483
3760
  backendUrl: baseUrl,
3484
3761
  metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
3485
- };
3762
+ });
3486
3763
  }
3487
3764
  catch (error) {
3488
3765
  if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
@@ -3490,14 +3767,14 @@ document.addEventListener('DOMContentLoaded', () => {
3490
3767
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3491
3768
  await this.ensureAgentFrontendPolish(message, executionContext);
3492
3769
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3493
- return {
3770
+ return this.finalizeV3AgentWorkflowResponse(error.partialData, {
3494
3771
  content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
3495
3772
  taskId: error.partialData.task_id || null,
3496
3773
  contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
3497
3774
  backendUrl: baseUrl,
3498
3775
  partial: true,
3499
3776
  metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
3500
- };
3777
+ });
3501
3778
  }
3502
3779
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3503
3780
  }
@@ -3559,6 +3836,158 @@ document.addEventListener('DOMContentLoaded', () => {
3559
3836
  }
3560
3837
  throw new Error(errors.join(' | '));
3561
3838
  }
3839
+ async startV3BackgroundJob(message, context = {}) {
3840
+ await this.ensureV3ServiceKey();
3841
+ const executionContext = await this.bindExecutionContext({
3842
+ ...context,
3843
+ backgroundJob: true,
3844
+ clientToolExecution: false,
3845
+ });
3846
+ const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
3847
+ const resolvedModel = this.resolvePermittedModelId(requestedModel);
3848
+ const body = {
3849
+ request: message,
3850
+ context: this.buildV3AgentContext({
3851
+ ...executionContext,
3852
+ backgroundJob: true,
3853
+ clientToolExecution: false,
3854
+ }),
3855
+ stream: false,
3856
+ model: resolvedModel,
3857
+ mcp_context_id: executionContext.mcpContextId || undefined,
3858
+ };
3859
+ const errors = [];
3860
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3861
+ try {
3862
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
3863
+ method: 'POST',
3864
+ headers: await this.getV3AgentHeaders(),
3865
+ body: JSON.stringify(body),
3866
+ });
3867
+ if (!response.ok) {
3868
+ errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
3869
+ continue;
3870
+ }
3871
+ return await response.json();
3872
+ }
3873
+ catch (error) {
3874
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3875
+ }
3876
+ }
3877
+ throw new Error(`Unable to start background job: ${errors.join('; ')}`);
3878
+ }
3879
+ async listV3BackgroundJobs(limit = 20) {
3880
+ await this.ensureV3ServiceKey();
3881
+ const errors = [];
3882
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3883
+ try {
3884
+ const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
3885
+ method: 'GET',
3886
+ headers: await this.getV3AgentHeaders(),
3887
+ });
3888
+ if (!response.ok) {
3889
+ errors.push(`${baseUrl}: ${response.status}`);
3890
+ continue;
3891
+ }
3892
+ const data = await response.json();
3893
+ return Array.isArray(data.jobs) ? data.jobs : [];
3894
+ }
3895
+ catch (error) {
3896
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3897
+ }
3898
+ }
3899
+ throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
3900
+ }
3901
+ async getV3BackgroundJob(jobId) {
3902
+ await this.ensureV3ServiceKey();
3903
+ const errors = [];
3904
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3905
+ try {
3906
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
3907
+ method: 'GET',
3908
+ headers: await this.getV3AgentHeaders(),
3909
+ });
3910
+ if (!response.ok) {
3911
+ errors.push(`${baseUrl}: ${response.status}`);
3912
+ continue;
3913
+ }
3914
+ return await response.json();
3915
+ }
3916
+ catch (error) {
3917
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3918
+ }
3919
+ }
3920
+ throw new Error(`Unable to get background job: ${errors.join('; ')}`);
3921
+ }
3922
+ async cancelV3BackgroundJob(jobId) {
3923
+ await this.ensureV3ServiceKey();
3924
+ const errors = [];
3925
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3926
+ try {
3927
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
3928
+ method: 'POST',
3929
+ headers: await this.getV3AgentHeaders(),
3930
+ });
3931
+ if (!response.ok) {
3932
+ errors.push(`${baseUrl}: ${response.status}`);
3933
+ continue;
3934
+ }
3935
+ return await response.json();
3936
+ }
3937
+ catch (error) {
3938
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3939
+ }
3940
+ }
3941
+ throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
3942
+ }
3943
+ async getV3BackgroundJobFiles(jobId) {
3944
+ await this.ensureV3ServiceKey();
3945
+ const errors = [];
3946
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3947
+ try {
3948
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
3949
+ method: 'GET',
3950
+ headers: await this.getV3AgentHeaders(),
3951
+ });
3952
+ if (!response.ok) {
3953
+ errors.push(`${baseUrl}: ${response.status}`);
3954
+ continue;
3955
+ }
3956
+ return await response.json();
3957
+ }
3958
+ catch (error) {
3959
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3960
+ }
3961
+ }
3962
+ throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
3963
+ }
3964
+ async applyV3BackgroundJobFiles(jobId, context = {}) {
3965
+ const data = await this.getV3BackgroundJobFiles(jobId);
3966
+ const rootPath = this.resolveAgentTargetPath({
3967
+ ...context,
3968
+ workspacePath: context.workspacePath || data.local_workspace_path,
3969
+ projectPath: context.projectPath || data.local_workspace_path,
3970
+ targetPath: context.targetPath || data.local_workspace_path,
3971
+ });
3972
+ if (!rootPath) {
3973
+ throw new Error('No local workspace path available for applying background job files.');
3974
+ }
3975
+ const applied = [];
3976
+ const skipped = [];
3977
+ for (const [relativePath, content] of Object.entries(data.files || {})) {
3978
+ if (typeof content !== 'string') {
3979
+ skipped.push(relativePath);
3980
+ continue;
3981
+ }
3982
+ if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
3983
+ applied.push(relativePath);
3984
+ }
3985
+ else {
3986
+ skipped.push(relativePath);
3987
+ }
3988
+ }
3989
+ return { applied, skipped, status: data.status };
3990
+ }
3562
3991
  formatOperatorResponse(data = {}) {
3563
3992
  // If the server returned a direct answer field, prefer it (for lookup tasks)
3564
3993
  if (typeof data.answer === 'string' && data.answer.trim()) {
@@ -5094,6 +5523,19 @@ document.addEventListener('DOMContentLoaded', () => {
5094
5523
  getLastChatTransportErrors() {
5095
5524
  return this.lastChatTransportErrors;
5096
5525
  }
5526
+ getClientToolErrors() {
5527
+ return [...this.clientToolErrors];
5528
+ }
5529
+ clearAgentRunDiagnostics() {
5530
+ this.clientToolErrors = [];
5531
+ this.pendingV3ClientToolTasks.clear();
5532
+ }
5533
+ recordClientToolError(message) {
5534
+ const trimmed = sanitizeUserFacingErrorText(String(message || '')).slice(0, 280);
5535
+ if (trimmed && !this.clientToolErrors.includes(trimmed)) {
5536
+ this.clientToolErrors.push(trimmed);
5537
+ }
5538
+ }
5097
5539
  async submitClientToolResult(contextId, callId, result, backendUrl) {
5098
5540
  const trimmedContextId = String(contextId || '').trim();
5099
5541
  const trimmedCallId = String(callId || '').trim();
@@ -5130,6 +5572,10 @@ document.addEventListener('DOMContentLoaded', () => {
5130
5572
  await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
5131
5573
  continue;
5132
5574
  }
5575
+ if (response.status === 404 && /no pending client tool call/i.test(errorText)) {
5576
+ // Server already consumed or timed out this call — treat as idempotent when local work succeeded.
5577
+ return;
5578
+ }
5133
5579
  throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
5134
5580
  }
5135
5581
  }