@myagentroam/marctl 0.11.2 → 0.11.4

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/src/terminal.mjs CHANGED
@@ -1,11 +1,10 @@
1
- import { EventEmitter } from 'node:events';
2
- import { MarctlError } from './config.mjs';
1
+ import { MarctlError } from './errors.mjs';
2
+ import { DirectRejectedError } from './direct-errors.mjs';
3
3
  import { createTranslator } from './i18n/index.mjs';
4
- import { TerminalRelaySender } from './terminal-relay-sender.mjs';
4
+ import { raceTerminalConnections } from './terminal-race.mjs';
5
+ import { openRelayTransport } from './terminal-relay.mjs';
6
+ import { getDimensions, runAttachedTerminal } from './terminal-interaction.mjs';
5
7
 
6
- const MAX_INPUT_BYTES = 65536;
7
- const DEFAULT_ROWS = 24;
8
- const DEFAULT_COLS = 80;
9
8
  const DEFAULT_CONNECTION_TIMEOUT_MS = 15000;
10
9
 
11
10
  export async function runTerminalSession({
@@ -17,7 +16,7 @@ export async function runTerminalSession({
17
16
  attached,
18
17
  stdin = process.stdin,
19
18
  stdout = process.stdout,
20
- signals = process,
19
+ signal,
21
20
  translate = createTranslator(),
22
21
  connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS,
23
22
  openDirectTerminal: openDirectTerminalImpl,
@@ -31,10 +30,12 @@ export async function runTerminalSession({
31
30
  let manifest;
32
31
 
33
32
  try {
33
+ if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
34
34
  if (binary) {
35
35
  if (command === undefined)
36
36
  throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
37
37
  manifest = await client.getTerminalManifest(nodeId);
38
+ if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
38
39
  if (manifest?.binaryTerminal !== true)
39
40
  throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
40
41
  }
@@ -47,6 +48,7 @@ export async function runTerminalSession({
47
48
  terminalId = binary ? created?.terminalId : created;
48
49
  if (binary && created?.binary !== true)
49
50
  throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
51
+ if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
50
52
  try {
51
53
  const openDirectTerminal =
52
54
  openDirectTerminalImpl ?? (await import('./direct.mjs')).openDirectTerminal;
@@ -59,7 +61,8 @@ export async function runTerminalSession({
59
61
  terminalId,
60
62
  manifest,
61
63
  afterSequence: 0,
62
- timeoutMs: connectionTimeoutMs
64
+ timeoutMs: connectionTimeoutMs,
65
+ signal
63
66
  };
64
67
  const canRace = raceConnections && manifest?.deferredRelay === true;
65
68
  transport = canRace
@@ -71,9 +74,11 @@ export async function runTerminalSession({
71
74
  nodeId,
72
75
  terminalId,
73
76
  translate,
74
- connectionTimeoutMs
77
+ connectionTimeoutMs,
78
+ signal
75
79
  })
76
80
  : await openDirectTerminal(directOptions);
81
+ if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
77
82
  result =
78
83
  attached === undefined
79
84
  ? await runAttachedTerminal({
@@ -82,37 +87,44 @@ export async function runTerminalSession({
82
87
  commandMode: command !== undefined,
83
88
  stdin,
84
89
  stdout,
85
- signals,
90
+ signal,
86
91
  translate
87
92
  })
88
93
  : await attached({ transport, terminalId, translate });
89
94
  } catch (error) {
90
- if (error?.name === 'DirectUnsafeError')
91
- throw new MarctlError(translate('errors.terminalDirectUnsafeFallback'));
92
- if (error?.name !== 'DirectFallbackError') throw error;
93
- transport = await openRelayTransport({
94
- client,
95
- WebSocketClass,
96
- nodeId,
97
- terminalId,
98
- translate,
99
- connectionTimeoutMs
100
- });
101
- result =
102
- attached === undefined
103
- ? await runAttachedTerminal({
104
- transport,
105
- terminalId,
106
- commandMode: command !== undefined,
107
- stdin,
108
- stdout,
109
- signals,
110
- translate
111
- })
112
- : await attached({ transport, terminalId, translate });
95
+ if (signal?.aborted) {
96
+ result = signal.reason === 'SIGTERM' ? 143 : 130;
97
+ } else {
98
+ if (error?.name === 'DirectUnsafeError')
99
+ throw new MarctlError(translate('errors.terminalDirectUnsafeFallback'));
100
+ if (error?.name !== 'DirectFallbackError') throw error;
101
+ transport = await openRelayTransport({
102
+ client,
103
+ WebSocketClass,
104
+ nodeId,
105
+ terminalId,
106
+ translate,
107
+ connectionTimeoutMs,
108
+ signal
109
+ });
110
+ if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
111
+ result =
112
+ attached === undefined
113
+ ? await runAttachedTerminal({
114
+ transport,
115
+ terminalId,
116
+ commandMode: command !== undefined,
117
+ stdin,
118
+ stdout,
119
+ signal,
120
+ translate
121
+ })
122
+ : await attached({ transport, terminalId, translate });
123
+ }
113
124
  }
114
125
  } catch (error) {
115
- primaryError = toMarctlError(error, translate);
126
+ if (signal?.aborted) result = signal.reason === 'SIGTERM' ? 143 : 130;
127
+ else primaryError = toMarctlError(error, translate);
116
128
  } finally {
117
129
  transport?.close?.();
118
130
  if (terminalId !== undefined) {
@@ -129,576 +141,11 @@ export async function runTerminalSession({
129
141
  return result;
130
142
  }
131
143
 
132
- async function raceTerminalConnections({
133
- openDirectTerminal,
134
- directOptions,
135
- client,
136
- WebSocketClass,
137
- nodeId,
138
- terminalId,
139
- translate,
140
- connectionTimeoutMs
141
- }) {
142
- const abort = new AbortController();
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
- };
154
- let selected;
155
- let announce;
156
- const first = new Promise((resolve) => {
157
- announce = resolve;
158
- });
159
- const select = (candidate) => {
160
- if (selected) return false;
161
- selected = candidate;
162
- announce(candidate);
163
- if (candidate === 'relay') abort.abort();
164
- else relayAbort.abort();
165
- return true;
166
- };
167
- const direct = openDirectTerminal({
168
- ...directOptions,
169
- signal: abort.signal,
170
- onPrepared: () => prepared('direct'),
171
- onIceConnected: () => {
172
- if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
173
- },
174
- onTransportReady: () => {
175
- if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
176
- }
177
- }).then(
178
- (value) => ({ value }),
179
- (error) => {
180
- prepared('direct');
181
- return { error };
182
- }
183
- );
184
- const relay = prepareRelayTransport({
185
- client,
186
- WebSocketClass,
187
- nodeId,
188
- terminalId,
189
- translate,
190
- connectionTimeoutMs,
191
- signal: relayAbort.signal,
192
- onPrepared: () => prepared('relay')
193
- }).then(
194
- (value) => {
195
- if (!select('relay')) value.close();
196
- return { value };
197
- },
198
- (error) => {
199
- prepared('relay');
200
- return { error };
201
- }
202
- );
203
- const winner = await Promise.race([
204
- first,
205
- Promise.all([direct, relay]).then(() => selected ?? 'none')
206
- ]);
207
- if (winner === 'direct') {
208
- const result = await direct;
209
- if (result.error) throw result.error;
210
- return result.value;
211
- }
212
- if (winner === 'relay') {
213
- const result = await relay;
214
- if (result.error) throw result.error;
215
- return result.value.activate();
216
- }
217
- const [directResult, relayResult] = await Promise.all([direct, relay]);
218
- throw relayResult.error ?? directResult.error;
219
- }
220
-
221
- export function splitInput(text, maxBytes = MAX_INPUT_BYTES) {
222
- const value = Buffer.isBuffer(text) ? text.toString('utf8') : String(text);
223
- const chunks = [];
224
- let chunk = '';
225
- let chunkBytes = 0;
226
-
227
- for (const character of value) {
228
- const characterBytes = Buffer.byteLength(character);
229
- if (chunk !== '' && chunkBytes + characterBytes > maxBytes) {
230
- chunks.push(chunk);
231
- chunk = '';
232
- chunkBytes = 0;
233
- }
234
- if (characterBytes > maxBytes) continue;
235
- chunk += character;
236
- chunkBytes += characterBytes;
237
- }
238
- if (chunk !== '') chunks.push(chunk);
239
- return chunks;
240
- }
241
-
242
- function getDimensions(stdout) {
243
- const rows = Number.isSafeInteger(stdout.rows) && stdout.rows > 0 ? stdout.rows : DEFAULT_ROWS;
244
- const cols =
245
- Number.isSafeInteger(stdout.columns) && stdout.columns > 0 ? stdout.columns : DEFAULT_COLS;
246
- return { rows: Math.min(rows, 1000), cols: Math.min(cols, 1000) };
247
- }
248
-
249
- async function openRelayTransport({
250
- client,
251
- WebSocketClass,
252
- nodeId,
253
- terminalId,
254
- translate,
255
- connectionTimeoutMs
256
- }) {
257
- const ticket = await client.createAttachTicket({ nodeId, terminalId });
258
- const socket = client.openTerminalWebSocket(WebSocketClass, { terminalId, ticket });
259
- const transport = new BufferedRelayTransport({
260
- socket,
261
- terminalId,
262
- translate,
263
- connectionTimeoutMs
264
- });
265
- try {
266
- await transport.waitForAttachment();
267
- return transport;
268
- } catch (error) {
269
- transport.close();
270
- throw error;
271
- }
272
- }
273
-
274
- async function prepareRelayTransport({
275
- client,
276
- WebSocketClass,
277
- nodeId,
278
- terminalId,
279
- translate,
280
- connectionTimeoutMs,
281
- signal,
282
- onPrepared
283
- }) {
284
- const ticket = await client.createAttachTicket({ nodeId, terminalId });
285
- if (onPrepared) await onPrepared();
286
- if (signal?.aborted) throw new MarctlError(translate('errors.terminalAttachConnection'));
287
- const socket = client.openTerminalWebSocket(WebSocketClass, {
288
- terminalId,
289
- ticket,
290
- deferred: true
291
- });
292
- // Keep an error listener after readiness until the socket is discarded.
293
- socket.on('error', () => {});
294
- let activated = false;
295
- const ready = new Promise((resolve, reject) => {
296
- const timer = setTimeout(() => fail(), connectionTimeoutMs);
297
- const cleanup = () => {
298
- clearTimeout(timer);
299
- socket.removeListener?.('message', onMessage);
300
- socket.removeListener?.('error', fail);
301
- socket.removeListener?.('close', fail);
302
- signal?.removeEventListener('abort', fail);
303
- };
304
- const fail = () => {
305
- cleanup();
306
- try {
307
- socket.close();
308
- } catch {
309
- /* Connection not yet established. */
310
- }
311
- reject(new MarctlError(translate('errors.terminalAttachConnection')));
312
- };
313
- const onMessage = (raw) => {
314
- let frame;
315
- try {
316
- frame = JSON.parse(String(raw));
317
- } catch {
318
- return fail();
319
- }
320
- if (frame?.type !== 'ready' || frame.terminalId !== terminalId) return fail();
321
- cleanup();
322
- resolve();
323
- };
324
- socket.on('message', onMessage);
325
- socket.on('error', fail);
326
- socket.on('close', fail);
327
- signal?.addEventListener('abort', fail, { once: true });
328
- if (signal?.aborted) fail();
329
- });
330
- await ready;
331
- return {
332
- async activate() {
333
- if (activated || socket.readyState !== 1)
334
- throw new MarctlError(translate('errors.terminalAttachConnection'));
335
- activated = true;
336
- const transport = new BufferedRelayTransport({
337
- socket,
338
- terminalId,
339
- translate,
340
- connectionTimeoutMs
341
- });
342
- try {
343
- socket.send(JSON.stringify({ type: 'activate', terminalId }));
344
- await transport.waitForAttachment();
345
- return transport;
346
- } catch (error) {
347
- transport.close();
348
- throw error;
349
- }
350
- },
351
- close() {
352
- socket.close();
353
- }
354
- };
355
- }
356
-
357
- class BufferedRelayTransport extends EventEmitter {
358
- #socket;
359
- #sender;
360
- #terminalId;
361
- #translate;
362
- #attached = false;
363
- #active = false;
364
- #pendingEvents = [];
365
- #attachmentResolve;
366
- #attachmentReject;
367
- #openTimer;
368
- #attachTimer;
369
- #closed = false;
370
- #onOpen;
371
- #onMessage;
372
- #onError;
373
- #onClose;
374
-
375
- constructor({ socket, terminalId, translate, connectionTimeoutMs }) {
376
- super();
377
- this.#socket = socket;
378
- this.#sender = new TerminalRelaySender(socket);
379
- this.#terminalId = terminalId;
380
- this.#translate = translate;
381
- this.#onOpen = () => {
382
- clearTimeout(this.#openTimer);
383
- this.#armAttachTimer(connectionTimeoutMs);
384
- };
385
- this.#onMessage = (raw, isBinary) => {
386
- try {
387
- const frame = parseFrame(raw, isBinary, this.#terminalId, this.#translate);
388
- if (!this.#attached) {
389
- if (frame.type === 'attach') {
390
- this.#attached = true;
391
- clearTimeout(this.#attachTimer);
392
- this.#attachmentResolve?.();
393
- this.#attachmentResolve = undefined;
394
- this.#attachmentReject = undefined;
395
- return;
396
- }
397
- if (frame.type === 'error')
398
- throw new MarctlError(this.#translate('errors.terminalRemoteError'));
399
- this.#queueOrEmit('message', raw, isBinary);
400
- return;
401
- }
402
- this.#queueOrEmit('message', raw, isBinary);
403
- } catch (error) {
404
- this.#fail(
405
- error instanceof MarctlError
406
- ? error
407
- : new MarctlError(this.#translate('errors.terminalFrameInvalid'))
408
- );
409
- }
410
- };
411
- this.#onError = () => {
412
- const error = new MarctlError(
413
- this.#translate(
414
- this.#attached ? 'errors.terminalConnectionFailed' : 'errors.terminalAttachConnection'
415
- )
416
- );
417
- if (this.#attached) this.#queueOrEmit('error', error);
418
- else this.#fail(error);
419
- };
420
- this.#onClose = () => {
421
- this.#sender.close();
422
- const error = new MarctlError(
423
- this.#translate(
424
- this.#attached ? 'errors.terminalConnectionLost' : 'errors.terminalAttachLost'
425
- )
426
- );
427
- if (this.#attached) this.#queueOrEmit('close');
428
- else this.#fail(error);
429
- };
430
-
431
- socket.on?.('open', this.#onOpen);
432
- socket.on?.('message', this.#onMessage);
433
- socket.on?.('error', this.#onError);
434
- socket.on?.('close', this.#onClose);
435
- if (socket.readyState === 1) this.#onOpen();
436
- else
437
- this.#openTimer = setTimeout(
438
- () => this.#fail(new MarctlError(this.#translate('errors.terminalConnectionOpen'))),
439
- connectionTimeoutMs
440
- );
441
- }
442
-
443
- waitForAttachment() {
444
- return new Promise((resolve, reject) => {
445
- if (this.#closed) {
446
- reject(new MarctlError(this.#translate('errors.terminalAttachLost')));
447
- return;
448
- }
449
- this.#attachmentResolve = resolve;
450
- this.#attachmentReject = reject;
451
- if (this.#attached) {
452
- this.#attachmentResolve = undefined;
453
- this.#attachmentReject = undefined;
454
- resolve();
455
- }
456
- });
457
- }
458
-
459
- activate() {
460
- if (this.#active) return;
461
- this.#active = true;
462
- const pendingEvents = this.#pendingEvents;
463
- this.#pendingEvents = [];
464
- for (const { type, args } of pendingEvents) this.emit(type, ...args);
465
- }
466
-
467
- get readyState() {
468
- return this.#socket.readyState;
469
- }
470
-
471
- send(value) {
472
- if (this.#closed) throw new Error('TERMINAL_CONNECTION_LOST');
473
- if (JSON.parse(value).type === 'input') return this.#sender.send(value);
474
- this.#socket.send(value);
475
- }
476
-
477
- close() {
478
- if (this.#closed) return;
479
- this.#closed = true;
480
- this.#sender.close();
481
- clearTimeout(this.#openTimer);
482
- clearTimeout(this.#attachTimer);
483
- this.#socket.removeListener?.('open', this.#onOpen);
484
- this.#socket.removeListener?.('message', this.#onMessage);
485
- this.#socket.removeListener?.('error', this.#onError);
486
- this.#socket.removeListener?.('close', this.#onClose);
487
- try {
488
- this.#socket.close();
489
- } catch {
490
- // The connection is already unavailable.
491
- }
492
- }
493
-
494
- #armAttachTimer(connectionTimeoutMs) {
495
- if (this.#attached) return;
496
- this.#attachTimer = setTimeout(
497
- () => this.#fail(new MarctlError(this.#translate('errors.terminalAttachTimeout'))),
498
- connectionTimeoutMs
499
- );
500
- }
501
-
502
- #fail(error) {
503
- if (this.#closed) return;
504
- clearTimeout(this.#openTimer);
505
- clearTimeout(this.#attachTimer);
506
- this.#attachmentResolve = undefined;
507
- const reject = this.#attachmentReject;
508
- this.#attachmentReject = undefined;
509
- if (!this.#attached) reject?.(error);
510
- else this.#queueOrEmit('error', error);
511
- }
512
-
513
- #queueOrEmit(type, ...args) {
514
- if (this.#active) {
515
- if (type !== 'error' || this.listenerCount('error') > 0) this.emit(type, ...args);
516
- return;
517
- }
518
- this.#pendingEvents.push({ type, args });
519
- }
520
- }
521
-
522
- function runAttachedTerminal({
523
- transport,
524
- terminalId,
525
- commandMode,
526
- stdin,
527
- stdout,
528
- signals,
529
- translate
530
- }) {
531
- return new Promise((resolve, reject) => {
532
- let settled = false;
533
- let inputEnabled = false;
534
- let rawModeEnabled = false;
535
- let inputTasks = 0;
536
-
537
- const cleanup = () => {
538
- transport.removeListener?.('message', onMessage);
539
- transport.removeListener?.('error', onError);
540
- transport.removeListener?.('close', onClose);
541
- stdin.removeListener?.('data', onInput);
542
- stdout.removeListener?.('resize', onResize);
543
- signals.removeListener?.('SIGINT', onSigint);
544
- signals.removeListener?.('SIGTERM', onSigterm);
545
- if (rawModeEnabled) {
546
- try {
547
- stdin.setRawMode(false);
548
- } catch {
549
- // The terminal may already be gone.
550
- }
551
- }
552
- stdin.pause?.();
553
- };
554
-
555
- const finish = (error, exitCode) => {
556
- if (settled) return;
557
- settled = true;
558
- cleanup();
559
- sendCloseFrame(transport, terminalId);
560
- transport.close?.();
561
- if (error === undefined) resolve(exitCode);
562
- else reject(error);
563
- };
564
- const fail = (key) => finish(new MarctlError(translate(key)));
565
- const send = (frame) => {
566
- return transport.send(JSON.stringify(frame));
567
- };
568
- const onInput = async (data) => {
569
- inputTasks += 1;
570
- stdin.pause?.();
571
- try {
572
- for (const chunk of splitInput(data)) {
573
- if (settled) return;
574
- await send({ type: 'input', terminalId, data: chunk });
575
- }
576
- } catch {
577
- fail('errors.terminalInputFailed');
578
- } finally {
579
- inputTasks -= 1;
580
- if (!settled && inputTasks === 0) stdin.resume?.();
581
- }
582
- };
583
- const onResize = () => {
584
- try {
585
- const { rows, cols } = getDimensions(stdout);
586
- Promise.resolve(send({ type: 'resize', terminalId, rows, cols })).catch(() =>
587
- fail('errors.terminalResizeFailed')
588
- );
589
- } catch {
590
- fail('errors.terminalResizeFailed');
591
- }
592
- };
593
- const onMessage = (raw, isBinary) => {
594
- try {
595
- const frame = parseFrame(raw, isBinary, terminalId, translate);
596
- if (frame.type === 'output') stdout.write(frame.data);
597
- else if (frame.type === 'exit') {
598
- finish(undefined, frame.exitCode === null ? 1 : frame.exitCode);
599
- } else if (frame.type === 'error') fail('errors.terminalRemoteError');
600
- else fail('errors.terminalUnexpectedFrame');
601
- } catch (error) {
602
- finish(
603
- error instanceof MarctlError
604
- ? error
605
- : new MarctlError(translate('errors.terminalFrameInvalid'))
606
- );
607
- }
608
- };
609
- const onError = () => finish(new MarctlError(translate('errors.terminalConnectionFailed')));
610
- const onClose = () => finish(new MarctlError(translate('errors.terminalConnectionLost')));
611
- const onSigint = () => finish(undefined, 130);
612
- const onSigterm = () => finish(undefined, 143);
613
- const startInput = () => {
614
- if (settled || inputEnabled) return;
615
- inputEnabled = true;
616
- try {
617
- if (!commandMode) {
618
- if (typeof stdin.setRawMode !== 'function') {
619
- fail('errors.terminalInteractiveRequired');
620
- return;
621
- }
622
- stdin.setRawMode(true);
623
- rawModeEnabled = true;
624
- }
625
- stdin.resume?.();
626
- stdin.on?.('data', onInput);
627
- stdout.on?.('resize', onResize);
628
- onResize();
629
- } catch {
630
- fail(commandMode ? 'errors.terminalSessionFailed' : 'errors.terminalInteractiveInit');
631
- }
632
- };
633
- transport.on?.('message', onMessage);
634
- transport.on?.('error', onError);
635
- transport.on?.('close', onClose);
636
- signals.on?.('SIGINT', onSigint);
637
- signals.on?.('SIGTERM', onSigterm);
638
- transport.activate?.();
639
- startInput();
640
- });
641
- }
642
-
643
- export function parseFrame(raw, isBinary, terminalId, translate) {
644
- if (isBinary) throw new MarctlError(translate('errors.terminalBinaryFrame'));
645
- let frame;
646
- try {
647
- frame = JSON.parse(rawToString(raw));
648
- } catch {
649
- throw new MarctlError(translate('errors.terminalInvalidJson'));
650
- }
651
- if (frame === null || typeof frame !== 'object' || frame.terminalId !== terminalId)
652
- throw new MarctlError(translate('errors.terminalFrameInvalid'));
653
-
654
- if (frame.type === 'attach') {
655
- if (!Number.isSafeInteger(frame.afterSequence) || frame.afterSequence < 0)
656
- throw new MarctlError(translate('errors.terminalFrameInvalid'));
657
- return frame;
658
- }
659
- if (frame.type === 'output') {
660
- if (
661
- !Number.isSafeInteger(frame.sequence) ||
662
- frame.sequence < 1 ||
663
- typeof frame.data !== 'string' ||
664
- Buffer.byteLength(frame.data) > MAX_INPUT_BYTES
665
- )
666
- throw new MarctlError(translate('errors.terminalFrameInvalid'));
667
- return frame;
668
- }
669
- if (frame.type === 'exit') {
670
- if (frame.exitCode !== null && !Number.isSafeInteger(frame.exitCode))
671
- throw new MarctlError(translate('errors.terminalFrameInvalid'));
672
- return frame;
673
- }
674
- if (frame.type === 'error' && typeof frame.code === 'string' && frame.code.length > 0)
675
- return frame;
676
- throw new MarctlError(translate('errors.terminalFrameInvalid'));
677
- }
678
-
679
- function rawToString(raw) {
680
- if (typeof raw === 'string') return raw;
681
- if (Buffer.isBuffer(raw)) return raw.toString('utf8');
682
- if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
683
- if (ArrayBuffer.isView(raw))
684
- return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
685
- return String(raw);
686
- }
687
-
688
- function sendCloseFrame(socket, terminalId) {
689
- if (!terminalId || !isSocketOpen(socket)) return;
690
- try {
691
- Promise.resolve(socket.send(JSON.stringify({ type: 'close', terminalId }))).catch(() => {});
692
- } catch {
693
- // The DELETE request below remains the authoritative cleanup path.
694
- }
695
- }
696
-
697
- function isSocketOpen(socket) {
698
- return socket.readyState === undefined || socket.readyState === 1;
699
- }
700
-
701
144
  function toMarctlError(error, translate) {
145
+ if (error instanceof DirectRejectedError)
146
+ return new MarctlError(`${translate('errors.terminalSessionFailed')} [${error.code}]`, {
147
+ code: error.code
148
+ });
702
149
  return error instanceof MarctlError
703
150
  ? error
704
151
  : new MarctlError(translate('errors.terminalSessionFailed'));