vigthoria-cli 1.10.51 → 1.11.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/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
  }
@@ -1485,6 +1513,12 @@ export class APIClient {
1485
1513
  */
1486
1514
  compactV3Context(payload) {
1487
1515
  const LIMIT = APIClient.V3_CONTEXT_CHAR_LIMIT;
1516
+ if (payload.vigthoriaBrain) {
1517
+ payload.vigthoriaBrain = this.dedupeBrainPayload(payload.vigthoriaBrain);
1518
+ }
1519
+ if (payload.vigthoria_brain) {
1520
+ payload.vigthoria_brain = payload.vigthoriaBrain || this.dedupeBrainPayload(payload.vigthoria_brain);
1521
+ }
1488
1522
  let json = JSON.stringify(payload);
1489
1523
  if (json.length <= LIMIT)
1490
1524
  return json;
@@ -1558,6 +1592,51 @@ export class APIClient {
1558
1592
  }
1559
1593
  return json;
1560
1594
  }
1595
+ /** Remove duplicate functionIndex entries that poison model context after compaction. */
1596
+ dedupeBrainPayload(brain) {
1597
+ if (!brain || typeof brain !== 'object') {
1598
+ return brain;
1599
+ }
1600
+ const copy = { ...brain };
1601
+ const dedupeIndexEntries = (entries) => {
1602
+ const seen = new Set();
1603
+ const unique = [];
1604
+ for (const entry of entries) {
1605
+ if (!entry || typeof entry !== 'object')
1606
+ continue;
1607
+ const file = String(entry.file || '')
1608
+ .replace(/\\/g, '/')
1609
+ .replace(/^home\/user\//i, '')
1610
+ .toLowerCase();
1611
+ const name = String(entry.name || '');
1612
+ const line = String(entry.line ?? '');
1613
+ const key = `${name}::${file}::${line}`;
1614
+ if (seen.has(key))
1615
+ continue;
1616
+ seen.add(key);
1617
+ unique.push(entry);
1618
+ }
1619
+ return unique;
1620
+ };
1621
+ if (copy.functionIndex && typeof copy.functionIndex === 'object') {
1622
+ const deduped = {};
1623
+ for (const [symbol, entries] of Object.entries(copy.functionIndex)) {
1624
+ deduped[symbol] = Array.isArray(entries) ? dedupeIndexEntries(entries) : [];
1625
+ }
1626
+ copy.functionIndex = deduped;
1627
+ }
1628
+ if (copy.files && typeof copy.files === 'object') {
1629
+ const normalized = {};
1630
+ for (const [filePath, meta] of Object.entries(copy.files)) {
1631
+ const norm = String(filePath).replace(/\\/g, '/').replace(/^home\/user\//i, '');
1632
+ if (!(norm in normalized)) {
1633
+ normalized[norm] = meta;
1634
+ }
1635
+ }
1636
+ copy.files = normalized;
1637
+ }
1638
+ return copy;
1639
+ }
1561
1640
  buildMinimalV3AgentContext(context = {}) {
1562
1641
  const resolvedContext = this.ensureExecutionContext(context);
1563
1642
  const targetPath = this.resolveAgentTargetPath(resolvedContext)
@@ -1602,6 +1681,8 @@ export class APIClient {
1602
1681
  contextId: resolvedContext.contextId,
1603
1682
  traceId: resolvedContext.traceId,
1604
1683
  requestStartedAt: resolvedContext.requestStartedAt,
1684
+ vigthoriaBrain: resolvedContext.vigthoriaBrain || null,
1685
+ vigthoria_brain: resolvedContext.vigthoriaBrain || null,
1605
1686
  });
1606
1687
  }
1607
1688
  extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
@@ -2075,7 +2156,7 @@ menu {
2075
2156
  const headers = await this.getMcpHeaders();
2076
2157
  const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
2077
2158
  const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
2078
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
2159
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
2079
2160
  const metadata = {
2080
2161
  source: 'vigthoria-cli',
2081
2162
  sharedContextId: executionContext.contextId,
@@ -2236,7 +2317,7 @@ menu {
2236
2317
  cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
2237
2318
  };
2238
2319
  }
2239
- buildLocalWorkspaceSummary(rootPath) {
2320
+ buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
2240
2321
  if (!rootPath || !fs.existsSync(rootPath)) {
2241
2322
  return null;
2242
2323
  }
@@ -2247,8 +2328,31 @@ menu {
2247
2328
  files: [],
2248
2329
  };
2249
2330
  const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
2331
+ const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
2332
+ let orderedPaths = candidatePaths;
2333
+ try {
2334
+ const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
2335
+ const changes = getChangedFiles(rootPath, candidatePaths);
2336
+ const ordered = new Set();
2337
+ for (const filePath of changes.changed)
2338
+ ordered.add(filePath);
2339
+ for (const filePath of ranked)
2340
+ ordered.add(filePath);
2341
+ for (const filePath of changes.unchanged)
2342
+ ordered.add(filePath);
2343
+ orderedPaths = Array.from(ordered);
2344
+ summary.semanticRank = ranked.slice(0, 20);
2345
+ summary.changeSummary = {
2346
+ changed: changes.changed.length,
2347
+ unchanged: changes.unchanged.length,
2348
+ total: changes.total,
2349
+ };
2350
+ }
2351
+ catch {
2352
+ orderedPaths = candidatePaths;
2353
+ }
2250
2354
  summary.fileCount = snapshot.fileCount;
2251
- summary.files = snapshot.paths.slice(0, 40);
2355
+ summary.files = orderedPaths.slice(0, 40);
2252
2356
  const packageJsonPath = path.join(rootPath, 'package.json');
2253
2357
  if (fs.existsSync(packageJsonPath)) {
2254
2358
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -2266,7 +2370,7 @@ menu {
2266
2370
  }
2267
2371
  // Hydrate workspace: include actual file contents so the V3 server
2268
2372
  // can populate the remote workspace before the agent starts.
2269
- summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, snapshot.paths);
2373
+ summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
2270
2374
  return summary;
2271
2375
  }
2272
2376
  catch (error) {
@@ -2302,6 +2406,8 @@ menu {
2302
2406
  continue;
2303
2407
  if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
2304
2408
  continue;
2409
+ if (/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
2410
+ continue;
2305
2411
  const absolutePath = path.join(rootPath, relativePath);
2306
2412
  try {
2307
2413
  const stat = fs.statSync(absolutePath);
@@ -2481,6 +2587,19 @@ menu {
2481
2587
  return;
2482
2588
  }
2483
2589
  if (event.type !== 'tool_call') {
2590
+ if (event.type === 'tool_result' && event.success) {
2591
+ const name = String(event.name || event.tool || '').trim();
2592
+ const args = event.arguments || {};
2593
+ const targetPath = typeof event.target === 'string'
2594
+ ? event.target
2595
+ : (typeof args.path === 'string' ? args.path : '');
2596
+ if ((name === 'write_file' || name === 'edit_file') && targetPath) {
2597
+ const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
2598
+ if (filePath && typeof args.content === 'string') {
2599
+ streamedFiles[filePath] = args.content;
2600
+ }
2601
+ }
2602
+ }
2484
2603
  return;
2485
2604
  }
2486
2605
  const args = event.arguments || {};
@@ -2533,6 +2652,178 @@ menu {
2533
2652
  }
2534
2653
  return false;
2535
2654
  }
2655
+ resolveV3ClientToolPath(rootPath, rawPath) {
2656
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(String(rawPath || '.'), rootPath);
2657
+ if (!relativePath && String(rawPath || '.').trim() !== '.') {
2658
+ return null;
2659
+ }
2660
+ const absolutePath = path.resolve(rootPath, relativePath || '.');
2661
+ const resolvedRoot = path.resolve(rootPath);
2662
+ if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
2663
+ return null;
2664
+ }
2665
+ return { relativePath: relativePath || '.', absolutePath };
2666
+ }
2667
+ recordV3ClientToolMutation(event, context, result) {
2668
+ if (!result.success) {
2669
+ return;
2670
+ }
2671
+ const name = String(event.name || event.tool || '').trim();
2672
+ if (name !== 'write_file' && name !== 'edit_file') {
2673
+ return;
2674
+ }
2675
+ const args = event.arguments || {};
2676
+ const rootPath = this.resolveAgentTargetPath(context);
2677
+ if (!rootPath) {
2678
+ return;
2679
+ }
2680
+ const target = this.resolveV3ClientToolPath(rootPath, args.path || '.');
2681
+ if (!target) {
2682
+ return;
2683
+ }
2684
+ const bucket = this.activeV3StreamedFiles || context.__v3StreamedFiles;
2685
+ if (!bucket) {
2686
+ return;
2687
+ }
2688
+ if (name === 'write_file' && typeof args.content === 'string') {
2689
+ bucket[target.relativePath] = args.content;
2690
+ return;
2691
+ }
2692
+ if (name === 'edit_file') {
2693
+ try {
2694
+ bucket[target.relativePath] = fs.readFileSync(target.absolutePath, 'utf8');
2695
+ }
2696
+ catch {
2697
+ // ignore read failures; success was already reported by the client tool
2698
+ }
2699
+ }
2700
+ }
2701
+ async executeV3ClientToolRequest(event, context = {}) {
2702
+ const rootPath = this.resolveAgentTargetPath(context);
2703
+ if (!rootPath) {
2704
+ return { success: false, output: '', error: 'No local workspace path is available for client tool execution.' };
2705
+ }
2706
+ const name = String(event.name || event.tool || '').trim();
2707
+ const args = event.arguments || {};
2708
+ const target = this.resolveV3ClientToolPath(rootPath, args.path || args.cwd || '.');
2709
+ if (!target) {
2710
+ return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2711
+ }
2712
+ try {
2713
+ if (name === 'write_file') {
2714
+ if (typeof args.content !== 'string')
2715
+ return { success: false, output: '', error: 'write_file requires string content.' };
2716
+ fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
2717
+ fs.writeFileSync(target.absolutePath, args.content, 'utf8');
2718
+ const result = { success: true, output: `Updated ${target.relativePath}` };
2719
+ this.recordV3ClientToolMutation(event, context, result);
2720
+ return result;
2721
+ }
2722
+ if (name === 'edit_file') {
2723
+ const oldString = typeof args.old_string === 'string' ? args.old_string : String(args.old_text || '');
2724
+ const newString = typeof args.new_string === 'string' ? args.new_string : String(args.new_text || '');
2725
+ if (!oldString)
2726
+ return { success: false, output: '', error: 'edit_file requires old_string.' };
2727
+ const existing = fs.readFileSync(target.absolutePath, 'utf8');
2728
+ const count = existing.split(oldString).length - 1;
2729
+ if (count !== 1)
2730
+ return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
2731
+ fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
2732
+ const result = { success: true, output: `Edited ${target.relativePath}` };
2733
+ this.recordV3ClientToolMutation(event, context, result);
2734
+ return result;
2735
+ }
2736
+ if (name === 'read_file') {
2737
+ return { success: true, output: fs.readFileSync(target.absolutePath, 'utf8') };
2738
+ }
2739
+ if (name === 'list_directory') {
2740
+ const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
2741
+ .filter((entry) => !['node_modules', '.git'].includes(entry.name))
2742
+ .map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`);
2743
+ return { success: true, output: entries.join('\n') || '(empty)' };
2744
+ }
2745
+ if (name === 'glob' || name === 'search_files') {
2746
+ const pattern = String(args.pattern || args.query || '').toLowerCase();
2747
+ const files = [];
2748
+ const walk = (dir) => {
2749
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2750
+ if (['node_modules', '.git'].includes(entry.name))
2751
+ continue;
2752
+ const absolute = path.join(dir, entry.name);
2753
+ if (entry.isDirectory()) {
2754
+ walk(absolute);
2755
+ continue;
2756
+ }
2757
+ const relative = path.relative(rootPath, absolute).replace(/\\/g, '/');
2758
+ if (!pattern || relative.toLowerCase().includes(pattern.replace(/\*/g, ''))) {
2759
+ files.push(relative);
2760
+ }
2761
+ else if (name === 'search_files') {
2762
+ const content = fs.readFileSync(absolute, 'utf8');
2763
+ if (content.toLowerCase().includes(pattern))
2764
+ files.push(relative);
2765
+ }
2766
+ }
2767
+ };
2768
+ walk(fs.statSync(target.absolutePath).isDirectory() ? target.absolutePath : rootPath);
2769
+ return { success: true, output: files.slice(0, 200).join('\n') || '(no matches)' };
2770
+ }
2771
+ if (name === 'syntax_check') {
2772
+ const content = fs.readFileSync(target.absolutePath, 'utf8');
2773
+ if (/\.json$/i.test(target.absolutePath))
2774
+ JSON.parse(content);
2775
+ return { success: true, output: `Syntax check passed: ${target.relativePath}` };
2776
+ }
2777
+ if (name === 'run_command') {
2778
+ const command = String(args.command || '').trim();
2779
+ if (!command)
2780
+ return { success: false, output: '', error: 'run_command requires command.' };
2781
+ const { exec } = await import('child_process');
2782
+ return await new Promise((resolve) => {
2783
+ exec(command, { cwd: target.absolutePath, timeout: Number(args.timeout || 30000) }, (error, stdout, stderr) => {
2784
+ resolve({
2785
+ success: !error,
2786
+ output: String(stdout || stderr || '').slice(0, 12000),
2787
+ error: error ? String(stderr || error.message) : '',
2788
+ });
2789
+ });
2790
+ });
2791
+ }
2792
+ return { success: false, output: '', error: `Unsupported client tool: ${name}` };
2793
+ }
2794
+ catch (error) {
2795
+ return { success: false, output: '', error: error.message };
2796
+ }
2797
+ }
2798
+ async handleV3ClientToolRequest(event, context = {}, responseUrl) {
2799
+ const contextId = String(event.context_id || context.contextId || '').trim();
2800
+ const callId = String(event.call_id || '').trim();
2801
+ const toolName = String(event.tool || event.name || 'client_tool').trim();
2802
+ if (!contextId || !callId)
2803
+ return;
2804
+ const backendUrl = responseUrl ? new URL(responseUrl).origin : (context.mcpContextBackendUrl || null);
2805
+ try {
2806
+ const result = await this.executeV3ClientToolRequest(event, context);
2807
+ try {
2808
+ await this.submitClientToolResult(contextId, callId, result, backendUrl);
2809
+ }
2810
+ catch (submitError) {
2811
+ const submitMessage = submitError.message || String(submitError);
2812
+ if (result.success) {
2813
+ 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.`);
2814
+ return;
2815
+ }
2816
+ throw submitError;
2817
+ }
2818
+ if (!result.success && result.error) {
2819
+ this.recordClientToolError(`Local ${toolName} failed: ${result.error}`);
2820
+ }
2821
+ }
2822
+ catch (error) {
2823
+ this.recordClientToolError(`Local tool bridge failed for ${toolName}: ${error.message}`);
2824
+ throw error;
2825
+ }
2826
+ }
2536
2827
  writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
2537
2828
  const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
2538
2829
  if (!relativePath) {
@@ -3054,6 +3345,7 @@ document.addEventListener('DOMContentLoaded', () => {
3054
3345
  .replace(/<\/think>/gi, '</thinking>')
3055
3346
  .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
3056
3347
  .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
3348
+ .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
3057
3349
  .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
3058
3350
  .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
3059
3351
  .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
@@ -3146,10 +3438,27 @@ document.addEventListener('DOMContentLoaded', () => {
3146
3438
  };
3147
3439
  return sanitizeValue(event);
3148
3440
  }
3441
+ resolveAgentChangedFiles(data) {
3442
+ const streamed = data?.files && typeof data.files === 'object' ? data.files : {};
3443
+ const clientMutations = this.activeV3StreamedFiles && typeof this.activeV3StreamedFiles === 'object'
3444
+ ? this.activeV3StreamedFiles
3445
+ : {};
3446
+ return { ...streamed, ...clientMutations };
3447
+ }
3448
+ finalizeV3AgentWorkflowResponse(data, response) {
3449
+ const changedFiles = this.resolveAgentChangedFiles(data);
3450
+ this.activeV3StreamedFiles = null;
3451
+ this.activeV3StreamContext = null;
3452
+ return {
3453
+ ...response,
3454
+ changedFiles,
3455
+ };
3456
+ }
3149
3457
  async collectV3AgentStream(response, context = {}) {
3150
3458
  if (!response.body || typeof response.body.getReader !== 'function') {
3151
3459
  return response.json();
3152
3460
  }
3461
+ this.clearAgentRunDiagnostics();
3153
3462
  const reader = response.body.getReader();
3154
3463
  const decoder = new TextDecoder();
3155
3464
  let buffer = '';
@@ -3158,183 +3467,202 @@ document.addEventListener('DOMContentLoaded', () => {
3158
3467
  let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
3159
3468
  let serverWorkspaceRoot = null;
3160
3469
  const streamedFiles = {};
3470
+ this.activeV3StreamedFiles = streamedFiles;
3471
+ this.activeV3StreamContext = context;
3472
+ context.__v3StreamedFiles = streamedFiles;
3161
3473
  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;
3474
+ try {
3475
+ while (true) {
3476
+ let chunk;
3477
+ try {
3478
+ const readPromise = reader.read();
3479
+ while (true) {
3480
+ const timeoutSentinel = Symbol('v3-agent-idle-timeout');
3481
+ const result = idleTimeoutMs > 0
3482
+ ? await Promise.race([
3483
+ readPromise,
3484
+ new Promise((resolve) => {
3485
+ setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
3486
+ }),
3487
+ ])
3488
+ : await readPromise;
3489
+ if (result !== timeoutSentinel) {
3490
+ chunk = result;
3491
+ break;
3492
+ }
3493
+ if (this.hasAgentWorkspaceOutput(context)) {
3494
+ const stalledError = new Error('V3 agent stream stalled after writing workspace output');
3495
+ stalledError.name = 'AbortError';
3496
+ stalledError.partialData = {
3497
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
3498
+ context_id: contextId,
3499
+ result: final,
3500
+ events,
3501
+ files: streamedFiles,
3502
+ };
3503
+ throw stalledError;
3504
+ }
3179
3505
  }
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 = {
3506
+ }
3507
+ catch (error) {
3508
+ if (error && error.name === 'AbortError') {
3509
+ error.partialData = {
3184
3510
  task_id: events.find((event) => event && event.task_id)?.task_id || null,
3185
3511
  context_id: contextId,
3186
3512
  result: final,
3187
3513
  events,
3188
3514
  files: streamedFiles,
3189
3515
  };
3190
- throw stalledError;
3191
3516
  }
3517
+ throw error;
3192
3518
  }
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
- };
3519
+ const { done, value } = chunk;
3520
+ if (done) {
3521
+ break;
3203
3522
  }
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;
3523
+ buffer += decoder.decode(value, { stream: true });
3524
+ const frames = buffer.split(/\r?\n\r?\n/);
3525
+ buffer = frames.pop() || '';
3526
+ for (const frame of frames) {
3527
+ const lines = frame.split(/\r?\n/);
3528
+ const dataLines = [];
3529
+ let explicitEventType = null;
3530
+ for (const rawLine of lines) {
3531
+ const line = rawLine.trimEnd();
3532
+ if (!line || line.startsWith(':')) {
3533
+ continue;
3534
+ }
3535
+ if (line.startsWith('event:')) {
3536
+ explicitEventType = line.slice(6).trim() || null;
3537
+ continue;
3538
+ }
3539
+ if (line.startsWith('data:')) {
3540
+ dataLines.push(line.slice(5).trimStart());
3541
+ }
3221
3542
  }
3222
- if (line.startsWith('event:')) {
3223
- explicitEventType = line.slice(6).trim() || null;
3543
+ const payload = dataLines.join('\n').trim();
3544
+ if (!payload || payload === '[DONE]') {
3224
3545
  continue;
3225
3546
  }
3226
- if (line.startsWith('data:')) {
3227
- dataLines.push(line.slice(5).trimStart());
3547
+ let event;
3548
+ try {
3549
+ event = JSON.parse(payload);
3228
3550
  }
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.');
3551
+ catch {
3552
+ event = {
3553
+ type: explicitEventType || 'message',
3554
+ content: payload,
3555
+ };
3556
+ }
3557
+ if (explicitEventType && (!event.type || event.type === 'message')) {
3558
+ event.type = explicitEventType;
3559
+ }
3560
+ const userEvent = this.sanitizeV3AgentEventForUser(event);
3561
+ events.push(userEvent);
3562
+ if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
3563
+ contextId = event.context_id.trim();
3564
+ }
3565
+ if (!serverWorkspaceRoot && event.type === 'context' && typeof event.workspace_root === 'string' && event.workspace_root.trim()) {
3566
+ serverWorkspaceRoot = event.workspace_root.trim();
3567
+ }
3568
+ if (event.type === 'client_tool_request') {
3569
+ const toolTask = this.handleV3ClientToolRequest(event, context, response.url).catch((error) => {
3570
+ this.logger.warn(`Failed to handle V3 client tool request: ${error.message}`);
3571
+ });
3572
+ this.pendingV3ClientToolTasks.add(toolTask);
3573
+ void toolTask.finally(() => {
3574
+ this.pendingV3ClientToolTasks.delete(toolTask);
3575
+ });
3576
+ }
3577
+ this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
3578
+ this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
3579
+ // Empty workspace soft guard: the first list_directory result can be
3580
+ // transiently empty before remote workspace hydration catches up.
3581
+ // Avoid hard-failing here; let the stream continue.
3582
+ if (event.type === 'tool_result'
3583
+ && event.name === 'list_directory'
3584
+ && event.success === true
3585
+ && serverWorkspaceRoot
3586
+ && typeof event.output === 'string') {
3587
+ const listOutput = event.output.trim();
3588
+ const looksEmpty = listOutput === `[${serverWorkspaceRoot}]`
3589
+ || listOutput === `[${serverWorkspaceRoot}/]`
3590
+ || listOutput === `[${serverWorkspaceRoot}]\\n`
3591
+ || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
3592
+ if (looksEmpty) {
3593
+ const localPath = this.resolveAgentTargetPath(context);
3594
+ const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
3595
+ if (localHasFiles) {
3596
+ this.logger?.debug?.('V3 remote workspace initially empty; waiting for hydration instead of aborting.');
3597
+ }
3275
3598
  }
3276
3599
  }
3277
- }
3278
- if (typeof context.onStreamEvent === 'function') {
3279
- try {
3280
- context.onStreamEvent(userEvent);
3600
+ if (typeof context.onStreamEvent === 'function') {
3601
+ try {
3602
+ context.onStreamEvent(userEvent);
3603
+ }
3604
+ catch {
3605
+ // Ignore UI callback failures; never break the agent stream for them.
3606
+ }
3281
3607
  }
3282
- catch {
3283
- // Ignore UI callback failures; never break the agent stream for them.
3608
+ if (event.type === 'error') {
3609
+ if (event.checkpointed && event.task_id) {
3610
+ // Agent checkpointed — return data so caller can auto-continue
3611
+ return {
3612
+ task_id: event.task_id,
3613
+ context_id: contextId,
3614
+ result: final || event,
3615
+ events,
3616
+ files: streamedFiles,
3617
+ partial: true,
3618
+ checkpointed: true,
3619
+ checkpointed_task_id: event.task_id,
3620
+ };
3621
+ }
3622
+ if (this.hasAgentWorkspaceOutput(context)) {
3623
+ return {
3624
+ task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3625
+ context_id: contextId,
3626
+ result: final || event,
3627
+ events,
3628
+ files: streamedFiles,
3629
+ partial: true,
3630
+ };
3631
+ }
3632
+ throw new Error(event.message || 'V3 agent returned an error');
3284
3633
  }
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
- };
3634
+ if (event.type === 'complete' || event.type === 'message') {
3635
+ final = event;
3299
3636
  }
3300
- if (this.hasAgentWorkspaceOutput(context)) {
3637
+ // Exit stream early on complete — agent is done; server-side teardown
3638
+ // can hold the connection open for many seconds otherwise.
3639
+ if (event.type === 'complete') {
3640
+ reader.cancel().catch(() => { });
3301
3641
  return {
3302
3642
  task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3303
3643
  context_id: contextId,
3304
- result: final || event,
3644
+ result: final,
3305
3645
  events,
3306
3646
  files: streamedFiles,
3307
- partial: true,
3647
+ serverWorkspaceRoot: serverWorkspaceRoot || null,
3308
3648
  };
3309
3649
  }
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
3650
  }
3328
3651
  }
3652
+ await Promise.allSettled([...this.pendingV3ClientToolTasks]);
3653
+ return {
3654
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
3655
+ context_id: contextId,
3656
+ result: final,
3657
+ events,
3658
+ files: streamedFiles,
3659
+ serverWorkspaceRoot: serverWorkspaceRoot || null,
3660
+ clientToolErrors: this.getClientToolErrors(),
3661
+ };
3662
+ }
3663
+ finally {
3664
+ await Promise.allSettled([...this.pendingV3ClientToolTasks]);
3329
3665
  }
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
3666
  }
3339
3667
  async runV3AgentWorkflow(message, context = {}) {
3340
3668
  const executionContext = await this.bindExecutionContext(context);
@@ -3457,14 +3785,14 @@ document.addEventListener('DOMContentLoaded', () => {
3457
3785
  await this.ensureAgentFrontendPolish(message, executionContext);
3458
3786
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3459
3787
  const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3460
- return {
3788
+ return this.finalizeV3AgentWorkflowResponse(continuationData, {
3461
3789
  content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
3462
3790
  taskId: continuationData.task_id || data.task_id || null,
3463
3791
  contextId: finalContextId,
3464
3792
  backendUrl: baseUrl,
3465
3793
  partial: continuationData.checkpointed === true,
3466
3794
  metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
3467
- };
3795
+ });
3468
3796
  }
3469
3797
  const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3470
3798
  const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
@@ -3476,13 +3804,13 @@ document.addEventListener('DOMContentLoaded', () => {
3476
3804
  errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
3477
3805
  continue;
3478
3806
  }
3479
- return {
3807
+ return this.finalizeV3AgentWorkflowResponse(data, {
3480
3808
  content: this.formatV3AgentResponse(data),
3481
3809
  taskId: data.task_id || null,
3482
3810
  contextId,
3483
3811
  backendUrl: baseUrl,
3484
3812
  metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
3485
- };
3813
+ });
3486
3814
  }
3487
3815
  catch (error) {
3488
3816
  if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
@@ -3490,14 +3818,14 @@ document.addEventListener('DOMContentLoaded', () => {
3490
3818
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3491
3819
  await this.ensureAgentFrontendPolish(message, executionContext);
3492
3820
  const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3493
- return {
3821
+ return this.finalizeV3AgentWorkflowResponse(error.partialData, {
3494
3822
  content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
3495
3823
  taskId: error.partialData.task_id || null,
3496
3824
  contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
3497
3825
  backendUrl: baseUrl,
3498
3826
  partial: true,
3499
3827
  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
- };
3828
+ });
3501
3829
  }
3502
3830
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3503
3831
  }
@@ -3559,6 +3887,158 @@ document.addEventListener('DOMContentLoaded', () => {
3559
3887
  }
3560
3888
  throw new Error(errors.join(' | '));
3561
3889
  }
3890
+ async startV3BackgroundJob(message, context = {}) {
3891
+ await this.ensureV3ServiceKey();
3892
+ const executionContext = await this.bindExecutionContext({
3893
+ ...context,
3894
+ backgroundJob: true,
3895
+ clientToolExecution: false,
3896
+ });
3897
+ const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
3898
+ const resolvedModel = this.resolvePermittedModelId(requestedModel);
3899
+ const body = {
3900
+ request: message,
3901
+ context: this.buildV3AgentContext({
3902
+ ...executionContext,
3903
+ backgroundJob: true,
3904
+ clientToolExecution: false,
3905
+ }),
3906
+ stream: false,
3907
+ model: resolvedModel,
3908
+ mcp_context_id: executionContext.mcpContextId || undefined,
3909
+ };
3910
+ const errors = [];
3911
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3912
+ try {
3913
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
3914
+ method: 'POST',
3915
+ headers: await this.getV3AgentHeaders(),
3916
+ body: JSON.stringify(body),
3917
+ });
3918
+ if (!response.ok) {
3919
+ errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
3920
+ continue;
3921
+ }
3922
+ return await response.json();
3923
+ }
3924
+ catch (error) {
3925
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3926
+ }
3927
+ }
3928
+ throw new Error(`Unable to start background job: ${errors.join('; ')}`);
3929
+ }
3930
+ async listV3BackgroundJobs(limit = 20) {
3931
+ await this.ensureV3ServiceKey();
3932
+ const errors = [];
3933
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3934
+ try {
3935
+ const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
3936
+ method: 'GET',
3937
+ headers: await this.getV3AgentHeaders(),
3938
+ });
3939
+ if (!response.ok) {
3940
+ errors.push(`${baseUrl}: ${response.status}`);
3941
+ continue;
3942
+ }
3943
+ const data = await response.json();
3944
+ return Array.isArray(data.jobs) ? data.jobs : [];
3945
+ }
3946
+ catch (error) {
3947
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3948
+ }
3949
+ }
3950
+ throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
3951
+ }
3952
+ async getV3BackgroundJob(jobId) {
3953
+ await this.ensureV3ServiceKey();
3954
+ const errors = [];
3955
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3956
+ try {
3957
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
3958
+ method: 'GET',
3959
+ headers: await this.getV3AgentHeaders(),
3960
+ });
3961
+ if (!response.ok) {
3962
+ errors.push(`${baseUrl}: ${response.status}`);
3963
+ continue;
3964
+ }
3965
+ return await response.json();
3966
+ }
3967
+ catch (error) {
3968
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3969
+ }
3970
+ }
3971
+ throw new Error(`Unable to get background job: ${errors.join('; ')}`);
3972
+ }
3973
+ async cancelV3BackgroundJob(jobId) {
3974
+ await this.ensureV3ServiceKey();
3975
+ const errors = [];
3976
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3977
+ try {
3978
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
3979
+ method: 'POST',
3980
+ headers: await this.getV3AgentHeaders(),
3981
+ });
3982
+ if (!response.ok) {
3983
+ errors.push(`${baseUrl}: ${response.status}`);
3984
+ continue;
3985
+ }
3986
+ return await response.json();
3987
+ }
3988
+ catch (error) {
3989
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3990
+ }
3991
+ }
3992
+ throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
3993
+ }
3994
+ async getV3BackgroundJobFiles(jobId) {
3995
+ await this.ensureV3ServiceKey();
3996
+ const errors = [];
3997
+ for (const baseUrl of this.getV3AgentBaseUrls(false)) {
3998
+ try {
3999
+ const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
4000
+ method: 'GET',
4001
+ headers: await this.getV3AgentHeaders(),
4002
+ });
4003
+ if (!response.ok) {
4004
+ errors.push(`${baseUrl}: ${response.status}`);
4005
+ continue;
4006
+ }
4007
+ return await response.json();
4008
+ }
4009
+ catch (error) {
4010
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
4011
+ }
4012
+ }
4013
+ throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
4014
+ }
4015
+ async applyV3BackgroundJobFiles(jobId, context = {}) {
4016
+ const data = await this.getV3BackgroundJobFiles(jobId);
4017
+ const rootPath = this.resolveAgentTargetPath({
4018
+ ...context,
4019
+ workspacePath: context.workspacePath || data.local_workspace_path,
4020
+ projectPath: context.projectPath || data.local_workspace_path,
4021
+ targetPath: context.targetPath || data.local_workspace_path,
4022
+ });
4023
+ if (!rootPath) {
4024
+ throw new Error('No local workspace path available for applying background job files.');
4025
+ }
4026
+ const applied = [];
4027
+ const skipped = [];
4028
+ for (const [relativePath, content] of Object.entries(data.files || {})) {
4029
+ if (typeof content !== 'string') {
4030
+ skipped.push(relativePath);
4031
+ continue;
4032
+ }
4033
+ if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
4034
+ applied.push(relativePath);
4035
+ }
4036
+ else {
4037
+ skipped.push(relativePath);
4038
+ }
4039
+ }
4040
+ return { applied, skipped, status: data.status };
4041
+ }
3562
4042
  formatOperatorResponse(data = {}) {
3563
4043
  // If the server returned a direct answer field, prefer it (for lookup tasks)
3564
4044
  if (typeof data.answer === 'string' && data.answer.trim()) {
@@ -5094,6 +5574,19 @@ document.addEventListener('DOMContentLoaded', () => {
5094
5574
  getLastChatTransportErrors() {
5095
5575
  return this.lastChatTransportErrors;
5096
5576
  }
5577
+ getClientToolErrors() {
5578
+ return [...this.clientToolErrors];
5579
+ }
5580
+ clearAgentRunDiagnostics() {
5581
+ this.clientToolErrors = [];
5582
+ this.pendingV3ClientToolTasks.clear();
5583
+ }
5584
+ recordClientToolError(message) {
5585
+ const trimmed = sanitizeUserFacingErrorText(String(message || '')).slice(0, 280);
5586
+ if (trimmed && !this.clientToolErrors.includes(trimmed)) {
5587
+ this.clientToolErrors.push(trimmed);
5588
+ }
5589
+ }
5097
5590
  async submitClientToolResult(contextId, callId, result, backendUrl) {
5098
5591
  const trimmedContextId = String(contextId || '').trim();
5099
5592
  const trimmedCallId = String(callId || '').trim();
@@ -5130,6 +5623,10 @@ document.addEventListener('DOMContentLoaded', () => {
5130
5623
  await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
5131
5624
  continue;
5132
5625
  }
5626
+ if (response.status === 404 && /no pending client tool call/i.test(errorText)) {
5627
+ // Server already consumed or timed out this call — treat as idempotent when local work succeeded.
5628
+ return;
5629
+ }
5133
5630
  throw new Error(`Client tool result rejected (${response.status}): ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
5134
5631
  }
5135
5632
  }