@myagentroam/node 0.9.111 → 0.9.113
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 +1 -1
- package/dist/service/direct-transfer-service.js +1 -1
- package/dist/service/mission-runtime.js +30 -7
- package/dist/service/node-terminal-channel.js +2 -3
- package/dist/service/session-message-service.js +3 -2
- package/dist/service/terminal-direct-service.js +7 -16
- package/dist/service/terminal-service.js +25 -15
- package/dist/service/workbench-manifest-service.js +1 -1
- package/dist/terminal.js +59 -35
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -907,6 +907,7 @@ export class NodeConnector {
|
|
|
907
907
|
sessionId: attempt.sessionId,
|
|
908
908
|
kind: 'start',
|
|
909
909
|
content: this.missionRuntime.prompt(attempt.sessionId, 'start'),
|
|
910
|
+
objective: mission.objective,
|
|
910
911
|
deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
|
|
911
912
|
mission: missionRef,
|
|
912
913
|
...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
|
|
@@ -1459,7 +1460,6 @@ export class NodeConnector {
|
|
|
1459
1460
|
send: (type, payload) => this.send(type, payload),
|
|
1460
1461
|
terminalManager: this.terminalManager,
|
|
1461
1462
|
sendTerminalReady: (directSessionId) => this.send('terminal.direct.ready', { directSessionId }),
|
|
1462
|
-
sendTerminalTakeover: (directSessionId) => this.send('terminal.direct.takeover', { directSessionId }),
|
|
1463
1463
|
udpPortRange: () => directTransferUdpPortRange(this.config),
|
|
1464
1464
|
openUpload: async (descriptor) => {
|
|
1465
1465
|
if (descriptor.direction !== 'UPLOAD' || descriptor.resource.kind !== 'WORKSPACE_FILE')
|
|
@@ -40,7 +40,7 @@ export class DirectTransferService {
|
|
|
40
40
|
this.leaseGraceMs = options.leaseGraceMs ?? LEASE_GRACE_MS;
|
|
41
41
|
this.leaseCheckMs = options.leaseCheckMs ?? LEASE_CHECK_MS;
|
|
42
42
|
if (options.terminalManager !== undefined)
|
|
43
|
-
this.terminalDirect = new TerminalDirectService(options.terminalManager, (id) => options.sendTerminalReady?.(id), (
|
|
43
|
+
this.terminalDirect = new TerminalDirectService(options.terminalManager, (id) => options.sendTerminalReady?.(id), (command, peer) => {
|
|
44
44
|
const transport = this.transports.get(command.transportSessionId);
|
|
45
45
|
return (transport?.peer === peer &&
|
|
46
46
|
transport.command.workbenchConnectionId === command.workbenchConnectionId &&
|
|
@@ -8,21 +8,42 @@ export function missionUnrestrictedAccess(runner) {
|
|
|
8
8
|
}
|
|
9
9
|
const START_OPEN = '<mar_mission_start>';
|
|
10
10
|
const START_CLOSE = '</mar_mission_start>';
|
|
11
|
+
const CONTINUE_OPEN = '<mar_mission_continue>';
|
|
12
|
+
const CONTINUE_CLOSE = '</mar_mission_continue>';
|
|
13
|
+
function missionPromptTags(text) {
|
|
14
|
+
if (text.startsWith(START_OPEN))
|
|
15
|
+
return { open: START_OPEN, close: START_CLOSE };
|
|
16
|
+
if (text.startsWith(CONTINUE_OPEN))
|
|
17
|
+
return { open: CONTINUE_OPEN, close: CONTINUE_CLOSE };
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
export function publicMissionStartText(objective) {
|
|
21
|
+
const normalized = objective.trim();
|
|
22
|
+
return normalized.length === 0 ? 'Mission Start' : `Mission Start\n${normalized}`;
|
|
23
|
+
}
|
|
11
24
|
export function stripMissionStartPrompt(text) {
|
|
12
|
-
|
|
25
|
+
const tags = missionPromptTags(text);
|
|
26
|
+
if (tags === undefined)
|
|
13
27
|
return text;
|
|
14
|
-
const close = text.
|
|
15
|
-
return close === -1 ? '' : text.slice(close +
|
|
28
|
+
const close = text.lastIndexOf(tags.close);
|
|
29
|
+
return close === -1 ? '' : text.slice(close + tags.close.length).trimStart();
|
|
16
30
|
}
|
|
17
31
|
export function publicMissionHistoryText(text) {
|
|
18
|
-
|
|
32
|
+
const tags = missionPromptTags(text);
|
|
33
|
+
if (tags === undefined)
|
|
19
34
|
return text;
|
|
20
35
|
const stripped = stripMissionStartPrompt(text);
|
|
21
36
|
if (stripped.length > 0)
|
|
22
37
|
return stripped;
|
|
23
|
-
|
|
38
|
+
if (tags.open === CONTINUE_OPEN)
|
|
39
|
+
return 'Mission Continue';
|
|
40
|
+
const close = text.lastIndexOf(tags.close);
|
|
24
41
|
const prompt = close === -1 ? text : text.slice(0, close);
|
|
25
|
-
|
|
42
|
+
const objectiveStart = prompt.indexOf('\nOriginal objective:\n');
|
|
43
|
+
const objectiveEnd = prompt.lastIndexOf('\n\nContinue executing this Mission across turns:');
|
|
44
|
+
return publicMissionStartText(objectiveStart === -1 || objectiveEnd === -1
|
|
45
|
+
? ''
|
|
46
|
+
: prompt.slice(objectiveStart + '\nOriginal objective:\n'.length, objectiveEnd));
|
|
26
47
|
}
|
|
27
48
|
export class MissionRuntime {
|
|
28
49
|
changed;
|
|
@@ -187,7 +208,9 @@ export class MissionRuntime {
|
|
|
187
208
|
if (mission?.status !== 'active')
|
|
188
209
|
throw new Error('MISSION_NOT_ACTIVE');
|
|
189
210
|
const heading = kind === 'start' ? 'A Mission has started or changed.' : 'Continue the active Mission.';
|
|
190
|
-
|
|
211
|
+
const open = kind === 'start' ? START_OPEN : CONTINUE_OPEN;
|
|
212
|
+
const close = kind === 'start' ? START_CLOSE : CONTINUE_CLOSE;
|
|
213
|
+
return `${open}\n${heading}\n\nOriginal objective:\n${mission.objective}\n\nContinue executing this Mission across turns:\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_mission with status=completed.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_mission only when you need to reread the authoritative objective or status.\n${close}`;
|
|
191
214
|
}
|
|
192
215
|
requireCurrent(sessionId, missionId, revision) {
|
|
193
216
|
const current = this.missions.get(sessionId);
|
|
@@ -101,9 +101,7 @@ export class NodeTerminalChannel {
|
|
|
101
101
|
this.send({
|
|
102
102
|
type: 'detach',
|
|
103
103
|
ticket: message.ticket,
|
|
104
|
-
reason:
|
|
105
|
-
? 'TERMINAL_RELAY_REPLACED'
|
|
106
|
-
: 'TERMINAL_ATTACH_FAILED'
|
|
104
|
+
reason: 'TERMINAL_ATTACH_FAILED'
|
|
107
105
|
});
|
|
108
106
|
return;
|
|
109
107
|
}
|
|
@@ -114,6 +112,7 @@ export class NodeTerminalChannel {
|
|
|
114
112
|
});
|
|
115
113
|
for (const frame of synchronization.frames)
|
|
116
114
|
this.sendFrame(message.ticket, frame);
|
|
115
|
+
this.options.manager.startCommand(message.terminalId);
|
|
117
116
|
return;
|
|
118
117
|
}
|
|
119
118
|
if (message.type === 'detach' || message.type === 'close') {
|
|
@@ -3,6 +3,7 @@ import { isImageAttachment, isRunnerImageAttachment, parseComposerAttachmentIds,
|
|
|
3
3
|
import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
4
4
|
import { personalInstructionsForRunner } from '../util/personal-instructions.js';
|
|
5
5
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
6
|
+
import { publicMissionStartText } from './mission-runtime.js';
|
|
6
7
|
const INTERNAL_MISSION = Symbol('internalMission');
|
|
7
8
|
export class SessionMessageService {
|
|
8
9
|
options;
|
|
@@ -22,7 +23,7 @@ export class SessionMessageService {
|
|
|
22
23
|
[INTERNAL_MISSION]: true,
|
|
23
24
|
sessionId: input.sessionId,
|
|
24
25
|
clientMessageId: `mission:${randomUUID()}`,
|
|
25
|
-
content: input.kind === 'start' ?
|
|
26
|
+
content: input.kind === 'start' ? publicMissionStartText(input.objective ?? '') : 'Mission Continue',
|
|
26
27
|
__runnerContent: input.content,
|
|
27
28
|
__missionRef: input.mission,
|
|
28
29
|
...(input.initiatedByUserId === undefined
|
|
@@ -85,7 +86,7 @@ export class SessionMessageService {
|
|
|
85
86
|
? undefined
|
|
86
87
|
: this.options.missionMessage(session, content);
|
|
87
88
|
const runnerContent = missionMessage?.runnerContent ?? internalRunnerContent;
|
|
88
|
-
const publicContent = missionMessage?.activated === true ?
|
|
89
|
+
const publicContent = missionMessage?.activated === true ? publicMissionStartText(content) : content;
|
|
89
90
|
const intent = missionMessage === undefined ? requestedIntent : 'REPLACE_CURRENT';
|
|
90
91
|
const operation = (async () => {
|
|
91
92
|
const acceptedSession = accepted.session;
|
|
@@ -6,16 +6,14 @@ const HEARTBEAT_GRACE_MS = 30_000;
|
|
|
6
6
|
export class TerminalDirectService {
|
|
7
7
|
manager;
|
|
8
8
|
sendReady;
|
|
9
|
-
sendTakeover;
|
|
10
9
|
verifyTransport;
|
|
11
10
|
sessions = new Map();
|
|
12
11
|
timer;
|
|
13
12
|
stopListening;
|
|
14
13
|
now;
|
|
15
|
-
constructor(manager, sendReady,
|
|
14
|
+
constructor(manager, sendReady, verifyTransport, now = Date.now) {
|
|
16
15
|
this.manager = manager;
|
|
17
16
|
this.sendReady = sendReady;
|
|
18
|
-
this.sendTakeover = sendTakeover;
|
|
19
17
|
this.verifyTransport = verifyTransport;
|
|
20
18
|
this.now = now;
|
|
21
19
|
this.stopListening = manager.onFrame((frame) => {
|
|
@@ -103,8 +101,7 @@ export class TerminalDirectService {
|
|
|
103
101
|
const attachment = this.manager.authorizeAttach({
|
|
104
102
|
terminalId: session.command.terminalId,
|
|
105
103
|
afterSequence: session.command.afterSequence,
|
|
106
|
-
readonly: session.command.readonly
|
|
107
|
-
takeover: session.command.takeover
|
|
104
|
+
readonly: session.command.readonly
|
|
108
105
|
});
|
|
109
106
|
session.attachmentId = attachment.attachmentId;
|
|
110
107
|
session.opened = true;
|
|
@@ -115,19 +112,13 @@ export class TerminalDirectService {
|
|
|
115
112
|
}));
|
|
116
113
|
for (const frame of attachment.frames)
|
|
117
114
|
this.sendFrame(session, frame);
|
|
118
|
-
if (!session.command.readonly && session.command.takeover) {
|
|
119
|
-
for (const other of [...this.sessions.values()])
|
|
120
|
-
if (other !== session &&
|
|
121
|
-
other.opened &&
|
|
122
|
-
!other.command.readonly &&
|
|
123
|
-
other.command.terminalId === session.command.terminalId)
|
|
124
|
-
this.remove(other, 'TERMINAL_RELAY_REPLACED');
|
|
125
|
-
this.sendTakeover(session.command.directSessionId);
|
|
126
|
-
}
|
|
127
115
|
channel.onmessage = (event) => this.accept(session, event.data);
|
|
116
|
+
this.manager.startCommand(session.command.terminalId);
|
|
128
117
|
}
|
|
129
|
-
catch {
|
|
130
|
-
this.remove(session, '
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.remove(session, error instanceof Error && error.message === 'TERMINAL_ATTACH_CONFLICT'
|
|
120
|
+
? 'TERMINAL_ATTACH_CONFLICT'
|
|
121
|
+
: 'TERMINAL_ATTACH_FAILED');
|
|
131
122
|
}
|
|
132
123
|
}
|
|
133
124
|
removeTransport(transportId) {
|
|
@@ -3,26 +3,27 @@ export class TerminalService {
|
|
|
3
3
|
database;
|
|
4
4
|
terminalManager;
|
|
5
5
|
retainEnvironment;
|
|
6
|
-
|
|
6
|
+
terminalOwners = new Map();
|
|
7
7
|
constructor(database, terminalManager, retainEnvironment) {
|
|
8
8
|
this.database = database;
|
|
9
9
|
this.terminalManager = terminalManager;
|
|
10
10
|
this.retainEnvironment = retainEnvironment;
|
|
11
11
|
terminalManager.onSummary((summary) => {
|
|
12
12
|
if (summary.state !== 'RUNNING') {
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
13
|
+
this.terminalOwners.get(summary.id)?.release();
|
|
14
|
+
this.terminalOwners.delete(summary.id);
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
operations() {
|
|
19
19
|
return {
|
|
20
20
|
'terminal.open': (data) => this.open(data),
|
|
21
|
+
'terminal.command.open': (data) => this.open(data, true),
|
|
21
22
|
'terminal.list': (data) => ({
|
|
22
23
|
terminals: this.database()
|
|
23
24
|
.listTerminalSessions()
|
|
24
|
-
.filter((terminal) => !this.
|
|
25
|
-
this.
|
|
25
|
+
.filter((terminal) => !this.terminalOwners.has(terminal.id) ||
|
|
26
|
+
this.terminalOwners.get(terminal.id)?.userId ===
|
|
26
27
|
data?.__requestUserId)
|
|
27
28
|
}),
|
|
28
29
|
'terminal.close': (data) => this.close(data),
|
|
@@ -30,13 +31,18 @@ export class TerminalService {
|
|
|
30
31
|
'terminal.direct.preflight': (data) => this.preflightDirect(data)
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
open(data) {
|
|
34
|
+
open(data, commandMode = false) {
|
|
34
35
|
if (data === null || typeof data !== 'object')
|
|
35
36
|
throw new Error('TERMINAL_OPEN_INVALID');
|
|
36
37
|
const input = data;
|
|
38
|
+
if (commandMode &&
|
|
39
|
+
(input.workspaceId !== undefined ||
|
|
40
|
+
typeof input.command !== 'string' ||
|
|
41
|
+
input.command.trim().length === 0))
|
|
42
|
+
throw new Error('TERMINAL_COMMAND_INVALID');
|
|
37
43
|
const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
|
|
38
|
-
if (secretEnvironment.MAR_GIT_TOKEN !== undefined &&
|
|
39
|
-
!Number.isSafeInteger(input.__requestUserId))
|
|
44
|
+
if ((commandMode || secretEnvironment.MAR_GIT_TOKEN !== undefined) &&
|
|
45
|
+
(!Number.isSafeInteger(input.__requestUserId) || Number(input.__requestUserId) <= 0))
|
|
40
46
|
throw new Error('USER_AUTHORIZATION_REQUIRED');
|
|
41
47
|
let cwd;
|
|
42
48
|
if (input.workspaceId !== undefined) {
|
|
@@ -49,16 +55,20 @@ export class TerminalService {
|
|
|
49
55
|
}
|
|
50
56
|
const release = this.retainEnvironment?.(secretEnvironment);
|
|
51
57
|
try {
|
|
52
|
-
const
|
|
58
|
+
const options = {
|
|
53
59
|
...input,
|
|
54
60
|
workspaceId: typeof input.workspaceId === 'string' ? input.workspaceId : null,
|
|
55
61
|
environment: secretEnvironment,
|
|
56
62
|
...(cwd === undefined ? {} : { cwd })
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this.
|
|
63
|
+
};
|
|
64
|
+
const terminal = commandMode
|
|
65
|
+
? this.terminalManager.openCommand(options, input.command)
|
|
66
|
+
: this.terminalManager.open(options);
|
|
67
|
+
if (commandMode || secretEnvironment.MAR_GIT_TOKEN !== undefined)
|
|
68
|
+
this.terminalOwners.set(terminal.id, {
|
|
60
69
|
userId: Number(input.__requestUserId),
|
|
61
|
-
release: release ?? (() => undefined)
|
|
70
|
+
release: release ?? (() => undefined),
|
|
71
|
+
kind: commandMode ? 'COMMAND' : 'GIT'
|
|
62
72
|
});
|
|
63
73
|
else
|
|
64
74
|
release?.();
|
|
@@ -96,8 +106,8 @@ export class TerminalService {
|
|
|
96
106
|
return { terminalId: terminal.id, workspaceId: terminal.workspaceId };
|
|
97
107
|
}
|
|
98
108
|
assertOwner(input) {
|
|
99
|
-
const owner = this.
|
|
109
|
+
const owner = this.terminalOwners.get(String(input.terminalId));
|
|
100
110
|
if (owner !== undefined && owner.userId !== input.__requestUserId)
|
|
101
|
-
throw new Error('TERMINAL_GIT_OWNER_REQUIRED');
|
|
111
|
+
throw new Error(owner.kind === 'COMMAND' ? 'TERMINAL_OWNER_REQUIRED' : 'TERMINAL_GIT_OWNER_REQUIRED');
|
|
102
112
|
}
|
|
103
113
|
}
|
|
@@ -63,7 +63,7 @@ export class WorkbenchManifestService {
|
|
|
63
63
|
supportsConPty: capabilities.platform === 'windows',
|
|
64
64
|
supportsReplay: true,
|
|
65
65
|
supportsReadonlyAttach: true,
|
|
66
|
-
supportsTakeover:
|
|
66
|
+
supportsTakeover: false,
|
|
67
67
|
...this.options.terminals.limits(),
|
|
68
68
|
profiles: this.options.terminals.availableProfiles()
|
|
69
69
|
}
|
package/dist/terminal.js
CHANGED
|
@@ -6,11 +6,11 @@ import { spawn } from 'node-pty';
|
|
|
6
6
|
import { terminalDataFrameSchema } from '@myagentroam/protocol';
|
|
7
7
|
import { terminateProcessTree } from './process-tree.js';
|
|
8
8
|
const MAX_FRAME_CHARACTERS = 65_536;
|
|
9
|
+
const TERMINAL_COMMAND_MAX_BYTES = 64 * 1024;
|
|
9
10
|
const DEFAULT_MAX_TERMINALS = 10;
|
|
10
11
|
const DEFAULT_MAX_TERMINALS_PER_WORKSPACE = 5;
|
|
11
12
|
const DEFAULT_MAX_REPLAY_BYTES = 512 * 1024;
|
|
12
13
|
const DEFAULT_IDLE_TIMEOUT_MS = 60 * 60_000;
|
|
13
|
-
const MAX_REPLACED_ATTACHMENTS = 1_024;
|
|
14
14
|
/**
|
|
15
15
|
* Owns Node-local PTY/ConPTY processes and their bounded output replay.
|
|
16
16
|
* Browser relay transport intentionally lives outside this class: callers pass
|
|
@@ -29,7 +29,6 @@ export class TerminalManager {
|
|
|
29
29
|
terminate;
|
|
30
30
|
terminals = new Map();
|
|
31
31
|
attachments = new Map();
|
|
32
|
-
replacedAttachments = new Map();
|
|
33
32
|
frameListeners = new Set();
|
|
34
33
|
summaryListeners = new Set();
|
|
35
34
|
idleTimer;
|
|
@@ -79,6 +78,35 @@ export class TerminalManager {
|
|
|
79
78
|
};
|
|
80
79
|
}
|
|
81
80
|
open(input = {}) {
|
|
81
|
+
return this.openInternal(input);
|
|
82
|
+
}
|
|
83
|
+
/** A command starts only after a relay or direct Terminal attachment is active. */
|
|
84
|
+
openCommand(input, command) {
|
|
85
|
+
if (typeof command !== 'string' ||
|
|
86
|
+
command.trim().length === 0 ||
|
|
87
|
+
Buffer.byteLength(command, 'utf8') > TERMINAL_COMMAND_MAX_BYTES ||
|
|
88
|
+
command.includes('\0'))
|
|
89
|
+
throw new Error('TERMINAL_COMMAND_INVALID');
|
|
90
|
+
return this.openInternal(input, command);
|
|
91
|
+
}
|
|
92
|
+
startCommand(terminalId) {
|
|
93
|
+
const terminal = this.requireTerminal(terminalId);
|
|
94
|
+
const deferred = terminal.deferredCommand;
|
|
95
|
+
if (terminal.state !== 'RUNNING' || deferred === undefined)
|
|
96
|
+
return;
|
|
97
|
+
delete terminal.deferredCommand;
|
|
98
|
+
try {
|
|
99
|
+
const pty = this.ptyFactory(deferred.shell, deferred.args, deferred.options);
|
|
100
|
+
terminal.pty = pty;
|
|
101
|
+
pty.onData((data) => this.recordOutput(terminal, data));
|
|
102
|
+
pty.onExit((event) => this.exited(terminal, event.exitCode));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
this.emit({ type: 'error', terminalId, code: 'TERMINAL_COMMAND_START_FAILED' });
|
|
106
|
+
this.exited(terminal, 127);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
openInternal(input = {}, command) {
|
|
82
110
|
const profileId = input.profileId ?? this.profiles.find((profile) => profile.isDefault)?.id;
|
|
83
111
|
if (typeof profileId !== 'string')
|
|
84
112
|
throw new Error('TERMINAL_PROFILE_INVALID');
|
|
@@ -94,19 +122,30 @@ export class TerminalManager {
|
|
|
94
122
|
typeof input.workspaceId !== 'string')
|
|
95
123
|
throw new Error('TERMINAL_OPEN_INVALID');
|
|
96
124
|
this.evictForNewTerminal(input.workspaceId ?? null);
|
|
97
|
-
const
|
|
125
|
+
const options = {
|
|
98
126
|
name: 'xterm-256color',
|
|
99
127
|
cols,
|
|
100
128
|
rows,
|
|
101
129
|
cwd: typeof input.cwd === 'string' ? input.cwd : this.cwd,
|
|
102
130
|
env: { ...process.env, ...input.environment }
|
|
103
|
-
}
|
|
131
|
+
};
|
|
132
|
+
const commandArgs = command === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: this.platform === 'win32'
|
|
135
|
+
? profile.shell.toLowerCase().endsWith('cmd.exe')
|
|
136
|
+
? ['/D', '/C', command]
|
|
137
|
+
: ['-NoLogo', '-NoProfile', '-Command', command]
|
|
138
|
+
: ['-c', command];
|
|
139
|
+
const pty = commandArgs === undefined ? this.ptyFactory(profile.shell, profile.args, options) : undefined;
|
|
104
140
|
const createdAt = this.now();
|
|
105
141
|
const terminal = {
|
|
106
142
|
id: randomUUID(),
|
|
107
143
|
workspaceId: input.workspaceId ?? null,
|
|
108
144
|
profile,
|
|
109
145
|
pty,
|
|
146
|
+
...(commandArgs === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { deferredCommand: { shell: profile.shell, args: commandArgs, options } }),
|
|
110
149
|
createdAt,
|
|
111
150
|
state: 'RUNNING',
|
|
112
151
|
updatedAt: createdAt,
|
|
@@ -121,8 +160,8 @@ export class TerminalManager {
|
|
|
121
160
|
detachedAt: createdAt
|
|
122
161
|
};
|
|
123
162
|
this.terminals.set(terminal.id, terminal);
|
|
124
|
-
pty
|
|
125
|
-
pty
|
|
163
|
+
pty?.onData((data) => this.recordOutput(terminal, data));
|
|
164
|
+
pty?.onExit((event) => this.exited(terminal, event.exitCode));
|
|
126
165
|
return this.notifySummary(terminal);
|
|
127
166
|
}
|
|
128
167
|
list() {
|
|
@@ -131,7 +170,7 @@ export class TerminalManager {
|
|
|
131
170
|
get(terminalId) {
|
|
132
171
|
return this.summary(this.requireTerminal(terminalId));
|
|
133
172
|
}
|
|
134
|
-
/** One attachment may write
|
|
173
|
+
/** One attachment may write; active writers cannot be replaced. */
|
|
135
174
|
authorizeAttach(input) {
|
|
136
175
|
if (typeof input.terminalId !== 'string')
|
|
137
176
|
throw new Error('TERMINAL_NOT_FOUND');
|
|
@@ -142,22 +181,10 @@ export class TerminalManager {
|
|
|
142
181
|
const readonly = input.readonly === true;
|
|
143
182
|
if (input.readonly !== undefined && typeof input.readonly !== 'boolean')
|
|
144
183
|
throw new Error('TERMINAL_ATTACH_INVALID');
|
|
145
|
-
if (input.takeover !== undefined &&
|
|
184
|
+
if (input.takeover !== undefined && input.takeover !== false)
|
|
146
185
|
throw new Error('TERMINAL_ATTACH_INVALID');
|
|
147
|
-
if (!readonly && terminal.writerAttachmentId !== undefined && input.takeover !== true) {
|
|
148
|
-
throw new Error('TERMINAL_ATTACH_CONFLICT');
|
|
149
|
-
}
|
|
150
186
|
if (!readonly && terminal.writerAttachmentId !== undefined) {
|
|
151
|
-
|
|
152
|
-
this.attachments.delete(replacedAttachmentId);
|
|
153
|
-
this.replacedAttachments.delete(replacedAttachmentId);
|
|
154
|
-
this.replacedAttachments.set(replacedAttachmentId, terminal.id);
|
|
155
|
-
while (this.replacedAttachments.size > MAX_REPLACED_ATTACHMENTS) {
|
|
156
|
-
const oldest = this.replacedAttachments.keys().next().value;
|
|
157
|
-
if (oldest === undefined)
|
|
158
|
-
break;
|
|
159
|
-
this.replacedAttachments.delete(oldest);
|
|
160
|
-
}
|
|
187
|
+
throw new Error('TERMINAL_ATTACH_CONFLICT');
|
|
161
188
|
}
|
|
162
189
|
const attachmentId = randomUUID();
|
|
163
190
|
this.attachments.set(attachmentId, { terminalId: terminal.id, readonly });
|
|
@@ -178,11 +205,7 @@ export class TerminalManager {
|
|
|
178
205
|
synchronizeAttachment(input) {
|
|
179
206
|
const attachment = this.attachments.get(input.attachmentId);
|
|
180
207
|
if (attachment === undefined || attachment.terminalId !== input.terminalId) {
|
|
181
|
-
|
|
182
|
-
? 'REPLACED'
|
|
183
|
-
: 'INACTIVE';
|
|
184
|
-
this.replacedAttachments.delete(input.attachmentId);
|
|
185
|
-
return { active: false, reason, frames: [] };
|
|
208
|
+
return { active: false, reason: 'INACTIVE', frames: [] };
|
|
186
209
|
}
|
|
187
210
|
const terminal = this.terminals.get(attachment.terminalId);
|
|
188
211
|
if (terminal === undefined || terminal.state !== 'RUNNING') {
|
|
@@ -194,7 +217,6 @@ export class TerminalManager {
|
|
|
194
217
|
};
|
|
195
218
|
}
|
|
196
219
|
detach(attachmentId) {
|
|
197
|
-
this.replacedAttachments.delete(attachmentId);
|
|
198
220
|
const attachment = this.attachments.get(attachmentId);
|
|
199
221
|
if (attachment === undefined)
|
|
200
222
|
return;
|
|
@@ -230,10 +252,14 @@ export class TerminalManager {
|
|
|
230
252
|
terminal.updatedAt = this.now();
|
|
231
253
|
terminal.lastUsedAt = terminal.updatedAt;
|
|
232
254
|
if (parsed.data.type === 'input') {
|
|
255
|
+
if (terminal.pty === undefined)
|
|
256
|
+
throw new Error('TERMINAL_COMMAND_NOT_STARTED');
|
|
233
257
|
terminal.pty.write(parsed.data.data);
|
|
234
258
|
return;
|
|
235
259
|
}
|
|
236
260
|
if (parsed.data.type === 'resize') {
|
|
261
|
+
if (terminal.pty === undefined)
|
|
262
|
+
throw new Error('TERMINAL_COMMAND_NOT_STARTED');
|
|
237
263
|
terminal.pty.resize(parsed.data.cols, parsed.data.rows);
|
|
238
264
|
terminal.cols = parsed.data.cols;
|
|
239
265
|
terminal.rows = parsed.data.rows;
|
|
@@ -251,7 +277,8 @@ export class TerminalManager {
|
|
|
251
277
|
this.detachAll(terminal);
|
|
252
278
|
const summary = this.notifySummary(terminal);
|
|
253
279
|
try {
|
|
254
|
-
|
|
280
|
+
if (terminal.pty !== undefined)
|
|
281
|
+
await this.terminate(terminal.pty, this.platform);
|
|
255
282
|
}
|
|
256
283
|
finally {
|
|
257
284
|
this.emit({ type: 'exit', terminalId: terminal.id, exitCode: terminal.exitCode });
|
|
@@ -371,10 +398,11 @@ export class TerminalManager {
|
|
|
371
398
|
}
|
|
372
399
|
evictLeastRecentlyUsed(predicate) {
|
|
373
400
|
const candidate = [...this.terminals.values()]
|
|
374
|
-
.filter((terminal) => terminal.state === 'RUNNING' && predicate(terminal))
|
|
401
|
+
.filter((terminal) => terminal.state === 'RUNNING' && predicate(terminal) && !this.hasAttachment(terminal.id))
|
|
375
402
|
.sort((left, right) => left.lastUsedAt - right.lastUsedAt || left.createdAt - right.createdAt)[0];
|
|
376
|
-
if (candidate
|
|
377
|
-
|
|
403
|
+
if (candidate === undefined)
|
|
404
|
+
throw new Error('TERMINAL_LIMIT_REACHED');
|
|
405
|
+
void this.close(candidate.id);
|
|
378
406
|
}
|
|
379
407
|
runningCountForWorkspace(workspaceId) {
|
|
380
408
|
return [...this.terminals.values()].filter((terminal) => terminal.state === 'RUNNING' && terminal.workspaceId === workspaceId).length;
|
|
@@ -387,10 +415,6 @@ export class TerminalManager {
|
|
|
387
415
|
if (attachment.terminalId === terminal.id)
|
|
388
416
|
this.attachments.delete(attachmentId);
|
|
389
417
|
}
|
|
390
|
-
for (const [attachmentId, terminalId] of this.replacedAttachments) {
|
|
391
|
-
if (terminalId === terminal.id)
|
|
392
|
-
this.replacedAttachments.delete(attachmentId);
|
|
393
|
-
}
|
|
394
418
|
terminal.writerAttachmentId = undefined;
|
|
395
419
|
terminal.detachedAt = undefined;
|
|
396
420
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.113",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"npm": "11.12.1",
|
|
31
31
|
"ws": "^8.21.3",
|
|
32
32
|
"zod": "4.4.3",
|
|
33
|
-
"@myagentroam/agent": "0.9.
|
|
34
|
-
"@myagentroam/
|
|
35
|
-
"@myagentroam/
|
|
33
|
+
"@myagentroam/agent": "0.9.113",
|
|
34
|
+
"@myagentroam/code-navigation": "0.9.113",
|
|
35
|
+
"@myagentroam/protocol": "0.9.113"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/ws": "^8.18.1"
|