@myagentroam/node 0.9.142 → 0.9.143

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/connector.js CHANGED
@@ -304,7 +304,7 @@ export class NodeConnector {
304
304
  terminalManager;
305
305
  upgradeCoordinator;
306
306
  workspaceState = new WorkspaceDomainState();
307
- workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId));
307
+ workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId), (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch));
308
308
  workspaceSessionService = new WorkspaceSessionService(this.workspaceState, {
309
309
  discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
310
310
  listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
@@ -8,6 +8,8 @@ export class MarAgentConversationNormalizer {
8
8
  #subagentDetails = new Map();
9
9
  #subagentStarts = new Map();
10
10
  #subagentLifecycles = new Map();
11
+ #codeModeObservationIds = new Set();
12
+ #codeModeSourceCallIds = new Set();
11
13
  messageItemId(nativeItemId) {
12
14
  return itemId('message', nativeItemId);
13
15
  }
@@ -71,8 +73,8 @@ export class MarAgentConversationNormalizer {
71
73
  if (event.type === 'tool.started')
72
74
  return this.#toolStarted(event);
73
75
  if (event.type === 'tool.completed')
74
- return this.#toolCompleted(event);
75
- return this.#toolFailed(event);
76
+ return [...this.#toolCompleted(event), ...this.#codeModeToolResults(event)];
77
+ return [...this.#toolFailed(event), ...this.#codeModeToolResults(event)];
76
78
  }
77
79
  if (event.type === 'hosted_tool.updated') {
78
80
  const sourceSummary = event.summary.sources
@@ -318,6 +320,72 @@ export class MarAgentConversationNormalizer {
318
320
  genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, 'FAILED', state?.input, event.message ?? event.code, event.code, false, undefined, state?.subagentTitle, state?.subagentDetails)
319
321
  ];
320
322
  }
323
+ #codeModeToolResults(event) {
324
+ const items = [];
325
+ for (const observation of codeModeToolResults(event)) {
326
+ if ((observation.id !== undefined && this.#codeModeObservationIds.has(observation.id)) ||
327
+ this.#codeModeSourceCallIds.has(observation.source.callId))
328
+ continue;
329
+ const { toolName, input } = observation.source;
330
+ let projected = [];
331
+ if (toolName === 'apply_patch') {
332
+ if ((typeof input !== 'string' && record(input) === undefined) ||
333
+ !isApplyPatchObservationData(observation.result.data))
334
+ continue;
335
+ projected = this.#normalizeObservedToolResult(event, observation, observation.result.data);
336
+ if (observation.outcome === 'failed')
337
+ projected = projected.map((item) => item.kind === 'file_change' ? { ...item, status: 'FAILED' } : item);
338
+ }
339
+ else if (toolName === 'todo_read') {
340
+ if (observation.outcome !== 'completed' ||
341
+ record(input) === undefined ||
342
+ !isTodoObservationData(observation.result.data))
343
+ continue;
344
+ const todo = observation.result.data;
345
+ projected = this.normalize({
346
+ ...eventMetadata(event, observation.source.callId),
347
+ type: 'todo.updated',
348
+ revision: todo.revision,
349
+ items: todo.items
350
+ });
351
+ }
352
+ else if (marAgentSubagentOperation(toolName) !== undefined) {
353
+ if (!isSubagentToolInput(toolName, input) ||
354
+ !isSubagentObservationData(observation.result.data, input))
355
+ continue;
356
+ projected = this.#normalizeObservedToolResult(event, observation, observation.result.data);
357
+ }
358
+ if (projected.length === 0)
359
+ continue;
360
+ if (observation.id !== undefined)
361
+ this.#codeModeObservationIds.add(observation.id);
362
+ this.#codeModeSourceCallIds.add(observation.source.callId);
363
+ items.push(...projected);
364
+ }
365
+ return items;
366
+ }
367
+ #normalizeObservedToolResult(event, observation, data) {
368
+ const { callId, toolName, input } = observation.source;
369
+ const resultId = observation.id ?? 'call-' + callId;
370
+ this.normalize({
371
+ ...eventMetadata(event, resultId + ':started'),
372
+ type: 'tool.started',
373
+ toolName,
374
+ callId,
375
+ input
376
+ });
377
+ return this.normalize({
378
+ ...eventMetadata(event, resultId + ':completed'),
379
+ type: 'tool.completed',
380
+ toolName,
381
+ callId,
382
+ summary: observation.result.content,
383
+ ...(observation.result.truncated === undefined
384
+ ? {}
385
+ : { truncated: observation.result.truncated }),
386
+ data
387
+ });
388
+ }
321
389
  #subagentLifecycle(event) {
322
390
  const eventDescription = record(event)?.description;
323
391
  const eventTitle = compactRunnerText(typeof eventDescription === 'string' ? eventDescription : '', 500);
@@ -358,6 +426,188 @@ export class MarAgentConversationNormalizer {
358
426
  return [subagentStartItem(start, event.agentId, title, details, lifecycle)];
359
427
  }
360
428
  }
429
+ function codeModeToolResults(event) {
430
+ if ((event.type !== 'tool.completed' && event.type !== 'tool.failed') ||
431
+ (event.toolName !== 'code_exec' && event.toolName !== 'code_exec_wait'))
432
+ return [];
433
+ const data = record(record(event)?.data);
434
+ if (data === undefined ||
435
+ typeof data.cellId !== 'string' ||
436
+ data.cellId.trim().length === 0 ||
437
+ (data.status !== 'running' &&
438
+ data.status !== 'completed' &&
439
+ data.status !== 'failed' &&
440
+ data.status !== 'cancelled') ||
441
+ ('cached' in data && typeof data.cached !== 'boolean'))
442
+ return [];
443
+ if (Object.hasOwn(data, 'calls'))
444
+ return codeModeCallResults(data.calls);
445
+ if (!Array.isArray(data.observations))
446
+ return [];
447
+ return codeModeObservationResults(data.observations);
448
+ }
449
+ function codeModeCallResults(value) {
450
+ if (!Array.isArray(value))
451
+ return [];
452
+ return value.flatMap((entry) => {
453
+ const call = record(entry);
454
+ const result = record(call?.result);
455
+ if (call === undefined ||
456
+ typeof call.callId !== 'string' ||
457
+ call.callId.trim().length === 0 ||
458
+ typeof call.toolName !== 'string' ||
459
+ !isPersonalizedToolName(call.toolName) ||
460
+ !('input' in call) ||
461
+ (call.status !== 'completed' && call.status !== 'failed') ||
462
+ result === undefined ||
463
+ typeof result.content !== 'string' ||
464
+ ('truncated' in result && typeof result.truncated !== 'boolean'))
465
+ return [];
466
+ return [
467
+ {
468
+ source: { callId: call.callId, toolName: call.toolName, input: call.input },
469
+ outcome: call.status,
470
+ result: {
471
+ content: result.content,
472
+ ...('data' in result ? { data: result.data } : {}),
473
+ ...(typeof result.truncated === 'boolean' ? { truncated: result.truncated } : {})
474
+ }
475
+ }
476
+ ];
477
+ });
478
+ }
479
+ function codeModeObservationResults(value) {
480
+ if (!Array.isArray(value))
481
+ return [];
482
+ return value.flatMap((value) => {
483
+ const observation = record(value);
484
+ const source = record(observation?.source);
485
+ const result = record(observation?.result);
486
+ if (observation?.type !== 'tool_result' ||
487
+ typeof observation.id !== 'string' ||
488
+ observation.id.trim().length === 0 ||
489
+ source === undefined ||
490
+ typeof source.callId !== 'string' ||
491
+ source.callId.trim().length === 0 ||
492
+ typeof source.toolName !== 'string' ||
493
+ !isPersonalizedToolName(source.toolName) ||
494
+ !('input' in source) ||
495
+ (observation.outcome !== 'completed' && observation.outcome !== 'failed') ||
496
+ result === undefined ||
497
+ typeof result.content !== 'string' ||
498
+ ('truncated' in result && typeof result.truncated !== 'boolean'))
499
+ return [];
500
+ return [
501
+ {
502
+ id: observation.id,
503
+ source: { callId: source.callId, toolName: source.toolName, input: source.input },
504
+ outcome: observation.outcome,
505
+ result: {
506
+ content: result.content,
507
+ ...('data' in result ? { data: result.data } : {}),
508
+ ...(typeof result.truncated === 'boolean' ? { truncated: result.truncated } : {})
509
+ }
510
+ }
511
+ ];
512
+ });
513
+ }
514
+ function isPersonalizedToolName(toolName) {
515
+ // todo_write already emits the native todo.updated event; avoid a duplicate card here.
516
+ return (toolName === 'apply_patch' ||
517
+ toolName === 'todo_read' ||
518
+ marAgentSubagentOperation(toolName) !== undefined);
519
+ }
520
+ function isApplyPatchObservationData(value) {
521
+ const data = record(value);
522
+ const totals = record(data?.totals);
523
+ return (data !== undefined &&
524
+ Array.isArray(data.files) &&
525
+ data.files.every((entry) => {
526
+ const file = record(entry);
527
+ return (file !== undefined &&
528
+ typeof file.path === 'string' &&
529
+ file.path.trim().length > 0 &&
530
+ (file.targetPath === undefined ||
531
+ (typeof file.targetPath === 'string' && file.targetPath.trim().length > 0)) &&
532
+ (file.operation === 'added' ||
533
+ file.operation === 'modified' ||
534
+ file.operation === 'deleted' ||
535
+ file.operation === 'moved') &&
536
+ (file.operation !== 'moved' ||
537
+ (typeof file.targetPath === 'string' && file.targetPath.trim().length > 0)) &&
538
+ Number.isSafeInteger(file.addedLines) &&
539
+ file.addedLines >= 0 &&
540
+ Number.isSafeInteger(file.deletedLines) &&
541
+ file.deletedLines >= 0);
542
+ }) &&
543
+ totals !== undefined &&
544
+ Number.isSafeInteger(totals.files) &&
545
+ totals.files >= 0 &&
546
+ Number.isSafeInteger(totals.addedLines) &&
547
+ totals.addedLines >= 0 &&
548
+ Number.isSafeInteger(totals.deletedLines) &&
549
+ totals.deletedLines >= 0);
550
+ }
551
+ function isTodoObservationData(value) {
552
+ const data = record(value);
553
+ if (data === undefined ||
554
+ !Number.isSafeInteger(data.revision) ||
555
+ data.revision < 0 ||
556
+ !Array.isArray(data.items))
557
+ return false;
558
+ return data.items.every((entry) => {
559
+ const item = record(entry);
560
+ if (item === undefined ||
561
+ typeof item.id !== 'string' ||
562
+ item.id.trim().length === 0 ||
563
+ typeof item.step !== 'string' ||
564
+ item.step.trim().length === 0 ||
565
+ (item.status !== 'pending' && item.status !== 'in_progress' && item.status !== 'completed') ||
566
+ !Number.isSafeInteger(data.revision))
567
+ return false;
568
+ return true;
569
+ });
570
+ }
571
+ function isSubagentToolInput(toolName, value) {
572
+ const input = record(value);
573
+ if (input === undefined)
574
+ return false;
575
+ if (toolName === 'agent_start')
576
+ return typeof input.description === 'string' && input.description.trim().length > 0;
577
+ if (toolName === 'agent_wait')
578
+ return (input.agentId === undefined || input.agentId === null || marAgentAgentId(input) !== undefined);
579
+ if (toolName === 'agent_message')
580
+ return (marAgentAgentId(input) !== undefined &&
581
+ typeof input.message === 'string' &&
582
+ input.message.trim().length > 0);
583
+ return toolName === 'agent_cancel' && marAgentAgentId(input) !== undefined;
584
+ }
585
+ function isSubagentObservationData(value, input) {
586
+ const data = record(value);
587
+ const inputAgentId = marAgentAgentId(input);
588
+ return (data !== undefined &&
589
+ typeof data.agentId === 'string' &&
590
+ data.agentId.trim().length > 0 &&
591
+ typeof data.description === 'string' &&
592
+ data.description.trim().length > 0 &&
593
+ (data.status === 'queued' ||
594
+ data.status === 'running' ||
595
+ data.status === 'completed' ||
596
+ data.status === 'failed' ||
597
+ data.status === 'cancelled' ||
598
+ data.status === 'interrupted') &&
599
+ (inputAgentId === undefined || data.agentId === inputAgentId));
600
+ }
601
+ function eventMetadata(event, eventId) {
602
+ return {
603
+ eventId,
604
+ sequence: event.sequence,
605
+ timestamp: event.timestamp,
606
+ sessionId: event.sessionId,
607
+ executionId: event.executionId,
608
+ ...(event.turnId === undefined ? {} : { turnId: event.turnId })
609
+ };
610
+ }
361
611
  function itemId(kind, value) {
362
612
  return boundedConversationItemId(`mar-agent-${kind}`, value);
363
613
  }
@@ -26,6 +26,7 @@ export function projectMarAgentHistory(session, entries, options = {}) {
26
26
  return created;
27
27
  };
28
28
  const replace = (turn, item) => {
29
+ turn = turns.get(turn.id) ?? turn;
29
30
  const index = turn.items.findIndex((candidate) => candidate.id === item.id);
30
31
  const items = [...turn.items];
31
32
  if (index < 0)
@@ -147,6 +147,15 @@ export class WorkspaceCodeNavigationService {
147
147
  truncated: targets.length > MAX_RESULTS || manifest.truncated
148
148
  };
149
149
  }
150
+ if (!current && cachedCandidates === undefined) {
151
+ const failure = this.failures.get(location.directory);
152
+ if (failure !== undefined) {
153
+ this.failures.delete(location.directory);
154
+ return emptyDefinitionResult({ state: 'FAILED', failureCode: failure });
155
+ }
156
+ this.startInitialBuild(workspace, location);
157
+ return emptyDefinitionResult({ state: 'BUILDING' });
158
+ }
150
159
  const requireFresh = !current;
151
160
  const prepared = await this.indexInput(workspace, requireFresh);
152
161
  if (!current) {
@@ -2,13 +2,20 @@ import { isAbsolute, relative, resolve, win32 } from 'node:path';
2
2
  import { realpath } from 'node:fs/promises';
3
3
  import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAbsoluteFileContentRange, readWorkspaceFileContentRange, readAbsoluteTextFile, readWorkspaceTextFile, writeWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
4
4
  const INDEX_CACHE_LIMIT = 32;
5
+ const INDEX_CACHE_BYTE_LIMIT = 128 * 1024 * 1024;
6
+ const INDEX_QUEUE_LIMIT = 16;
7
+ const INDEX_QUEUE_TIMEOUT_MS = 120_000;
5
8
  export class WorkspaceFileService {
6
9
  state;
7
10
  onInvalidated;
11
+ indexLimits;
8
12
  mutationTails = new Map();
9
- constructor(state, onInvalidated) {
13
+ indexQueue = [];
14
+ indexRunning = false;
15
+ constructor(state, onInvalidated, indexLimits = {}) {
10
16
  this.state = state;
11
17
  this.onInvalidated = onInvalidated;
18
+ this.indexLimits = indexLimits;
12
19
  }
13
20
  async list(workspace, input) {
14
21
  if ((input.path !== undefined && typeof input.path !== 'string') ||
@@ -151,12 +158,13 @@ export class WorkspaceFileService {
151
158
  if (typeof input.query !== 'string' ||
152
159
  (input.cursor !== undefined && typeof input.cursor !== 'string') ||
153
160
  (input.includeDirectories !== undefined && typeof input.includeDirectories !== 'boolean') ||
161
+ (input.waitForIndex !== undefined && typeof input.waitForIndex !== 'boolean') ||
154
162
  (input.limit !== undefined &&
155
163
  (!Number.isInteger(input.limit) ||
156
164
  input.limit < 1 ||
157
165
  input.limit > 200)))
158
166
  throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
159
- return this.index(workspace).search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined, input.includeDirectories === true);
167
+ return this.index(workspace).search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined, input.includeDirectories === true, input.waitForIndex !== false);
160
168
  }
161
169
  async mutate(workspace, input) {
162
170
  const action = input.action;
@@ -192,28 +200,104 @@ export class WorkspaceFileService {
192
200
  index(workspace) {
193
201
  let index = this.state.fileIndexes.get(workspace.id);
194
202
  if (index === undefined) {
195
- if (this.state.fileIndexes.size >= INDEX_CACHE_LIMIT) {
196
- const oldest = this.state.fileIndexes.entries().next().value;
197
- if (oldest !== undefined) {
198
- oldest[1].dispose();
199
- this.state.fileIndexes.delete(oldest[0]);
200
- }
201
- }
202
- index = new WorkspaceFileIndex(workspace.path);
203
+ index = new WorkspaceFileIndex(workspace.path, {
204
+ ...(this.indexLimits.indexByteLimit === undefined
205
+ ? {}
206
+ : { byteLimit: this.indexLimits.indexByteLimit }),
207
+ schedule: (run, background, needed) => this.scheduleIndex(run, background, needed),
208
+ onRebuilt: () => this.pruneIndexes(workspace.id)
209
+ });
203
210
  }
204
211
  this.state.fileIndexes.delete(workspace.id);
205
212
  this.state.fileIndexes.set(workspace.id, index);
213
+ this.pruneIndexes(workspace.id);
206
214
  return index;
207
215
  }
216
+ prepare(workspace, needed) {
217
+ if (!needed())
218
+ return Promise.resolve();
219
+ return this.index(workspace).prepare(needed);
220
+ }
221
+ /** Removes queued preparation whose Watch consumers have all gone away. */
222
+ prunePreparations() {
223
+ for (let index = this.indexQueue.length - 1; index >= 0; index -= 1) {
224
+ const job = this.indexQueue[index];
225
+ if (job.needed())
226
+ continue;
227
+ this.indexQueue.splice(index, 1);
228
+ clearTimeout(job.timer);
229
+ job.resolve();
230
+ }
231
+ }
232
+ pruneIndexes(protectedId) {
233
+ let bytes = [...this.state.fileIndexes.values()].reduce((total, index) => total + index.accountedBytes, 0);
234
+ const byteLimit = this.indexLimits.cacheByteLimit ?? INDEX_CACHE_BYTE_LIMIT;
235
+ for (const [id, index] of this.state.fileIndexes) {
236
+ if (this.state.fileIndexes.size <= INDEX_CACHE_LIMIT && bytes <= byteLimit)
237
+ break;
238
+ if (id === protectedId)
239
+ continue;
240
+ bytes -= index.accountedBytes;
241
+ index.dispose();
242
+ this.state.fileIndexes.delete(id);
243
+ }
244
+ this.prunePreparations();
245
+ }
246
+ scheduleIndex(run, background, needed) {
247
+ this.prunePreparations();
248
+ if (!needed())
249
+ return Promise.resolve();
250
+ if (this.indexQueue.length >= (this.indexLimits.queueLimit ?? INDEX_QUEUE_LIMIT))
251
+ return Promise.reject(new Error('FILE_SEARCH_LIMIT_EXCEEDED'));
252
+ return new Promise((resolve, reject) => {
253
+ const job = {
254
+ run,
255
+ background,
256
+ needed,
257
+ resolve: () => resolve(),
258
+ reject,
259
+ timer: setTimeout(() => {
260
+ const index = this.indexQueue.indexOf(job);
261
+ if (index < 0)
262
+ return;
263
+ this.indexQueue.splice(index, 1);
264
+ reject(new Error('FILE_SEARCH_LIMIT_EXCEEDED'));
265
+ }, this.indexLimits.queueTimeoutMs ?? INDEX_QUEUE_TIMEOUT_MS)
266
+ };
267
+ job.timer.unref();
268
+ this.indexQueue.push(job);
269
+ this.runNextIndex();
270
+ });
271
+ }
272
+ runNextIndex() {
273
+ if (this.indexRunning)
274
+ return;
275
+ this.prunePreparations();
276
+ const foreground = this.indexQueue.findIndex((job) => !job.background());
277
+ const job = this.indexQueue.splice(foreground < 0 ? 0 : foreground, 1)[0];
278
+ if (job === undefined)
279
+ return;
280
+ clearTimeout(job.timer);
281
+ this.indexRunning = true;
282
+ void Promise.resolve()
283
+ .then(job.run)
284
+ .then(job.resolve, job.reject)
285
+ .finally(() => {
286
+ this.indexRunning = false;
287
+ this.runNextIndex();
288
+ });
289
+ }
208
290
  dispose(workspaceId) {
209
291
  if (workspaceId !== undefined) {
210
292
  this.state.fileIndexes.get(workspaceId)?.dispose();
211
293
  this.state.fileIndexes.delete(workspaceId);
294
+ this.prunePreparations();
212
295
  return;
213
296
  }
214
297
  for (const index of this.state.fileIndexes.values())
215
298
  index.dispose();
216
299
  this.state.fileIndexes.clear();
300
+ this.prunePreparations();
217
301
  }
218
302
  }
219
303
  function isFilePreviewAbsolutePath(path) {
@@ -24,12 +24,15 @@ export class WorkspaceWatchController {
24
24
  const existing = this.options.watch.existing(workspace.id, key);
25
25
  if (existing !== undefined &&
26
26
  existing.watchId === input.watchId &&
27
- JSON.stringify(existing.topics) === JSON.stringify(topics)) {
27
+ JSON.stringify(existing.topics) === JSON.stringify(topics) &&
28
+ (existing.prepareFiles === true) === (input.prepareFiles === true)) {
28
29
  this.options.watch.renew(workspace.id, key, input.watchId);
29
30
  return this.accepted(existing);
30
31
  }
31
- const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics);
32
+ const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics, input.prepareFiles === true);
32
33
  void this.options.snapshots.initial(workspace, watch);
34
+ if (watch.prepareFiles)
35
+ this.options.snapshots.prewarmFiles(workspace, watch);
33
36
  return this.accepted(watch);
34
37
  }
35
38
  unwatch(input) {
@@ -49,7 +52,9 @@ export class WorkspaceWatchController {
49
52
  typeof input.watchId !== 'string' ||
50
53
  typeof input.workbenchConnectionId !== 'string' ||
51
54
  !/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId) ||
52
- (input.mode !== 'initial' && input.mode !== 'renew'))
55
+ (input.mode !== 'initial' && input.mode !== 'renew') ||
56
+ (input.prepareFiles !== undefined &&
57
+ (input.mode !== 'initial' || input.prepareFiles !== true)))
53
58
  throw new Error('WORKSPACE_WATCH_INVALID');
54
59
  }
55
60
  accepted(watch) {
@@ -5,11 +5,13 @@ export class WorkspaceWatchService {
5
5
  onSessionWatchRequired;
6
6
  onSessionWatchUnused;
7
7
  onWorkspaceWatchUnused;
8
- constructor(state, onSessionWatchRequired, onSessionWatchUnused, onWorkspaceWatchUnused) {
8
+ onWatchRemoved;
9
+ constructor(state, onSessionWatchRequired, onSessionWatchUnused, onWorkspaceWatchUnused, onWatchRemoved = () => undefined) {
9
10
  this.state = state;
10
11
  this.onSessionWatchRequired = onSessionWatchRequired;
11
12
  this.onSessionWatchUnused = onSessionWatchUnused;
12
13
  this.onWorkspaceWatchUnused = onWorkspaceWatchUnused;
14
+ this.onWatchRemoved = onWatchRemoved;
13
15
  }
14
16
  parseTopics(input) {
15
17
  if (!plain(input.topics))
@@ -51,7 +53,7 @@ export class WorkspaceWatchService {
51
53
  throw new Error('WORKSPACE_WATCH_INVALID');
52
54
  return input.workbenchConnectionId;
53
55
  }
54
- create(workspaceId, watchId, sessionHash, workbenchConnectionId, nodeGeneration, topics) {
56
+ create(workspaceId, watchId, sessionHash, workbenchConnectionId, nodeGeneration, topics, prepareFiles = false) {
55
57
  const key = `${sessionHash}:${workbenchConnectionId}`;
56
58
  for (const [activeWorkspaceId, watches] of this.state.watches)
57
59
  if (watches.has(key))
@@ -64,6 +66,7 @@ export class WorkspaceWatchService {
64
66
  nodeGeneration,
65
67
  expiresAt: Date.now() + WATCH_TTL_MS,
66
68
  topics,
69
+ prepareFiles,
67
70
  timer: undefined
68
71
  };
69
72
  watch.timer = this.expiryTimer(workspaceId, key);
@@ -102,8 +105,10 @@ export class WorkspaceWatchService {
102
105
  return;
103
106
  clearTimeout(watch.timer);
104
107
  watches?.delete(key);
105
- if (watches?.size === 0) {
108
+ if (watches?.size === 0)
106
109
  this.state.watches.delete(workspaceId);
110
+ this.onWatchRemoved(watch);
111
+ if (watches?.size === 0) {
107
112
  this.clearRevisions(workspaceId);
108
113
  this.onWorkspaceWatchUnused(workspaceId);
109
114
  }
@@ -112,20 +117,25 @@ export class WorkspaceWatchService {
112
117
  }
113
118
  clear(workspaceId) {
114
119
  const watches = this.state.watches.get(workspaceId);
120
+ this.state.watches.delete(workspaceId);
115
121
  if (watches !== undefined)
116
- for (const watch of watches.values())
122
+ for (const watch of watches.values()) {
117
123
  clearTimeout(watch.timer);
118
- this.state.watches.delete(workspaceId);
124
+ this.onWatchRemoved(watch);
125
+ }
119
126
  this.clearRevisions(workspaceId);
120
127
  this.onSessionWatchUnused(workspaceId);
121
128
  this.onWorkspaceWatchUnused(workspaceId);
122
129
  }
123
130
  clearAll() {
124
131
  const workspaceIds = [...this.state.watches.keys()];
125
- for (const watches of this.state.watches.values())
126
- for (const watch of watches.values())
127
- clearTimeout(watch.timer);
132
+ const watchesByWorkspace = [...this.state.watches.values()];
128
133
  this.state.watches.clear();
134
+ for (const watches of watchesByWorkspace)
135
+ for (const watch of watches.values()) {
136
+ clearTimeout(watch.timer);
137
+ this.onWatchRemoved(watch);
138
+ }
129
139
  this.state.watchRevisions.clear();
130
140
  for (const workspaceId of workspaceIds) {
131
141
  this.onSessionWatchUnused(workspaceId);
@@ -2,6 +2,7 @@ const FILES_REFRESH_INTERVAL_MS = 30_000;
2
2
  /** Owns Workspace topic reads, single-flight invalidation and directed snapshot delivery. */
3
3
  export class WorkspaceWatchSnapshotService {
4
4
  options;
5
+ filePrewarmTimers = new Map();
5
6
  refreshes = new Map();
6
7
  dirty = new Set();
7
8
  reads = new Map();
@@ -26,6 +27,23 @@ export class WorkspaceWatchSnapshotService {
26
27
  }
27
28
  }));
28
29
  }
30
+ prewarmFiles(workspace, watch) {
31
+ this.cancelFilePrewarm(watch);
32
+ const timer = setTimeout(() => {
33
+ this.filePrewarmTimers.delete(watch);
34
+ const needed = () => this.matches(workspace.id, watch) && watch.prepareFiles === true;
35
+ if (needed())
36
+ void this.options.files.prepare(workspace, needed).catch(() => undefined);
37
+ }, 750);
38
+ timer.unref();
39
+ this.filePrewarmTimers.set(watch, timer);
40
+ }
41
+ cancelFilePrewarm(watch) {
42
+ const timer = this.filePrewarmTimers.get(watch);
43
+ if (timer !== undefined)
44
+ clearTimeout(timer);
45
+ this.filePrewarmTimers.delete(watch);
46
+ }
29
47
  async invalidate(workspaceId, topic) {
30
48
  const key = `${workspaceId}:${topic}`;
31
49
  const active = this.refreshes.get(key);
@@ -54,6 +72,11 @@ export class WorkspaceWatchSnapshotService {
54
72
  return this.refreshes.get(`${workspaceId}:${topic}`) ?? this.invalidate(workspaceId, topic);
55
73
  }
56
74
  clear(workspaceId) {
75
+ for (const [watch, timer] of this.filePrewarmTimers)
76
+ if (watch.workspaceId === workspaceId) {
77
+ clearTimeout(timer);
78
+ this.filePrewarmTimers.delete(watch);
79
+ }
57
80
  for (const key of [...this.dirty])
58
81
  if (key.startsWith(`${workspaceId}:`))
59
82
  this.dirty.delete(key);
@@ -62,6 +85,9 @@ export class WorkspaceWatchSnapshotService {
62
85
  this.signatures.delete(key);
63
86
  }
64
87
  clearAll() {
88
+ for (const timer of this.filePrewarmTimers.values())
89
+ clearTimeout(timer);
90
+ this.filePrewarmTimers.clear();
65
91
  if (this.filesRefreshTimer !== undefined)
66
92
  clearInterval(this.filesRefreshTimer);
67
93
  this.filesRefreshTimer = undefined;
@@ -26,6 +26,10 @@ export class WorkspaceWorkbenchService {
26
26
  nodeGeneration: options.nodeGeneration
27
27
  });
28
28
  }
29
+ cancelFilePrewarm(watch) {
30
+ this.snapshots.cancelFilePrewarm(watch);
31
+ this.options.files.prunePreparations();
32
+ }
29
33
  operations() {
30
34
  return {
31
35
  ...this.watchController.operations(),
package/dist/workspace.js CHANGED
@@ -3,7 +3,7 @@ import { lstat, mkdir, opendir, open, readdir, readFile, realpath, rename, rm, s
3
3
  import { homedir, platform as hostPlatform } from 'node:os';
4
4
  import { basename, dirname, isAbsolute, posix, relative, resolve, sep, win32 } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import { minimatch } from 'minimatch';
6
+ import { Minimatch } from 'minimatch';
7
7
  import { logNodeCommandFailure } from './operational.js';
8
8
  import { terminateProcessTree } from './process-tree.js';
9
9
  import { gitOperationEnvironment } from './service/git-operation-service.js';
@@ -21,6 +21,7 @@ const FILE_INDEX_IDLE_TTL_MS = 60 * 60 * 1_000;
21
21
  const FILE_INDEX_FRESHNESS_TTL_MS = 30_000;
22
22
  const FILE_INDEX_ENTRY_LIMIT = 100_000;
23
23
  const FILE_INDEX_METADATA_CONCURRENCY = 32;
24
+ const FILE_INDEX_BYTE_LIMIT = 64 * 1024 * 1024;
24
25
  const DEFAULT_QUICK_OPEN_EXCLUDES = [
25
26
  '**/.git/**',
26
27
  '**/node_modules/**',
@@ -84,6 +85,11 @@ export class WorkspaceFileIndex {
84
85
  workspaceRoot;
85
86
  limits;
86
87
  entries = [];
88
+ fileEntries = [];
89
+ retainedBytes = 0;
90
+ failure;
91
+ foregroundRequested = false;
92
+ preparationConsumers = new Set();
87
93
  truncated = false;
88
94
  root;
89
95
  loading;
@@ -96,6 +102,28 @@ export class WorkspaceFileIndex {
96
102
  this.workspaceRoot = workspaceRoot;
97
103
  this.limits = limits;
98
104
  }
105
+ get accountedBytes() {
106
+ return this.retainedBytes;
107
+ }
108
+ /** Prepares only metadata; a queued background scan may lose its consumer. */
109
+ prepare(needed = () => true) {
110
+ if (!this.stale && this.root !== undefined) {
111
+ this.touch();
112
+ return Promise.resolve();
113
+ }
114
+ if (!needed())
115
+ return Promise.resolve();
116
+ this.preparationConsumers.add(needed);
117
+ this.startLoading(true, () => {
118
+ for (const consumer of this.preparationConsumers) {
119
+ if (consumer())
120
+ return true;
121
+ this.preparationConsumers.delete(consumer);
122
+ }
123
+ return false;
124
+ });
125
+ return this.loading;
126
+ }
99
127
  async contentCandidates(options = {}) {
100
128
  await this.ensureLoaded(options.requireFresh === true);
101
129
  return this.currentContentCandidates();
@@ -118,16 +146,12 @@ export class WorkspaceFileIndex {
118
146
  if (this.loading !== undefined)
119
147
  return this.loading;
120
148
  if (this.stale || this.root === undefined) {
121
- this.loading = this.rebuild().finally(() => {
122
- this.loading = undefined;
123
- });
149
+ this.startLoading();
124
150
  return this.loading;
125
151
  }
126
152
  if (requireFresh) {
127
153
  this.refreshAttemptedAt = Date.now();
128
- this.loading = this.rebuild().finally(() => {
129
- this.loading = undefined;
130
- });
154
+ this.startLoading();
131
155
  return this.loading;
132
156
  }
133
157
  if (Date.now() - this.refreshAttemptedAt >=
@@ -138,7 +162,7 @@ export class WorkspaceFileIndex {
138
162
  currentContentCandidates() {
139
163
  this.touch();
140
164
  return {
141
- entries: this.entries.filter((entry) => entry.kind === 'FILE'),
165
+ entries: this.fileEntries,
142
166
  truncated: this.truncated
143
167
  };
144
168
  }
@@ -148,25 +172,45 @@ export class WorkspaceFileIndex {
148
172
  clearTimeout(this.expiryTimer);
149
173
  this.expiryTimer = undefined;
150
174
  this.entries = [];
175
+ this.fileEntries = [];
176
+ this.root = undefined;
177
+ this.retainedBytes = 0;
178
+ this.failure = undefined;
179
+ this.foregroundRequested = false;
180
+ this.preparationConsumers.clear();
151
181
  this.truncated = false;
152
182
  this.stale = true;
153
183
  this.refreshedAt = 0;
154
184
  this.refreshAttemptedAt = 0;
155
185
  }
156
- async search(query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
186
+ async search(query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false, waitForIndex = true) {
187
+ const generation = this.lifecycleGeneration;
157
188
  if (query.trim().length === 0 ||
158
189
  query.length > 256 ||
159
190
  !Number.isSafeInteger(limit) ||
160
191
  limit < 1 ||
161
192
  limit > SEARCH_RESULT_LIMIT)
162
193
  throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
194
+ const after = decodeSearchCursor(cursor, query);
195
+ if (!waitForIndex && (this.stale || this.root === undefined)) {
196
+ if (this.failure !== undefined) {
197
+ const failure = this.failure;
198
+ this.failure = undefined;
199
+ throw failure;
200
+ }
201
+ this.foregroundRequested = true;
202
+ this.startLoading();
203
+ return { entries: [], nextCursor: null, truncated: false, indexState: 'BUILDING' };
204
+ }
163
205
  await this.ensureLoaded();
206
+ if (generation !== this.lifecycleGeneration || this.root === undefined)
207
+ throw new Error('FILE_INDEX_INVALIDATED');
164
208
  this.touch();
165
- const after = decodeSearchCursor(cursor, query);
209
+ const terms = query.trim().toLocaleLowerCase().split(/\s+/u);
166
210
  const afterEntry = after === undefined
167
211
  ? undefined
168
212
  : this.entries.find((entry) => entry.path === after && (includeDirectories || entry.kind === 'FILE'));
169
- const afterScore = afterEntry === undefined ? -1 : fuzzyPathScore(afterEntry.path, query);
213
+ const afterScore = afterEntry === undefined ? -1 : fuzzyPathScore(afterEntry.path, terms);
170
214
  const afterCandidate = afterEntry === undefined || afterScore < 0
171
215
  ? undefined
172
216
  : { entry: afterEntry, score: afterScore };
@@ -174,7 +218,7 @@ export class WorkspaceFileIndex {
174
218
  for (const entry of this.entries) {
175
219
  if (!includeDirectories && entry.kind !== 'FILE')
176
220
  continue;
177
- const score = fuzzyPathScore(entry.path, query);
221
+ const score = fuzzyPathScore(entry.path, terms);
178
222
  if (score < 0)
179
223
  continue;
180
224
  const candidate = { entry, score };
@@ -183,38 +227,75 @@ export class WorkspaceFileIndex {
183
227
  ranked.add(candidate);
184
228
  }
185
229
  const candidates = ranked.sorted();
186
- const page = candidates.slice(0, limit).map((candidate) => candidate.entry);
230
+ const page = candidates.slice(0, limit).map((candidate) => ({
231
+ ...candidate.entry,
232
+ score: candidate.score
233
+ }));
187
234
  const next = candidates[limit]?.entry.path;
188
235
  return {
189
236
  entries: page,
190
237
  nextCursor: next === undefined ? null : encodeSearchCursor(query, page.at(-1)?.path ?? after ?? ''),
191
- truncated: next !== undefined || this.truncated
238
+ truncated: next !== undefined || this.truncated,
239
+ ...(!waitForIndex ? { indexState: 'READY' } : {})
192
240
  };
193
241
  }
194
242
  async ensureLoaded(requireFresh = false) {
243
+ const generation = this.lifecycleGeneration;
195
244
  if (!this.stale && this.root !== undefined) {
196
245
  if (Date.now() - this.refreshAttemptedAt >=
197
246
  (this.limits.freshnessTtlMs ?? FILE_INDEX_FRESHNESS_TTL_MS)) {
198
247
  if (requireFresh) {
248
+ this.foregroundRequested = true;
199
249
  if (this.loading === undefined) {
200
250
  this.refreshAttemptedAt = Date.now();
201
- this.loading = this.rebuild().finally(() => {
202
- this.loading = undefined;
203
- });
251
+ this.startLoading();
204
252
  }
205
253
  await this.loading;
254
+ if (generation !== this.lifecycleGeneration)
255
+ throw new Error('FILE_INDEX_INVALIDATED');
206
256
  }
207
257
  else
208
258
  this.refreshInBackground();
209
259
  }
210
260
  return;
211
261
  }
212
- if (this.loading === undefined) {
213
- this.loading = this.rebuild().finally(() => {
214
- this.loading = undefined;
215
- });
216
- }
262
+ this.startLoading();
217
263
  await this.loading;
264
+ if (generation !== this.lifecycleGeneration)
265
+ throw new Error('FILE_INDEX_INVALIDATED');
266
+ }
267
+ startLoading(background = false, needed = () => true) {
268
+ if (!background)
269
+ this.foregroundRequested = true;
270
+ if (this.loading !== undefined)
271
+ return;
272
+ const generation = this.lifecycleGeneration;
273
+ const current = () => generation === this.lifecycleGeneration;
274
+ const required = () => current() && (this.foregroundRequested || needed());
275
+ const run = async () => {
276
+ if (!current())
277
+ throw new Error('FILE_INDEX_INVALIDATED');
278
+ if (required())
279
+ await this.rebuild();
280
+ if (!current())
281
+ throw new Error('FILE_INDEX_INVALIDATED');
282
+ };
283
+ const pending = (this.limits.schedule?.(run, () => !this.foregroundRequested, required) ?? run())
284
+ .catch((error) => {
285
+ if (current())
286
+ this.failure = error;
287
+ throw error;
288
+ })
289
+ .finally(() => {
290
+ if (this.loading === pending) {
291
+ this.loading = undefined;
292
+ this.foregroundRequested = false;
293
+ this.preparationConsumers.clear();
294
+ }
295
+ });
296
+ this.loading = pending;
297
+ // A fast acknowledgement has no awaiting consumer; retain failure for its next query.
298
+ void this.loading.catch(() => undefined);
218
299
  }
219
300
  touch() {
220
301
  if (this.expiryTimer !== undefined)
@@ -227,11 +308,23 @@ export class WorkspaceFileIndex {
227
308
  const root = await resolveRegisteredWorkspaceRoot(this.workspaceRoot);
228
309
  if (generation !== this.lifecycleGeneration)
229
310
  return;
230
- const excludes = [...DEFAULT_QUICK_OPEN_EXCLUDES, ...(await workspaceExcludePatterns(root))];
311
+ const excludeOptions = { dot: true, nocase: hostPlatform() !== 'linux' };
312
+ const excludes = [
313
+ // Fixed directory globs can use their compiled regex without splitting every path.
314
+ ...DEFAULT_QUICK_OPEN_EXCLUDES.map((pattern) => {
315
+ const matcher = new Minimatch(pattern, excludeOptions);
316
+ const regex = matcher.makeRe();
317
+ // Globstar also matches line terminators in valid filesystem names.
318
+ return regex ? new RegExp(regex.source, `${regex.flags}s`) : matcher;
319
+ }),
320
+ ...(await workspaceExcludePatterns(root)).map((pattern) => new Minimatch(pattern, excludeOptions))
321
+ ];
231
322
  if (generation !== this.lifecycleGeneration)
232
323
  return;
233
324
  const indexed = [];
234
325
  const entryLimit = this.limits.entryLimit ?? FILE_INDEX_ENTRY_LIMIT;
326
+ const byteLimit = this.limits.byteLimit ?? FILE_INDEX_BYTE_LIMIT;
327
+ let bytes = 0;
235
328
  let truncated = false;
236
329
  const directories = ['.'];
237
330
  let directoryCursor = 0;
@@ -255,10 +348,14 @@ export class WorkspaceFileIndex {
255
348
  for (const entry of resolvedEntries) {
256
349
  if (entry === undefined)
257
350
  continue;
258
- if (entry.path === '.git' ||
259
- entry.path.startsWith('.git/') ||
260
- isExcluded(entry.path, excludes))
351
+ if (entry.path === '.git' || entry.path.startsWith('.git/'))
261
352
  continue;
353
+ const entryBytes = 160 + 2 * (entry.path.length + entry.name.length);
354
+ if (bytes + entryBytes > byteLimit) {
355
+ truncated = true;
356
+ break;
357
+ }
358
+ bytes += entryBytes;
262
359
  if (entry.kind === 'DIRECTORY')
263
360
  directories.push(entry.path);
264
361
  indexed.push(entry);
@@ -278,7 +375,7 @@ export class WorkspaceFileIndex {
278
375
  candidatePath.startsWith('.git/') ||
279
376
  isExcluded(candidatePath, excludes))
280
377
  continue;
281
- pending.push(inspectWorkspaceIndexEntry(root, candidatePath, child.name));
378
+ pending.push(inspectWorkspaceIndexEntry(root, candidatePath, child.name, excludes));
282
379
  if (pending.length >= FILE_INDEX_METADATA_CONCURRENCY)
283
380
  await appendPending();
284
381
  if (truncated)
@@ -291,20 +388,21 @@ export class WorkspaceFileIndex {
291
388
  return;
292
389
  this.root = root;
293
390
  this.entries = indexed;
391
+ this.fileEntries = indexed.filter((entry) => entry.kind === 'FILE');
392
+ this.retainedBytes = bytes;
393
+ this.failure = undefined;
294
394
  this.truncated = truncated;
295
395
  this.stale = false;
296
396
  this.refreshedAt = Date.now();
297
397
  this.refreshAttemptedAt = this.refreshedAt;
398
+ this.touch();
399
+ this.limits.onRebuilt?.();
298
400
  }
299
401
  refreshInBackground() {
300
402
  if (this.loading !== undefined)
301
403
  return;
302
404
  this.refreshAttemptedAt = Date.now();
303
- this.loading = this.rebuild()
304
- .catch(() => undefined)
305
- .finally(() => {
306
- this.loading = undefined;
307
- });
405
+ this.startLoading(true);
308
406
  }
309
407
  }
310
408
  async function workspaceExcludePatterns(root) {
@@ -322,8 +420,9 @@ async function workspaceExcludePatterns(root) {
322
420
  });
323
421
  }
324
422
  function isExcluded(path, patterns) {
325
- return patterns.some((pattern) => minimatch(path, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }) ||
326
- minimatch(`${path}/`, pattern, { dot: true, nocase: hostPlatform() !== 'linux' }));
423
+ return patterns.some((pattern) => pattern instanceof RegExp
424
+ ? pattern.test(path) || pattern.test(`${path}/`)
425
+ : pattern.match(path) || pattern.match(`${path}/`));
327
426
  }
328
427
  function compareRankedCandidate(left, right) {
329
428
  return right.score - left.score || left.entry.path.localeCompare(right.entry.path);
@@ -383,9 +482,8 @@ class RankedCandidateHeap {
383
482
  }
384
483
  }
385
484
  }
386
- function fuzzyPathScore(path, query) {
485
+ function fuzzyPathScore(path, terms) {
387
486
  const source = path.toLocaleLowerCase();
388
- const terms = query.trim().toLocaleLowerCase().split(/\s+/u);
389
487
  const name = source.slice(source.lastIndexOf('/') + 1);
390
488
  let total = 0;
391
489
  for (const needle of terms) {
@@ -397,6 +495,14 @@ function fuzzyPathScore(path, query) {
397
495
  return total - path.length / 1_000;
398
496
  }
399
497
  function fuzzyTermScore(source, name, needle) {
498
+ if (name === needle || source === needle)
499
+ return 1_000;
500
+ if (name.startsWith(needle) || source.startsWith(needle))
501
+ return 700;
502
+ if (source.endsWith(`/${needle}`) || source.includes(`/${needle}/`))
503
+ return 600;
504
+ if (name.includes(needle) || source.includes(needle))
505
+ return 500;
400
506
  let cursor = 0;
401
507
  let score = 0;
402
508
  let previous = -2;
@@ -410,11 +516,7 @@ function fuzzyTermScore(source, name, needle) {
410
516
  previous = index;
411
517
  cursor = index + 1;
412
518
  }
413
- if (name.includes(needle))
414
- score += 40;
415
- else if (source.includes(needle))
416
- score += 16;
417
- return score;
519
+ return 100 + score / 1_000;
418
520
  }
419
521
  /**
420
522
  * Lists directory names only for the Workspace picker. Browsing begins at the
@@ -939,12 +1041,15 @@ async function resolveWorkspaceEntryMetadata(root, requestedPath) {
939
1041
  const metadata = await lstat(candidate);
940
1042
  return { path: candidate, metadata };
941
1043
  }
942
- async function inspectWorkspaceIndexEntry(root, requestedPath, name) {
1044
+ async function inspectWorkspaceIndexEntry(root, requestedPath, name, excludes) {
943
1045
  try {
944
1046
  const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
945
1047
  if (!entry.metadata.isFile() && !entry.metadata.isDirectory())
946
1048
  return undefined;
947
1049
  const safePath = relative(root, entry.path).split(sep).join('/');
1050
+ // The requested path was already checked; aliases need the canonical-path check too.
1051
+ if (safePath !== requestedPath && isExcluded(safePath, excludes))
1052
+ return undefined;
948
1053
  return entry.metadata.isDirectory()
949
1054
  ? {
950
1055
  path: safePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.142",
3
+ "version": "0.9.143",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -35,9 +35,9 @@
35
35
  "ws": "^8.21.3",
36
36
  "zod": "4.4.3",
37
37
  "@myagentroam/marctl": "0.9.120",
38
- "@myagentroam/agent": "0.9.142",
39
- "@myagentroam/protocol": "0.9.142",
40
- "@myagentroam/code-navigation": "0.9.129"
38
+ "@myagentroam/code-navigation": "0.9.129",
39
+ "@myagentroam/agent": "0.9.143",
40
+ "@myagentroam/protocol": "0.9.143"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/ws": "^8.18.1"