@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 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
@@ -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 pty = this.ptyFactory(profile.shell, profile.args, {
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.onData((data) => this.recordOutput(terminal, data));
125
- pty.onExit((event) => this.exited(terminal, event.exitCode));
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. A new writer must explicitly take over. */
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 && typeof input.takeover !== 'boolean')
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
- 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
- }
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
- 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: [] };
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
- await this.terminate(terminal.pty, this.platform);
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 !== undefined)
377
- void this.close(candidate.id);
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.110",
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.110",
34
- "@myagentroam/code-navigation": "0.9.110",
35
- "@myagentroam/protocol": "0.9.110"
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"