@myagentroam/marctl 0.9.121 → 0.9.122

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/marctl",
3
- "version": "0.9.121",
3
+ "version": "0.9.122",
4
4
  "description": "MyAgentRoam command-line interface for authorized Node access.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/direct.mjs CHANGED
@@ -27,14 +27,17 @@ export async function openDirectTerminal({
27
27
  WebSocketClass,
28
28
  nodeId,
29
29
  terminalId,
30
+ manifest,
30
31
  afterSequence = 0,
31
32
  timeoutMs = DIRECT_SETUP_TIMEOUT_MS,
32
33
  heartbeatMs = DIRECT_HEARTBEAT_MS,
33
34
  peerConnectionFactory,
35
+ onPrepared,
36
+ onIceConnected,
34
37
  onTransportReady,
35
38
  signal
36
39
  }) {
37
- if (typeof client.getTerminalManifest !== 'function')
40
+ if (manifest === undefined && typeof client.getTerminalManifest !== 'function')
38
41
  throw new DirectFallbackError('DIRECT_MANIFEST_UNAVAILABLE');
39
42
 
40
43
  let live;
@@ -49,7 +52,7 @@ export async function openDirectTerminal({
49
52
  let transportNegotiationActive = true;
50
53
  let cleanupNative;
51
54
  try {
52
- const manifest = await client.getTerminalManifest(nodeId);
55
+ manifest ??= await client.getTerminalManifest(nodeId);
53
56
  if (
54
57
  typeof manifest?.nodeGeneration !== 'string' ||
55
58
  manifest.nodeGeneration.length < 8 ||
@@ -84,7 +87,12 @@ export async function openDirectTerminal({
84
87
  peerConnectionFactory ?? ((name, options) => new native.PeerConnection(name, options));
85
88
  peer = createPeer(`marctl-${transportSessionId}`, { iceServers: [] });
86
89
 
90
+ // Start negotiation only after both paths finish their control-plane preparation.
91
+ if (onPrepared) await onPrepared();
92
+ if (signal?.aborted) throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
93
+
87
94
  const transportReady = new Promise((resolve, reject) => {
95
+ let iceConnected = false;
88
96
  let connected = false;
89
97
  let serverOpened = false;
90
98
  let settled = false;
@@ -162,6 +170,17 @@ export async function openDirectTerminal({
162
170
  )
163
171
  finish(new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE'));
164
172
  });
173
+ peer.onIceStateChange((state) => {
174
+ if (!transportNegotiationActive || settled || iceConnected) return;
175
+ const normalizedState = String(state).toLowerCase();
176
+ if (normalizedState !== 'connected' && normalizedState !== 'completed') return;
177
+ iceConnected = true;
178
+ try {
179
+ onIceConnected?.();
180
+ } catch (error) {
181
+ finish(error);
182
+ }
183
+ });
165
184
  peer.onStateChange((state) => {
166
185
  if (!transportNegotiationActive || settled) return;
167
186
  const normalizedState = String(state).toLowerCase();
package/src/terminal.mjs CHANGED
@@ -28,13 +28,16 @@ export async function runTerminalSession({
28
28
  let transport;
29
29
  let result;
30
30
  let primaryError;
31
+ let manifest;
31
32
 
32
33
  try {
33
- if (
34
- binary &&
35
- (command === undefined || (await client.getTerminalManifest(nodeId))?.binaryTerminal !== true)
36
- )
37
- throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
34
+ if (binary) {
35
+ if (command === undefined)
36
+ throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
37
+ manifest = await client.getTerminalManifest(nodeId);
38
+ if (manifest?.binaryTerminal !== true)
39
+ throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
40
+ }
38
41
  const created = await client.createTerminal({
39
42
  nodeId,
40
43
  ...dimensions,
@@ -47,17 +50,18 @@ export async function runTerminalSession({
47
50
  try {
48
51
  const openDirectTerminal =
49
52
  openDirectTerminalImpl ?? (await import('./direct.mjs')).openDirectTerminal;
53
+ if (raceConnections && manifest === undefined)
54
+ manifest = await client.getTerminalManifest(nodeId).catch(() => undefined);
50
55
  const directOptions = {
51
56
  client,
52
57
  WebSocketClass,
53
58
  nodeId,
54
59
  terminalId,
60
+ manifest,
55
61
  afterSequence: 0,
56
62
  timeoutMs: connectionTimeoutMs
57
63
  };
58
- const canRace =
59
- raceConnections &&
60
- (await client.getTerminalManifest(nodeId).catch(() => undefined))?.deferredRelay === true;
64
+ const canRace = raceConnections && manifest?.deferredRelay === true;
61
65
  transport = canRace
62
66
  ? await raceTerminalConnections({
63
67
  openDirectTerminal,
@@ -137,6 +141,16 @@ async function raceTerminalConnections({
137
141
  }) {
138
142
  const abort = new AbortController();
139
143
  const relayAbort = new AbortController();
144
+ const preparing = new Set(['direct', 'relay']);
145
+ let start;
146
+ const preparation = new Promise((resolve) => {
147
+ start = resolve;
148
+ });
149
+ const prepared = (candidate) => {
150
+ preparing.delete(candidate);
151
+ if (preparing.size === 0) start();
152
+ return preparation;
153
+ };
140
154
  let selected;
141
155
  let announce;
142
156
  const first = new Promise((resolve) => {
@@ -153,12 +167,19 @@ async function raceTerminalConnections({
153
167
  const direct = openDirectTerminal({
154
168
  ...directOptions,
155
169
  signal: abort.signal,
156
- onTransportReady: () => {
170
+ onPrepared: () => prepared('direct'),
171
+ onIceConnected: () => {
157
172
  if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
173
+ },
174
+ onTransportReady: () => {
175
+ if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
158
176
  }
159
177
  }).then(
160
178
  (value) => ({ value }),
161
- (error) => ({ error })
179
+ (error) => {
180
+ prepared('direct');
181
+ return { error };
182
+ }
162
183
  );
163
184
  const relay = prepareRelayTransport({
164
185
  client,
@@ -167,13 +188,17 @@ async function raceTerminalConnections({
167
188
  terminalId,
168
189
  translate,
169
190
  connectionTimeoutMs,
170
- signal: relayAbort.signal
191
+ signal: relayAbort.signal,
192
+ onPrepared: () => prepared('relay')
171
193
  }).then(
172
194
  (value) => {
173
195
  if (!select('relay')) value.close();
174
196
  return { value };
175
197
  },
176
- (error) => ({ error })
198
+ (error) => {
199
+ prepared('relay');
200
+ return { error };
201
+ }
177
202
  );
178
203
  const winner = await Promise.race([
179
204
  first,
@@ -253,9 +278,11 @@ async function prepareRelayTransport({
253
278
  terminalId,
254
279
  translate,
255
280
  connectionTimeoutMs,
256
- signal
281
+ signal,
282
+ onPrepared
257
283
  }) {
258
284
  const ticket = await client.createAttachTicket({ nodeId, terminalId });
285
+ if (onPrepared) await onPrepared();
259
286
  if (signal?.aborted) throw new MarctlError(translate('errors.terminalAttachConnection'));
260
287
  const socket = client.openTerminalWebSocket(WebSocketClass, {
261
288
  terminalId,