@myagentroam/agent 0.10.61 → 0.10.62

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.
@@ -28,11 +28,7 @@ export function createExtensionAgentsBackend(input) {
28
28
  throw new MarAgentError(created.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED', created.errorMessage ?? 'Agent could not start.');
29
29
  }
30
30
  })),
31
- list: (call) => withController(call, async (controller) => {
32
- await controller.prepareAll(call.signal);
33
- call.signal.throwIfAborted();
34
- return controller.inspectAll();
35
- }),
31
+ list: (call) => withController(call, (controller) => controller.list(call.signal)),
36
32
  inspect: (target, call) => withController(call, (controller) => controller.inspect(target.agentId)),
37
33
  wait: (target, call) => withController(call, (controller) => {
38
34
  const selection = parseAgentWaitTargets(target);
@@ -282,6 +282,7 @@ export function createSessionOpener(services) {
282
282
  let releaseSession;
283
283
  let exhaustedModelAttempt;
284
284
  // Serialize durable events before either the local stream or Host observes them.
285
+ let persistedTerminal = false;
285
286
  const emit = (event, assertLive) => {
286
287
  assertLive?.();
287
288
  const full = {
@@ -303,6 +304,10 @@ export function createSessionOpener(services) {
303
304
  executionId,
304
305
  turnId
305
306
  });
307
+ if (full.type === 'execution.completed' ||
308
+ full.type === 'execution.failed' ||
309
+ full.type === 'execution.cancelled')
310
+ persistedTerminal = true;
306
311
  if (full.type === 'message.completed') {
307
312
  const message = liveModelMessages.findLast((item) => item.role === 'assistant' && item.content === full.text);
308
313
  if (message)
@@ -1953,6 +1958,9 @@ export function createSessionOpener(services) {
1953
1958
  executionResults.delete(executionKey);
1954
1959
  });
1955
1960
  return {
1961
+ get persistedEvents() {
1962
+ return persistedTerminal;
1963
+ },
1956
1964
  executionId,
1957
1965
  turnId,
1958
1966
  [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator](),
@@ -16,6 +16,8 @@ export interface SubagentModelOption {
16
16
  export interface SubagentTurnHandle extends AsyncIterable<MarAgentEvent> {
17
17
  readonly executionId: string;
18
18
  readonly turnId: string;
19
+ /** True only after the child terminal event was durably appended. */
20
+ readonly persistedEvents?: boolean;
19
21
  result(): Promise<ExecutionResult>;
20
22
  cancel(): void;
21
23
  enqueueMessage(message: string, delivery?: SubagentMessageDelivery): boolean;
@@ -109,6 +111,7 @@ export declare class SubagentSessionController implements SubagentController {
109
111
  withOperation<T>(operation: SubagentParentOperation, run: () => T): T;
110
112
  inspect(agentId: string): Promise<SubagentTaskSnapshot>;
111
113
  inspectAll(): readonly SubagentTaskSnapshot[];
114
+ list(signal?: AbortSignal): Promise<readonly SubagentTaskSnapshot[]>;
112
115
  /** Full listing is explicit; ordinary opens and executions never require it. */
113
116
  prepareAll(signal?: AbortSignal): Promise<void>;
114
117
  prepareSessionResources(signal?: AbortSignal): Promise<void>;
@@ -5,6 +5,16 @@ import { MarAgentError, describeMarAgentError } from '../error.js';
5
5
  import { TOOL_EXECUTION_LIMITS } from '../tools/execution-limits.js';
6
6
  import { parseAgentWaitTargets, waitForAgentCondition } from './wait.js';
7
7
  const RECENT_TERMINAL_RESOURCE_LIMIT = 16;
8
+ const DETAIL_FIELDS = [
9
+ 'latestMessage',
10
+ 'latestActivity',
11
+ 'summary',
12
+ 'changedFiles',
13
+ 'verification',
14
+ 'evidence',
15
+ 'permissionStop',
16
+ 'errorMessage'
17
+ ];
8
18
  export class SubagentSessionController {
9
19
  parentSessionId;
10
20
  rootSessionId;
@@ -14,6 +24,8 @@ export class SubagentSessionController {
14
24
  // Discovery order is independent of the order in which history is requested.
15
25
  #discovered = new Map();
16
26
  #loading = new Map();
27
+ #readers = new Map();
28
+ #allReaders = 0;
17
29
  #queue = [];
18
30
  #completionWaiters = new Set();
19
31
  #activityWaiters = new Set();
@@ -63,6 +75,7 @@ export class SubagentSessionController {
63
75
  this.#creationBudget = { count: 0 };
64
76
  for (const state of this.#agents.values())
65
77
  await this.#publishTerminal(state);
78
+ this.#releaseOldDetails();
66
79
  }
67
80
  /** A retained cell must not borrow a later model Execution's identity or task budget. */
68
81
  captureExecution(executionId) {
@@ -98,13 +111,21 @@ export class SubagentSessionController {
98
111
  return this.#retainedExecution.run({ execution: operation, budget: { count: 0 }, native: true }, run);
99
112
  }
100
113
  async inspect(agentId) {
101
- return this.#snapshot(await this.#find(agentId), { full: true });
114
+ return this.#using([agentId], async () => {
115
+ return this.#snapshot(await this.#find(agentId), { full: true });
116
+ });
102
117
  }
103
118
  inspectAll() {
104
119
  if (this.#disposed)
105
120
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
106
121
  return this.#orderedStates().map((state) => this.#snapshot(state, { full: true }));
107
122
  }
123
+ async list(signal) {
124
+ return this.#using(undefined, async () => {
125
+ await this.prepareAll(signal);
126
+ return this.inspectAll();
127
+ });
128
+ }
108
129
  /** Full listing is explicit; ordinary opens and executions never require it. */
109
130
  async prepareAll(signal) {
110
131
  for (const id of this.#discovered.keys()) {
@@ -129,6 +150,7 @@ export class SubagentSessionController {
129
150
  break;
130
151
  }
131
152
  signal?.throwIfAborted();
153
+ this.#releaseOldDetails();
132
154
  }
133
155
  async #restoreDiscovered(agentId) {
134
156
  try {
@@ -250,183 +272,125 @@ export class SubagentSessionController {
250
272
  catch (error) {
251
273
  await this.#deleteUnstartedSession(created.id, error);
252
274
  }
253
- const state = {
254
- agentId: created.id,
255
- description: source.description,
256
- role: input.role,
257
- ...(source.ownerInstanceId === undefined ? {} : { ownerInstanceId: source.ownerInstanceId }),
258
- ...(source.extensionId === undefined ? {} : { extensionId: source.extensionId }),
259
- status: 'queued',
260
- messageRevision: 0,
261
- observedMessageRevision: 0,
262
- latestMessageActivityRevision: 0,
263
- changedFiles: [],
264
- verification: [],
265
- evidence: [],
266
- terminalPending: false,
267
- observedTerminalRevision: -1,
268
- resourceRevision: ++this.#resourceRevision
269
- };
270
- if (!native)
271
- (this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
272
- this.#agents.set(state.agentId, state);
273
- this.#discovered.set(state.agentId, state.resourceRevision);
274
- const knownModel = execution.models.get(input.modelId ?? execution.modelId);
275
- if (knownModel) {
276
- state.modelId = knownModel.id;
277
- state.modelName = knownModel.name;
278
- }
279
- try {
280
- const selected = resolveModel(execution, input);
281
- state.modelId = selected.id;
282
- state.modelName = selected.name;
283
- state.reasoningEffort = selected.reasoningEffort;
284
- }
285
- catch (error) {
286
- this.#setFailure(state, error);
287
- }
288
- try {
289
- await options.onCreated?.(this.#receiptSnapshot(state));
290
- if (this.#disposed)
291
- throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
292
- this.#requireExecution();
293
- options.signal?.throwIfAborted();
294
- }
295
- catch (error) {
296
- await this.#deleteUnstartedSession(state.agentId, error);
297
- }
298
- if (state.status === 'failed') {
299
- await this.#publishTerminal(state);
300
- this.#observeTerminal(state);
301
- return this.#snapshot(state);
302
- }
303
- this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
304
- if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
305
- return this.#receiptSnapshot(state);
306
- await waitForPromise(state.turn.completion, options.signal);
307
- const includeLatestMessage = this.#hasUnreadMessage(state);
308
- const includeTerminalDetails = this.#observeTerminal(state);
309
- this.#observeLatestMessage(state);
310
- return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
311
- }
312
- async output(agentId, options = {}) {
313
- options.signal?.throwIfAborted();
314
- const state = await this.#find(agentId);
315
- options.signal?.throwIfAborted();
316
- validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
317
- if (options.untilSettled) {
318
- if (state.turn)
319
- await waitForPromise(state.turn.completion, options.signal);
320
- options.signal?.throwIfAborted();
321
- const includeLatestMessage = this.#hasUnreadMessage(state);
322
- const includeTerminalDetails = this.#observeTerminal(state);
323
- this.#observeLatestMessage(state);
324
- return {
325
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
326
- waitOutcome: 'completion'
275
+ return this.#using([created.id], async () => {
276
+ const state = {
277
+ agentId: created.id,
278
+ description: source.description,
279
+ role: input.role,
280
+ ...(source.ownerInstanceId === undefined
281
+ ? {}
282
+ : { ownerInstanceId: source.ownerInstanceId }),
283
+ ...(source.extensionId === undefined ? {} : { extensionId: source.extensionId }),
284
+ status: 'queued',
285
+ messageRevision: 0,
286
+ observedMessageRevision: 0,
287
+ latestMessageActivityRevision: 0,
288
+ changedFiles: [],
289
+ verification: [],
290
+ evidence: [],
291
+ terminalPending: false,
292
+ observedTerminalRevision: -1,
293
+ resourceRevision: ++this.#resourceRevision
327
294
  };
328
- }
329
- if (options.waitMs === undefined || options.waitMs === 0)
330
- return {
331
- ...this.#snapshot(state, {
332
- includeLatestMessage: false,
333
- includeTerminalDetails: false
334
- }),
335
- waitOutcome: 'snapshot'
336
- };
337
- if (isTerminalState(state)) {
295
+ if (!native)
296
+ (this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
297
+ this.#agents.set(state.agentId, state);
298
+ this.#discovered.set(state.agentId, state.resourceRevision);
299
+ const knownModel = execution.models.get(input.modelId ?? execution.modelId);
300
+ if (knownModel) {
301
+ state.modelId = knownModel.id;
302
+ state.modelName = knownModel.name;
303
+ }
304
+ try {
305
+ const selected = resolveModel(execution, input);
306
+ state.modelId = selected.id;
307
+ state.modelName = selected.name;
308
+ state.reasoningEffort = selected.reasoningEffort;
309
+ }
310
+ catch (error) {
311
+ this.#setFailure(state, error);
312
+ }
313
+ try {
314
+ await options.onCreated?.(this.#receiptSnapshot(state));
315
+ if (this.#disposed)
316
+ throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
317
+ this.#requireExecution();
318
+ options.signal?.throwIfAborted();
319
+ }
320
+ catch (error) {
321
+ await this.#deleteUnstartedSession(state.agentId, error);
322
+ }
323
+ if (state.status === 'failed') {
324
+ await this.#publishTerminal(state);
325
+ this.#observeTerminal(state);
326
+ return this.#snapshot(state);
327
+ }
328
+ this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
329
+ if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
330
+ return this.#receiptSnapshot(state);
331
+ await waitForPromise(state.turn.completion, options.signal);
338
332
  const includeLatestMessage = this.#hasUnreadMessage(state);
339
333
  const includeTerminalDetails = this.#observeTerminal(state);
340
334
  this.#observeLatestMessage(state);
341
- return {
342
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
343
- waitOutcome: 'completion'
344
- };
345
- }
346
- const waitFor = options.waitFor ?? 'completion';
347
- if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
348
- this.#observeLatestMessage(state);
349
- return { ...this.#snapshot(state), waitOutcome: 'message' };
350
- }
351
- const waitMs = this.#retainedExecution.getStore()?.native
352
- ? options.waitMs
353
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
354
- if (waitFor === 'completion') {
355
- const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
356
- options.signal?.throwIfAborted();
357
- const includeLatestMessage = this.#hasUnreadMessage(state);
358
- const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
359
- if (includeLatestMessage)
360
- this.#observeLatestMessage(state);
361
- return {
362
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
363
- ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
364
- };
365
- }
366
- const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
367
- options.signal?.throwIfAborted();
368
- const includeLatestMessage = activity?.kind === 'message' ||
369
- (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
370
- const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
371
- if (activity)
372
- this.#observeLatestMessage(activity.state);
373
- return {
374
- ...this.#snapshot(activity?.state ?? state, {
375
- includeLatestMessage,
376
- includeTerminalDetails
377
- }),
378
- ...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
379
- };
335
+ return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
336
+ });
380
337
  }
381
- async outputAny(options = {}) {
382
- options.signal?.throwIfAborted();
383
- if (options.untilSettled)
384
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
385
- validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
386
- const hasLive = [...this.#agents.values()].some((state) => !isTerminalState(state));
387
- if (!hasLive) {
388
- for (const id of [...this.#discovered.keys()].reverse()) {
338
+ async output(agentId, options = {}) {
339
+ return this.#using([agentId], async () => {
340
+ options.signal?.throwIfAborted();
341
+ const state = await this.#find(agentId);
342
+ options.signal?.throwIfAborted();
343
+ validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
344
+ if (options.untilSettled) {
345
+ if (state.turn)
346
+ await waitForPromise(state.turn.completion, options.signal);
389
347
  options.signal?.throwIfAborted();
390
- if (await this.#restoreDiscovered(id))
391
- break;
348
+ const includeLatestMessage = this.#hasUnreadMessage(state);
349
+ const includeTerminalDetails = this.#observeTerminal(state);
350
+ this.#observeLatestMessage(state);
351
+ return {
352
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
353
+ waitOutcome: 'completion'
354
+ };
392
355
  }
393
- }
394
- options.signal?.throwIfAborted();
395
- const states = this.#orderedStates();
396
- if (states.length === 0)
397
- throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
398
- const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
399
- if (options.waitMs === undefined || options.waitMs === 0)
400
- return {
401
- ...this.#snapshot(live.at(-1) ?? states.at(-1), {
402
- includeLatestMessage: false,
403
- includeTerminalDetails: false
404
- }),
405
- waitOutcome: 'snapshot'
406
- };
407
- if (live.length === 0) {
408
- const state = states.at(-1);
409
- const includeLatestMessage = this.#hasUnreadMessage(state);
410
- const includeTerminalDetails = this.#observeTerminal(state);
411
- this.#observeLatestMessage(state);
412
- return {
413
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
414
- waitOutcome: 'completion'
415
- };
416
- }
417
- const waitFor = options.waitFor ?? 'completion';
418
- if (waitFor === 'message') {
419
- const pending = this.#oldestUnreadMessage(live);
420
- if (pending) {
421
- this.#observeLatestMessage(pending);
422
- return { ...this.#snapshot(pending), waitOutcome: 'message' };
356
+ if (options.waitMs === undefined || options.waitMs === 0)
357
+ return {
358
+ ...this.#snapshot(state, {
359
+ includeLatestMessage: false,
360
+ includeTerminalDetails: false
361
+ }),
362
+ waitOutcome: 'snapshot'
363
+ };
364
+ if (isTerminalState(state)) {
365
+ const includeLatestMessage = this.#hasUnreadMessage(state);
366
+ const includeTerminalDetails = this.#observeTerminal(state);
367
+ this.#observeLatestMessage(state);
368
+ return {
369
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
370
+ waitOutcome: 'completion'
371
+ };
423
372
  }
424
- }
425
- const waitMs = this.#retainedExecution.getStore()?.native
426
- ? options.waitMs
427
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
428
- if (waitFor === 'message') {
429
- const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal, options.waitInterrupt);
373
+ const waitFor = options.waitFor ?? 'completion';
374
+ if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
375
+ this.#observeLatestMessage(state);
376
+ return { ...this.#snapshot(state), waitOutcome: 'message' };
377
+ }
378
+ const waitMs = this.#retainedExecution.getStore()?.native
379
+ ? options.waitMs
380
+ : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
381
+ if (waitFor === 'completion') {
382
+ const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
383
+ options.signal?.throwIfAborted();
384
+ const includeLatestMessage = this.#hasUnreadMessage(state);
385
+ const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
386
+ if (includeLatestMessage)
387
+ this.#observeLatestMessage(state);
388
+ return {
389
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
390
+ ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
391
+ };
392
+ }
393
+ const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
430
394
  options.signal?.throwIfAborted();
431
395
  const includeLatestMessage = activity?.kind === 'message' ||
432
396
  (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
@@ -434,27 +398,93 @@ export class SubagentSessionController {
434
398
  if (activity)
435
399
  this.#observeLatestMessage(activity.state);
436
400
  return {
437
- ...this.#snapshot(activity?.state ?? live.at(-1), {
401
+ ...this.#snapshot(activity?.state ?? state, {
438
402
  includeLatestMessage,
439
403
  includeTerminalDetails
440
404
  }),
441
405
  ...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
442
406
  };
443
- }
444
- const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal, options.waitInterrupt);
445
- options.signal?.throwIfAborted();
446
- const state = completed ?? this.#oldestUnreadMessage(live) ?? live.at(-1);
447
- const includeLatestMessage = this.#hasUnreadMessage(state);
448
- const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
449
- if (includeLatestMessage)
450
- this.#observeLatestMessage(state);
451
- return {
452
- ...this.#snapshot(state, {
453
- includeLatestMessage,
454
- includeTerminalDetails
455
- }),
456
- ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
457
- };
407
+ });
408
+ }
409
+ async outputAny(options = {}) {
410
+ return this.#using(undefined, async () => {
411
+ options.signal?.throwIfAborted();
412
+ if (options.untilSettled)
413
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
414
+ validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
415
+ const hasLive = [...this.#agents.values()].some((state) => !isTerminalState(state));
416
+ if (!hasLive) {
417
+ for (const id of [...this.#discovered.keys()].reverse()) {
418
+ options.signal?.throwIfAborted();
419
+ if (await this.#restoreDiscovered(id))
420
+ break;
421
+ }
422
+ }
423
+ options.signal?.throwIfAborted();
424
+ const states = this.#orderedStates();
425
+ if (states.length === 0)
426
+ throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
427
+ const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
428
+ if (options.waitMs === undefined || options.waitMs === 0)
429
+ return {
430
+ ...this.#snapshot(live.at(-1) ?? states.at(-1), {
431
+ includeLatestMessage: false,
432
+ includeTerminalDetails: false
433
+ }),
434
+ waitOutcome: 'snapshot'
435
+ };
436
+ if (live.length === 0) {
437
+ const state = states.at(-1);
438
+ const includeLatestMessage = this.#hasUnreadMessage(state);
439
+ const includeTerminalDetails = this.#observeTerminal(state);
440
+ this.#observeLatestMessage(state);
441
+ return {
442
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
443
+ waitOutcome: 'completion'
444
+ };
445
+ }
446
+ const waitFor = options.waitFor ?? 'completion';
447
+ if (waitFor === 'message') {
448
+ const pending = this.#oldestUnreadMessage(live);
449
+ if (pending) {
450
+ this.#observeLatestMessage(pending);
451
+ return { ...this.#snapshot(pending), waitOutcome: 'message' };
452
+ }
453
+ }
454
+ const waitMs = this.#retainedExecution.getStore()?.native
455
+ ? options.waitMs
456
+ : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
457
+ if (waitFor === 'message') {
458
+ const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal, options.waitInterrupt);
459
+ options.signal?.throwIfAborted();
460
+ const includeLatestMessage = activity?.kind === 'message' ||
461
+ (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
462
+ const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
463
+ if (activity)
464
+ this.#observeLatestMessage(activity.state);
465
+ return {
466
+ ...this.#snapshot(activity?.state ?? live.at(-1), {
467
+ includeLatestMessage,
468
+ includeTerminalDetails
469
+ }),
470
+ ...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
471
+ };
472
+ }
473
+ const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal, options.waitInterrupt);
474
+ options.signal?.throwIfAborted();
475
+ const state = completed ?? this.#oldestUnreadMessage(live) ?? live.at(-1);
476
+ const includeLatestMessage = this.#hasUnreadMessage(state);
477
+ const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
478
+ if (includeLatestMessage)
479
+ this.#observeLatestMessage(state);
480
+ return {
481
+ ...this.#snapshot(state, {
482
+ includeLatestMessage,
483
+ includeTerminalDetails
484
+ }),
485
+ ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
486
+ };
487
+ });
458
488
  }
459
489
  async outputMany(agentIds, options = {}) {
460
490
  options.signal?.throwIfAborted();
@@ -464,187 +494,198 @@ export class SubagentSessionController {
464
494
  validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
465
495
  if (!ids || 'untilSettled' in options)
466
496
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Group waits require a bounded deadline.');
467
- const states = await Promise.all(ids.map((id) => this.#find(id)));
468
- options.signal?.throwIfAborted();
469
- const waitFor = options.waitFor ?? 'completion';
470
- const targets = states.map((state) => ({
471
- state,
472
- turn: state.turn,
473
- view: copyWaitState(state),
474
- observation: this.#nativeObservation(state)
475
- }));
476
- const refresh = () => {
477
- for (const target of targets) {
478
- if (!isTerminalState(target.view) &&
479
- (target.state.turn === target.turn ||
480
- (target.state.turn === undefined && isTerminalState(target.state))))
481
- target.view = copyWaitState(target.state);
497
+ return this.#using(ids, async () => {
498
+ const states = await Promise.all(ids.map((id) => this.#find(id)));
499
+ options.signal?.throwIfAborted();
500
+ const waitFor = options.waitFor ?? 'completion';
501
+ const targets = states.map((state) => ({
502
+ state,
503
+ turn: state.turn,
504
+ view: copyWaitState(state),
505
+ observation: this.#nativeObservation(state)
506
+ }));
507
+ const refresh = () => {
508
+ for (const target of targets) {
509
+ if (!isTerminalState(target.view) &&
510
+ (target.state.turn === target.turn ||
511
+ (target.state.turn === undefined && isTerminalState(target.state))))
512
+ target.view = copyWaitState(target.state);
513
+ }
514
+ };
515
+ const unread = (target) => target.view.messageRevision >
516
+ (target.observation?.messageRevision ?? target.state.observedMessageRevision);
517
+ const ready = (target) => isTerminalState(target.view) || (waitFor === 'message' && unread(target));
518
+ const snapshot = options.waitMs === undefined || options.waitMs === 0;
519
+ let satisfied = false;
520
+ if (!snapshot) {
521
+ const waitMs = this.#retainedExecution.getStore()?.native
522
+ ? options.waitMs
523
+ : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
524
+ satisfied = await waitForAgentCondition({
525
+ waitMs,
526
+ signal: options.signal,
527
+ interrupt: options.waitInterrupt,
528
+ ready: () => {
529
+ refresh();
530
+ return mode === 'all' ? targets.every(ready) : targets.some(ready);
531
+ },
532
+ subscribe: (notify) => {
533
+ const listener = () => notify();
534
+ this.#activityWaiters.add(listener);
535
+ return () => this.#activityWaiters.delete(listener);
536
+ }
537
+ });
538
+ options.signal?.throwIfAborted();
482
539
  }
483
- };
484
- const unread = (target) => target.view.messageRevision >
485
- (target.observation?.messageRevision ?? target.state.observedMessageRevision);
486
- const ready = (target) => isTerminalState(target.view) || (waitFor === 'message' && unread(target));
487
- const snapshot = options.waitMs === undefined || options.waitMs === 0;
488
- let satisfied = false;
489
- if (!snapshot) {
490
- const waitMs = this.#retainedExecution.getStore()?.native
491
- ? options.waitMs
492
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
493
- satisfied = await waitForAgentCondition({
494
- waitMs,
495
- signal: options.signal,
496
- interrupt: options.waitInterrupt,
497
- ready: () => {
498
- refresh();
499
- return mode === 'all' ? targets.every(ready) : targets.some(ready);
500
- },
501
- subscribe: (notify) => {
502
- const listener = () => notify();
503
- this.#activityWaiters.add(listener);
504
- return () => this.#activityWaiters.delete(listener);
540
+ refresh();
541
+ if (!snapshot)
542
+ satisfied = mode === 'all' ? targets.every(ready) : targets.some(ready);
543
+ const pendingAgentIds = targets
544
+ .filter((target) => !ready(target))
545
+ .map((target) => target.state.agentId);
546
+ const agents = targets.map((target) => {
547
+ const { state, view, observation } = target;
548
+ const terminal = isTerminalState(view);
549
+ const outcome = snapshot
550
+ ? 'snapshot'
551
+ : terminal
552
+ ? 'completion'
553
+ : ready(target)
554
+ ? 'message'
555
+ : satisfied
556
+ ? 'snapshot'
557
+ : options.waitInterrupt?.pending()
558
+ ? 'interrupted'
559
+ : 'timeout';
560
+ const includeLatestMessage = outcome !== 'snapshot' && unread(target);
561
+ const includeTerminalDetails = outcome === 'completion' &&
562
+ (observation
563
+ ? observation.terminalRevision < view.resourceRevision
564
+ : state.observedTerminalRevision < view.resourceRevision);
565
+ const result = {
566
+ ...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
567
+ ...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
568
+ ...waitMetadata(outcome)
569
+ };
570
+ if (includeLatestMessage) {
571
+ if (observation)
572
+ observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
573
+ else
574
+ state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
575
+ }
576
+ if (includeTerminalDetails) {
577
+ if (observation)
578
+ observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
579
+ else
580
+ state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
505
581
  }
582
+ return result;
506
583
  });
507
- options.signal?.throwIfAborted();
508
- }
509
- refresh();
510
- if (!snapshot)
511
- satisfied = mode === 'all' ? targets.every(ready) : targets.some(ready);
512
- const pendingAgentIds = targets
513
- .filter((target) => !ready(target))
514
- .map((target) => target.state.agentId);
515
- const agents = targets.map((target) => {
516
- const { state, view, observation } = target;
517
- const terminal = isTerminalState(view);
518
- const outcome = snapshot
519
- ? 'snapshot'
520
- : terminal
521
- ? 'completion'
522
- : ready(target)
523
- ? 'message'
524
- : satisfied
525
- ? 'snapshot'
526
- : options.waitInterrupt?.pending()
527
- ? 'interrupted'
528
- : 'timeout';
529
- const includeLatestMessage = outcome !== 'snapshot' && unread(target);
530
- const includeTerminalDetails = outcome === 'completion' &&
531
- (observation
532
- ? observation.terminalRevision < view.resourceRevision
533
- : state.observedTerminalRevision < view.resourceRevision);
534
- const result = {
535
- ...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
536
- ...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
537
- ...waitMetadata(outcome)
584
+ return {
585
+ mode,
586
+ waitFor,
587
+ agents,
588
+ pendingAgentIds,
589
+ ...waitMetadata(snapshot
590
+ ? 'snapshot'
591
+ : !satisfied
592
+ ? options.waitInterrupt?.pending()
593
+ ? 'interrupted'
594
+ : 'timeout'
595
+ : agents.some((agent) => agent.waitOutcome === 'message')
596
+ ? 'message'
597
+ : 'completion')
538
598
  };
539
- if (includeLatestMessage) {
540
- if (observation)
541
- observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
542
- else
543
- state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
544
- }
545
- if (includeTerminalDetails) {
546
- if (observation)
547
- observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
548
- else
549
- state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
550
- }
551
- return result;
552
599
  });
553
- return {
554
- mode,
555
- waitFor,
556
- agents,
557
- pendingAgentIds,
558
- ...waitMetadata(snapshot
559
- ? 'snapshot'
560
- : !satisfied
561
- ? options.waitInterrupt?.pending()
562
- ? 'interrupted'
563
- : 'timeout'
564
- : agents.some((agent) => agent.waitOutcome === 'message')
565
- ? 'message'
566
- : 'completion')
567
- };
568
600
  }
569
601
  async message(agentId, message, options = {}) {
570
- const execution = this.#requireExecution();
571
- if (typeof message !== 'string' ||
572
- message.trim().length === 0 ||
573
- (this.#retainedExecution.getStore()?.native !== true &&
574
- Buffer.byteLength(message) > TOOL_EXECUTION_LIMITS.subagentMessageMaxBytes) ||
575
- (options.delivery !== undefined &&
576
- options.delivery !== 'append' &&
577
- options.delivery !== 'replace'))
578
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent message input is invalid.');
579
- const state = await this.#find(agentId);
580
- return this.#control(state, async () => {
581
- this.#requireExecution();
582
- options.signal?.throwIfAborted();
583
- // Resolve before cancelling so an unavailable model does not stop valid work.
584
- const selected = resolveExistingModel(execution, state);
585
- if (options.delivery === 'replace')
586
- await this.#cancelState(state);
587
- this.#requireExecution();
588
- options.signal?.throwIfAborted();
589
- const turnId = state.turn?.turnId ?? randomUUID();
590
- const receipt = { messageId: randomUUID(), turnId, deliveryStatus: 'accepted' };
591
- const mutableReceipt = receipt;
592
- const delivery = this.#createDelivery(state, execution, mutableReceipt, options.signal);
593
- try {
594
- if (state.status === 'running' && state.turn) {
595
- const turn = state.turn;
596
- if (!turn.handle)
597
- await turn.handleReady;
598
- this.#requireExecution();
599
- options.signal?.throwIfAborted();
600
- delivery.signal?.throwIfAborted();
601
- if (state.status === 'running' &&
602
- state.turn === turn &&
603
- turn.handle?.enqueueMessage(message, delivery)) {
602
+ return this.#using([agentId], async () => {
603
+ const execution = this.#requireExecution();
604
+ if (typeof message !== 'string' ||
605
+ message.trim().length === 0 ||
606
+ (this.#retainedExecution.getStore()?.native !== true &&
607
+ Buffer.byteLength(message) > TOOL_EXECUTION_LIMITS.subagentMessageMaxBytes) ||
608
+ (options.delivery !== undefined &&
609
+ options.delivery !== 'append' &&
610
+ options.delivery !== 'replace'))
611
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent message input is invalid.');
612
+ const state = await this.#find(agentId);
613
+ return this.#control(state, async () => {
614
+ this.#requireExecution();
615
+ options.signal?.throwIfAborted();
616
+ // Resolve before cancelling so an unavailable model does not stop valid work.
617
+ const selected = resolveExistingModel(execution, state);
618
+ if (options.delivery === 'replace')
619
+ await this.#cancelState(state);
620
+ this.#requireExecution();
621
+ options.signal?.throwIfAborted();
622
+ const turnId = state.turn?.turnId ?? randomUUID();
623
+ const receipt = { messageId: randomUUID(), turnId, deliveryStatus: 'accepted' };
624
+ const mutableReceipt = receipt;
625
+ const delivery = this.#createDelivery(state, execution, mutableReceipt, options.signal);
626
+ try {
627
+ if (state.status === 'running' && state.turn) {
628
+ const turn = state.turn;
629
+ if (!turn.handle)
630
+ await turn.handleReady;
631
+ this.#requireExecution();
632
+ options.signal?.throwIfAborted();
633
+ delivery.signal?.throwIfAborted();
634
+ if (state.status === 'running' &&
635
+ state.turn === turn &&
636
+ turn.handle?.enqueueMessage(message, delivery)) {
637
+ delivery.accepted();
638
+ state.receipt = mutableReceipt;
639
+ return this.#messageReceiptSnapshot(state);
640
+ }
641
+ await turn.completion;
642
+ }
643
+ else if (state.status === 'queued' && state.turn) {
604
644
  delivery.accepted();
645
+ state.turn.pendingMessages.push({ text: message, delivery });
605
646
  state.receipt = mutableReceipt;
606
647
  return this.#messageReceiptSnapshot(state);
607
648
  }
608
- await turn.completion;
609
- }
610
- else if (state.status === 'queued' && state.turn) {
649
+ this.#requireExecution();
650
+ options.signal?.throwIfAborted();
651
+ delivery.signal?.throwIfAborted();
652
+ state.modelId = selected.id;
653
+ state.modelName = selected.name;
654
+ state.reasoningEffort = selected.reasoningEffort;
655
+ // A closing turn may have rejected append; reserve a fresh identity for its successor.
656
+ mutableReceipt.turnId = randomUUID();
611
657
  delivery.accepted();
612
- state.turn.pendingMessages.push({ text: message, delivery });
613
658
  state.receipt = mutableReceipt;
659
+ this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
614
660
  return this.#messageReceiptSnapshot(state);
615
661
  }
616
- this.#requireExecution();
617
- options.signal?.throwIfAborted();
618
- delivery.signal?.throwIfAborted();
619
- state.modelId = selected.id;
620
- state.modelName = selected.name;
621
- state.reasoningEffort = selected.reasoningEffort;
622
- // A closing turn may have rejected append; reserve a fresh identity for its successor.
623
- mutableReceipt.turnId = randomUUID();
624
- delivery.accepted();
625
- state.receipt = mutableReceipt;
626
- this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
627
- return this.#messageReceiptSnapshot(state);
628
- }
629
- catch (error) {
630
- delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
631
- ? 'cancelled'
632
- : 'failed');
633
- throw error;
634
- }
662
+ catch (error) {
663
+ delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
664
+ ? 'cancelled'
665
+ : 'failed');
666
+ throw error;
667
+ }
668
+ });
635
669
  });
636
670
  }
637
671
  async cancel(agentId) {
638
- const state = await this.#find(agentId);
639
- await this.#control(state, () => this.#cancelState(state));
640
- const includeLatestMessage = this.#hasUnreadMessage(state);
641
- const includeTerminalDetails = this.#observeTerminal(state);
642
- this.#observeLatestMessage(state);
643
- return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
672
+ return this.#using([agentId], async () => {
673
+ const state = await this.#find(agentId);
674
+ await this.#control(state, () => this.#cancelState(state));
675
+ const includeLatestMessage = this.#hasUnreadMessage(state);
676
+ const includeTerminalDetails = this.#observeTerminal(state);
677
+ this.#observeLatestMessage(state);
678
+ return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
679
+ });
644
680
  }
645
681
  #control(state, operation) {
646
682
  const result = (state.controlTail ?? Promise.resolve()).then(operation);
647
- state.controlTail = result.catch(() => undefined);
683
+ const tail = result.then(() => undefined, () => undefined);
684
+ state.controlTail = tail;
685
+ void tail.then(() => {
686
+ if (state.controlTail === tail)
687
+ delete state.controlTail;
688
+ });
648
689
  return result;
649
690
  }
650
691
  async #cancelState(state) {
@@ -730,6 +771,7 @@ export class SubagentSessionController {
730
771
  ? native.signal
731
772
  : AbortSignal.any([native.signal, signal]));
732
773
  state.status = 'queued';
774
+ state.restorable = false;
733
775
  this.#touchResource(state);
734
776
  delete state.summary;
735
777
  delete state.permissionStop;
@@ -807,9 +849,18 @@ export class SubagentSessionController {
807
849
  await this.#consumeTurn(state, turn);
808
850
  }
809
851
  async #consumeTurn(state, turn) {
852
+ const previousActivity = state.latestActivity;
853
+ const previousMessageRevision = state.messageRevision;
854
+ let terminal;
810
855
  try {
811
856
  const consume = (async () => {
812
857
  for await (const event of turn.handle) {
858
+ if (event.type === 'execution.completed')
859
+ terminal = 'completed';
860
+ else if (event.type === 'execution.failed')
861
+ terminal = 'failed';
862
+ else if (event.type === 'execution.cancelled')
863
+ terminal = 'cancelled';
813
864
  if (!observeAgentEvent(state, event))
814
865
  continue;
815
866
  state.messageRevision++;
@@ -835,6 +886,14 @@ export class SubagentSessionController {
835
886
  state.errorCode = result.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED';
836
887
  state.errorMessage = result.errorMessage ?? 'Subagent execution failed.';
837
888
  }
889
+ state.restorable =
890
+ turn.handle.persistedEvents === true &&
891
+ terminal === state.status &&
892
+ (previousActivity === undefined || state.latestActivity !== previousActivity) &&
893
+ (state.latestMessage === undefined || state.messageRevision > previousMessageRevision) &&
894
+ (state.status !== 'completed' ||
895
+ (state.latestMessage?.phase === 'final_answer' &&
896
+ state.summary === state.latestMessage.text));
838
897
  state.terminalPending = true;
839
898
  this.#touchResource(state, true);
840
899
  this.#closeReceipt(state);
@@ -869,6 +928,7 @@ export class SubagentSessionController {
869
928
  #setFailure(state, error) {
870
929
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_SUBAGENT_FAILED');
871
930
  state.status = 'failed';
931
+ state.restorable = false;
872
932
  state.errorCode = diagnostic.code;
873
933
  state.errorMessage = diagnostic.message;
874
934
  state.terminalPending = true;
@@ -1014,11 +1074,61 @@ export class SubagentSessionController {
1014
1074
  }
1015
1075
  throw error;
1016
1076
  }
1077
+ async #using(ids, operation) {
1078
+ if (ids === undefined)
1079
+ this.#allReaders++;
1080
+ else
1081
+ for (const id of ids)
1082
+ this.#readers.set(id, (this.#readers.get(id) ?? 0) + 1);
1083
+ try {
1084
+ return await operation();
1085
+ }
1086
+ finally {
1087
+ if (ids === undefined)
1088
+ this.#allReaders--;
1089
+ else
1090
+ for (const id of ids) {
1091
+ const count = this.#readers.get(id) - 1;
1092
+ if (count)
1093
+ this.#readers.set(id, count);
1094
+ else
1095
+ this.#readers.delete(id);
1096
+ }
1097
+ }
1098
+ }
1099
+ #releaseOldDetails() {
1100
+ if (this.#allReaders)
1101
+ return;
1102
+ const terminal = [...this.#agents.values()]
1103
+ .filter(isTerminalState)
1104
+ .sort((a, b) => b.resourceRevision - a.resourceRevision);
1105
+ for (const state of terminal.slice(RECENT_TERMINAL_RESOURCE_LIMIT)) {
1106
+ if (state.detailsEvicted ||
1107
+ !state.restorable ||
1108
+ state.turn ||
1109
+ state.controlTail ||
1110
+ this.#readers.has(state.agentId) ||
1111
+ this.#loading.has(state.agentId) ||
1112
+ state.terminalPending ||
1113
+ state.messageRevision > state.observedMessageRevision ||
1114
+ (state.terminalRevision !== undefined &&
1115
+ state.terminalRevision > state.observedTerminalRevision) ||
1116
+ [...this.#pendingDeliveries].some((pending) => pending.state === state))
1117
+ continue;
1118
+ // Keep identity and cursors for old acknowledgements; only the disk-backed body unloads.
1119
+ for (const key of DETAIL_FIELDS)
1120
+ delete state[key];
1121
+ state.changedFiles = [];
1122
+ state.verification = [];
1123
+ state.evidence = [];
1124
+ state.detailsEvicted = true;
1125
+ }
1126
+ }
1017
1127
  async #find(agentId) {
1018
1128
  if (this.#disposed)
1019
1129
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
1020
1130
  const known = this.#agents.get(agentId);
1021
- if (known)
1131
+ if (known && !known.detailsEvicted)
1022
1132
  return known;
1023
1133
  const pending = this.#loading.get(agentId);
1024
1134
  if (pending)
@@ -1027,12 +1137,25 @@ export class SubagentSessionController {
1027
1137
  const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
1028
1138
  if (this.#disposed)
1029
1139
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
1030
- if (!persisted)
1140
+ if (!persisted) {
1141
+ this.#agents.delete(agentId);
1142
+ this.#discovered.delete(agentId);
1031
1143
  throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
1144
+ }
1032
1145
  const existing = this.#agents.get(agentId);
1033
- if (existing)
1146
+ if (existing && !existing.detailsEvicted)
1034
1147
  return existing;
1035
1148
  const restored = restoreAgentState(persisted);
1149
+ if (existing) {
1150
+ for (const key of DETAIL_FIELDS) {
1151
+ if (key in restored)
1152
+ Object.assign(existing, { [key]: restored[key] });
1153
+ else
1154
+ delete existing[key];
1155
+ }
1156
+ delete existing.detailsEvicted;
1157
+ return existing;
1158
+ }
1036
1159
  let revision = this.#discovered.get(agentId);
1037
1160
  if (revision === undefined) {
1038
1161
  revision = ++this.#resourceRevision;
@@ -1223,6 +1346,7 @@ function restoreAgentState(session) {
1223
1346
  const terminal = currentRecords.findLast((record) => ['execution.completed', 'execution.failed', 'execution.cancelled'].includes(eventType(record) ?? ''));
1224
1347
  const terminalType = eventType(terminal);
1225
1348
  const state = {
1349
+ restorable: true,
1226
1350
  agentId: session.id,
1227
1351
  description: session.source.description,
1228
1352
  role: session.source.role,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.61",
3
+ "version": "0.10.62",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",