vigthoria-cli 1.10.50 → 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,
@@ -1432,6 +1453,15 @@ export class APIClient {
1432
1453
  activeFile: resolvedContext.activeFile || null,
1433
1454
  history: resolvedContext.history || [],
1434
1455
  agentTaskType: resolvedContext.agentTaskType || 'general',
1456
+ rawPrompt: resolvedContext.rawPrompt || resolvedContext.prompt || '',
1457
+ contextualPrompt: resolvedContext.contextualPrompt || '',
1458
+ executionHints: {
1459
+ task_kind: resolvedContext.agentTaskType || 'general',
1460
+ requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
1461
+ ? false
1462
+ : undefined,
1463
+ classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
1464
+ },
1435
1465
  model: resolvedModel,
1436
1466
  requestedModel,
1437
1467
  requestedModelResolved: resolvedModel,
@@ -1439,7 +1469,9 @@ export class APIClient {
1439
1469
  legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
1440
1470
  executionSurface: resolvedContext.executionSurface || 'cli',
1441
1471
  clientSurface: resolvedContext.clientSurface || 'cli',
1442
- localMachineCapable: resolvedContext.localMachineCapable !== false,
1472
+ localMachineCapable,
1473
+ clientToolExecution,
1474
+ client_tool_execution: clientToolExecution,
1443
1475
  runtimeEnvironment: publicRuntimeEnvironment,
1444
1476
  workspacePath: effectiveWorkspacePath,
1445
1477
  projectPath: effectiveWorkspacePath,
@@ -1462,6 +1494,11 @@ export class APIClient {
1462
1494
  requestStartedAt: resolvedContext.requestStartedAt,
1463
1495
  subscriptionPlan: this.config.getNormalizedPlan() || null,
1464
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 || '',
1465
1502
  };
1466
1503
  return this.compactV3Context(payload);
1467
1504
  }
@@ -1568,6 +1605,15 @@ export class APIClient {
1568
1605
  activeFile: resolvedContext.activeFile || null,
1569
1606
  history: resolvedContext.history || [],
1570
1607
  agentTaskType: resolvedContext.agentTaskType || 'general',
1608
+ rawPrompt: resolvedContext.rawPrompt || resolvedContext.prompt || '',
1609
+ contextualPrompt: resolvedContext.contextualPrompt || '',
1610
+ executionHints: {
1611
+ task_kind: resolvedContext.agentTaskType || 'general',
1612
+ requires_file_changes: resolvedContext.agentTaskType === 'analysis' || resolvedContext.agentTaskType === 'verification'
1613
+ ? false
1614
+ : undefined,
1615
+ classify_from: resolvedContext.rawPrompt || resolvedContext.prompt || '',
1616
+ },
1571
1617
  executionSurface: resolvedContext.executionSurface || 'cli',
1572
1618
  clientSurface: resolvedContext.clientSurface || 'cli',
1573
1619
  localMachineCapable: resolvedContext.localMachineCapable !== false,
@@ -1584,6 +1630,8 @@ export class APIClient {
1584
1630
  contextId: resolvedContext.contextId,
1585
1631
  traceId: resolvedContext.traceId,
1586
1632
  requestStartedAt: resolvedContext.requestStartedAt,
1633
+ vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
1634
+ vigthoria_brain: resolvedContext.vigthoriaBrain || null,
1587
1635
  });
1588
1636
  }
1589
1637
  extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
@@ -2057,7 +2105,7 @@ menu {
2057
2105
  const headers = await this.getMcpHeaders();
2058
2106
  const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
2059
2107
  const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
2060
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
2108
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
2061
2109
  const metadata = {
2062
2110
  source: 'vigthoria-cli',
2063
2111
  sharedContextId: executionContext.contextId,
@@ -2218,7 +2266,7 @@ menu {
2218
2266
  cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
2219
2267
  };
2220
2268
  }
2221
- buildLocalWorkspaceSummary(rootPath) {
2269
+ buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
2222
2270
  if (!rootPath || !fs.existsSync(rootPath)) {
2223
2271
  return null;
2224
2272
  }
@@ -2229,8 +2277,31 @@ menu {
2229
2277
  files: [],
2230
2278
  };
2231
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
+ }
2232
2303
  summary.fileCount = snapshot.fileCount;
2233
- summary.files = snapshot.paths.slice(0, 40);
2304
+ summary.files = orderedPaths.slice(0, 40);
2234
2305
  const packageJsonPath = path.join(rootPath, 'package.json');
2235
2306
  if (fs.existsSync(packageJsonPath)) {
2236
2307
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -2248,7 +2319,7 @@ menu {
2248
2319
  }
2249
2320
  // Hydrate workspace: include actual file contents so the V3 server
2250
2321
  // can populate the remote workspace before the agent starts.
2251
- summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, snapshot.paths);
2322
+ summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
2252
2323
  return summary;
2253
2324
  }
2254
2325
  catch (error) {
@@ -2284,6 +2355,8 @@ menu {
2284
2355
  continue;
2285
2356
  if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
2286
2357
  continue;
2358
+ if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
2359
+ continue;
2287
2360
  const absolutePath = path.join(rootPath, relativePath);
2288
2361
  try {
2289
2362
  const stat = fs.statSync(absolutePath);
@@ -2463,6 +2536,19 @@ menu {
2463
2536
  return;
2464
2537
  }
2465
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
+ }
2466
2552
  return;
2467
2553
  }
2468
2554
  const args = event.arguments || {};
@@ -2515,6 +2601,178 @@ menu {
2515
2601
  }
2516
2602
  return false;
2517
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
+ }
2518
2776
  writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
2519
2777
  const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
2520
2778
  if (!relativePath) {
@@ -3036,6 +3294,7 @@ document.addEventListener('DOMContentLoaded', () => {
3036
3294
  .replace(/<\/think>/gi, '</thinking>')
3037
3295
  .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
3038
3296
  .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
3297
+ .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
3039
3298
  .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
3040
3299
  .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
3041
3300
  .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
@@ -3128,10 +3387,27 @@ document.addEventListener('DOMContentLoaded', () => {
3128
3387
  };
3129
3388
  return sanitizeValue(event);
3130
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
+ }
3131
3406
  async collectV3AgentStream(response, context = {}) {
3132
3407
  if (!response.body || typeof response.body.getReader !== 'function') {
3133
3408
  return response.json();
3134
3409
  }
3410
+ this.clearAgentRunDiagnostics();
3135
3411
  const reader = response.body.getReader();
3136
3412
  const decoder = new TextDecoder();
3137
3413
  let buffer = '';
@@ -3140,183 +3416,202 @@ document.addEventListener('DOMContentLoaded', () => {
3140
3416
  let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
3141
3417
  let serverWorkspaceRoot = null;
3142
3418
  const streamedFiles = {};
3419
+ this.activeV3StreamedFiles = streamedFiles;
3420
+ this.activeV3StreamContext = context;
3421
+ context.__v3StreamedFiles = streamedFiles;
3143
3422
  const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
3144
- while (true) {
3145
- let chunk;
3146
- try {
3147
- const readPromise = reader.read();
3148
- while (true) {
3149
- const timeoutSentinel = Symbol('v3-agent-idle-timeout');
3150
- const result = idleTimeoutMs > 0
3151
- ? await Promise.race([
3152
- readPromise,
3153
- new Promise((resolve) => {
3154
- setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
3155
- }),
3156
- ])
3157
- : await readPromise;
3158
- if (result !== timeoutSentinel) {
3159
- chunk = result;
3160
- 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
+ }
3161
3454
  }
3162
- if (this.hasAgentWorkspaceOutput(context)) {
3163
- const stalledError = new Error('V3 agent stream stalled after writing workspace output');
3164
- stalledError.name = 'AbortError';
3165
- stalledError.partialData = {
3455
+ }
3456
+ catch (error) {
3457
+ if (error && error.name === 'AbortError') {
3458
+ error.partialData = {
3166
3459
  task_id: events.find((event) => event && event.task_id)?.task_id || null,
3167
3460
  context_id: contextId,
3168
3461
  result: final,
3169
3462
  events,
3170
3463
  files: streamedFiles,
3171
3464
  };
3172
- throw stalledError;
3173
3465
  }
3466
+ throw error;
3174
3467
  }
3175
- }
3176
- catch (error) {
3177
- if (error && error.name === 'AbortError') {
3178
- error.partialData = {
3179
- task_id: events.find((event) => event && event.task_id)?.task_id || null,
3180
- context_id: contextId,
3181
- result: final,
3182
- events,
3183
- files: streamedFiles,
3184
- };
3468
+ const { done, value } = chunk;
3469
+ if (done) {
3470
+ break;
3185
3471
  }
3186
- throw error;
3187
- }
3188
- const { done, value } = chunk;
3189
- if (done) {
3190
- break;
3191
- }
3192
- buffer += decoder.decode(value, { stream: true });
3193
- const frames = buffer.split(/\r?\n\r?\n/);
3194
- buffer = frames.pop() || '';
3195
- for (const frame of frames) {
3196
- const lines = frame.split(/\r?\n/);
3197
- const dataLines = [];
3198
- let explicitEventType = null;
3199
- for (const rawLine of lines) {
3200
- const line = rawLine.trimEnd();
3201
- if (!line || line.startsWith(':')) {
3202
- 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
+ }
3203
3491
  }
3204
- if (line.startsWith('event:')) {
3205
- explicitEventType = line.slice(6).trim() || null;
3492
+ const payload = dataLines.join('\n').trim();
3493
+ if (!payload || payload === '[DONE]') {
3206
3494
  continue;
3207
3495
  }
3208
- if (line.startsWith('data:')) {
3209
- dataLines.push(line.slice(5).trimStart());
3496
+ let event;
3497
+ try {
3498
+ event = JSON.parse(payload);
3210
3499
  }
3211
- }
3212
- const payload = dataLines.join('\n').trim();
3213
- if (!payload || payload === '[DONE]') {
3214
- continue;
3215
- }
3216
- let event;
3217
- try {
3218
- event = JSON.parse(payload);
3219
- }
3220
- catch {
3221
- event = {
3222
- type: explicitEventType || 'message',
3223
- content: payload,
3224
- };
3225
- }
3226
- if (explicitEventType && (!event.type || event.type === 'message')) {
3227
- event.type = explicitEventType;
3228
- }
3229
- const userEvent = this.sanitizeV3AgentEventForUser(event);
3230
- events.push(userEvent);
3231
- if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
3232
- contextId = event.context_id.trim();
3233
- }
3234
- if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
3235
- serverWorkspaceRoot = event.workspace_root.trim();
3236
- }
3237
- this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
3238
- this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
3239
- // Empty workspace soft guard: the first list_directory result can be
3240
- // transiently empty before remote workspace hydration catches up.
3241
- // Avoid hard-failing here; let the stream continue.
3242
- if (event.type === 'tool_result'
3243
- && event.name === 'list_directory'
3244
- && event.success === true
3245
- && serverWorkspaceRoot
3246
- && typeof event.output === 'string') {
3247
- const listOutput = event.output.trim();
3248
- const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
3249
- || listOutput === `[${serverWorkspaceRoot}/]`
3250
- || listOutput === `[${serverWorkspaceRoot}]\\n`
3251
- || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
3252
- if (looksEmpty) {
3253
- const localPath = this.resolveAgentTargetPath(context);
3254
- const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
3255
- if (localHasFiles) {
3256
- 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
+ }
3257
3547
  }
3258
3548
  }
3259
- }
3260
- if (typeof context.onStreamEvent === 'function') {
3261
- try {
3262
- 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
+ }
3263
3556
  }
3264
- catch {
3265
- // 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');
3266
3582
  }
3267
- }
3268
- if (event.type === 'error') {
3269
- if (event.checkpointed && event.task_id) {
3270
- // Agent checkpointed — return data so caller can auto-continue
3271
- return {
3272
- task_id: event.task_id,
3273
- context_id: contextId,
3274
- result: final || event,
3275
- events,
3276
- files: streamedFiles,
3277
- partial: true,
3278
- checkpointed: true,
3279
- checkpointed_task_id: event.task_id,
3280
- };
3583
+ if (event.type === 'complete' || event.type === 'message') {
3584
+ final = event;
3281
3585
  }
3282
- 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(() => { });
3283
3590
  return {
3284
3591
  task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3285
3592
  context_id: contextId,
3286
- result: final || event,
3593
+ result: final,
3287
3594
  events,
3288
3595
  files: streamedFiles,
3289
- partial: true,
3596
+ serverWorkspaceRoot: serverWorkspaceRoot || null,
3290
3597
  };
3291
3598
  }
3292
- throw new Error(event.message || 'V3 agent returned an error');
3293
- }
3294
- if (event.type === 'complete' || event.type === 'message') {
3295
- final = event;
3296
- }
3297
- // Exit stream early on complete — agent is done; server-side teardown
3298
- // can hold the connection open for many seconds otherwise.
3299
- if (event.type === 'complete') {
3300
- reader.cancel().catch(() => { });
3301
- return {
3302
- task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3303
- context_id: contextId,
3304
- result: final,
3305
- events,
3306
- files: streamedFiles,
3307
- serverWorkspaceRoot: serverWorkspaceRoot || null,
3308
- };
3309
3599
  }
3310
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]);
3311
3614
  }
3312
- return {
3313
- task_id: events.find((event) => event && event.task_id)?.task_id || null,
3314
- context_id: contextId,
3315
- result: final,
3316
- events,
3317
- files: streamedFiles,
3318
- serverWorkspaceRoot: serverWorkspaceRoot || null,
3319
- };
3320
3615
  }
3321
3616
  async runV3AgentWorkflow(message, context = {}) {
3322
3617
  const executionContext = await this.bindExecutionContext(context);
@@ -3439,14 +3734,14 @@ document.addEventListener('DOMContentLoaded', () => {
3439
3734
  await this.ensureAgentFrontendPolish(message, executionContext);
3440
3735
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3441
3736
  const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3442
- return {
3737
+ return this.finalizeV3AgentWorkflowResponse(continuationData, {
3443
3738
  content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
3444
3739
  taskId: continuationData.task_id || data.task_id || null,
3445
3740
  contextId: finalContextId,
3446
3741
  backendUrl: baseUrl,
3447
3742
  partial: continuationData.checkpointed === true,
3448
3743
  metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
3449
- };
3744
+ });
3450
3745
  }
3451
3746
  const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3452
3747
  const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
@@ -3458,13 +3753,13 @@ document.addEventListener('DOMContentLoaded', () => {
3458
3753
  errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
3459
3754
  continue;
3460
3755
  }
3461
- return {
3756
+ return this.finalizeV3AgentWorkflowResponse(data, {
3462
3757
  content: this.formatV3AgentResponse(data),
3463
3758
  taskId: data.task_id || null,
3464
3759
  contextId,
3465
3760
  backendUrl: baseUrl,
3466
3761
  metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
3467
- };
3762
+ });
3468
3763
  }
3469
3764
  catch (error) {
3470
3765
  if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
@@ -3472,14 +3767,14 @@ document.addEventListener('DOMContentLoaded', () => {
3472
3767
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3473
3768
  await this.ensureAgentFrontendPolish(message, executionContext);
3474
3769
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3475
- return {
3770
+ return this.finalizeV3AgentWorkflowResponse(error.partialData, {
3476
3771
  content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
3477
3772
  taskId: error.partialData.task_id || null,
3478
3773
  contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
3479
3774
  backendUrl: baseUrl,
3480
3775
  partial: true,
3481
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 },
3482
- };
3777
+ });
3483
3778
  }
3484
3779
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3485
3780
  }
@@ -3541,6 +3836,158 @@ document.addEventListener('DOMContentLoaded', () => {
3541
3836
  }
3542
3837
  throw new Error(errors.join(' | '));
3543
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
+ }
3544
3991
  formatOperatorResponse(data = {}) {
3545
3992
  // If the server returned a direct answer field, prefer it (for lookup tasks)
3546
3993
  if (typeof data.answer === 'string' && data.answer.trim()) {
@@ -5076,6 +5523,19 @@ document.addEventListener('DOMContentLoaded', () => {
5076
5523
  getLastChatTransportErrors() {
5077
5524
  return this.lastChatTransportErrors;
5078
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
+ }
5079
5539
  async submitClientToolResult(contextId, callId, result, backendUrl) {
5080
5540
  const trimmedContextId = String(contextId || '').trim();
5081
5541
  const trimmedCallId = String(callId || '').trim();
@@ -5112,6 +5572,10 @@ document.addEventListener('DOMContentLoaded', () => {
5112
5572
  await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
5113
5573
  continue;
5114
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
+ }
5115
5579
  throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
5116
5580
  }
5117
5581
  }