@myagentroam/node 0.9.68 → 0.9.69

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
@@ -1,5 +1,5 @@
1
1
  import { dirname, resolve } from 'node:path';
2
- import { createEnvelope } from '@myagentroam/protocol';
2
+ import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
3
3
  import { hostedConversationStorePath, loadNodeConfig, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
4
4
  import { NodeMetrics, nodeLog } from './operational.js';
5
5
  import { assertLocalSqlitePath } from './storage.js';
@@ -1075,36 +1075,6 @@ export class NodeConnector {
1075
1075
  void this.terminalManager.shutdown();
1076
1076
  }
1077
1077
  });
1078
- this.directTransferService = new DirectTransferService({
1079
- nodeGeneration: () => this.nodeGeneration,
1080
- send: (type, payload) => this.send(type, payload),
1081
- udpPortRange: () => directTransferUdpPortRange(this.config),
1082
- openUpload: async (descriptor) => {
1083
- if (descriptor.resource.kind !== 'WORKSPACE_FILE')
1084
- throw new Error('DIRECT_RESOURCE_UNSUPPORTED');
1085
- const workspace = this.workspaceCoordinator.require(descriptor.resource.workspaceId);
1086
- const upload = await this.workspaceUploadService.createDirect(workspace, {
1087
- path: descriptor.resource.path,
1088
- size: descriptor.size,
1089
- sha256: descriptor.sha256,
1090
- overwrite: descriptor.overwrite === true,
1091
- ...(descriptor.composerAttachment === undefined
1092
- ? {}
1093
- : { composerAttachment: descriptor.composerAttachment })
1094
- });
1095
- return {
1096
- offset: upload.receivedBytes,
1097
- write: (bytes) => this.workspaceUploadService.writeDirect(upload.uploadId, bytes),
1098
- complete: (size, sha256) => this.workspaceUploadService.completeDirect(upload.uploadId, size, sha256),
1099
- cancel: () => this.workspaceUploadService.cancel(upload.uploadId)
1100
- };
1101
- },
1102
- openDownload: async (descriptor) => {
1103
- if (this.workspaceWorkbenchService === undefined)
1104
- throw new Error('DIRECT_RESOURCE_UNAVAILABLE');
1105
- return this.workspaceWorkbenchService.directDownload(descriptor);
1106
- }
1107
- });
1108
1078
  this.controlMessages = new NodeControlMessageService({
1109
1079
  config: () => this.config,
1110
1080
  register: (nodeId, credential) => this.connectionLifecycle.register(nodeId, credential),
@@ -1113,12 +1083,7 @@ export class NodeConnector {
1113
1083
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
1114
1084
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
1115
1085
  handleRuntimeCredentialResponse: (envelope) => this.runtimeCredentials.handle(envelope),
1116
- handleDirectTransfer: (envelope) => this.directTransferService.handle({
1117
- type: envelope.type,
1118
- ...(typeof envelope.payload === 'object' && envelope.payload !== null
1119
- ? envelope.payload
1120
- : {})
1121
- }),
1086
+ handleDirectTransfer: (envelope) => this.handleDirectTransfer(envelope),
1122
1087
  reconcileRuns: (envelope) => this.runEventBridge.reconcile(envelope),
1123
1088
  runners: this.runners,
1124
1089
  deliverSessionChannelMessage: (envelope) => this.runnerChannelMessages.handle(envelope),
@@ -1419,9 +1384,72 @@ export class NodeConnector {
1419
1384
  this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
1420
1385
  this.database.closeRunningTerminalSessions();
1421
1386
  await this.initializeConversationHosting();
1387
+ this.directTransferService ??= this.createDirectTransferService();
1422
1388
  this.connectionLifecycle.start();
1423
1389
  this.controlChannel.connect();
1424
1390
  }
1391
+ createDirectTransferService() {
1392
+ try {
1393
+ return new DirectTransferService({
1394
+ nodeGeneration: () => this.nodeGeneration,
1395
+ send: (type, payload) => this.send(type, payload),
1396
+ udpPortRange: () => directTransferUdpPortRange(this.config),
1397
+ openUpload: async (descriptor) => {
1398
+ if (descriptor.resource.kind !== 'WORKSPACE_FILE')
1399
+ throw new Error('DIRECT_RESOURCE_UNSUPPORTED');
1400
+ const workspace = this.workspaceCoordinator.require(descriptor.resource.workspaceId);
1401
+ const upload = await this.workspaceUploadService.createDirect(workspace, {
1402
+ path: descriptor.resource.path,
1403
+ size: descriptor.size,
1404
+ sha256: descriptor.sha256,
1405
+ overwrite: descriptor.overwrite === true,
1406
+ ...(descriptor.composerAttachment === undefined
1407
+ ? {}
1408
+ : { composerAttachment: descriptor.composerAttachment })
1409
+ });
1410
+ return {
1411
+ offset: upload.receivedBytes,
1412
+ write: (bytes) => this.workspaceUploadService.writeDirect(upload.uploadId, bytes),
1413
+ complete: (size, sha256) => this.workspaceUploadService.completeDirect(upload.uploadId, size, sha256),
1414
+ cancel: () => this.workspaceUploadService.cancel(upload.uploadId)
1415
+ };
1416
+ },
1417
+ openDownload: async (descriptor) => {
1418
+ if (this.workspaceWorkbenchService === undefined)
1419
+ throw new Error('DIRECT_RESOURCE_UNAVAILABLE');
1420
+ return this.workspaceWorkbenchService.directDownload(descriptor);
1421
+ }
1422
+ });
1423
+ }
1424
+ catch (error) {
1425
+ if (!(error instanceof Error) || error.message !== 'DIRECT_UDP_PORT_UNAVAILABLE')
1426
+ throw error;
1427
+ const range = directTransferUdpPortRange(this.config);
1428
+ nodeLog('direct-transfer.udp-unavailable', {
1429
+ portRangeBegin: range.begin,
1430
+ portRangeEnd: range.end
1431
+ });
1432
+ return undefined;
1433
+ }
1434
+ }
1435
+ handleDirectTransfer(envelope) {
1436
+ const command = {
1437
+ type: envelope.type,
1438
+ ...(typeof envelope.payload === 'object' && envelope.payload !== null ? envelope.payload : {})
1439
+ };
1440
+ if (this.directTransferService !== undefined)
1441
+ return this.directTransferService.handle(command);
1442
+ const parsed = directNodeCommandSchema.safeParse(command);
1443
+ if (!parsed.success)
1444
+ return false;
1445
+ this.send('direct.error', {
1446
+ type: 'direct.error',
1447
+ directSessionId: parsed.data.directSessionId,
1448
+ code: 'DIRECT_UDP_PORT_UNAVAILABLE',
1449
+ message: 'The configured direct-transfer UDP port range is unavailable.'
1450
+ });
1451
+ return true;
1452
+ }
1425
1453
  clearMissionRetry(sessionId) {
1426
1454
  const retry = this.missionRetryTimers.get(sessionId);
1427
1455
  if (retry !== undefined)
@@ -1600,7 +1628,8 @@ export class NodeConnector {
1600
1628
  const terminalDrain = this.terminalManager.shutdown();
1601
1629
  this.terminalChannel.stop();
1602
1630
  this.runtimeCredentials.close();
1603
- this.directTransferService.close();
1631
+ this.directTransferService?.dispose();
1632
+ this.directTransferService = undefined;
1604
1633
  this.controlChannel.stop();
1605
1634
  const database = this.database;
1606
1635
  this.database = undefined;
@@ -1,4 +1,5 @@
1
1
  import { directDataControlFrameSchema, directNodeCommandSchema } from '@myagentroam/protocol';
2
+ import { IceUdpMuxListener as NativeIceUdpMuxListener } from 'node-datachannel';
2
3
  import { RTCPeerConnection } from 'node-datachannel/polyfill';
3
4
  const MAX_HOST_CANDIDATES = 32;
4
5
  const MAX_ACTIVE_SESSIONS = 32;
@@ -11,21 +12,27 @@ export class DirectTransferService {
11
12
  sessions = new Map();
12
13
  now;
13
14
  createPeerConnection;
15
+ iceUdpMuxListener;
16
+ peerConfiguration;
14
17
  leaseGraceMs;
15
18
  leaseCheckMs;
19
+ disposed = false;
16
20
  constructor(options) {
17
21
  this.options = options;
18
22
  this.now = options.now ?? Date.now;
23
+ const range = options.udpPortRange?.() ?? { begin: 49_152, end: 49_215 };
24
+ const createIceUdpMuxListener = options.createIceUdpMuxListener ?? ((port) => new NativeIceUdpMuxListener(port));
25
+ this.iceUdpMuxListener = bindIceUdpMuxListener(range, createIceUdpMuxListener);
26
+ const udpPort = this.iceUdpMuxListener.port();
27
+ this.peerConfiguration = {
28
+ iceServers: [],
29
+ enableIceUdpMux: true,
30
+ portRangeBegin: udpPort,
31
+ portRangeEnd: udpPort
32
+ };
19
33
  this.createPeerConnection =
20
34
  options.createPeerConnection ??
21
- (() => {
22
- const range = options.udpPortRange?.() ?? { begin: 49_152, end: 49_215 };
23
- return new RTCPeerConnection({
24
- iceServers: [],
25
- portRangeBegin: range.begin,
26
- portRangeEnd: range.end
27
- });
28
- });
35
+ ((configuration) => new RTCPeerConnection(configuration));
29
36
  this.leaseGraceMs = options.leaseGraceMs ?? LEASE_GRACE_MS;
30
37
  this.leaseCheckMs = options.leaseCheckMs ?? LEASE_CHECK_MS;
31
38
  }
@@ -49,6 +56,13 @@ export class DirectTransferService {
49
56
  for (const session of [...this.sessions.values()])
50
57
  void this.remove(session, 'DIRECT_NODE_STOPPED');
51
58
  }
59
+ dispose() {
60
+ if (this.disposed)
61
+ return;
62
+ this.disposed = true;
63
+ this.close();
64
+ this.iceUdpMuxListener.stop();
65
+ }
52
66
  async dispatch(command) {
53
67
  if (command.type === 'direct.prepare') {
54
68
  this.prepare(command);
@@ -86,7 +100,7 @@ export class DirectTransferService {
86
100
  throw new Error('DIRECT_SESSION_CONFLICT');
87
101
  if (this.sessions.size >= MAX_ACTIVE_SESSIONS)
88
102
  throw new Error('DIRECT_SESSION_LIMIT_REACHED');
89
- const peer = this.createPeerConnection();
103
+ const peer = this.createPeerConnection(this.peerConfiguration);
90
104
  const session = {
91
105
  command,
92
106
  peer,
@@ -360,6 +374,18 @@ export class DirectTransferService {
360
374
  });
361
375
  }
362
376
  }
377
+ function bindIceUdpMuxListener(range, create) {
378
+ let lastError;
379
+ for (let port = range.begin; port <= range.end; port += 1) {
380
+ try {
381
+ return create(port);
382
+ }
383
+ catch (error) {
384
+ lastError = error;
385
+ }
386
+ }
387
+ throw new Error('DIRECT_UDP_PORT_UNAVAILABLE', { cause: lastError });
388
+ }
363
389
  export function fingerprint(sdp) {
364
390
  const match = sdp?.match(/^a=fingerprint:(sha-256 [A-F0-9:]+)\r?$/imu);
365
391
  return match?.[1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.68",
3
+ "version": "0.9.69",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "node-pty": "1.1.0",
29
29
  "ws": "^8.21.3",
30
30
  "zod": "4.4.3",
31
- "@myagentroam/agent": "0.9.68",
32
- "@myagentroam/protocol": "0.9.68"
31
+ "@myagentroam/agent": "0.9.69",
32
+ "@myagentroam/protocol": "0.9.69"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/ws": "^8.18.1"