@myagentroam/node 0.9.165 → 0.9.167
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/service/node-terminal-channel.js +60 -11
- package/dist/terminal.js +33 -0
- package/package.json +4 -4
|
@@ -2,12 +2,15 @@ import WebSocket from 'ws';
|
|
|
2
2
|
import { terminalDataFrameSchema } from '@myagentroam/protocol';
|
|
3
3
|
import { nodeTerminalConnectEndpoint, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
4
4
|
import { TerminalRelayState } from './terminal-relay-state.js';
|
|
5
|
+
import { TerminalRelaySender } from '@myagentroam/marctl/src/terminal-relay-sender.mjs';
|
|
5
6
|
export class NodeTerminalChannel {
|
|
6
7
|
options;
|
|
7
8
|
socket;
|
|
8
9
|
reconnectTimer;
|
|
9
10
|
attempts = 0;
|
|
10
11
|
state = new TerminalRelayState();
|
|
12
|
+
sender;
|
|
13
|
+
pausedTerminals = new Set();
|
|
11
14
|
constructor(options) {
|
|
12
15
|
this.options = options;
|
|
13
16
|
options.manager.onFrame((frame) => this.publish(frame));
|
|
@@ -35,6 +38,7 @@ export class NodeTerminalChannel {
|
|
|
35
38
|
socket.once('error', () => undefined);
|
|
36
39
|
}
|
|
37
40
|
disconnect() {
|
|
41
|
+
this.releaseAttachments();
|
|
38
42
|
this.socket?.close();
|
|
39
43
|
}
|
|
40
44
|
stop() {
|
|
@@ -124,6 +128,7 @@ export class NodeTerminalChannel {
|
|
|
124
128
|
const attachment = this.state.attachments.get(message.ticket);
|
|
125
129
|
if (attachment === undefined)
|
|
126
130
|
return;
|
|
131
|
+
this.sender?.cancel(message.ticket);
|
|
127
132
|
this.state.attachments.delete(message.ticket);
|
|
128
133
|
if (message.type === 'detach')
|
|
129
134
|
this.options.manager.detach(attachment.attachmentId);
|
|
@@ -155,18 +160,12 @@ export class NodeTerminalChannel {
|
|
|
155
160
|
return;
|
|
156
161
|
}
|
|
157
162
|
void this.options.manager.acceptFrame(attachment.attachmentId, parsed.data).catch(() => {
|
|
158
|
-
this.
|
|
159
|
-
this.options.manager.detach(attachment.attachmentId);
|
|
160
|
-
this.sendFrame(message.ticket, {
|
|
161
|
-
type: 'error',
|
|
162
|
-
terminalId: attachment.terminalId,
|
|
163
|
-
code: 'TERMINAL_FRAME_REJECTED'
|
|
164
|
-
});
|
|
163
|
+
this.rejectAttachment(message.ticket, attachment, 'TERMINAL_FRAME_REJECTED');
|
|
165
164
|
});
|
|
166
165
|
return;
|
|
167
166
|
}
|
|
168
167
|
if (message.type === 'shutdown' && Object.keys(message).length === 1) {
|
|
169
|
-
this.
|
|
168
|
+
this.releaseAttachments();
|
|
170
169
|
void this.options.manager.shutdown();
|
|
171
170
|
return;
|
|
172
171
|
}
|
|
@@ -177,12 +176,60 @@ export class NodeTerminalChannel {
|
|
|
177
176
|
if (attachment.terminalId !== frame.terminalId)
|
|
178
177
|
continue;
|
|
179
178
|
this.sendFrame(ticket, frame);
|
|
180
|
-
if (frame.type === 'exit')
|
|
181
|
-
this.state.attachments.delete(ticket);
|
|
182
179
|
}
|
|
183
180
|
}
|
|
184
181
|
sendFrame(ticket, frame) {
|
|
185
|
-
|
|
182
|
+
const socket = this.socket;
|
|
183
|
+
if (socket === undefined || socket.readyState !== socket.OPEN)
|
|
184
|
+
return;
|
|
185
|
+
this.sender ??= new TerminalRelaySender(socket, {
|
|
186
|
+
onPendingChange: (keys) => this.updatePausedTerminals(keys)
|
|
187
|
+
});
|
|
188
|
+
const attachment = this.state.attachments.get(ticket);
|
|
189
|
+
void this.sender.send(JSON.stringify({ type: 'frame', ticket, frame }), ticket).then(() => {
|
|
190
|
+
if (frame.type === 'exit' && this.state.attachments.get(ticket) === attachment)
|
|
191
|
+
this.state.attachments.delete(ticket);
|
|
192
|
+
}, (error) => {
|
|
193
|
+
if (this.state.attachments.get(ticket) !== attachment || attachment === undefined)
|
|
194
|
+
return;
|
|
195
|
+
if (error?.code === 'TERMINAL_RELAY_BUFFER_EXCEEDED')
|
|
196
|
+
this.rejectAttachment(ticket, attachment, 'TERMINAL_BACKPRESSURE');
|
|
197
|
+
else
|
|
198
|
+
socket.close(1011, 'TERMINAL_SEND_FAILED');
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
updatePausedTerminals(keys) {
|
|
202
|
+
const pending = new Map();
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
const attachment = this.state.attachments.get(key);
|
|
205
|
+
if (attachment !== undefined) {
|
|
206
|
+
const ids = pending.get(attachment.terminalId) ?? new Set();
|
|
207
|
+
ids.add(attachment.attachmentId);
|
|
208
|
+
pending.set(attachment.terminalId, ids);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const terminalId of this.pausedTerminals) {
|
|
212
|
+
if (pending.has(terminalId))
|
|
213
|
+
continue;
|
|
214
|
+
this.pausedTerminals.delete(terminalId);
|
|
215
|
+
this.options.manager.setOutputBackpressure(terminalId, new Set());
|
|
216
|
+
}
|
|
217
|
+
for (const [terminalId, attachmentIds] of pending) {
|
|
218
|
+
this.pausedTerminals.add(terminalId);
|
|
219
|
+
this.options.manager.setOutputBackpressure(terminalId, attachmentIds);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
rejectAttachment(ticket, attachment, code) {
|
|
223
|
+
if (this.state.attachments.get(ticket) !== attachment)
|
|
224
|
+
return;
|
|
225
|
+
this.state.attachments.delete(ticket);
|
|
226
|
+
this.sender?.cancel(ticket);
|
|
227
|
+
this.options.manager.detach(attachment.attachmentId);
|
|
228
|
+
this.send({
|
|
229
|
+
type: 'frame',
|
|
230
|
+
ticket,
|
|
231
|
+
frame: { type: 'error', terminalId: attachment.terminalId, code }
|
|
232
|
+
});
|
|
186
233
|
}
|
|
187
234
|
send(message) {
|
|
188
235
|
const socket = this.socket;
|
|
@@ -202,5 +249,7 @@ export class NodeTerminalChannel {
|
|
|
202
249
|
for (const attachment of this.state.attachments.values())
|
|
203
250
|
this.options.manager.detach(attachment.attachmentId);
|
|
204
251
|
this.state.attachments.clear();
|
|
252
|
+
this.sender?.close();
|
|
253
|
+
this.sender = undefined;
|
|
205
254
|
}
|
|
206
255
|
}
|
package/dist/terminal.js
CHANGED
|
@@ -103,6 +103,8 @@ export class TerminalManager {
|
|
|
103
103
|
if (terminal.binary) {
|
|
104
104
|
const pipe = this.pipeFactory(deferred.shell, deferred.args, deferred.options);
|
|
105
105
|
terminal.pipe = pipe;
|
|
106
|
+
if (terminal.outputPaused)
|
|
107
|
+
pipe.pause?.();
|
|
106
108
|
pipe.onData((data) => this.recordBinaryOutput(terminal, data));
|
|
107
109
|
pipe.onExit((exitCode) => this.exited(terminal, exitCode));
|
|
108
110
|
}
|
|
@@ -204,6 +206,7 @@ export class TerminalManager {
|
|
|
204
206
|
this.attachments.set(attachmentId, { terminalId: terminal.id, readonly });
|
|
205
207
|
if (!readonly)
|
|
206
208
|
terminal.writerAttachmentId = attachmentId;
|
|
209
|
+
this.updateOutputPause(terminal);
|
|
207
210
|
terminal.updatedAt = this.now();
|
|
208
211
|
terminal.lastUsedAt = terminal.updatedAt;
|
|
209
212
|
terminal.detachedAt = undefined;
|
|
@@ -238,6 +241,8 @@ export class TerminalManager {
|
|
|
238
241
|
const terminal = this.terminals.get(attachment.terminalId);
|
|
239
242
|
if (terminal?.writerAttachmentId === attachmentId)
|
|
240
243
|
terminal.writerAttachmentId = undefined;
|
|
244
|
+
if (terminal !== undefined)
|
|
245
|
+
this.updateOutputPause(terminal);
|
|
241
246
|
if (terminal !== undefined && !this.hasAttachment(terminal.id)) {
|
|
242
247
|
terminal.detachedAt = this.now();
|
|
243
248
|
terminal.lastUsedAt = terminal.detachedAt;
|
|
@@ -324,6 +329,30 @@ export class TerminalManager {
|
|
|
324
329
|
this.frameListeners.add(listener);
|
|
325
330
|
return () => this.frameListeners.delete(listener);
|
|
326
331
|
}
|
|
332
|
+
/** A relay can pause only its own pipe writer, never an unrelated direct writer or PTY. */
|
|
333
|
+
setOutputBackpressure(terminalId, attachmentIds) {
|
|
334
|
+
const terminal = this.terminals.get(terminalId);
|
|
335
|
+
if (terminal === undefined)
|
|
336
|
+
return;
|
|
337
|
+
if (attachmentIds.size === 0)
|
|
338
|
+
delete terminal.outputBackpressureAttachments;
|
|
339
|
+
else
|
|
340
|
+
terminal.outputBackpressureAttachments = new Set(attachmentIds);
|
|
341
|
+
this.updateOutputPause(terminal);
|
|
342
|
+
}
|
|
343
|
+
updateOutputPause(terminal) {
|
|
344
|
+
const paused = terminal.state === 'RUNNING' &&
|
|
345
|
+
terminal.binary &&
|
|
346
|
+
terminal.writerAttachmentId !== undefined &&
|
|
347
|
+
terminal.outputBackpressureAttachments?.has(terminal.writerAttachmentId) === true;
|
|
348
|
+
if (Boolean(terminal.outputPaused) === paused)
|
|
349
|
+
return;
|
|
350
|
+
terminal.outputPaused = paused;
|
|
351
|
+
if (paused)
|
|
352
|
+
terminal.pipe?.pause?.();
|
|
353
|
+
else
|
|
354
|
+
terminal.pipe?.resume?.();
|
|
355
|
+
}
|
|
327
356
|
onSummary(listener) {
|
|
328
357
|
this.summaryListeners.add(listener);
|
|
329
358
|
return () => this.summaryListeners.delete(listener);
|
|
@@ -468,6 +497,8 @@ export class TerminalManager {
|
|
|
468
497
|
this.attachments.delete(attachmentId);
|
|
469
498
|
}
|
|
470
499
|
terminal.writerAttachmentId = undefined;
|
|
500
|
+
delete terminal.outputBackpressureAttachments;
|
|
501
|
+
this.updateOutputPause(terminal);
|
|
471
502
|
terminal.detachedAt = undefined;
|
|
472
503
|
}
|
|
473
504
|
emit(frame) {
|
|
@@ -498,6 +529,8 @@ function nodePipeFactory(shell, args, options) {
|
|
|
498
529
|
return {
|
|
499
530
|
pid: child.pid,
|
|
500
531
|
kill: (signal) => child.kill(signal),
|
|
532
|
+
pause: () => child.stdout.pause(),
|
|
533
|
+
resume: () => child.stdout.resume(),
|
|
501
534
|
write: (bytes) => {
|
|
502
535
|
if (child.stdin.destroyed)
|
|
503
536
|
throw new Error('TERMINAL_PIPE_CLOSED');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.167",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"playwright": "1.62.1",
|
|
37
37
|
"ws": "^8.21.3",
|
|
38
38
|
"zod": "4.4.3",
|
|
39
|
-
"@myagentroam/agent": "0.9.
|
|
39
|
+
"@myagentroam/agent": "0.9.167",
|
|
40
40
|
"@myagentroam/code-navigation": "0.9.129",
|
|
41
|
-
"@myagentroam/
|
|
42
|
-
"@myagentroam/
|
|
41
|
+
"@myagentroam/protocol": "0.9.167",
|
|
42
|
+
"@myagentroam/marctl": "0.9.121"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/ws": "^8.18.1"
|