@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 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), (id) => options.sendTerminalTakeover?.(id), (command, peer) => {
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
- if (!text.startsWith(START_OPEN))
25
+ const tags = missionPromptTags(text);
26
+ if (tags === undefined)
13
27
  return text;
14
- const close = text.indexOf(START_CLOSE);
15
- return close === -1 ? '' : text.slice(close + START_CLOSE.length).trimStart();
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
- if (!text.startsWith(START_OPEN))
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
- const close = text.indexOf(START_CLOSE);
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
- return prompt.includes('Continue the active Mission.') ? 'Mission Continue' : 'Mission Start';
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
- return `${START_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${START_CLOSE}`;
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: synchronization.reason === 'REPLACED'
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' ? 'Mission Start' : 'Mission Continue',
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 ? 'Mission Start' : content;
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, sendTakeover, verifyTransport, now = Date.now) {
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, 'TERMINAL_ATTACH_FAILED');
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
- gitOwners = new Map();
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.gitOwners.get(summary.id)?.release();
14
- this.gitOwners.delete(summary.id);
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.gitOwners.has(terminal.id) ||
25
- this.gitOwners.get(terminal.id)?.userId ===
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 terminal = this.terminalManager.open({
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
- if (secretEnvironment.MAR_GIT_TOKEN !== undefined)
59
- this.gitOwners.set(terminal.id, {
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.gitOwners.get(String(input.terminalId));
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: true,
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 pty = this.ptyFactory(profile.shell, profile.args, {
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.onData((data) => this.recordOutput(terminal, data));
125
- pty.onExit((event) => this.exited(terminal, event.exitCode));
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. A new writer must explicitly take over. */
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 && typeof input.takeover !== 'boolean')
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
- const replacedAttachmentId = terminal.writerAttachmentId;
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
- const reason = this.replacedAttachments.get(input.attachmentId) === input.terminalId
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
- await this.terminate(terminal.pty, this.platform);
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 !== undefined)
377
- void this.close(candidate.id);
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.111",
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.111",
34
- "@myagentroam/protocol": "0.9.111",
35
- "@myagentroam/code-navigation": "0.9.111"
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"