@myagentroam/node 0.9.110 → 0.9.112
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 +58 -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
|
@@ -10,7 +10,6 @@ const DEFAULT_MAX_TERMINALS = 10;
|
|
|
10
10
|
const DEFAULT_MAX_TERMINALS_PER_WORKSPACE = 5;
|
|
11
11
|
const DEFAULT_MAX_REPLAY_BYTES = 512 * 1024;
|
|
12
12
|
const DEFAULT_IDLE_TIMEOUT_MS = 60 * 60_000;
|
|
13
|
-
const MAX_REPLACED_ATTACHMENTS = 1_024;
|
|
14
13
|
/**
|
|
15
14
|
* Owns Node-local PTY/ConPTY processes and their bounded output replay.
|
|
16
15
|
* Browser relay transport intentionally lives outside this class: callers pass
|
|
@@ -29,7 +28,6 @@ export class TerminalManager {
|
|
|
29
28
|
terminate;
|
|
30
29
|
terminals = new Map();
|
|
31
30
|
attachments = new Map();
|
|
32
|
-
replacedAttachments = new Map();
|
|
33
31
|
frameListeners = new Set();
|
|
34
32
|
summaryListeners = new Set();
|
|
35
33
|
idleTimer;
|
|
@@ -79,6 +77,35 @@ export class TerminalManager {
|
|
|
79
77
|
};
|
|
80
78
|
}
|
|
81
79
|
open(input = {}) {
|
|
80
|
+
return this.openInternal(input);
|
|
81
|
+
}
|
|
82
|
+
/** A command starts only after a relay or direct Terminal attachment is active. */
|
|
83
|
+
openCommand(input, command) {
|
|
84
|
+
if (typeof command !== 'string' ||
|
|
85
|
+
command.trim().length === 0 ||
|
|
86
|
+
Buffer.byteLength(command, 'utf8') > 8192 ||
|
|
87
|
+
command.includes('\0'))
|
|
88
|
+
throw new Error('TERMINAL_COMMAND_INVALID');
|
|
89
|
+
return this.openInternal(input, command);
|
|
90
|
+
}
|
|
91
|
+
startCommand(terminalId) {
|
|
92
|
+
const terminal = this.requireTerminal(terminalId);
|
|
93
|
+
const deferred = terminal.deferredCommand;
|
|
94
|
+
if (terminal.state !== 'RUNNING' || deferred === undefined)
|
|
95
|
+
return;
|
|
96
|
+
delete terminal.deferredCommand;
|
|
97
|
+
try {
|
|
98
|
+
const pty = this.ptyFactory(deferred.shell, deferred.args, deferred.options);
|
|
99
|
+
terminal.pty = pty;
|
|
100
|
+
pty.onData((data) => this.recordOutput(terminal, data));
|
|
101
|
+
pty.onExit((event) => this.exited(terminal, event.exitCode));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.emit({ type: 'error', terminalId, code: 'TERMINAL_COMMAND_START_FAILED' });
|
|
105
|
+
this.exited(terminal, 127);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
openInternal(input = {}, command) {
|
|
82
109
|
const profileId = input.profileId ?? this.profiles.find((profile) => profile.isDefault)?.id;
|
|
83
110
|
if (typeof profileId !== 'string')
|
|
84
111
|
throw new Error('TERMINAL_PROFILE_INVALID');
|
|
@@ -94,19 +121,30 @@ export class TerminalManager {
|
|
|
94
121
|
typeof input.workspaceId !== 'string')
|
|
95
122
|
throw new Error('TERMINAL_OPEN_INVALID');
|
|
96
123
|
this.evictForNewTerminal(input.workspaceId ?? null);
|
|
97
|
-
const
|
|
124
|
+
const options = {
|
|
98
125
|
name: 'xterm-256color',
|
|
99
126
|
cols,
|
|
100
127
|
rows,
|
|
101
128
|
cwd: typeof input.cwd === 'string' ? input.cwd : this.cwd,
|
|
102
129
|
env: { ...process.env, ...input.environment }
|
|
103
|
-
}
|
|
130
|
+
};
|
|
131
|
+
const commandArgs = command === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: this.platform === 'win32'
|
|
134
|
+
? profile.shell.toLowerCase().endsWith('cmd.exe')
|
|
135
|
+
? ['/D', '/C', command]
|
|
136
|
+
: ['-NoLogo', '-NoProfile', '-Command', command]
|
|
137
|
+
: ['-c', command];
|
|
138
|
+
const pty = commandArgs === undefined ? this.ptyFactory(profile.shell, profile.args, options) : undefined;
|
|
104
139
|
const createdAt = this.now();
|
|
105
140
|
const terminal = {
|
|
106
141
|
id: randomUUID(),
|
|
107
142
|
workspaceId: input.workspaceId ?? null,
|
|
108
143
|
profile,
|
|
109
144
|
pty,
|
|
145
|
+
...(commandArgs === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: { deferredCommand: { shell: profile.shell, args: commandArgs, options } }),
|
|
110
148
|
createdAt,
|
|
111
149
|
state: 'RUNNING',
|
|
112
150
|
updatedAt: createdAt,
|
|
@@ -121,8 +159,8 @@ export class TerminalManager {
|
|
|
121
159
|
detachedAt: createdAt
|
|
122
160
|
};
|
|
123
161
|
this.terminals.set(terminal.id, terminal);
|
|
124
|
-
pty
|
|
125
|
-
pty
|
|
162
|
+
pty?.onData((data) => this.recordOutput(terminal, data));
|
|
163
|
+
pty?.onExit((event) => this.exited(terminal, event.exitCode));
|
|
126
164
|
return this.notifySummary(terminal);
|
|
127
165
|
}
|
|
128
166
|
list() {
|
|
@@ -131,7 +169,7 @@ export class TerminalManager {
|
|
|
131
169
|
get(terminalId) {
|
|
132
170
|
return this.summary(this.requireTerminal(terminalId));
|
|
133
171
|
}
|
|
134
|
-
/** One attachment may write
|
|
172
|
+
/** One attachment may write; active writers cannot be replaced. */
|
|
135
173
|
authorizeAttach(input) {
|
|
136
174
|
if (typeof input.terminalId !== 'string')
|
|
137
175
|
throw new Error('TERMINAL_NOT_FOUND');
|
|
@@ -142,22 +180,10 @@ export class TerminalManager {
|
|
|
142
180
|
const readonly = input.readonly === true;
|
|
143
181
|
if (input.readonly !== undefined && typeof input.readonly !== 'boolean')
|
|
144
182
|
throw new Error('TERMINAL_ATTACH_INVALID');
|
|
145
|
-
if (input.takeover !== undefined &&
|
|
183
|
+
if (input.takeover !== undefined && input.takeover !== false)
|
|
146
184
|
throw new Error('TERMINAL_ATTACH_INVALID');
|
|
147
|
-
if (!readonly && terminal.writerAttachmentId !== undefined && input.takeover !== true) {
|
|
148
|
-
throw new Error('TERMINAL_ATTACH_CONFLICT');
|
|
149
|
-
}
|
|
150
185
|
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
|
-
}
|
|
186
|
+
throw new Error('TERMINAL_ATTACH_CONFLICT');
|
|
161
187
|
}
|
|
162
188
|
const attachmentId = randomUUID();
|
|
163
189
|
this.attachments.set(attachmentId, { terminalId: terminal.id, readonly });
|
|
@@ -178,11 +204,7 @@ export class TerminalManager {
|
|
|
178
204
|
synchronizeAttachment(input) {
|
|
179
205
|
const attachment = this.attachments.get(input.attachmentId);
|
|
180
206
|
if (attachment === undefined || attachment.terminalId !== input.terminalId) {
|
|
181
|
-
|
|
182
|
-
? 'REPLACED'
|
|
183
|
-
: 'INACTIVE';
|
|
184
|
-
this.replacedAttachments.delete(input.attachmentId);
|
|
185
|
-
return { active: false, reason, frames: [] };
|
|
207
|
+
return { active: false, reason: 'INACTIVE', frames: [] };
|
|
186
208
|
}
|
|
187
209
|
const terminal = this.terminals.get(attachment.terminalId);
|
|
188
210
|
if (terminal === undefined || terminal.state !== 'RUNNING') {
|
|
@@ -194,7 +216,6 @@ export class TerminalManager {
|
|
|
194
216
|
};
|
|
195
217
|
}
|
|
196
218
|
detach(attachmentId) {
|
|
197
|
-
this.replacedAttachments.delete(attachmentId);
|
|
198
219
|
const attachment = this.attachments.get(attachmentId);
|
|
199
220
|
if (attachment === undefined)
|
|
200
221
|
return;
|
|
@@ -230,10 +251,14 @@ export class TerminalManager {
|
|
|
230
251
|
terminal.updatedAt = this.now();
|
|
231
252
|
terminal.lastUsedAt = terminal.updatedAt;
|
|
232
253
|
if (parsed.data.type === 'input') {
|
|
254
|
+
if (terminal.pty === undefined)
|
|
255
|
+
throw new Error('TERMINAL_COMMAND_NOT_STARTED');
|
|
233
256
|
terminal.pty.write(parsed.data.data);
|
|
234
257
|
return;
|
|
235
258
|
}
|
|
236
259
|
if (parsed.data.type === 'resize') {
|
|
260
|
+
if (terminal.pty === undefined)
|
|
261
|
+
throw new Error('TERMINAL_COMMAND_NOT_STARTED');
|
|
237
262
|
terminal.pty.resize(parsed.data.cols, parsed.data.rows);
|
|
238
263
|
terminal.cols = parsed.data.cols;
|
|
239
264
|
terminal.rows = parsed.data.rows;
|
|
@@ -251,7 +276,8 @@ export class TerminalManager {
|
|
|
251
276
|
this.detachAll(terminal);
|
|
252
277
|
const summary = this.notifySummary(terminal);
|
|
253
278
|
try {
|
|
254
|
-
|
|
279
|
+
if (terminal.pty !== undefined)
|
|
280
|
+
await this.terminate(terminal.pty, this.platform);
|
|
255
281
|
}
|
|
256
282
|
finally {
|
|
257
283
|
this.emit({ type: 'exit', terminalId: terminal.id, exitCode: terminal.exitCode });
|
|
@@ -371,10 +397,11 @@ export class TerminalManager {
|
|
|
371
397
|
}
|
|
372
398
|
evictLeastRecentlyUsed(predicate) {
|
|
373
399
|
const candidate = [...this.terminals.values()]
|
|
374
|
-
.filter((terminal) => terminal.state === 'RUNNING' && predicate(terminal))
|
|
400
|
+
.filter((terminal) => terminal.state === 'RUNNING' && predicate(terminal) && !this.hasAttachment(terminal.id))
|
|
375
401
|
.sort((left, right) => left.lastUsedAt - right.lastUsedAt || left.createdAt - right.createdAt)[0];
|
|
376
|
-
if (candidate
|
|
377
|
-
|
|
402
|
+
if (candidate === undefined)
|
|
403
|
+
throw new Error('TERMINAL_LIMIT_REACHED');
|
|
404
|
+
void this.close(candidate.id);
|
|
378
405
|
}
|
|
379
406
|
runningCountForWorkspace(workspaceId) {
|
|
380
407
|
return [...this.terminals.values()].filter((terminal) => terminal.state === 'RUNNING' && terminal.workspaceId === workspaceId).length;
|
|
@@ -387,10 +414,6 @@ export class TerminalManager {
|
|
|
387
414
|
if (attachment.terminalId === terminal.id)
|
|
388
415
|
this.attachments.delete(attachmentId);
|
|
389
416
|
}
|
|
390
|
-
for (const [attachmentId, terminalId] of this.replacedAttachments) {
|
|
391
|
-
if (terminalId === terminal.id)
|
|
392
|
-
this.replacedAttachments.delete(attachmentId);
|
|
393
|
-
}
|
|
394
417
|
terminal.writerAttachmentId = undefined;
|
|
395
418
|
terminal.detachedAt = undefined;
|
|
396
419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.112",
|
|
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.112",
|
|
34
|
+
"@myagentroam/protocol": "0.9.112",
|
|
35
|
+
"@myagentroam/code-navigation": "0.9.112"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/ws": "^8.18.1"
|