@yeaft/webchat-agent 1.0.317 → 1.0.320

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.317",
3
+ "version": "1.0.320",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -4,12 +4,18 @@ import { basename, join, relative, resolve } from 'node:path';
4
4
  import { defineTool } from './types.js';
5
5
  import { managedCliToolReady, resolveManagedCliCommand } from '../managed-cli.js';
6
6
  import { runProcess } from './process-runner.js';
7
- import { isAbortError, throwIfAborted, waitForAbortable } from './search-paths.js';
7
+ import {
8
+ isAbortError,
9
+ SearchBackendLimitError,
10
+ throwIfAborted,
11
+ waitForAbortable,
12
+ } from './search-paths.js';
8
13
 
9
14
  const FALLBACK_CONCURRENCY = 16;
10
15
  const MAX_LIMIT = 200;
11
16
  const MAX_DEPTH = 10;
12
17
  const MAX_OUTPUT_BYTES = 512 * 1024;
18
+ const DUST_RESULT_ROWS = MAX_LIMIT;
13
19
 
14
20
  function formatSize(bytes) {
15
21
  if (bytes < 1024) return `${bytes}B`;
@@ -40,10 +46,16 @@ function flattenDustTree(root, baseDir, depth, limit) {
40
46
  return [total, ...rows.slice(0, Math.max(0, limit - 1))].filter(Boolean);
41
47
  }
42
48
 
43
- async function runDust(command, baseDir, { depth, limit, signal }) {
44
- const result = await runProcess(command, [
49
+ export async function runDust(
50
+ command,
51
+ baseDir,
52
+ { depth, limit, signal },
53
+ processRunner = runProcess,
54
+ ) {
55
+ const result = await processRunner(command, [
45
56
  '--output-json',
46
57
  '--depth', String(depth),
58
+ '--number-of-lines', String(DUST_RESULT_ROWS),
47
59
  '--apparent-size',
48
60
  '--only-dir',
49
61
  '--no-progress',
@@ -56,8 +68,10 @@ async function runDust(command, baseDir, { depth, limit, signal }) {
56
68
  maxBytes: MAX_OUTPUT_BYTES,
57
69
  env: { ...process.env, NO_COLOR: '1' },
58
70
  });
59
- if (result.timedOut) throw new Error('dust timed out');
60
- if (result.truncated) throw new Error('dust output exceeded the tool limit');
71
+ if (result.timedOut) throw new SearchBackendLimitError('dust timed out');
72
+ if (result.truncated) {
73
+ throw new SearchBackendLimitError('dust output exceeded the tool limit');
74
+ }
61
75
  if (result.code !== 0) throw new Error(result.stderr.trim() || `dust exited with code ${result.code}`);
62
76
  return flattenDustTree(JSON.parse(result.stdout), baseDir, depth, limit);
63
77
  }
@@ -229,7 +243,7 @@ symlink is scanned like dust; descendant symlinks are not followed.`,
229
243
  try {
230
244
  rows = await runDust(dustCommand, baseDir, { depth, limit, signal: ctx?.signal });
231
245
  } catch (error) {
232
- if (isAbortError(error)) throw error;
246
+ if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
233
247
  rows = null;
234
248
  }
235
249
  }
@@ -12,9 +12,11 @@ import { resolve, join, relative } from 'path';
12
12
  import { managedCliToolReady, resolveManagedCliCommand } from '../managed-cli.js';
13
13
  import { runProcess } from './process-runner.js';
14
14
  import {
15
+ createFdPathRegex,
15
16
  createSearchPathMatcher,
16
17
  isAbortError,
17
18
  isSkippedSearchDirectory,
19
+ SearchBackendLimitError,
18
20
  SEARCH_SKIP_DIRS,
19
21
  throwIfAborted,
20
22
  waitForAbortable,
@@ -68,9 +70,16 @@ async function* walkDir(dir, baseDir, signal, maxDepth = 10, depth = 0) {
68
70
  }
69
71
  }
70
72
 
71
- async function listFilesWithFd(fdCommand, baseDir, signal) {
73
+ export async function listFilesWithFd(
74
+ fdCommand,
75
+ baseDir,
76
+ pattern,
77
+ signal,
78
+ processRunner = runProcess,
79
+ ) {
72
80
  const args = [
73
- '.', baseDir,
81
+ '--full-path',
82
+ '--case-sensitive',
74
83
  '--type', 'file',
75
84
  '--type', 'symlink',
76
85
  '--hidden',
@@ -81,15 +90,18 @@ async function listFilesWithFd(fdCommand, baseDir, signal) {
81
90
  ];
82
91
  for (const skipped of SEARCH_SKIP_DIRS) args.push('--exclude', skipped);
83
92
  args.push('--exclude', '.yeaft/worktrees', '--exclude', '**/.yeaft/worktrees/**');
84
- const result = await runProcess(fdCommand, args, {
93
+ args.push('--', createFdPathRegex(pattern), '.');
94
+ const result = await processRunner(fdCommand, args, {
85
95
  cwd: baseDir,
86
96
  signal,
87
97
  timeoutMs: 120_000,
88
98
  maxBytes: 16 * 1024 * 1024,
89
99
  preserveCarriageReturns: true,
90
100
  });
91
- if (result.timedOut) throw new Error('fd timed out');
92
- if (result.truncated) throw new Error('fd output exceeded the tool limit');
101
+ if (result.timedOut) throw new SearchBackendLimitError('fd timed out');
102
+ if (result.truncated) {
103
+ throw new SearchBackendLimitError('fd output exceeded the tool limit');
104
+ }
93
105
  if (result.code !== 0) {
94
106
  throw new Error(result.stderr.trim() || `fd exited with code ${result.code}`);
95
107
  }
@@ -176,10 +188,10 @@ Guidelines:
176
188
  }
177
189
  if (fdCommand) {
178
190
  try {
179
- paths = (await listFilesWithFd(fdCommand, baseDir, ctx?.signal))
191
+ paths = (await listFilesWithFd(fdCommand, baseDir, pattern, ctx?.signal))
180
192
  .filter(path => matchGlob(pattern, path));
181
193
  } catch (error) {
182
- if (isAbortError(error)) throw error;
194
+ if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
183
195
  paths = null;
184
196
  }
185
197
  }
@@ -18,6 +18,7 @@ import {
18
18
  SEARCH_SKIP_GLOBS,
19
19
  isAbortError,
20
20
  isSkippedSearchDirectory,
21
+ SearchBackendLimitError,
21
22
  throwIfAborted,
22
23
  waitForAbortable,
23
24
  } from './search-paths.js';
@@ -348,7 +349,6 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn, c
348
349
  if (options.after) args.push('-A', String(options.after));
349
350
  if (options.context || options.before || options.after) args.push('--no-context-separator');
350
351
  if (options.multiline) args.push('-U', '--multiline-dotall');
351
- args.push('--sort', 'path');
352
352
  args.push('--', pattern);
353
353
  if (searchTarget) args.push(searchTarget);
354
354
 
@@ -517,7 +517,12 @@ export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn, c
517
517
  });
518
518
  }
519
519
 
520
- async function listRipgrepCandidatePaths(command, searchPath, options) {
520
+ export async function listRipgrepCandidatePaths(
521
+ command,
522
+ searchPath,
523
+ options,
524
+ processRunner = runProcess,
525
+ ) {
521
526
  throwIfAborted(options.signal);
522
527
  const searchStat = await lstat(searchPath);
523
528
  const baseDir = searchStat.isDirectory() ? searchPath : dirname(searchPath);
@@ -532,8 +537,8 @@ async function listRipgrepCandidatePaths(command, searchPath, options) {
532
537
  ];
533
538
  if (options.fixedStrings) args.push('-F');
534
539
  for (const skipGlob of SEARCH_SKIP_GLOBS) args.push('--glob', skipGlob);
535
- args.push('--sort', 'path', '--', options.pattern, target);
536
- const result = await runProcess(command, args, {
540
+ args.push('--max-filesize', String(MAX_TEXT_FILE_BYTES), '--', options.pattern, target);
541
+ const result = await processRunner(command, args, {
537
542
  cwd: baseDir,
538
543
  env: createRipgrepEnv(),
539
544
  signal: options.signal,
@@ -541,8 +546,10 @@ async function listRipgrepCandidatePaths(command, searchPath, options) {
541
546
  maxBytes: MAX_CANDIDATE_BYTES,
542
547
  preserveCarriageReturns: true,
543
548
  });
544
- if (result.timedOut) throw new Error('rg timed out');
545
- if (result.truncated) throw new Error('rg candidate output exceeded the tool limit');
549
+ if (result.timedOut) throw new SearchBackendLimitError('rg timed out');
550
+ if (result.truncated) {
551
+ throw new SearchBackendLimitError('rg candidate output exceeded the tool limit');
552
+ }
546
553
  if (result.code !== 0 && result.code !== 1) {
547
554
  throw new Error(result.stderr.trim() || `rg exited with code ${result.code}`);
548
555
  }
@@ -761,7 +768,23 @@ export async function nodeGrep(pattern, searchPath, options) {
761
768
  }
762
769
 
763
770
  throwIfAborted(options.signal);
764
- if (rootStat.isDirectory()) await searchDir(searchPath);
771
+ if (candidatePaths) {
772
+ const orderedCandidates = [...candidatePaths]
773
+ .sort((left, right) => compareSearchPaths(
774
+ relative(searchBase, left).replace(/\\/g, '/'),
775
+ relative(searchBase, right).replace(/\\/g, '/'),
776
+ ));
777
+ for (let index = 0; index < orderedCandidates.length && !stopped; index += FALLBACK_CONCURRENCY) {
778
+ throwIfAborted(options.signal);
779
+ const batches = await Promise.all(
780
+ orderedCandidates.slice(index, index + FALLBACK_CONCURRENCY).map(collectFileRecords),
781
+ );
782
+ for (const batch of batches) {
783
+ addFileRecords(batch);
784
+ if (stopped) break;
785
+ }
786
+ }
787
+ } else if (rootStat.isDirectory()) await searchDir(searchPath);
765
788
  else addFileRecords(await collectFileRecords(searchPath));
766
789
  const result = output.toString();
767
790
  return options.structured
@@ -960,7 +983,7 @@ Guidelines:
960
983
  });
961
984
  result = await nodeGrep(pattern, absPath, { ...options, candidatePaths });
962
985
  } catch (error) {
963
- if (isAbortError(error)) throw error;
986
+ if (isAbortError(error) || error instanceof SearchBackendLimitError) throw error;
964
987
  result = await nodeGrep(pattern, absPath, options);
965
988
  }
966
989
  } else {
@@ -34,7 +34,7 @@ function expandBraces(pattern) {
34
34
  ));
35
35
  }
36
36
 
37
- function globToRegExp(pattern) {
37
+ function globToRegExpSource(pattern, separator = '/') {
38
38
  let source = '';
39
39
  for (let index = 0; index < pattern.length; index += 1) {
40
40
  const char = pattern[index];
@@ -42,19 +42,33 @@ function globToRegExp(pattern) {
42
42
  index += 1;
43
43
  if (pattern[index + 1] === '/') {
44
44
  index += 1;
45
- source += '(?:.*/)?';
45
+ source += `(?:[\\s\\S]*${separator})?`;
46
46
  } else {
47
- source += '.*';
47
+ source += '[\\s\\S]*';
48
48
  }
49
49
  } else if (char === '*') {
50
- source += '[^/]*';
50
+ source += separator === '/' ? '[^/]*' : '[^\\\\/]*';
51
51
  } else if (char === '?') {
52
- source += '[^/]';
52
+ source += separator === '/' ? '[^/]' : '[^\\\\/]';
53
+ } else if (char === '/') {
54
+ source += separator;
53
55
  } else {
54
56
  source += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
55
57
  }
56
58
  }
57
- return new RegExp(`^${source}$`);
59
+ return source;
60
+ }
61
+
62
+ function globToRegExp(pattern) {
63
+ return new RegExp(`^${globToRegExpSource(pattern)}$`);
64
+ }
65
+
66
+ /** Compile the public glob dialect for fd --full-path on POSIX and Windows. */
67
+ export function createFdPathRegex(glob) {
68
+ const normalizedGlob = String(glob || '').replace(/\\/g, '/');
69
+ const alternatives = expandBraces(normalizedGlob || '**/*')
70
+ .map(pattern => globToRegExpSource(pattern, '[\\\\/]'));
71
+ return `(?:^|[\\\\/])(?:${alternatives.join('|')})$`;
58
72
  }
59
73
 
60
74
  export function createSearchPathMatcher({ glob, type } = {}) {
@@ -106,3 +120,10 @@ export function waitForAbortable(promise, signal) {
106
120
  );
107
121
  });
108
122
  }
123
+
124
+ export class SearchBackendLimitError extends Error {
125
+ constructor(message) {
126
+ super(message);
127
+ this.name = 'SearchBackendLimitError';
128
+ }
129
+ }
@@ -86,7 +86,8 @@ import { listMcpServers, upsertMcpServer, removeMcpServer } from './config-api.j
86
86
  import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
87
87
  import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
88
88
  import { enqueueSubAgentPrompt } from './sub-agent/prompt-queue.js';
89
- import { isPromptableAgentStatus } from './sub-agent/status.js';
89
+ import { isPromptableAgentStatus, isTerminalAgentStatus, STATUS } from './sub-agent/status.js';
90
+ import { consumeNotificationForAgent } from './sub-agent/notifications.js';
90
91
  import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
91
92
  import { recordAgentSessionCreated, recordAgentTurn } from '../metrics.js';
92
93
  import { TASK_RESULT_DELIVERY, isTerminalTaskStatus, taskResultDeliveryFor } from './tasks/store.js';
@@ -6577,6 +6578,38 @@ export function handleYeaftTaskCancel(msg) {
6577
6578
  return;
6578
6579
  }
6579
6580
 
6581
+ const existingTask = session.taskManager.getTask?.(sessionId, taskId) || null;
6582
+ if (existingTask?.kind === 'sub_agent' && existingTask.status === 'running') {
6583
+ const subAgentId = existingTask.runtime?.subAgentId || '';
6584
+ const agent = subAgentId ? getAgentRegistry().get(subAgentId) : null;
6585
+ const scope = {
6586
+ sessionId,
6587
+ parentVpId: existingTask.ownerVpId || null,
6588
+ parentThreadId: existingTask.source?.threadId || 'main',
6589
+ };
6590
+ if (!agent || !agentBelongsToScope(agent, scope)) {
6591
+ fail('sub-agent not found', existingTask);
6592
+ return;
6593
+ }
6594
+ if (!isTerminalAgentStatus(agent.status)) {
6595
+ agent.status = STATUS.CLOSED;
6596
+ if (agent.abortController && !agent.abortController.signal.aborted) {
6597
+ try { agent.abortController.abort('stopped_by_user'); } catch { /* best effort */ }
6598
+ }
6599
+ }
6600
+ try { consumeNotificationForAgent(agent.id); } catch { /* best effort */ }
6601
+ const task = session.taskManager.completeTask(sessionId, taskId, { status: 'cancelled' });
6602
+ sendSessionEvent({
6603
+ type: 'yeaft_task_cancel_result',
6604
+ success: true,
6605
+ taskId,
6606
+ clientRequestId: clientRequestId || null,
6607
+ pending: false,
6608
+ task,
6609
+ }, { sessionId, vpId: task?.ownerVpId || null, threadId: task?.source?.threadId || null });
6610
+ return;
6611
+ }
6612
+
6580
6613
  let result;
6581
6614
  try {
6582
6615
  result = session.taskManager.cancelTask(sessionId, taskId);