@myagentroam/node 0.10.66 → 0.10.68

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
@@ -7,6 +7,7 @@ import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
7
7
  import { loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
8
8
  import { NodeMetrics, nodeLog } from './operational.js';
9
9
  import { assertLocalSqlitePath } from './storage.js';
10
+ import { resolveWorkspaceExecutionDirectory } from './workspace.js';
10
11
  import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
11
12
  import { NodeDatabase } from './database.js';
12
13
  import { TerminalManager } from './terminal.js';
@@ -734,7 +735,17 @@ export class NodeConnector {
734
735
  discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
735
736
  workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
736
737
  });
738
+ const resolveExecutionDirectory = async (session) => {
739
+ const workspace = this.database?.getWorkspace(session.workspaceId);
740
+ if (workspace === undefined)
741
+ throw new Error('WORKSPACE_NOT_FOUND');
742
+ const directory = await resolveWorkspaceExecutionDirectory(workspace.path, session.cwd);
743
+ if (this.database?.getWorkspace(workspace.id)?.path !== workspace.path)
744
+ throw new Error('WORKSPACE_NOT_FOUND');
745
+ return directory;
746
+ };
737
747
  this.continuousRunCoordinator = new ContinuousRunCoordinator({
748
+ resolveExecutionDirectory,
738
749
  runtime: this.runtime,
739
750
  steer: (prepared, item, executionId) => {
740
751
  const runner = this.runners.optional(prepared.runner);
@@ -868,6 +879,7 @@ export class NodeConnector {
868
879
  : { runnerTitle: session.runnerTitle.slice(0, 160) })
869
880
  });
870
881
  },
882
+ validateCommands: (previousId, nextId) => this.commandStates.validateSessionIdentityPromotion(previousId, nextId),
871
883
  migrateCommands: (previousId, nextId) => this.commandStates.promoteSessionIdentity(previousId, nextId),
872
884
  migrateGoal: (previousId, nextId) => {
873
885
  this.runAttachmentService.migrateSession(previousId, nextId);
@@ -905,6 +917,7 @@ export class NodeConnector {
905
917
  interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
906
918
  });
907
919
  this.externalSessionResumeService = new ExternalSessionResumeService({
920
+ resolveExecutionDirectory,
908
921
  runtime: this.runtime,
909
922
  runners: this.runners,
910
923
  state: this.sessionState,
@@ -939,6 +952,7 @@ export class NodeConnector {
939
952
  clearGoal: (session) => this.goalCoordinator.clear(session)
940
953
  });
941
954
  this.sessionMessageService = new SessionMessageService({
955
+ resolveExecutionDirectory,
942
956
  runtime: this.runtime,
943
957
  uploads: this.workspaceUploadService,
944
958
  attachments: this.runAttachmentService,
@@ -986,6 +1000,7 @@ export class NodeConnector {
986
1000
  diagnose: (code, sessionId) => nodeLog('node.restart.diagnostic', { code, sessionId })
987
1001
  });
988
1002
  this.sessionCatalogService = new SessionCatalogService({
1003
+ resolveExecutionDirectory,
989
1004
  database: () => {
990
1005
  if (this.database === undefined)
991
1006
  throw new Error('NODE_DATABASE_UNAVAILABLE');
package/dist/main.js CHANGED
@@ -8,6 +8,8 @@ import { browserCachePath, removeServiceDefinition, writeServiceDefinition } fro
8
8
  import { superviseNode } from './supervisor.js';
9
9
  import { configureNodeLog } from './rotating-log.js';
10
10
  import { nodeLog } from './operational.js';
11
+ import { acquireNodeProcessLock, NodeProcessOwnerConflictError, NODE_OWNER_CONFLICT_EXIT_CODE } from './node-process-lock.js';
12
+ import { safeErrorCode } from './util/safe-error.js';
11
13
  const [command = 'status', ...args] = process.argv.slice(2);
12
14
  const config = readOption(args, '--config') ?? nodeConfigPath();
13
15
  const output = readOption(args, '--output') ?? defaultServicePath();
@@ -15,11 +17,22 @@ const browserPath = readOption(args, '--browser-cache');
15
17
  if (browserPath !== undefined)
16
18
  process.env['PLAYWRIGHT_BROWSERS_PATH'] = browserPath;
17
19
  if (command === 'run') {
20
+ const nodeConfig = await loadNodeConfig(config);
21
+ let releaseOwner;
22
+ try {
23
+ releaseOwner = await acquireNodeProcessLock(nodeDataDirectory(nodeConfig, config));
24
+ }
25
+ catch (error) {
26
+ if (!(error instanceof NodeProcessOwnerConflictError))
27
+ throw error;
28
+ console.error(`${error.code}: ${error.message}`);
29
+ process.exit(NODE_OWNER_CONFLICT_EXIT_CODE);
30
+ }
18
31
  const { NodeConnector } = await import('./connector.js');
19
- configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
20
- const connector = new NodeConnector({ configPath: config });
32
+ configureNodeLog(nodeDataDirectory(nodeConfig, config));
33
+ const connector = new NodeConnector({ configPath: config, config: nodeConfig });
21
34
  let shuttingDown = false;
22
- const shutdown = () => {
35
+ const shutdown = (handoff = true, exitCode = 0) => {
23
36
  if (shuttingDown)
24
37
  return;
25
38
  shuttingDown = true;
@@ -27,9 +40,10 @@ if (command === 'run') {
27
40
  nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_TIMEOUT' });
28
41
  process.exit(1);
29
42
  }, 25_000);
30
- void connector.stop({ handoff: true }).then(() => {
43
+ void connector.stop({ handoff }).then(() => {
31
44
  clearTimeout(deadline);
32
- process.exit(0);
45
+ releaseOwner();
46
+ process.exit(exitCode);
33
47
  }, () => {
34
48
  clearTimeout(deadline);
35
49
  nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_FAILED' });
@@ -37,9 +51,15 @@ if (command === 'run') {
37
51
  });
38
52
  };
39
53
  for (const signal of ['SIGINT', 'SIGTERM']) {
40
- process.on(signal, shutdown);
54
+ process.on(signal, () => shutdown());
55
+ }
56
+ try {
57
+ await connector.start();
58
+ }
59
+ catch (error) {
60
+ nodeLog('node.start.failed', { code: safeErrorCode(error, 'NODE_START_FAILED') });
61
+ shutdown(false, 1);
41
62
  }
42
- await connector.start();
43
63
  }
44
64
  else if (command === 'supervise') {
45
65
  if (process.argv[1] === undefined)
@@ -0,0 +1,43 @@
1
+ import { mkdir, realpath } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { assertLocalSqlitePath, isUnsupportedSqlitePath } from './storage.js';
5
+ export const NODE_OWNER_CONFLICT_EXIT_CODE = 76;
6
+ export class NodeProcessOwnerConflictError extends Error {
7
+ code = 'NODE_DATA_DIRECTORY_IN_USE';
8
+ constructor() {
9
+ super('Another Node process is using this data directory. Stop that instance before starting another.');
10
+ }
11
+ }
12
+ /** The independent transaction owns the directory, not the business database. */
13
+ export async function acquireNodeProcessLock(dataDirectory) {
14
+ if (isUnsupportedSqlitePath(dataDirectory))
15
+ throw new Error('SQLITE_NETWORK_FILESYSTEM_UNSUPPORTED');
16
+ await mkdir(dataDirectory, { recursive: true, mode: 0o700 });
17
+ const path = join(await realpath(dataDirectory), 'node-owner.sqlite');
18
+ await assertLocalSqlitePath(path);
19
+ let database;
20
+ try {
21
+ database = new DatabaseSync(path, { timeout: 0 });
22
+ database.exec('PRAGMA journal_mode = DELETE; BEGIN EXCLUSIVE;');
23
+ }
24
+ catch (error) {
25
+ try {
26
+ database?.close();
27
+ }
28
+ catch {
29
+ /* Preserve the acquisition error. */
30
+ }
31
+ if (error instanceof Error && 'errcode' in error && error.errcode === 5)
32
+ throw new NodeProcessOwnerConflictError();
33
+ throw error;
34
+ }
35
+ const owner = database;
36
+ let released = false;
37
+ return () => {
38
+ if (released)
39
+ return;
40
+ owner.close();
41
+ released = true;
42
+ };
43
+ }
@@ -54,14 +54,19 @@ export class CommandStateStore {
54
54
  removeSession(sessionId) {
55
55
  this.states.delete(sessionId);
56
56
  }
57
+ validateSessionIdentityPromotion(sessionId, nativeSessionId) {
58
+ if (sessionId === nativeSessionId)
59
+ return;
60
+ if (this.states.has(sessionId) && this.states.has(nativeSessionId))
61
+ throw new Error('SESSION_ID_CONFLICT');
62
+ }
57
63
  promoteSessionIdentity(sessionId, nativeSessionId) {
64
+ this.validateSessionIdentityPromotion(sessionId, nativeSessionId);
58
65
  if (sessionId === nativeSessionId)
59
66
  return;
60
67
  const values = this.states.get(sessionId);
61
68
  if (values === undefined)
62
69
  return;
63
- if (this.states.has(nativeSessionId))
64
- throw new Error('SESSION_ID_CONFLICT');
65
70
  this.states.delete(sessionId);
66
71
  this.states.set(nativeSessionId, values);
67
72
  }
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { nodeLog } from './operational.js';
3
+ import { safeErrorCode } from './util/safe-error.js';
2
4
  import { permissionStopSchema } from '@myagentroam/protocol';
3
5
  /** User inputs arrive with a durable Session identity, unlike execution-local output IDs. */
4
6
  export function runConversationItemId(runId, itemId, kind) {
@@ -206,22 +208,25 @@ export class NodeRuntimeState {
206
208
  throw new Error('SESSION_EXTERNAL_ID_INVALID');
207
209
  this.promoteSessionIdentity(id, externalSessionId);
208
210
  }
211
+ validateSessionIdentityPromotion(id, nativeSessionId) {
212
+ if (nativeSessionId.length === 0)
213
+ throw new Error('SESSION_EXTERNAL_ID_INVALID');
214
+ const current = this.requireSession(id);
215
+ if (id !== nativeSessionId && this.sessions.has(nativeSessionId))
216
+ throw new Error('SESSION_ID_CONFLICT');
217
+ return current;
218
+ }
209
219
  /**
210
220
  * Atomically replaces the bootstrap Session key with the Runner-native ID.
211
221
  * No caller may migrate the individual runtime maps independently.
212
222
  */
213
- promoteSessionIdentity(id, nativeSessionId) {
214
- if (nativeSessionId.length === 0)
215
- throw new Error('SESSION_EXTERNAL_ID_INVALID');
216
- const current = this.requireSession(id);
223
+ promoteSessionIdentity(id, nativeSessionId, options = {}) {
224
+ const current = this.validateSessionIdentityPromotion(id, nativeSessionId);
217
225
  if (id === nativeSessionId)
218
226
  return this.replaceSession(id, {
219
227
  externalSessionId: nativeSessionId,
220
228
  nativeControl: 'MAR_MANAGED'
221
229
  });
222
- const collision = this.sessions.get(nativeSessionId);
223
- if (collision !== undefined && collision.id !== current.id)
224
- throw new Error('SESSION_ID_CONFLICT');
225
230
  const next = {
226
231
  ...current,
227
232
  id: nativeSessionId,
@@ -286,12 +291,27 @@ export class NodeRuntimeState {
286
291
  ...reference,
287
292
  sessionId: nativeSessionId
288
293
  });
289
- for (const run of runs) {
290
- const promoted = this.requireRun(run.id);
291
- this.emitRunChanged(promoted, next);
292
- }
294
+ if (options.deferRunChanges !== true)
295
+ this.publishSessionIdentity(nativeSessionId);
293
296
  return next;
294
297
  }
298
+ /** Identity observers run only after the owning identity commit is complete. */
299
+ publishSessionIdentity(sessionId) {
300
+ const session = this.requireSession(sessionId);
301
+ for (const run of this.listRunsForSession(sessionId)) {
302
+ try {
303
+ this.emitRunChanged(run, session);
304
+ }
305
+ catch (error) {
306
+ nodeLog('session.identity.publish.failed', {
307
+ sessionId,
308
+ runId: run.id,
309
+ phase: 'run.observer',
310
+ errorCode: safeErrorCode(error, 'SESSION_IDENTITY_PUBLICATION_FAILED')
311
+ });
312
+ }
313
+ }
314
+ }
295
315
  setChannelToken(id, channelToken) {
296
316
  if (channelToken.length < 16)
297
317
  throw new Error('SESSION_CHANNEL_TOKEN_INVALID');
@@ -528,6 +548,7 @@ export class NodeRuntimeState {
528
548
  }
529
549
  startRunFromQueueItem(itemId, content) {
530
550
  const item = this.requireQueueItem(itemId);
551
+ this.assertSessionHistoryMutable(item.sessionId);
531
552
  if (this.activeSessionRun(item.sessionId) !== undefined)
532
553
  throw new Error('RUN_ALREADY_ACTIVE');
533
554
  const queue = this.queuedItems.get(item.sessionId) ?? [];
@@ -1081,12 +1102,12 @@ export class NodeRuntimeState {
1081
1102
  if (runIds.has(approval.runId) || executionIds.has(approval.runId))
1082
1103
  this.approvals.delete(approvalId);
1083
1104
  }
1084
- beginSessionHistoryMutation(sessionId) {
1105
+ beginSessionHistoryMutation(sessionId, options) {
1085
1106
  if (this.sessionHistoryMutations.has(sessionId))
1086
1107
  throw new Error('SESSION_ACTIVE');
1087
1108
  if (this.activeSessionRun(sessionId) !== undefined)
1088
1109
  throw new Error('SESSION_ACTIVE');
1089
- if (this.sessionQueue(sessionId).itemIds.length > 0)
1110
+ if (options?.allowQueued !== true && this.sessionQueue(sessionId).itemIds.length > 0)
1090
1111
  throw new Error('SESSION_QUEUE_NOT_EMPTY');
1091
1112
  this.sessionHistoryMutations.add(sessionId);
1092
1113
  let released = false;
@@ -110,6 +110,25 @@ export class ContinuousRunCoordinator {
110
110
  this.options.emitRun(run.id, 'run.rejected', { code }, 'FAILED');
111
111
  }
112
112
  settle(input) {
113
+ const pending = this.preparedInputs.get(input.submissionId);
114
+ if (this.options.resolveExecutionDirectory &&
115
+ pending &&
116
+ this.options.runtime.getQueueItem(input.submissionId)) {
117
+ const version = this.options.runtime.sessionQueue(pending.sessionId).version;
118
+ return this.directoryForInput(input.submissionId, pending).then((cwd) => {
119
+ if (cwd === undefined)
120
+ return 'QUEUED';
121
+ if (!this.inputCurrent(input.submissionId, pending) ||
122
+ this.options.runtime.sessionQueue(pending.sessionId).version !== version) {
123
+ this.schedule(pending.sessionId);
124
+ return 'QUEUED';
125
+ }
126
+ return this.settleReady(input);
127
+ });
128
+ }
129
+ return this.settleReady(input);
130
+ }
131
+ settleReady(input) {
113
132
  const item = this.options.runtime.getQueueItem(input.submissionId);
114
133
  const active = item && this.options.runtime.activeSessionRun(item.sessionId);
115
134
  const turn = active && this.options.runtime.conversationTurnForRun(active.id);
@@ -192,8 +211,7 @@ export class ContinuousRunCoordinator {
192
211
  if (active === undefined && item !== undefined) {
193
212
  if (input.queueBefore.paused && intent === 'SEND')
194
213
  this.options.runtime.prioritizeQueueItem(item.id);
195
- this.startIdleQueue(sessionId);
196
- return 'STARTED';
214
+ return this.startIdleQueue(sessionId).then((started) => started === item.id ? 'STARTED' : 'QUEUED');
197
215
  }
198
216
  if (executionEnded && item !== undefined) {
199
217
  if (input.queueBefore.paused && intent === 'SEND') {
@@ -208,6 +226,20 @@ export class ContinuousRunCoordinator {
208
226
  return 'QUEUED';
209
227
  }
210
228
  executeQueueItem(itemId) {
229
+ const pending = this.preparedInputs.get(itemId);
230
+ if (this.options.resolveExecutionDirectory && pending) {
231
+ const version = this.options.runtime.sessionQueue(pending.sessionId).version;
232
+ return this.directoryForInput(itemId, pending, true).then((cwd) => {
233
+ if (cwd === undefined || !this.inputCurrent(itemId, pending))
234
+ throw new Error('QUEUE_ITEM_INVALID');
235
+ if (this.options.runtime.sessionQueue(pending.sessionId).version !== version)
236
+ throw new Error('QUEUE_VERSION_CONFLICT');
237
+ return this.executeQueueItemReady(itemId);
238
+ });
239
+ }
240
+ return this.executeQueueItemReady(itemId);
241
+ }
242
+ executeQueueItemReady(itemId) {
211
243
  const item = this.requirePreparedItem(itemId);
212
244
  const active = this.options.runtime.activeSessionRun(item.sessionId);
213
245
  if (active) {
@@ -301,7 +333,7 @@ export class ContinuousRunCoordinator {
301
333
  this.steerInFlight.set(itemId, { sessionId: item.sessionId, task });
302
334
  return task;
303
335
  }
304
- executeQueueItemFallback(itemId) {
336
+ async executeQueueItemFallback(itemId) {
305
337
  const item = this.requirePreparedItem(itemId);
306
338
  const active = this.options.runtime.activeSessionRun(item.sessionId);
307
339
  if (active !== undefined && this.canSteer(active))
@@ -319,7 +351,7 @@ export class ContinuousRunCoordinator {
319
351
  }
320
352
  return { run: active, delivery: 'ACCEPTED' };
321
353
  }
322
- this.startIdleQueue(item.sessionId);
354
+ await this.startIdleQueue(item.sessionId, true);
323
355
  const started = this.options.runtime.activeSessionRun(item.sessionId);
324
356
  if (started === undefined)
325
357
  throw new Error('RUN_NOT_FOUND');
@@ -408,9 +440,29 @@ export class ContinuousRunCoordinator {
408
440
  this.options.markInterrupted(executionId);
409
441
  this.options.control('run.interrupt', { runId: executionId });
410
442
  }
411
- resume(sessionId) {
443
+ async resume(sessionId) {
444
+ const session = this.options.runtime.getAgentSession(sessionId);
445
+ if (!session)
446
+ throw new Error('SESSION_NOT_FOUND');
447
+ const version = this.options.runtime.sessionQueue(sessionId).version;
448
+ if (this.options.resolveExecutionDirectory) {
449
+ try {
450
+ await this.options.resolveExecutionDirectory(session);
451
+ }
452
+ catch (error) {
453
+ if (this.options.runtime.getAgentSession(sessionId)?.id === session.id &&
454
+ this.options.runtime.sessionQueue(sessionId).version === version)
455
+ this.pauseForDirectory(sessionId, error);
456
+ throw error;
457
+ }
458
+ if (this.options.runtime.getAgentSession(sessionId)?.id !== session.id ||
459
+ this.options.runtime.sessionQueue(sessionId).version !== version)
460
+ return;
461
+ }
412
462
  this.options.runtime.resumeSessionQueue(sessionId);
413
- this.startIdleQueue(sessionId);
463
+ if (this.suspended || this.options.stopped())
464
+ return;
465
+ await this.startIdleQueue(sessionId, true);
414
466
  }
415
467
  schedule(sessionId, delayMs = 0) {
416
468
  if (this.suspended)
@@ -420,7 +472,10 @@ export class ContinuousRunCoordinator {
420
472
  const timer = setTimeout(() => {
421
473
  this.scheduledSessions.delete(sessionId);
422
474
  if (!this.options.stopped())
423
- void this.dispatch(sessionId);
475
+ void this.dispatch(sessionId).catch((error) => nodeLog('queue.dispatch.failed', {
476
+ sessionId,
477
+ code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED')
478
+ }));
424
479
  }, delayMs);
425
480
  this.scheduledSessions.set(sessionId, timer);
426
481
  timer.unref();
@@ -438,14 +493,26 @@ export class ContinuousRunCoordinator {
438
493
  if ([...this.steerInFlight.values()].some((entry) => entry.sessionId === sessionId))
439
494
  return;
440
495
  const active = this.options.runtime.activeSessionRun(sessionId);
441
- if (active === undefined)
496
+ if (active === undefined) {
497
+ await this.startIdleQueue(sessionId);
442
498
  return;
499
+ }
443
500
  let executionId = this.options.runtime.currentExecutionId(active.id);
444
501
  if (executionId !== undefined && this.dispatchedExecutions.has(executionId))
445
502
  return;
446
503
  let pending = executionId === undefined
447
504
  ? this.preparedInputs.get(active.id)
448
505
  : this.preparedInputs.get(executionId);
506
+ let executionCwd = pending?.cwd;
507
+ if (pending && this.options.resolveExecutionDirectory) {
508
+ const key = executionId ?? active.id;
509
+ executionCwd = await this.directoryForInput(key, pending);
510
+ if (executionCwd === undefined ||
511
+ !this.inputCurrent(key, pending) ||
512
+ this.options.runtime.activeSessionRun(sessionId)?.id !== active.id ||
513
+ this.options.runtime.currentExecutionId(active.id) !== executionId)
514
+ return;
515
+ }
449
516
  if (executionId === undefined && pending === undefined) {
450
517
  const itemId = this.nextQueueItem(sessionId);
451
518
  if (itemId === undefined)
@@ -456,6 +523,20 @@ export class ContinuousRunCoordinator {
456
523
  const item = this.options.runtime.getQueueItem(itemId);
457
524
  if (item === undefined)
458
525
  return;
526
+ if (this.options.resolveExecutionDirectory) {
527
+ const version = this.options.runtime.sessionQueue(sessionId).version;
528
+ executionCwd = await this.directoryForInput(itemId, pending);
529
+ if (executionCwd === undefined)
530
+ return;
531
+ if (!this.inputCurrent(itemId, pending) ||
532
+ this.options.runtime.activeSessionRun(sessionId)?.id !== active.id ||
533
+ this.options.runtime.currentExecutionId(active.id) !== undefined ||
534
+ this.options.runtime.sessionQueue(sessionId).version !== version ||
535
+ this.nextQueueItem(sessionId) !== itemId) {
536
+ this.schedule(sessionId);
537
+ return;
538
+ }
539
+ }
459
540
  if (!this.goalCurrent(pending)) {
460
541
  this.deleteQueueItem(item.id);
461
542
  this.schedule(sessionId);
@@ -501,7 +582,11 @@ export class ContinuousRunCoordinator {
501
582
  let dispatched = false;
502
583
  try {
503
584
  // Capabilities belong to this Execution, never the Run or a claimed QueueItem.
504
- const executionInput = { ...pending, runId: executionId };
585
+ const executionInput = {
586
+ ...pending,
587
+ cwd: executionCwd ?? pending.cwd,
588
+ runId: executionId
589
+ };
505
590
  const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
506
591
  const scheduleMcp = await this.options.scheduleMcp?.(executionInput);
507
592
  const notificationMcp = await this.options.notificationMcp?.(executionInput);
@@ -524,7 +609,7 @@ export class ContinuousRunCoordinator {
524
609
  ? {}
525
610
  : { clientMessageId: pending.clientMessageId }),
526
611
  ...(pending.attachments.length === 0 ? {} : { attachments: pending.attachments }),
527
- cwd: pending.cwd,
612
+ cwd: executionCwd ?? pending.cwd,
528
613
  ...(pending.externalSessionId === null
529
614
  ? {}
530
615
  : { externalSessionId: pending.externalSessionId }),
@@ -638,7 +723,7 @@ export class ContinuousRunCoordinator {
638
723
  clearTimeout(timer);
639
724
  this.scheduledSessions.clear();
640
725
  }
641
- startIdleQueue(sessionId) {
726
+ async startIdleQueue(sessionId, throwOnError = false) {
642
727
  if (this.options.runtime.activeSessionRun(sessionId) !== undefined) {
643
728
  this.schedule(sessionId);
644
729
  return;
@@ -654,8 +739,20 @@ export class ContinuousRunCoordinator {
654
739
  throw new Error('QUEUE_ITEM_INVALID');
655
740
  if (!this.goalCurrent(pending)) {
656
741
  this.deleteQueueItem(item.id);
657
- this.startIdleQueue(sessionId);
658
- return;
742
+ return this.startIdleQueue(sessionId, throwOnError);
743
+ }
744
+ if (this.options.resolveExecutionDirectory) {
745
+ const version = this.options.runtime.sessionQueue(sessionId).version;
746
+ const cwd = await this.directoryForInput(itemId, pending, throwOnError);
747
+ if (cwd === undefined)
748
+ return;
749
+ if (!this.inputCurrent(itemId, pending) ||
750
+ this.options.runtime.activeSessionRun(sessionId) !== undefined ||
751
+ this.options.runtime.sessionQueue(sessionId).version !== version ||
752
+ this.nextQueueItem(sessionId) !== itemId) {
753
+ this.schedule(sessionId);
754
+ return;
755
+ }
659
756
  }
660
757
  this.prioritizeNextQueueItem(sessionId, itemId);
661
758
  const { run, turn } = this.options.runtime.startRunFromQueueItem(itemId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths, pending.attachmentFiles));
@@ -665,6 +762,61 @@ export class ContinuousRunCoordinator {
665
762
  this.options.emitQueue(run.workspaceId, sessionId);
666
763
  this.options.emitConversation(turn);
667
764
  this.schedule(sessionId);
765
+ return itemId;
766
+ }
767
+ inputCurrent(id, pending) {
768
+ const session = this.options.runtime.getAgentSession(pending.sessionId);
769
+ return (!this.suspended &&
770
+ !this.options.stopped() &&
771
+ this.preparedInputs.get(id) === pending &&
772
+ session?.id === pending.sessionId &&
773
+ session.workspaceId === pending.workspaceId);
774
+ }
775
+ async directoryForInput(id, pending, throwOnError = false) {
776
+ const session = this.options.runtime.getAgentSession(pending.sessionId);
777
+ if (!session || !this.inputCurrent(id, pending))
778
+ return undefined;
779
+ const version = this.options.runtime.sessionQueue(session.id).version;
780
+ try {
781
+ return await this.options.resolveExecutionDirectory(session);
782
+ }
783
+ catch (error) {
784
+ if (!this.inputCurrent(id, pending)) {
785
+ if (this.options.runtime.getAgentSession(session.id) !== undefined &&
786
+ this.preparedInputs.get(id) !== pending)
787
+ this.schedule(session.id);
788
+ return undefined;
789
+ }
790
+ if (this.options.runtime.getQueueItem(id)) {
791
+ if (this.options.runtime.sessionQueue(session.id).version === version)
792
+ this.pauseForDirectory(session.id, error);
793
+ else
794
+ this.schedule(session.id);
795
+ }
796
+ else {
797
+ this.options.emitRun(pending.runId, 'run.rejected', { code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED') }, 'FAILED');
798
+ }
799
+ if (throwOnError)
800
+ throw error;
801
+ return undefined;
802
+ }
803
+ }
804
+ pauseForDirectory(sessionId, error) {
805
+ if (!this.options.runtime.sessionQueue(sessionId).paused)
806
+ this.options.runtime.pauseSessionQueue(sessionId);
807
+ nodeLog('queue.execution_directory.invalid', {
808
+ sessionId,
809
+ code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED')
810
+ });
811
+ const active = this.options.runtime.activeSessionRun(sessionId);
812
+ if (active && this.options.runtime.currentExecutionId(active.id) === undefined)
813
+ this.finishTerminalCommandRun(active);
814
+ try {
815
+ this.publish(sessionId);
816
+ }
817
+ catch {
818
+ nodeLog('queue.execution_directory.publish_failed', { sessionId });
819
+ }
668
820
  }
669
821
  nextQueueItem(sessionId) {
670
822
  const queue = this.options.runtime.sessionQueue(sessionId);
@@ -35,10 +35,16 @@ export class ExternalSessionResumeService {
35
35
  this.setFailure(external.id, 'RUNNER_UNAVAILABLE');
36
36
  return undefined;
37
37
  }
38
+ const releaseMutation = this.options.runtime.beginSessionHistoryMutation(external.id, {
39
+ allowQueued: true
40
+ });
38
41
  this.transitions.add(external.id);
39
42
  let result;
40
43
  try {
41
- result = await runner.resumeExternal(external, {
44
+ const cwd = this.options.resolveExecutionDirectory
45
+ ? await this.options.resolveExecutionDirectory(external)
46
+ : external.cwd;
47
+ result = await runner.resumeExternal({ ...external, cwd }, {
42
48
  available: this.options.available(external.runner),
43
49
  projectionFor: this.options.projectionFor,
44
50
  managedAliasFor: this.options.managedAliasFor,
@@ -62,7 +68,7 @@ export class ExternalSessionResumeService {
62
68
  ? {}
63
69
  : { externalSessionId: session.externalSessionId }),
64
70
  nativeControl: 'MAR_MANAGED',
65
- cwd: session.cwd,
71
+ cwd,
66
72
  createdAt: session.createdAt,
67
73
  lastActivityAt: session.lastActivityAt,
68
74
  model: configuration.model,
@@ -80,6 +86,7 @@ export class ExternalSessionResumeService {
80
86
  }
81
87
  finally {
82
88
  this.transitions.delete(external.id);
89
+ releaseMutation();
83
90
  }
84
91
  if (result.activity !== undefined)
85
92
  this.options.state.externalActivity.set(external.id, result.activity);
@@ -131,7 +131,7 @@ ${RESTART_NOTICE}`,
131
131
  }));
132
132
  }
133
133
  if (!entry.paused)
134
- coordinator.resume(session.id);
134
+ await coordinator.resume(session.id);
135
135
  this.options.publishQueue(session);
136
136
  }
137
137
  catch (error) {
@@ -240,11 +240,14 @@ export class SessionCatalogService {
240
240
  .listAgentSessions()
241
241
  .flatMap((candidate) => [candidate.customTitle ?? candidate.runnerTitle].filter(isString)));
242
242
  const runner = this.options.runners.require(source.runner);
243
- const nativeId = await runner.forkNative(source, input.boundary, title);
243
+ const cwd = this.options.resolveExecutionDirectory
244
+ ? await this.options.resolveExecutionDirectory(source)
245
+ : source.cwd;
246
+ const nativeId = await runner.forkNative({ ...source, cwd }, input.boundary, title);
244
247
  const session = this.options.runtime.createAgentSession({
245
248
  workspaceId: source.workspaceId,
246
249
  runner: source.runner,
247
- cwd: source.cwd,
250
+ cwd,
248
251
  nativeControl: 'MAR_MANAGED',
249
252
  ...(nativeId === null ? {} : { externalSessionId: nativeId }),
250
253
  ...(source.model === null ? {} : { model: source.model }),
@@ -375,9 +378,7 @@ export class SessionCatalogService {
375
378
  async removeResolved(session, requireCleanupSafety = false) {
376
379
  if (session.externalSessionId === null)
377
380
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
378
- const releaseMutation = requireCleanupSafety
379
- ? this.options.runtime.beginSessionHistoryMutation(session.id)
380
- : undefined;
381
+ const releaseMutation = this.options.runtime.beginSessionHistoryMutation(session.id, requireCleanupSafety ? undefined : { allowQueued: true });
381
382
  try {
382
383
  if (this.options.isRecovering?.(session.id))
383
384
  throw new Error('SESSION_RESTART_PENDING');
@@ -431,7 +432,7 @@ export class SessionCatalogService {
431
432
  return deleted ? 'deleted' : 'skipped';
432
433
  }
433
434
  finally {
434
- releaseMutation?.();
435
+ releaseMutation();
435
436
  }
436
437
  }
437
438
  }
@@ -1,5 +1,6 @@
1
1
  import { nodeLog } from '../operational.js';
2
2
  import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
3
+ import { safeErrorCode } from '../util/safe-error.js';
3
4
  export class SessionIdentityService {
4
5
  options;
5
6
  bootstrapIds = new Set();
@@ -12,12 +13,21 @@ export class SessionIdentityService {
12
13
  this.bootstrapIds.add(sessionId);
13
14
  }
14
15
  promote(sessionId, nativeSessionId) {
15
- if (this.failedBootstrapIds.has(sessionId))
16
- throw new Error('SESSION_START_FAILED');
17
- const current = this.options.runtime.getAgentSession(sessionId);
18
- this.options.runtime.promoteSessionIdentity(sessionId, nativeSessionId);
19
- if (current !== undefined)
16
+ try {
17
+ if (this.failedBootstrapIds.has(sessionId))
18
+ throw new Error('SESSION_START_FAILED');
19
+ const current = this.options.runtime.validateSessionIdentityPromotion(sessionId, nativeSessionId);
20
+ this.options.validateCommands(sessionId, nativeSessionId);
20
21
  this.options.persist(current, nativeSessionId);
22
+ }
23
+ catch (error) {
24
+ this.reject(sessionId, error instanceof Error ? error : new Error('SESSION_START_FAILED'));
25
+ throw error;
26
+ }
27
+ // No I/O or publication may interrupt the synchronous ownership migration.
28
+ const session = this.options.runtime.promoteSessionIdentity(sessionId, nativeSessionId, {
29
+ deferRunChanges: true
30
+ });
21
31
  this.options.migrateGoal(sessionId, nativeSessionId);
22
32
  this.options.migrateCommands(sessionId, nativeSessionId);
23
33
  this.options.migrateQueue(sessionId, nativeSessionId);
@@ -29,16 +39,27 @@ export class SessionIdentityService {
29
39
  this.waiters.delete(sessionId);
30
40
  waiter.resolve(nativeSessionId);
31
41
  }
32
- const session = this.options.runtime.getAgentSession(nativeSessionId);
33
- if (session === undefined)
34
- return;
35
- this.options.emitSession(session);
42
+ if (sessionId !== nativeSessionId)
43
+ this.options.runtime.publishSessionIdentity(nativeSessionId);
44
+ this.publish(nativeSessionId, 'session', () => this.options.emitSession(session));
36
45
  for (const run of this.options.runtime.listRunsForSession(nativeSessionId))
37
- this.options.emitRun(run);
38
- this.options.emitQueue(nativeSessionId);
46
+ this.publish(nativeSessionId, 'run', () => this.options.emitRun(run));
47
+ this.publish(nativeSessionId, 'queue', () => this.options.emitQueue(nativeSessionId));
39
48
  for (const turn of this.options.runtime.listConversationTurns({ sessionId: nativeSessionId })
40
49
  .turns)
41
- this.options.emitTurn(turn);
50
+ this.publish(nativeSessionId, 'turn', () => this.options.emitTurn(turn));
51
+ }
52
+ publish(sessionId, phase, emit) {
53
+ try {
54
+ emit();
55
+ }
56
+ catch (error) {
57
+ nodeLog('session.identity.publish.failed', {
58
+ sessionId,
59
+ phase,
60
+ errorCode: safeErrorCode(error, 'SESSION_IDENTITY_PUBLICATION_FAILED')
61
+ });
62
+ }
42
63
  }
43
64
  wait(bootstrapSessionId) {
44
65
  const existing = this.waiters.get(bootstrapSessionId);
@@ -64,7 +85,7 @@ export class SessionIdentityService {
64
85
  });
65
86
  return promise;
66
87
  }
67
- reject(sessionId, code) {
88
+ reject(sessionId, reason) {
68
89
  const bootstrap = this.bootstrapIds.has(sessionId);
69
90
  if (bootstrap) {
70
91
  this.failedBootstrapIds.add(sessionId);
@@ -74,7 +95,7 @@ export class SessionIdentityService {
74
95
  if (waiter !== undefined) {
75
96
  clearTimeout(waiter.timer);
76
97
  this.waiters.delete(sessionId);
77
- waiter.reject(new Error(code));
98
+ waiter.reject(typeof reason === 'string' ? new Error(reason) : reason);
78
99
  }
79
100
  if (!bootstrap)
80
101
  return;
@@ -3,6 +3,7 @@ import { isPlainRecord } from '../util/node-operation-parsers.js';
3
3
  import { parseSecretEnvironment } from '../util/secret-environment.js';
4
4
  import { ScheduledSessionStore } from './scheduled-session-store.js';
5
5
  import { normalizeSessionPrompt } from '../util/personal-instructions.js';
6
+ import { resolveWorkspaceExecutionDirectory } from '../workspace.js';
6
7
  export class SessionLifecycleService {
7
8
  options;
8
9
  starts = new Map();
@@ -83,7 +84,7 @@ export class SessionLifecycleService {
83
84
  this.options.invalidateWorkspaceSessions(workspaceId);
84
85
  return { moved: true };
85
86
  }
86
- create(input) {
87
+ async create(input) {
87
88
  const runner = this.options.runners.product(input.runner);
88
89
  if (typeof input.workspaceId !== 'string' ||
89
90
  runner === undefined ||
@@ -100,6 +101,9 @@ export class SessionLifecycleService {
100
101
  const workspace = database.getWorkspace(input.workspaceId);
101
102
  if (workspace === undefined)
102
103
  throw new Error('WORKSPACE_NOT_FOUND');
104
+ const cwd = await resolveWorkspaceExecutionDirectory(workspace.path, input.cwd);
105
+ if (database.getWorkspace(workspace.id)?.path !== workspace.path)
106
+ throw new Error('WORKSPACE_NOT_FOUND');
103
107
  let parentSessionId;
104
108
  if (input.parentSessionId !== undefined) {
105
109
  if (typeof input.parentSessionId !== 'string' ||
@@ -133,7 +137,7 @@ export class SessionLifecycleService {
133
137
  ...(parentSessionId === undefined ? {} : { parentSessionId }),
134
138
  workspaceId: input.workspaceId,
135
139
  runner: runner.name,
136
- cwd: input.cwd,
140
+ cwd,
137
141
  ...(typeof input.externalSessionId === 'string'
138
142
  ? { externalSessionId: input.externalSessionId }
139
143
  : {}),
@@ -208,7 +208,18 @@ export class SessionMessageService {
208
208
  let reservation;
209
209
  let nativeAccepted = false;
210
210
  try {
211
- const currentSession = this.options.runtime.getAgentSession(session.id) ?? session;
211
+ let currentSession = this.options.runtime.getAgentSession(session.id) ?? session;
212
+ const cwd = this.options.resolveExecutionDirectory
213
+ ? await this.options.resolveExecutionDirectory(currentSession)
214
+ : currentSession.cwd;
215
+ if (this.options.resolveExecutionDirectory) {
216
+ const latest = this.options.runtime.getAgentSession(currentSession.id);
217
+ if (!latest ||
218
+ latest.workspaceId !== currentSession.workspaceId ||
219
+ latest.cwd !== currentSession.cwd)
220
+ throw new Error('SESSION_NOT_FOUND');
221
+ currentSession = latest;
222
+ }
212
223
  if (attachmentIds.length > 0)
213
224
  reservation = await this.options.uploads.reserve(session.workspaceId, attachmentIds);
214
225
  const attachments = reservation === undefined
@@ -228,11 +239,11 @@ export class SessionMessageService {
228
239
  this.options.attachments.resourcePath(attachmentPaths[index])
229
240
  }));
230
241
  const runnerFiles = attachmentFiles.map((file, index) => {
231
- const path = relative(currentSession.cwd, attachmentPaths[index]).replaceAll('\\', '/');
242
+ const path = relative(cwd, attachmentPaths[index]).replaceAll('\\', '/');
232
243
  return path === file.path ? file : { ...file, path, workspacePath: file.path };
233
244
  });
234
245
  const displayInput = withNonImageAttachmentPrompt(publicContent, attachmentPaths, attachmentFiles);
235
- const resourceInput = prepareComposerResourceInput(runnerContent, currentSession.cwd, workspacePath);
246
+ const resourceInput = prepareComposerResourceInput(runnerContent, cwd, workspacePath);
236
247
  const runnerInput = withNonImageAttachmentPrompt(resourceInput, attachmentPaths, runnerFiles);
237
248
  const planActive = this.options.commands
238
249
  .list(session.id)
@@ -270,7 +281,7 @@ export class SessionMessageService {
270
281
  attachments: runnerImageAttachments,
271
282
  attachmentPaths,
272
283
  attachmentFiles: runnerFiles,
273
- cwd: currentSession.cwd,
284
+ cwd,
274
285
  workspacePath,
275
286
  externalSessionId: currentSession.externalSessionId,
276
287
  model: currentSession.model,
@@ -1,6 +1,8 @@
1
1
  import { steerComposerMessage } from '@myagentroam/protocol';
2
2
  import { isPlainRecord, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
3
3
  import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
4
+ import { nodeLog } from '../operational.js';
5
+ import { safeErrorCode } from '../util/safe-error.js';
4
6
  export class WorkspaceQueueWorkbenchService {
5
7
  options;
6
8
  constructor(options) {
@@ -30,8 +32,13 @@ export class WorkspaceQueueWorkbenchService {
30
32
  throw new Error('SESSION_NOT_FOUND');
31
33
  if (!this.options.runtime.sessionQueue(session.id).paused)
32
34
  return;
33
- this.options.coordinator.resume(session.id);
34
- this.options.emit(session.workspaceId, session.id);
35
+ void this.options.coordinator
36
+ .resume(session.id)
37
+ .then(() => this.options.emit(session.workspaceId, session.id))
38
+ .catch((error) => nodeLog('queue.resume.failed', {
39
+ sessionId,
40
+ code: safeErrorCode(error, 'QUEUE_RESUME_FAILED')
41
+ }));
35
42
  }
36
43
  present(workspaceId, sessionId) {
37
44
  const session = this.options.runtime.getAgentSession(sessionId);
@@ -75,11 +82,11 @@ export class WorkspaceQueueWorkbenchService {
75
82
  queue: this.snapshot(session.id)
76
83
  };
77
84
  }
78
- pauseOrResume(mode, input) {
85
+ async pauseOrResume(mode, input) {
79
86
  const session = this.requireSession(input);
80
87
  assertSessionControlOwner(this.options.runtime, session.id, input.__requestUserId);
81
88
  if (mode === 'resume')
82
- this.options.coordinator.resume(session.id);
89
+ await this.options.coordinator.resume(session.id);
83
90
  else
84
91
  this.options.runtime.pauseSessionQueue(session.id);
85
92
  this.options.emit(session.workspaceId, session.id);
@@ -5,6 +5,7 @@ import { loadNodeConfig, nodeDatabasePath } from './config.js';
5
5
  import { nodeLog, platformCliInvocation } from './operational.js';
6
6
  import { nodeUpgradeRequestPath } from './service/node-upgrade-coordinator.js';
7
7
  import { safeErrorCode } from './util/safe-error.js';
8
+ import { NODE_OWNER_CONFLICT_EXIT_CODE } from './node-process-lock.js';
8
9
  const UPGRADE_EXIT_CODE = 75;
9
10
  const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
10
11
  export async function superviseNode(configPath, bootstrapEntrypoint) {
@@ -19,6 +20,10 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
19
20
  return;
20
21
  }
21
22
  const code = childResult.code;
23
+ if (code === NODE_OWNER_CONFLICT_EXIT_CODE) {
24
+ process.exitCode = code;
25
+ return;
26
+ }
22
27
  if (code !== UPGRADE_EXIT_CODE) {
23
28
  if (code === 0)
24
29
  return;
package/dist/workspace.js CHANGED
@@ -999,9 +999,52 @@ export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit =
999
999
  async function resolveRegisteredWorkspaceRoot(workspaceRoot) {
1000
1000
  const root = await realpath(workspaceRoot);
1001
1001
  if (normalizeWorkspacePath(root) !== normalizeWorkspacePath(workspaceRoot))
1002
- throw new Error('WORKSPACE_NOT_ALLOWED');
1002
+ throw Object.assign(new Error('WORKSPACE_NOT_ALLOWED'), { code: 'WORKSPACE_NOT_ALLOWED' });
1003
1003
  return root;
1004
1004
  }
1005
+ export class SessionExecutionDirectoryError extends Error {
1006
+ publicMessage;
1007
+ code = 'SESSION_EXECUTION_DIRECTORY_INVALID';
1008
+ publicParameters;
1009
+ constructor(reason, publicMessage, options) {
1010
+ super(publicMessage, options);
1011
+ this.publicMessage = publicMessage;
1012
+ this.publicParameters = { reason };
1013
+ }
1014
+ }
1015
+ /** Bind ordinary Session execution to one registered Workspace, not all allowed roots. */
1016
+ export async function resolveWorkspaceExecutionDirectory(workspaceRoot, cwd) {
1017
+ if (!isAbsolute(cwd) || (process.platform === 'win32' && win32.parse(cwd).root.length <= 1))
1018
+ throw new SessionExecutionDirectoryError('ABSOLUTE_PATH_REQUIRED', 'The execution directory must be an absolute path on this Node.');
1019
+ try {
1020
+ const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
1021
+ const directory = await realpath(cwd);
1022
+ if (!isWithinAllowedRoot(directory, [root]))
1023
+ throw new SessionExecutionDirectoryError('OUTSIDE_WORKSPACE', 'The execution directory is outside the authorized Workspace.');
1024
+ if (!(await stat(directory)).isDirectory())
1025
+ throw new SessionExecutionDirectoryError('NOT_DIRECTORY', 'The execution directory is not a directory.');
1026
+ return directory;
1027
+ }
1028
+ catch (error) {
1029
+ if (error instanceof SessionExecutionDirectoryError)
1030
+ throw error;
1031
+ const code = error !== null && typeof error === 'object' && 'code' in error ? error.code : undefined;
1032
+ const [reason, message] = code === 'WORKSPACE_NOT_ALLOWED'
1033
+ ? [
1034
+ 'WORKSPACE_ROOT_CHANGED',
1035
+ 'The registered Workspace root no longer identifies the same directory.'
1036
+ ]
1037
+ : code === 'ENOENT' || code === 'ENOTDIR'
1038
+ ? ['NOT_FOUND', 'The execution directory or Workspace root is unavailable.']
1039
+ : code === 'EACCES' || code === 'EPERM'
1040
+ ? [
1041
+ 'ACCESS_DENIED',
1042
+ 'The Node account cannot access the execution directory or Workspace root.'
1043
+ ]
1044
+ : ['IO_ERROR', 'The execution directory could not be verified.'];
1045
+ throw new SessionExecutionDirectoryError(reason, message, { cause: error });
1046
+ }
1047
+ }
1005
1048
  async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
1006
1049
  const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
1007
1050
  return resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.10.66",
3
+ "version": "0.10.68",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.10.66",
40
+ "@myagentroam/agent": "0.10.68",
41
41
  "@myagentroam/code-navigation": "0.10.1",
42
42
  "@myagentroam/marctl": "0.10.12",
43
- "@myagentroam/protocol": "0.10.66"
43
+ "@myagentroam/protocol": "0.10.68"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"