@myagentroam/agent 0.10.60 → 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.
@@ -4,12 +4,28 @@ import { randomUUID } from 'node:crypto';
4
4
  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
+ 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
+ ];
7
18
  export class SubagentSessionController {
8
19
  parentSessionId;
9
20
  rootSessionId;
10
21
  backend;
11
22
  limits;
12
23
  #agents = new Map();
24
+ // Discovery order is independent of the order in which history is requested.
25
+ #discovered = new Map();
26
+ #loading = new Map();
27
+ #readers = new Map();
28
+ #allReaders = 0;
13
29
  #queue = [];
14
30
  #completionWaiters = new Set();
15
31
  #activityWaiters = new Set();
@@ -35,7 +51,7 @@ export class SubagentSessionController {
35
51
  #retainedExecution = new AsyncLocalStorage();
36
52
  #execution;
37
53
  #pendingDeliveries = new Set();
38
- constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks: 8, maxConcurrent: 4 }) {
54
+ constructor(parentSessionId, rootSessionId, backend, limits = { maxTasks: 16, maxConcurrent: 16 }) {
39
55
  this.parentSessionId = parentSessionId;
40
56
  this.rootSessionId = rootSessionId;
41
57
  this.backend = backend;
@@ -43,11 +59,12 @@ export class SubagentSessionController {
43
59
  }
44
60
  static async open(input) {
45
61
  const controller = new SubagentSessionController(input.parentSessionId, input.rootSessionId, input.backend, input.limits);
46
- const sessions = [...(await input.backend.listChildren(input.parentSessionId))].sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt));
47
- for (const session of sessions) {
48
- const state = restoreAgentState(session);
49
- state.resourceRevision = ++controller.#resourceRevision;
50
- controller.#agents.set(session.id, state);
62
+ const sessions = await input.backend.listChildren(input.parentSessionId);
63
+ // Map insertion order is creation order; values independently rank activity.
64
+ for (const session of sessions)
65
+ controller.#discovered.set(session.id, 0);
66
+ for (const session of [...sessions].sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt))) {
67
+ controller.#discovered.set(session.id, ++controller.#resourceRevision);
51
68
  }
52
69
  return controller;
53
70
  }
@@ -58,6 +75,7 @@ export class SubagentSessionController {
58
75
  this.#creationBudget = { count: 0 };
59
76
  for (const state of this.#agents.values())
60
77
  await this.#publishTerminal(state);
78
+ this.#releaseOldDetails();
61
79
  }
62
80
  /** A retained cell must not borrow a later model Execution's identity or task budget. */
63
81
  captureExecution(executionId) {
@@ -93,12 +111,63 @@ export class SubagentSessionController {
93
111
  return this.#retainedExecution.run({ execution: operation, budget: { count: 0 }, native: true }, run);
94
112
  }
95
113
  async inspect(agentId) {
96
- 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
+ });
97
117
  }
98
118
  inspectAll() {
99
119
  if (this.#disposed)
100
120
  throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
101
- return [...this.#agents.values()].map((state) => this.#snapshot(state, { full: true }));
121
+ return this.#orderedStates().map((state) => this.#snapshot(state, { full: true }));
122
+ }
123
+ async list(signal) {
124
+ return this.#using(undefined, async () => {
125
+ await this.prepareAll(signal);
126
+ return this.inspectAll();
127
+ });
128
+ }
129
+ /** Full listing is explicit; ordinary opens and executions never require it. */
130
+ async prepareAll(signal) {
131
+ for (const id of this.#discovered.keys()) {
132
+ signal?.throwIfAborted();
133
+ await this.#restoreDiscovered(id);
134
+ }
135
+ signal?.throwIfAborted();
136
+ }
137
+ async prepareSessionResources(signal) {
138
+ const candidates = [...this.#discovered]
139
+ .filter(([id]) => {
140
+ const state = this.#agents.get(id);
141
+ return !state || isTerminalState(state);
142
+ })
143
+ .sort(([a, ar], [b, br]) => (this.#agents.get(b)?.resourceRevision ?? br) -
144
+ (this.#agents.get(a)?.resourceRevision ?? ar));
145
+ let prepared = 0;
146
+ for (const [id] of candidates) {
147
+ signal?.throwIfAborted();
148
+ const state = await this.#restoreDiscovered(id);
149
+ if (state && isTerminalState(state) && ++prepared === RECENT_TERMINAL_RESOURCE_LIMIT)
150
+ break;
151
+ }
152
+ signal?.throwIfAborted();
153
+ this.#releaseOldDetails();
154
+ }
155
+ async #restoreDiscovered(agentId) {
156
+ try {
157
+ return await this.#find(agentId);
158
+ }
159
+ catch (error) {
160
+ if (!(error instanceof MarAgentError) || error.code !== 'MAR_AGENT_SUBAGENT_NOT_FOUND')
161
+ throw error;
162
+ this.#discovered.delete(agentId);
163
+ return undefined;
164
+ }
165
+ }
166
+ #orderedStates() {
167
+ return [...this.#discovered.keys()].flatMap((id) => {
168
+ const state = this.#agents.get(id);
169
+ return state ? [state] : [];
170
+ });
102
171
  }
103
172
  collectModelUpdates() {
104
173
  return [...this.#agents.values()]
@@ -203,173 +272,125 @@ export class SubagentSessionController {
203
272
  catch (error) {
204
273
  await this.#deleteUnstartedSession(created.id, error);
205
274
  }
206
- const state = {
207
- agentId: created.id,
208
- description: source.description,
209
- role: input.role,
210
- ...(source.ownerInstanceId === undefined ? {} : { ownerInstanceId: source.ownerInstanceId }),
211
- ...(source.extensionId === undefined ? {} : { extensionId: source.extensionId }),
212
- status: 'queued',
213
- messageRevision: 0,
214
- observedMessageRevision: 0,
215
- latestMessageActivityRevision: 0,
216
- changedFiles: [],
217
- verification: [],
218
- evidence: [],
219
- terminalPending: false,
220
- observedTerminalRevision: -1,
221
- resourceRevision: ++this.#resourceRevision
222
- };
223
- if (!native)
224
- (this.#retainedExecution.getStore()?.budget ?? this.#creationBudget).count++;
225
- this.#agents.set(state.agentId, state);
226
- const knownModel = execution.models.get(input.modelId ?? execution.modelId);
227
- if (knownModel) {
228
- state.modelId = knownModel.id;
229
- state.modelName = knownModel.name;
230
- }
231
- try {
232
- const selected = resolveModel(execution, input);
233
- state.modelId = selected.id;
234
- state.modelName = selected.name;
235
- state.reasoningEffort = selected.reasoningEffort;
236
- }
237
- catch (error) {
238
- this.#setFailure(state, error);
239
- }
240
- try {
241
- await options.onCreated?.(this.#receiptSnapshot(state));
242
- if (this.#disposed)
243
- throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
244
- this.#requireExecution();
245
- options.signal?.throwIfAborted();
246
- }
247
- catch (error) {
248
- await this.#deleteUnstartedSession(state.agentId, error);
249
- }
250
- if (state.status === 'failed') {
251
- await this.#publishTerminal(state);
252
- this.#observeTerminal(state);
253
- return this.#snapshot(state);
254
- }
255
- this.#queueTurn(state, input.prompt, execution, [], undefined, undefined, options.signal, options.turnSignal);
256
- if (options.waitFor === 'start' || (options.waitFor === undefined && input.background))
257
- return this.#receiptSnapshot(state);
258
- await waitForPromise(state.turn.completion, options.signal);
259
- const includeLatestMessage = this.#hasUnreadMessage(state);
260
- const includeTerminalDetails = this.#observeTerminal(state);
261
- this.#observeLatestMessage(state);
262
- return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
263
- }
264
- async output(agentId, options = {}) {
265
- options.signal?.throwIfAborted();
266
- const state = await this.#find(agentId);
267
- options.signal?.throwIfAborted();
268
- validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
269
- if (options.untilSettled) {
270
- if (state.turn)
271
- await waitForPromise(state.turn.completion, options.signal);
272
- options.signal?.throwIfAborted();
273
- const includeLatestMessage = this.#hasUnreadMessage(state);
274
- const includeTerminalDetails = this.#observeTerminal(state);
275
- this.#observeLatestMessage(state);
276
- return {
277
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
278
- waitOutcome: 'completion'
279
- };
280
- }
281
- if (options.waitMs === undefined || options.waitMs === 0)
282
- return {
283
- ...this.#snapshot(state, {
284
- includeLatestMessage: false,
285
- includeTerminalDetails: false
286
- }),
287
- waitOutcome: 'snapshot'
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
288
294
  };
289
- 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);
290
332
  const includeLatestMessage = this.#hasUnreadMessage(state);
291
333
  const includeTerminalDetails = this.#observeTerminal(state);
292
334
  this.#observeLatestMessage(state);
293
- return {
294
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
295
- waitOutcome: 'completion'
296
- };
297
- }
298
- const waitFor = options.waitFor ?? 'completion';
299
- if (waitFor === 'message' && this.#hasUnreadMessage(state)) {
300
- this.#observeLatestMessage(state);
301
- return { ...this.#snapshot(state), waitOutcome: 'message' };
302
- }
303
- const waitMs = this.#retainedExecution.getStore()?.native
304
- ? options.waitMs
305
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
306
- if (waitFor === 'completion') {
307
- const completed = await this.#waitForCompletion(state, waitMs, options.signal, options.waitInterrupt);
335
+ return this.#snapshot(state, { includeLatestMessage, includeTerminalDetails });
336
+ });
337
+ }
338
+ async output(agentId, options = {}) {
339
+ return this.#using([agentId], async () => {
308
340
  options.signal?.throwIfAborted();
309
- const includeLatestMessage = this.#hasUnreadMessage(state);
310
- const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
311
- if (includeLatestMessage)
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);
347
+ options.signal?.throwIfAborted();
348
+ const includeLatestMessage = this.#hasUnreadMessage(state);
349
+ const includeTerminalDetails = this.#observeTerminal(state);
312
350
  this.#observeLatestMessage(state);
313
- return {
314
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
315
- ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
316
- };
317
- }
318
- const activity = await this.#waitForMessageOrCompletion([state], waitMs, options.signal, options.waitInterrupt);
319
- options.signal?.throwIfAborted();
320
- const includeLatestMessage = activity?.kind === 'message' ||
321
- (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
322
- const includeTerminalDetails = activity?.kind === 'completion' ? this.#observeTerminal(activity.state) : false;
323
- if (activity)
324
- this.#observeLatestMessage(activity.state);
325
- return {
326
- ...this.#snapshot(activity?.state ?? state, {
327
- includeLatestMessage,
328
- includeTerminalDetails
329
- }),
330
- ...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
331
- };
332
- }
333
- async outputAny(options = {}) {
334
- options.signal?.throwIfAborted();
335
- if (options.untilSettled)
336
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'An unbounded wait requires an agent id.');
337
- validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
338
- const states = [...this.#agents.values()];
339
- if (states.length === 0)
340
- throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
341
- const live = states.filter((state) => state.status === 'queued' || state.status === 'running');
342
- if (options.waitMs === undefined || options.waitMs === 0)
343
- return {
344
- ...this.#snapshot(live.at(-1) ?? states.at(-1), {
345
- includeLatestMessage: false,
346
- includeTerminalDetails: false
347
- }),
348
- waitOutcome: 'snapshot'
349
- };
350
- if (live.length === 0) {
351
- const state = states.at(-1);
352
- const includeLatestMessage = this.#hasUnreadMessage(state);
353
- const includeTerminalDetails = this.#observeTerminal(state);
354
- this.#observeLatestMessage(state);
355
- return {
356
- ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
357
- waitOutcome: 'completion'
358
- };
359
- }
360
- const waitFor = options.waitFor ?? 'completion';
361
- if (waitFor === 'message') {
362
- const pending = this.#oldestUnreadMessage(live);
363
- if (pending) {
364
- this.#observeLatestMessage(pending);
365
- return { ...this.#snapshot(pending), waitOutcome: 'message' };
351
+ return {
352
+ ...this.#snapshot(state, { includeLatestMessage, includeTerminalDetails }),
353
+ waitOutcome: 'completion'
354
+ };
366
355
  }
367
- }
368
- const waitMs = this.#retainedExecution.getStore()?.native
369
- ? options.waitMs
370
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
371
- if (waitFor === 'message') {
372
- const activity = await this.#waitForMessageOrCompletion(live, waitMs, options.signal, options.waitInterrupt);
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
+ };
372
+ }
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);
373
394
  options.signal?.throwIfAborted();
374
395
  const includeLatestMessage = activity?.kind === 'message' ||
375
396
  (activity?.kind === 'completion' && this.#hasUnreadMessage(activity.state));
@@ -377,27 +398,93 @@ export class SubagentSessionController {
377
398
  if (activity)
378
399
  this.#observeLatestMessage(activity.state);
379
400
  return {
380
- ...this.#snapshot(activity?.state ?? live.at(-1), {
401
+ ...this.#snapshot(activity?.state ?? state, {
381
402
  includeLatestMessage,
382
403
  includeTerminalDetails
383
404
  }),
384
405
  ...waitMetadata(activity?.kind ?? (options.waitInterrupt?.pending() ? 'interrupted' : 'timeout'))
385
406
  };
386
- }
387
- const completed = await this.#waitForAnyCompletion(live, waitMs, options.signal, options.waitInterrupt);
388
- options.signal?.throwIfAborted();
389
- const state = completed ?? this.#oldestUnreadMessage(live) ?? live.at(-1);
390
- const includeLatestMessage = this.#hasUnreadMessage(state);
391
- const includeTerminalDetails = completed ? this.#observeTerminal(state) : false;
392
- if (includeLatestMessage)
393
- this.#observeLatestMessage(state);
394
- return {
395
- ...this.#snapshot(state, {
396
- includeLatestMessage,
397
- includeTerminalDetails
398
- }),
399
- ...waitMetadata(completed ? 'completion' : options.waitInterrupt?.pending() ? 'interrupted' : 'timeout')
400
- };
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
+ });
401
488
  }
402
489
  async outputMany(agentIds, options = {}) {
403
490
  options.signal?.throwIfAborted();
@@ -407,187 +494,198 @@ export class SubagentSessionController {
407
494
  validateOutputOptions(options, this.#retainedExecution.getStore()?.native === true);
408
495
  if (!ids || 'untilSettled' in options)
409
496
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Group waits require a bounded deadline.');
410
- const states = await Promise.all(ids.map((id) => this.#find(id)));
411
- options.signal?.throwIfAborted();
412
- const waitFor = options.waitFor ?? 'completion';
413
- const targets = states.map((state) => ({
414
- state,
415
- turn: state.turn,
416
- view: copyWaitState(state),
417
- observation: this.#nativeObservation(state)
418
- }));
419
- const refresh = () => {
420
- for (const target of targets) {
421
- if (!isTerminalState(target.view) &&
422
- (target.state.turn === target.turn ||
423
- (target.state.turn === undefined && isTerminalState(target.state))))
424
- 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();
425
539
  }
426
- };
427
- const unread = (target) => target.view.messageRevision >
428
- (target.observation?.messageRevision ?? target.state.observedMessageRevision);
429
- const ready = (target) => isTerminalState(target.view) || (waitFor === 'message' && unread(target));
430
- const snapshot = options.waitMs === undefined || options.waitMs === 0;
431
- let satisfied = false;
432
- if (!snapshot) {
433
- const waitMs = this.#retainedExecution.getStore()?.native
434
- ? options.waitMs
435
- : Math.max(options.waitMs, TOOL_EXECUTION_LIMITS.agentWaitWaitFloorMs);
436
- satisfied = await waitForAgentCondition({
437
- waitMs,
438
- signal: options.signal,
439
- interrupt: options.waitInterrupt,
440
- ready: () => {
441
- refresh();
442
- return mode === 'all' ? targets.every(ready) : targets.some(ready);
443
- },
444
- subscribe: (notify) => {
445
- const listener = () => notify();
446
- this.#activityWaiters.add(listener);
447
- 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);
448
581
  }
582
+ return result;
449
583
  });
450
- options.signal?.throwIfAborted();
451
- }
452
- refresh();
453
- if (!snapshot)
454
- satisfied = mode === 'all' ? targets.every(ready) : targets.some(ready);
455
- const pendingAgentIds = targets
456
- .filter((target) => !ready(target))
457
- .map((target) => target.state.agentId);
458
- const agents = targets.map((target) => {
459
- const { state, view, observation } = target;
460
- const terminal = isTerminalState(view);
461
- const outcome = snapshot
462
- ? 'snapshot'
463
- : terminal
464
- ? 'completion'
465
- : ready(target)
466
- ? 'message'
467
- : satisfied
468
- ? 'snapshot'
469
- : options.waitInterrupt?.pending()
470
- ? 'interrupted'
471
- : 'timeout';
472
- const includeLatestMessage = outcome !== 'snapshot' && unread(target);
473
- const includeTerminalDetails = outcome === 'completion' &&
474
- (observation
475
- ? observation.terminalRevision < view.resourceRevision
476
- : state.observedTerminalRevision < view.resourceRevision);
477
- const result = {
478
- ...this.#snapshot(view, { includeLatestMessage, includeTerminalDetails }),
479
- ...(target.turn === undefined ? {} : { turnId: target.turn.turnId }),
480
- ...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')
481
598
  };
482
- if (includeLatestMessage) {
483
- if (observation)
484
- observation.messageRevision = Math.max(observation.messageRevision, view.messageRevision);
485
- else
486
- state.observedMessageRevision = Math.max(state.observedMessageRevision, view.messageRevision);
487
- }
488
- if (includeTerminalDetails) {
489
- if (observation)
490
- observation.terminalRevision = Math.max(observation.terminalRevision, view.resourceRevision);
491
- else
492
- state.observedTerminalRevision = Math.max(state.observedTerminalRevision, view.resourceRevision);
493
- }
494
- return result;
495
599
  });
496
- return {
497
- mode,
498
- waitFor,
499
- agents,
500
- pendingAgentIds,
501
- ...waitMetadata(snapshot
502
- ? 'snapshot'
503
- : !satisfied
504
- ? options.waitInterrupt?.pending()
505
- ? 'interrupted'
506
- : 'timeout'
507
- : agents.some((agent) => agent.waitOutcome === 'message')
508
- ? 'message'
509
- : 'completion')
510
- };
511
600
  }
512
601
  async message(agentId, message, options = {}) {
513
- const execution = this.#requireExecution();
514
- if (typeof message !== 'string' ||
515
- message.trim().length === 0 ||
516
- (this.#retainedExecution.getStore()?.native !== true &&
517
- Buffer.byteLength(message) > TOOL_EXECUTION_LIMITS.subagentMessageMaxBytes) ||
518
- (options.delivery !== undefined &&
519
- options.delivery !== 'append' &&
520
- options.delivery !== 'replace'))
521
- throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Agent message input is invalid.');
522
- const state = await this.#find(agentId);
523
- return this.#control(state, async () => {
524
- this.#requireExecution();
525
- options.signal?.throwIfAborted();
526
- // Resolve before cancelling so an unavailable model does not stop valid work.
527
- const selected = resolveExistingModel(execution, state);
528
- if (options.delivery === 'replace')
529
- await this.#cancelState(state);
530
- this.#requireExecution();
531
- options.signal?.throwIfAborted();
532
- const turnId = state.turn?.turnId ?? randomUUID();
533
- const receipt = { messageId: randomUUID(), turnId, deliveryStatus: 'accepted' };
534
- const mutableReceipt = receipt;
535
- const delivery = this.#createDelivery(state, execution, mutableReceipt, options.signal);
536
- try {
537
- if (state.status === 'running' && state.turn) {
538
- const turn = state.turn;
539
- if (!turn.handle)
540
- await turn.handleReady;
541
- this.#requireExecution();
542
- options.signal?.throwIfAborted();
543
- delivery.signal?.throwIfAborted();
544
- if (state.status === 'running' &&
545
- state.turn === turn &&
546
- 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) {
547
644
  delivery.accepted();
645
+ state.turn.pendingMessages.push({ text: message, delivery });
548
646
  state.receipt = mutableReceipt;
549
647
  return this.#messageReceiptSnapshot(state);
550
648
  }
551
- await turn.completion;
552
- }
553
- 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();
554
657
  delivery.accepted();
555
- state.turn.pendingMessages.push({ text: message, delivery });
556
658
  state.receipt = mutableReceipt;
659
+ this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
557
660
  return this.#messageReceiptSnapshot(state);
558
661
  }
559
- this.#requireExecution();
560
- options.signal?.throwIfAborted();
561
- delivery.signal?.throwIfAborted();
562
- state.modelId = selected.id;
563
- state.modelName = selected.name;
564
- state.reasoningEffort = selected.reasoningEffort;
565
- // A closing turn may have rejected append; reserve a fresh identity for its successor.
566
- mutableReceipt.turnId = randomUUID();
567
- delivery.accepted();
568
- state.receipt = mutableReceipt;
569
- this.#queueTurn(state, message, execution, [], mutableReceipt.turnId, delivery, options.signal);
570
- return this.#messageReceiptSnapshot(state);
571
- }
572
- catch (error) {
573
- delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
574
- ? 'cancelled'
575
- : 'failed');
576
- throw error;
577
- }
662
+ catch (error) {
663
+ delivery.settled?.(options.signal?.aborted || ('signal' in execution && execution.signal.aborted)
664
+ ? 'cancelled'
665
+ : 'failed');
666
+ throw error;
667
+ }
668
+ });
578
669
  });
579
670
  }
580
671
  async cancel(agentId) {
581
- const state = await this.#find(agentId);
582
- await this.#control(state, () => this.#cancelState(state));
583
- const includeLatestMessage = this.#hasUnreadMessage(state);
584
- const includeTerminalDetails = this.#observeTerminal(state);
585
- this.#observeLatestMessage(state);
586
- 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
+ });
587
680
  }
588
681
  #control(state, operation) {
589
682
  const result = (state.controlTail ?? Promise.resolve()).then(operation);
590
- 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
+ });
591
689
  return result;
592
690
  }
593
691
  async #cancelState(state) {
@@ -625,7 +723,7 @@ export class SubagentSessionController {
625
723
  const terminal = states
626
724
  .filter((state) => state.status !== 'queued' && state.status !== 'running')
627
725
  .sort((left, right) => right.resourceRevision - left.resourceRevision)
628
- .slice(0, 8);
726
+ .slice(0, RECENT_TERMINAL_RESOURCE_LIMIT);
629
727
  return [...active, ...terminal].map((state) => ({
630
728
  agentId: state.agentId,
631
729
  status: state.status,
@@ -673,6 +771,7 @@ export class SubagentSessionController {
673
771
  ? native.signal
674
772
  : AbortSignal.any([native.signal, signal]));
675
773
  state.status = 'queued';
774
+ state.restorable = false;
676
775
  this.#touchResource(state);
677
776
  delete state.summary;
678
777
  delete state.permissionStop;
@@ -750,9 +849,18 @@ export class SubagentSessionController {
750
849
  await this.#consumeTurn(state, turn);
751
850
  }
752
851
  async #consumeTurn(state, turn) {
852
+ const previousActivity = state.latestActivity;
853
+ const previousMessageRevision = state.messageRevision;
854
+ let terminal;
753
855
  try {
754
856
  const consume = (async () => {
755
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';
756
864
  if (!observeAgentEvent(state, event))
757
865
  continue;
758
866
  state.messageRevision++;
@@ -778,6 +886,14 @@ export class SubagentSessionController {
778
886
  state.errorCode = result.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED';
779
887
  state.errorMessage = result.errorMessage ?? 'Subagent execution failed.';
780
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));
781
897
  state.terminalPending = true;
782
898
  this.#touchResource(state, true);
783
899
  this.#closeReceipt(state);
@@ -812,6 +928,7 @@ export class SubagentSessionController {
812
928
  #setFailure(state, error) {
813
929
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_SUBAGENT_FAILED');
814
930
  state.status = 'failed';
931
+ state.restorable = false;
815
932
  state.errorCode = diagnostic.code;
816
933
  state.errorMessage = diagnostic.message;
817
934
  state.terminalPending = true;
@@ -946,6 +1063,7 @@ export class SubagentSessionController {
946
1063
  }
947
1064
  async #deleteUnstartedSession(agentId, error) {
948
1065
  this.#agents.delete(agentId);
1066
+ this.#discovered.delete(agentId);
949
1067
  try {
950
1068
  await this.backend.deleteSession(agentId);
951
1069
  }
@@ -956,16 +1074,105 @@ export class SubagentSessionController {
956
1074
  }
957
1075
  throw error;
958
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
+ }
959
1127
  async #find(agentId) {
1128
+ if (this.#disposed)
1129
+ throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
960
1130
  const known = this.#agents.get(agentId);
961
- if (known)
1131
+ if (known && !known.detailsEvicted)
962
1132
  return known;
963
- const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
964
- if (!persisted)
965
- throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
966
- const restored = restoreAgentState(persisted);
967
- this.#agents.set(agentId, restored);
968
- return restored;
1133
+ const pending = this.#loading.get(agentId);
1134
+ if (pending)
1135
+ return pending;
1136
+ const load = (async () => {
1137
+ const persisted = await this.backend.loadChild(this.parentSessionId, agentId);
1138
+ if (this.#disposed)
1139
+ throw new MarAgentError('MAR_AGENT_DISPOSED', 'Subagent controller is disposed.');
1140
+ if (!persisted) {
1141
+ this.#agents.delete(agentId);
1142
+ this.#discovered.delete(agentId);
1143
+ throw new MarAgentError('MAR_AGENT_SUBAGENT_NOT_FOUND', 'Subagent was not found.');
1144
+ }
1145
+ const existing = this.#agents.get(agentId);
1146
+ if (existing && !existing.detailsEvicted)
1147
+ return existing;
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
+ }
1159
+ let revision = this.#discovered.get(agentId);
1160
+ if (revision === undefined) {
1161
+ revision = ++this.#resourceRevision;
1162
+ this.#discovered.set(agentId, revision);
1163
+ }
1164
+ restored.resourceRevision = revision;
1165
+ this.#agents.set(agentId, restored);
1166
+ return restored;
1167
+ })();
1168
+ this.#loading.set(agentId, load);
1169
+ try {
1170
+ return await load;
1171
+ }
1172
+ finally {
1173
+ if (this.#loading.get(agentId) === load)
1174
+ this.#loading.delete(agentId);
1175
+ }
969
1176
  }
970
1177
  #requireExecution() {
971
1178
  if (this.#disposed)
@@ -1139,6 +1346,7 @@ function restoreAgentState(session) {
1139
1346
  const terminal = currentRecords.findLast((record) => ['execution.completed', 'execution.failed', 'execution.cancelled'].includes(eventType(record) ?? ''));
1140
1347
  const terminalType = eventType(terminal);
1141
1348
  const state = {
1349
+ restorable: true,
1142
1350
  agentId: session.id,
1143
1351
  description: session.source.description,
1144
1352
  role: session.source.role,