@fishwithcat/dsh-subagent 0.2.2-alpha.2 → 0.2.2-alpha.3

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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Owns subagent startup, message admission, cold resume, and execution lifetimes.
3
3
  * One child lock orders input acceptance and closure; one activation map owns
4
- * capacity, parent relationships, and resource release across all providers.
4
+ * capacity, runtime ownership, and resource release across all providers.
5
5
  *
6
6
  * @module @deepseek-ai/dsh-subagent/manager
7
7
  */
@@ -63,12 +63,12 @@ import { brandString } from '@deepseek-ai/dsh-brand';
63
63
  import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
64
64
  import { SessionLogOffset } from '@deepseek-ai/dsh-session';
65
65
  import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
66
- import { createAgentMessage, withContinuableReturnGuidance, createSettlementMessage } from "./continuation-messages.js";
67
- import { assertSubagentMaxDepth } from "./depth.js";
66
+ import { createAgentMessage, createSettlementMessage, withResultDeliveryGuidance, } from "./continuation-messages.js";
67
+ import { installSubagentArchiveAdmission } from "./archive-admission.js";
68
68
  import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
69
69
  import { establishCatalogChild, establishExternalCatalogChild } from "./catalog.js";
70
70
  import { SubagentError } from "./error.js";
71
- import { isAdjacentAgentSendMessageTool } from "./internal.js";
71
+ import { SubagentRunId } from "./types.js";
72
72
  import { attachStructuredRuntime } from "./structured.js";
73
73
  import { ActivationPool, ChildLock, activationAgent, captureLocalResult, observeLocalFailure, requireLocalActivation } from "./activation.js";
74
74
  /** Render only the safe outer message from execution or teardown failures. */
@@ -91,18 +91,13 @@ export class SubagentManager {
91
91
  locks = new ChildLock();
92
92
  /** Structural Cordis owner of every Activation handle. */
93
93
  ownerCtx;
94
- /**
95
- * Exact roots whose host teardown has begun, with the live lineage members
96
- * observed under each root. Entries remain until that exact root leaves the
97
- * Agent registry, closing admission throughout its host's teardown without
98
- * poisoning a later same-id replacement.
99
- */
100
- closingScopes = new Map();
94
+ /** Exact subagent tree roots whose host teardown has closed admission. */
95
+ closingRoots = new Set();
101
96
  draining = false;
102
97
  /**
103
98
  * Build one manager inside the service's Agent-injected context.
104
99
  * @param ctx - context providing Agents, Sessions, and teardown ownership.
105
- * @param host - provider dispatch and lifecycle observation owned by the service.
100
+ * @param host - descriptor reads and lifecycle publication owned by the service.
106
101
  * @param maxActiveSubagents - the current configured child capacity.
107
102
  * @param delegatedApproval - the configured approval policy seed for each local child.
108
103
  */
@@ -111,16 +106,15 @@ export class SubagentManager {
111
106
  this.host = host;
112
107
  this.maxActiveSubagents = maxActiveSubagents;
113
108
  this.delegatedApproval = delegatedApproval;
114
- // Ordinary Cordis owner effects unwind in reverse registration order, which
115
- // cannot express the dynamic child graph. Register the private scope's
116
- // structural disposer FIRST and the drain SECOND, so reverse unwind invokes
117
- // the drain before releasing the scope; a cleanup effect on the same scope
118
- // as the Agent handles would let structural handle disposal bypass
119
- // child-first ordering.
109
+ // Drain managed instances before Cordis releases their owning scope.
120
110
  const scope = ctx.plugin(function activationOwner() { });
121
111
  this.ownerCtx = scope.ctx;
112
+ installSubagentArchiveAdmission(ctx, (agent) => {
113
+ const activation = this.resident.get(agent.id);
114
+ return activation !== undefined && activationAgent(activation) === agent ? activation.ancestors : undefined;
115
+ }, session => host.identityOf(session));
122
116
  ctx.on('agent/disposed', ({ agent }) => {
123
- this.closingScopes.delete(agent);
117
+ this.closingRoots.delete(agent);
124
118
  });
125
119
  ctx.effect(function* () {
126
120
  yield scope.dispose;
@@ -130,7 +124,7 @@ export class SubagentManager {
130
124
  /**
131
125
  * Interrupt one live continuable child's current turn under the supplied authority.
132
126
  * @param targetSessionId - the durable child session id to interrupt.
133
- * @param authority - the human parent address or exact live ancestor Agent.
127
+ * @param authority - a trusted user operation or exact live ancestor Agent.
134
128
  */
135
129
  interrupt(targetSessionId, authority) {
136
130
  if (authority.kind === 'ancestor') {
@@ -145,12 +139,8 @@ export class SubagentManager {
145
139
  const activation = this.resident.get(targetSessionId);
146
140
  if (activation === undefined)
147
141
  return;
148
- if (authority.kind === 'user') {
149
- if (activation.parent.id !== authority.parentSessionId) {
150
- throw new SubagentError(`subagent "${targetSessionId}" belongs to another parent session`, 'UNAUTHORIZED');
151
- }
152
- }
153
- else if (!activation.ancestry.has(authority.agent)) {
142
+ if (authority.kind === 'ancestor'
143
+ && (this.rootOf(authority.agent) !== activation.root || !activation.ancestors.includes(authority.agent.id))) {
154
144
  throw new SubagentError(`subagent "${targetSessionId}" is not a live descendant of agent "${authority.agent.id}"`, 'UNAUTHORIZED');
155
145
  }
156
146
  // Disposal already stopped the target with a whole-Activation teardown;
@@ -169,32 +159,27 @@ export class SubagentManager {
169
159
  }
170
160
  /**
171
161
  * Close admission, await every already-admitted materialization through
172
- * publication or rollback, then dispose the stable live Activation graph
173
- * child-first.
162
+ * publication or rollback, then dispose every agent in the subagent tree.
174
163
  */
175
164
  async drain() {
176
165
  this.draining = true;
177
166
  for (const pending of this.materializations)
178
167
  pending.controller.abort();
179
168
  await Promise.all([...this.materializations].map(materialization => materialization.settled));
180
- const owned = new Set();
181
- for (const activation of this.resident.values()) {
182
- for (const child of activation.ownedChildren)
183
- owned.add(child);
184
- }
185
- const roots = [...this.resident.values()].filter(activation => !owned.has(activation.childId));
186
- await this.disposeRoots(roots, 'activation(s)');
169
+ await this.disposeRoots([...this.resident.values()], 'activation(s)');
187
170
  }
188
171
  /**
189
- * Join progressing child work, leaving idle parked inboxes resident.
190
- * @param parent - exact live parent whose descendants are observed.
191
- * @returns whether work was joined and the host must recheck parent completion.
172
+ * Join progressing subagent tree work for its root, leaving idle parked inboxes resident.
173
+ * @param root - exact live subagent tree root; ordinary members return false.
174
+ * @returns whether work was joined and the host must recheck root completion.
192
175
  */
193
- async waitForChildren(parent) {
176
+ async waitForTreeWork(root) {
177
+ if (this.rootOf(root) !== root)
178
+ return false;
194
179
  let observed = false;
195
180
  while (true) {
196
- const pending = [...this.materializations].filter(item => item.lineage.includes(parent));
197
- const children = [...this.resident.values()].filter(item => activationAgent(item) !== parent && item.ancestry.has(parent));
181
+ const pending = [...this.materializations].filter(item => item.root === root);
182
+ const children = [...this.resident.values()].filter(item => item.root === root);
198
183
  observed ||= children.some(item => item.kind === 'local' && item.handle.agent.status !== 'idle');
199
184
  await Promise.all(children.flatMap(item => item.kind === 'local' ? [item.handle.agent.whenIdle()] : []));
200
185
  const waits = pending.map(item => item.settled);
@@ -207,13 +192,6 @@ export class SubagentManager {
207
192
  waits.push(item.handle.agent.whenIdle());
208
193
  continue;
209
194
  }
210
- if (item.ownedChildren.size > 0) {
211
- // Published descendants are observed separately; preparation holds
212
- // keep the owner pending until publication or rollback wakes it.
213
- if ([...item.ownedChildren].some(id => !this.resident.has(id)))
214
- waits.push(item.poke.promise);
215
- continue;
216
- }
217
195
  const inbox = item.handle.agent.inbox;
218
196
  if (item.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
219
197
  continue;
@@ -225,147 +203,82 @@ export class SubagentManager {
225
203
  continue;
226
204
  }
227
205
  // A descendant can be published while whenIdle follows another child.
228
- if ([...this.materializations].some(item => item.lineage.includes(parent) && !pending.includes(item))
229
- || [...this.resident.values()].some(item => activationAgent(item) !== parent
230
- && item.ancestry.has(parent) && !children.includes(item)))
206
+ if ([...this.materializations].some(item => item.root === root && !pending.includes(item))
207
+ || [...this.resident.values()].some(item => item.root === root && !children.includes(item)))
231
208
  continue;
232
209
  return observed;
233
210
  }
234
211
  }
235
212
  /**
236
- * Stop the managed descendants of exact live host-owned parents.
237
- * @param parents - exact live roots whose managed descendants must stop.
213
+ * Stop every member of the selected subagent trees; ordinary members do not close a subagent tree.
214
+ * @param parents - exact live subagent tree roots whose instances must stop.
238
215
  */
239
216
  async drainDescendants(parents) {
240
- const roots = new Set(parents.filter(parent => this.ctx.agents.get(parent.id) === parent));
241
- if (roots.size === 0)
242
- return;
243
- for (const root of roots) {
244
- this.closingMembers(root).add(root);
245
- }
246
- const targets = [];
247
- for (const activation of this.resident.values()) {
248
- const child = activationAgent(activation);
249
- const lineage = this.liveLineage(child ?? activation.parent);
250
- const owners = [...roots].filter(root => child !== root
251
- && activation.ancestry.has(root));
252
- if (owners.length === 0)
253
- continue;
254
- targets.push(activation);
255
- for (const owner of owners) {
256
- const members = this.closingMembers(owner);
257
- if (child !== undefined)
258
- members.add(child);
259
- for (const agent of lineage)
260
- members.add(agent);
261
- }
262
- }
263
- const materializations = [...this.materializations].filter((materialization) => {
264
- const owners = [...roots].filter(root => materialization.lineage.includes(root));
265
- for (const owner of owners) {
266
- const members = this.closingMembers(owner);
267
- for (const agent of materialization.lineage)
268
- members.add(agent);
269
- }
270
- if (owners.length > 0)
271
- materialization.controller.abort();
272
- return owners.length > 0;
273
- });
274
- const ownedTargets = new Set();
275
- for (const activation of targets) {
276
- for (const child of activation.ownedChildren)
277
- ownedTargets.add(child);
278
- }
279
- const targetRoots = targets.filter(activation => !ownedTargets.has(activation.childId));
280
- for (const activation of targets) {
281
- const disposal = this.dispose(activation);
282
- void disposal.catch(() => undefined);
283
- }
284
- await Promise.all(materializations.map(materialization => materialization.settled));
285
- await this.disposeRoots(targetRoots, 'scoped activation(s)');
286
- }
287
- /**
288
- * Release selected resident direct children of one exact live parent.
289
- * @param parent - exact live direct parent authorizing the selected release.
290
- * @param childIds - durable direct-child ids to release when resident.
291
- */
292
- async drainChildren(parent, childIds) {
293
- if (this.ctx.agents.get(parent.id) !== parent) {
294
- throw new SubagentError('selected child teardown requires the exact live parent agent', 'UNAUTHORIZED');
295
- }
296
- const targets = [];
297
- for (const childId of new Set(childIds)) {
298
- const activation = this.resident.get(childId);
299
- if (activation === undefined)
300
- continue;
301
- if (activation.parent.id !== parent.id || !activation.ancestry.has(parent)) {
302
- throw new SubagentError(`subagent "${childId}" is not a direct child of agent "${parent.id}"`, 'UNAUTHORIZED');
303
- }
304
- targets.push(activation);
305
- }
306
- for (const activation of targets) {
307
- const disposal = this.dispose(activation);
308
- void disposal.catch(() => undefined);
309
- }
310
- await this.disposeRoots(targets, 'selected activation(s)');
217
+ const roots = new Set(parents.filter(root => this.ctx.agents.get(root.id) === root && this.rootOf(root) === root));
218
+ for (const root of roots)
219
+ this.closingRoots.add(root);
220
+ const pending = [...this.materializations].filter(item => roots.has(item.root));
221
+ for (const item of pending)
222
+ item.controller.abort();
223
+ const targets = [...this.resident.values()].filter(item => roots.has(item.root));
224
+ const disposal = this.disposeRoots(targets, 'scoped activation(s)');
225
+ const [disposed] = await Promise.allSettled([disposal, ...pending.map(item => item.settled)]);
226
+ if (disposed.status === 'rejected')
227
+ throw disposed.reason;
311
228
  }
312
229
  /**
313
230
  * Start one continuable background child and resolve at initial inbox acceptance.
314
231
  * Every earlier failure disposes any created handle and rolls back Activation
315
- * and parent ownership without returning either id.
232
+ * and its capacity slot without returning either id.
316
233
  * @param spec - provider, delegation request, and caller cancellation.
234
+ * @param prepare - creation contribution bound to the checked provider.
317
235
  * @returns the durable child id and accepted initial prompt message id.
318
236
  */
319
- async startLocal(spec) {
320
- const request = spec.request;
321
- const parent = request.parent;
237
+ async startLocal(spec, prepare) {
238
+ const parent = spec.parent;
322
239
  this.assertAdmitting(parent);
323
- const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence();
324
- assertSubagentMaxDepth(request.maxDepth);
325
- const childId = spec.childId ?? brandString(randomUUID());
326
- this.assertChildIdAvailable(childId);
327
- const childDepth = resolveChildDepth(parent, request.maxDepth);
328
- // Snapshot before any await: invalid descriptor JSON rejects the call
329
- // before a child exists, and the detached value is what reaches the log.
330
- const agentOptions = resolveChildAgentOptions(parent, request.agentOptions, childDepth);
331
- const agentProvider = agentOptions.provider;
332
- const agentModel = agentOptions.model;
333
- const agentReasoningEffort = agentOptions.reasoningEffort;
334
- const descriptor = snapshotSubagentDescriptor({
335
- mode: 'continuable',
336
- provider: spec.provider,
337
- label: spec.label,
338
- ...agentProvider !== undefined ? { agentProvider } : {},
339
- ...agentModel !== undefined ? { agentModel } : {},
340
- ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
341
- });
342
- // Capture before the first await: a later parent switch belongs to the
343
- // parent's future, not to this child.
344
- const delegatedPolicies = captureDelegatedPolicyOverrides(parent, this.delegatedApproval());
345
- // An idle continuation-managed parent must not settle while a caller is
346
- // still creating its child. A turn-scoped delegation does not need this,
347
- // but the service is also callable outside a turn.
348
- const releaseHold = this.holdOwnership(parent, childId);
349
- try {
350
- const cwd = await this.resolveDirectory(request, spec.signal);
351
- const prepared = await this.host.prepareContinuable(spec.provider, {
240
+ const ancestry = this.ancestryForChild(parent);
241
+ return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
242
+ const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence();
243
+ const childId = spec.childId ?? brandString(randomUUID());
244
+ this.assertChildIdAvailable(childId);
245
+ const childDepth = resolveChildDepth(parent, spec.maxDepth);
246
+ // Snapshot before any await: invalid descriptor JSON rejects the call
247
+ // before a child exists, and the detached value is what reaches the log.
248
+ const agentOptions = resolveChildAgentOptions(parent, spec.agentOptions, childDepth);
249
+ const agentProvider = agentOptions.provider;
250
+ const agentModel = agentOptions.model;
251
+ const agentReasoningEffort = agentOptions.reasoningEffort;
252
+ const descriptor = snapshotSubagentDescriptor({
253
+ mode: 'continuable',
254
+ provider: spec.provider,
255
+ label: spec.label,
256
+ ...agentProvider !== undefined ? { agentProvider } : {},
257
+ ...agentModel !== undefined ? { agentModel } : {},
258
+ ...agentReasoningEffort !== undefined ? { agentReasoningEffort } : {},
259
+ });
260
+ // Capture before the first await: a later parent switch belongs to the
261
+ // parent's future, not to this child.
262
+ const delegatedPolicies = captureDelegatedPolicyOverrides(parent, this.delegatedApproval());
263
+ const cwd = await this.resolveDirectory(spec, signal);
264
+ const prepared = await prepare({
352
265
  sessionId: childId,
353
266
  cwd,
354
267
  parent,
355
- signal: spec.signal,
268
+ signal,
356
269
  });
357
- spec.signal.throwIfAborted();
270
+ signal.throwIfAborted();
358
271
  this.assertAdmitting(parent);
359
272
  const inheritedEventCount = SessionLogOffset(prepared.seed?.length ?? 0);
360
273
  const seed = prepared.seed;
361
274
  let established;
362
275
  const messageId = await this.locks.run(childId, async () => {
363
- spec.signal.throwIfAborted();
276
+ signal.throwIfAborted();
364
277
  this.assertAdmitting(parent);
365
278
  this.assertChildIdAvailable(childId);
366
279
  if (spec.childId !== undefined) {
367
- const persisted = await persistence?.stat(childId, { signal: spec.signal });
368
- spec.signal.throwIfAborted();
280
+ const persisted = await persistence?.stat(childId, { signal: signal });
281
+ signal.throwIfAborted();
369
282
  this.assertAdmitting(parent);
370
283
  this.assertChildIdAvailable(childId);
371
284
  if (persisted !== undefined) {
@@ -376,84 +289,80 @@ export class SubagentManager {
376
289
  kind: 'local',
377
290
  childId,
378
291
  provider: spec.provider,
379
- parent,
292
+ ...ancestry,
380
293
  create: {
294
+ parent,
381
295
  cwd,
382
296
  seed,
383
- meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, request.agentPreset),
297
+ meta: childSessionMeta(parent, childDepth, prepared.seed !== undefined, spec.agentPreset),
384
298
  inheritedEventCount,
385
299
  delegatedPolicies,
386
300
  descriptor,
387
301
  },
388
302
  agentOptions,
389
- composition: { agentPreset: request.agentPreset },
390
- signal: spec.signal,
303
+ composition: { agentPreset: spec.agentPreset },
304
+ signal,
391
305
  delivery: spec.delivery,
392
- outputSchema: spec.request.outputSchema,
306
+ outputSchema: spec.outputSchema,
393
307
  });
394
308
  established = activation;
395
309
  const child = requireLocalActivation(activation).handle.agent;
396
310
  const childHeader = child.session.header;
397
- return await this.submitMaterialized(activation, spec.delivery !== 'caller' && isAdjacentAgentSendMessageTool(this.ctx.get('tools')?.get('send_message', child))
398
- ? withContinuableReturnGuidance(parent.id, request.prompt)
399
- : request.prompt, { source: { kind: 'user' }, signal: spec.signal, delivery: 'queue' }, parent, () => {
311
+ return await this.submitMaterialized(activation, spec.delivery !== 'caller'
312
+ ? withResultDeliveryGuidance(spec.prompt)
313
+ : spec.prompt, { source: { kind: 'user' }, signal, delivery: 'queue', authorize: () => { this.assertAdmitting(parent); } }, ancestry.root, () => {
314
+ this.assertAdmitting(parent);
400
315
  establishCatalogChild(parent.session, childHeader, descriptor);
401
316
  });
402
317
  });
403
318
  return { ...this.receipt(established), childId, messageId };
404
- }
405
- catch (error) {
406
- releaseHold();
407
- throw error;
408
- }
319
+ });
409
320
  }
410
321
  /**
411
322
  * Start an external backend under activation ownership.
412
323
  * @param spec - task, provider, cancellation, and delivery policy.
324
+ * @param start - execution bound to the checked provider.
413
325
  * @returns the accepted activation and its result/disposal capabilities.
414
326
  */
415
- async startExternal(spec) {
416
- const parent = spec.request.parent;
327
+ async startExternal(spec, start) {
328
+ const { provider, delivery, childId: _childId, ...request } = spec;
329
+ const parent = request.parent;
417
330
  this.assertAdmitting(parent);
418
- const pendingId = brandString(randomUUID());
419
- const releaseHold = this.holdOwnership(parent, pendingId);
420
- let activation;
421
- try {
422
- const cwd = await this.resolveDirectory(spec.request, spec.signal);
423
- activation = await this.materialize({
424
- kind: 'external',
425
- childId: pendingId,
426
- provider: spec.provider,
427
- parent,
428
- signal: spec.signal,
429
- delivery: spec.delivery,
430
- start: signal => this.host.startExternal(spec.provider, {
431
- ...spec.request, cwd, label: spec.label, signal,
432
- }),
433
- });
434
- this.assertAdmitting(parent);
435
- this.assertLiveParent(parent, activation.childId);
436
- spec.signal.throwIfAborted();
437
- if (spec.delivery === 'parent') {
438
- establishExternalCatalogChild(parent.session, activation.childId, spec.label);
439
- }
440
- this.announce(activation);
441
- return this.receipt(activation);
442
- }
443
- catch (error) {
444
- if (activation !== undefined) {
445
- try {
446
- await this.dispose(activation);
331
+ const ancestry = this.ancestryForChild(parent);
332
+ return this.trackStartup(ancestry.root, spec.signal, async (signal) => {
333
+ let activation;
334
+ try {
335
+ const cwd = await this.resolveDirectory(spec, signal);
336
+ activation = await this.materialize({
337
+ kind: 'external',
338
+ provider,
339
+ ...ancestry,
340
+ signal,
341
+ delivery,
342
+ start: signal => start({
343
+ ...request, cwd, signal,
344
+ }),
345
+ });
346
+ this.assertAdmitting(parent);
347
+ signal.throwIfAborted();
348
+ if (delivery === 'parent') {
349
+ establishExternalCatalogChild(parent.session, activation.childId, spec.label);
447
350
  }
448
- catch (cleanupError) {
449
- this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`);
351
+ this.accept(activation);
352
+ return this.receipt(activation);
353
+ }
354
+ catch (error) {
355
+ if (activation !== undefined) {
356
+ try {
357
+ await this.dispose(activation);
358
+ }
359
+ catch (cleanupError) {
360
+ this.ctx.logger.warn(`subagent "${activation.childId}" admission rollback failed: ${failureMessage(cleanupError)}`);
361
+ }
450
362
  }
363
+ throw error;
451
364
  }
452
- throw error;
453
- }
454
- finally {
455
- releaseHold();
456
- }
365
+ });
457
366
  }
458
367
  /** Capture the explicit or inherited directory while the parent still owns startup. */
459
368
  async resolveDirectory(request, signal) {
@@ -464,59 +373,126 @@ export class SubagentManager {
464
373
  return cwd;
465
374
  }
466
375
  /**
467
- * Deliver one model-authored message to a direct continuable child or to the
468
- * sender's direct parent. A missing direct child cold-resumes through the
469
- * ordinary continuation lifecycle.
470
- * @param sender - exact live Agent authorizing and originating the message.
471
- * @param targetId - durable direct-parent or direct-child session id.
472
- * @param content - model-authored content to deliver.
473
- * @param options - caller cancellation before acceptance.
474
- * @returns the accepted message's inbox id.
376
+ * Resolve the live subagent tree root from runtime ownership, independent of creator residency.
377
+ * @param caller - exact live Agent selecting the subagent tree.
378
+ * @returns the live subagent tree root.
379
+ * @throws when the caller has no active runtime owner.
380
+ */
381
+ rootOf(caller) {
382
+ if (this.ctx.agents.get(caller.id) !== caller) {
383
+ throw new SubagentError('subagent tree access requires the exact live caller', 'UNAUTHORIZED');
384
+ }
385
+ const identity = this.host.identityOf(caller.session);
386
+ const delegated = caller.session.header.origin === 'subagent' || identity != null && caller.session.isOwnSeq(identity.seq);
387
+ const activation = this.resident.get(caller.id);
388
+ if (activation !== undefined && activationAgent(activation) === caller
389
+ && this.ctx.agents.get(activation.root.id) === activation.root)
390
+ return activation.root;
391
+ if (delegated || activation !== undefined) {
392
+ throw new SubagentError('agent has no active runtime owner', 'UNAUTHORIZED');
393
+ }
394
+ return caller;
395
+ }
396
+ /** Capture creation ancestry separately from the root that owns the new instance. */
397
+ ancestryForChild(creator) {
398
+ const root = this.rootOf(creator);
399
+ const ancestors = this.resident.get(creator.id)?.ancestors ?? [];
400
+ return { root, ancestors: [...ancestors, creator.id] };
401
+ }
402
+ /**
403
+ * Deliver to another agent in the subagent tree, restoring only the target when absent.
404
+ * @param sender - exact live root or resident continuable Agent.
405
+ * @param targetId - another member's durable Session id.
406
+ * @param content - model-authored message blocks.
407
+ * @param options - cancellation before inbox acceptance.
408
+ * @returns the accepted inbox message id.
475
409
  */
476
410
  async sendMessage(sender, targetId, content, options) {
477
- if (this.ctx.agents.get(sender.id) !== sender) {
478
- throw new SubagentError('message delivery requires the exact live sender agent', 'UNAUTHORIZED');
479
- }
411
+ options.signal.throwIfAborted();
412
+ const root = this.rootOf(sender);
413
+ if (sender.id === targetId)
414
+ throw new SubagentError('send_message requires another agent in the subagent tree', 'UNAUTHORIZED');
480
415
  this.assertAdmitting(sender);
481
- const senderActivation = this.resident.get(sender.id);
482
- if (senderActivation !== undefined
483
- && senderActivation.kind === 'local'
484
- && senderActivation.handle.agent === sender
485
- && senderActivation.parent.id === targetId) {
486
- options.signal.throwIfAborted();
487
- return this.sendToParent(senderActivation, sender, content);
488
- }
489
- if (sender.session.header.parentSession === targetId) {
490
- throw new SubagentError(`agent "${sender.id}" is not a resident continuable child and cannot send to parent "${targetId}"`, 'UNAUTHORIZED');
491
- }
492
- return this.deliverToChild(sender, targetId, content, {
493
- signal: options.signal,
494
- delivery: 'steer',
416
+ const message = createAgentMessage(sender, content);
417
+ const activation = this.resident.get(sender.id);
418
+ return this.deliverToMember(root, targetId, [...message.content], {
419
+ signal: options.signal, delivery: 'steer', source: message.source,
420
+ authorize: () => { this.assertAdmitting(sender); },
421
+ onAccepted: () => {
422
+ if (activation?.kind === 'local' && activation.ancestors.at(-1) === targetId) {
423
+ activation.omitClosingText = true;
424
+ }
425
+ },
495
426
  });
496
427
  }
497
- /**
498
- * Queue one human-authored prompt as a distinct direct-child turn.
499
- * @param parent - exact live direct parent authorizing delivery.
500
- * @param childId - durable direct-child session id.
501
- * @param content - model-visible prompt blocks.
502
- * @param source - durable attribution for the human prompt.
503
- * @param signal - caller cancellation before inbox acceptance.
504
- * @returns the accepted durable message id.
505
- */
506
- async queuePrompt(parent, childId, content, source, signal) {
507
- return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'queue' });
428
+ /** Resolve the live owner through verified creation edges without activating ancestors. */
429
+ async resolveCreationAncestry(source, signal, root) {
430
+ const ancestors = [];
431
+ const seen = new Set([source.header.id]);
432
+ let { header, projections, inheritedEventCount } = source;
433
+ while (true) {
434
+ const env_1 = { stack: [], error: void 0, hasError: false };
435
+ try {
436
+ signal.throwIfAborted();
437
+ const parentId = header.parentSession;
438
+ const identity = projections?.values.subagent;
439
+ if (parentId === undefined || identity == null || identity.seq < inheritedEventCount)
440
+ break;
441
+ if (seen.has(parentId))
442
+ throw new SubagentError('cyclic subagent creation ancestry', 'UNAUTHORIZED');
443
+ seen.add(parentId);
444
+ const parent = __addDisposableResource(env_1, await this.observeContinuation(parentId, signal), false);
445
+ if (!parent.projections?.values.subagentCatalog?.some(entry => entry.id === header.id)) {
446
+ throw new SubagentError('subagent is absent from its creator catalog', 'UNAUTHORIZED');
447
+ }
448
+ ancestors.unshift(parentId);
449
+ if (parentId === root?.id)
450
+ return { root, ancestors };
451
+ ({ header, projections, inheritedEventCount } = parent);
452
+ }
453
+ catch (e_1) {
454
+ env_1.error = e_1;
455
+ env_1.hasError = true;
456
+ }
457
+ finally {
458
+ __disposeResources(env_1);
459
+ }
460
+ }
461
+ if (root === undefined && ancestors.length > 0 && header.origin !== 'subagent') {
462
+ const owner = this.ctx.agents.get(header.id);
463
+ if (owner === undefined || this.rootOf(owner) !== owner) {
464
+ throw new SubagentError('subagent tree root is not live', 'CONTINUATION_UNAVAILABLE');
465
+ }
466
+ return { root: owner, ancestors };
467
+ }
468
+ throw new SubagentError(`agent "${source.header.id}" is not a member of this subagent tree`, 'UNAUTHORIZED');
508
469
  }
509
470
  /**
510
- * Steer one host-authored prompt to a direct continuable child.
511
- * @param parent - exact live direct parent authorizing delivery.
512
- * @param childId - durable direct-child session id.
471
+ * Deliver one host-authored prompt through the target's live subagent tree root.
472
+ * Remote callers must be authorized for the target Session before invoking this operation.
473
+ * @param childId - durable continuable subagent session id.
513
474
  * @param content - model-visible prompt blocks.
514
- * @param source - durable attribution for the host prompt.
515
- * @param signal - caller cancellation before inbox acceptance.
475
+ * @param options - inbox destination, durable attribution, and cancellation before acceptance.
516
476
  * @returns the accepted durable message id.
517
477
  */
518
- async steerPrompt(parent, childId, content, source, signal) {
519
- return this.deliverToChild(parent, childId, content, { source, signal, delivery: 'steer' });
478
+ async deliverPrompt(childId, content, options) {
479
+ options.signal.throwIfAborted();
480
+ let root = this.resident.get(childId)?.root;
481
+ if (root === undefined) {
482
+ const env_2 = { stack: [], error: void 0, hasError: false };
483
+ try {
484
+ const source = __addDisposableResource(env_2, await this.observeContinuation(childId, options.signal), false);
485
+ ({ root } = await this.resolveCreationAncestry(source, options.signal));
486
+ }
487
+ catch (e_2) {
488
+ env_2.error = e_2;
489
+ env_2.hasError = true;
490
+ }
491
+ finally {
492
+ __disposeResources(env_2);
493
+ }
494
+ }
495
+ return this.deliverToMember(root, childId, content, options);
520
496
  }
521
497
  /** Admit local input synchronously before any teardown can begin. */
522
498
  deliver(activation, message, delivery) {
@@ -541,38 +517,6 @@ export class SubagentManager {
541
517
  throw new SubagentError(`subagent "${childId}" already exists`, 'DUPLICATE_CHILD');
542
518
  }
543
519
  }
544
- /**
545
- * Pre-register `childId` in a continuation-managed parent's owned set so the
546
- * parent cannot settle while a caller is still establishing or resuming that
547
- * child. Returns a releaser for the failure path; it removes only a hold
548
- * this call added, and leaves ownership in place once a live Activation for
549
- * the child exists.
550
- * @param parent - the live direct parent the operation is admitted under.
551
- * @param childId - the durable child the operation addresses.
552
- * @returns the failure-path releaser; a no-op when nothing was added.
553
- */
554
- holdOwnership(parent, childId) {
555
- const parentActivation = this.resident.get(parent.id);
556
- if (parentActivation === undefined || activationAgent(parentActivation) !== parent)
557
- return () => { };
558
- if (parentActivation.closing !== undefined) {
559
- throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
560
- }
561
- if (parentActivation.ownedChildren.has(childId))
562
- return () => { };
563
- parentActivation.ownedChildren.add(childId);
564
- return () => {
565
- const live = this.resident.get(childId);
566
- /* v8 ignore next 4 -- reaching this arm needs another delivery to establish the child
567
- * between this operation's failure and its releaser running, which no test can schedule
568
- * deterministically: the ownership edge then belongs to that live Activation, so the
569
- * conservative keep leaves it for finishDisposal's releaseOwnership. */
570
- if (live !== undefined && live.closing === undefined)
571
- return;
572
- if (parentActivation.ownedChildren.delete(childId))
573
- this.wake(parentActivation);
574
- };
575
- }
576
520
  /**
577
521
  * Apply activation admission to a resident parent before delivering input.
578
522
  * @param parent - exact live Agent receiving the message.
@@ -596,34 +540,20 @@ export class SubagentManager {
596
540
  parent.followup(message);
597
541
  }
598
542
  /**
599
- * Reject new admission once the manager or this exact parent tree began draining.
600
- * @param agent - exact live Agent whose lineage determines admission.
543
+ * Reject new work from closing members or subagent trees.
544
+ * @param agent - exact live caller whose instance and subagent tree must admit work.
601
545
  */
602
546
  assertAdmitting(agent) {
603
547
  const closing = this.closingTeardownFor(agent);
604
- if (closing === undefined)
548
+ if (closing === undefined) {
549
+ if (this.resident.get(agent.id)?.closing !== undefined) {
550
+ throw new SubagentError(`subagent "${agent.id}" is closing`, 'ACTIVATION_CLOSING');
551
+ }
605
552
  return;
553
+ }
606
554
  throw new SubagentError(closing === 'manager'
607
555
  ? 'subagents are draining; the operation was not admitted'
608
- : `subagents below parent "${closing.id}" are draining; the operation was not admitted`, 'DRAINING');
609
- }
610
- /**
611
- * Authorize one operation against the durable direct-parent lineage.
612
- * @param parent - exact live Agent claiming direct-parent authority.
613
- * @param childId - durable child session id addressed by the operation.
614
- * @param parentSession - durable direct-parent id recorded by the child.
615
- */
616
- authorizeLineage(parent, childId, parentSession) {
617
- this.assertLiveParent(parent, childId);
618
- if (parentSession !== parent.id) {
619
- throw new SubagentError(`subagent "${childId}" belongs to another parent session`, 'UNAUTHORIZED');
620
- }
621
- }
622
- /** Reject parent authority after the live registry entry changes. */
623
- assertLiveParent(parent, childId) {
624
- if (this.ctx.agents.get(parent.id) !== parent) {
625
- throw new SubagentError(`subagent "${childId}" delivery requires the exact live parent agent`, 'UNAUTHORIZED');
626
- }
556
+ : `subagent tree "${closing.id}" is draining; the operation was not admitted`, 'DRAINING');
627
557
  }
628
558
  /**
629
559
  * Create or resume one child Agent and publish its Activation.
@@ -631,23 +561,26 @@ export class SubagentManager {
631
561
  * @returns the published process-local Activation.
632
562
  */
633
563
  materialize(inputs) {
634
- this.assertAdmitting(inputs.parent);
564
+ this.assertAdmitting(inputs.root);
635
565
  inputs.signal.throwIfAborted();
636
- const lineage = this.liveLineage(inputs.parent);
637
- const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent);
566
+ const pool = this.rootPool(inputs.root);
638
567
  const releaseSlot = pool.reserve(this.maxActiveSubagents());
568
+ return this.materializeTracked(inputs, releaseSlot).catch((error) => {
569
+ releaseSlot();
570
+ throw error;
571
+ });
572
+ }
573
+ /** Keep subagent tree shutdown and host waits joined to preparation, publication, and rollback. */
574
+ trackStartup(root, signal, start) {
575
+ this.assertAdmitting(root);
576
+ signal.throwIfAborted();
639
577
  const settled = Promise.withResolvers();
640
578
  const controller = new AbortController();
641
- const materialization = {
642
- lineage,
643
- settled: settled.promise,
644
- controller,
645
- };
579
+ const materialization = { root, settled: settled.promise, controller };
646
580
  this.materializations.add(materialization);
647
- return this.materializeTracked({ ...inputs, signal: AbortSignal.any([inputs.signal, controller.signal]) }, lineage, pool, releaseSlot).catch((error) => {
648
- releaseSlot();
581
+ return start(AbortSignal.any([signal, controller.signal])).catch((error) => {
649
582
  if (error instanceof Error && error.name === 'AbortError')
650
- this.assertAdmitting(inputs.parent);
583
+ this.assertAdmitting(root);
651
584
  throw error;
652
585
  }).finally(() => {
653
586
  this.materializations.delete(materialization);
@@ -688,42 +621,14 @@ export class SubagentManager {
688
621
  + reasons.map(reason => failureMessage(reason)).join('; '), 'ACTIVATION_TEARDOWN_FAILED');
689
622
  }
690
623
  }
691
- /** Return the retained member set for one exact scoped-teardown root. */
692
- closingMembers(root) {
693
- const existing = this.closingScopes.get(root);
694
- if (existing !== undefined)
695
- return existing;
696
- const members = new Set();
697
- this.closingScopes.set(root, members);
698
- return members;
699
- }
700
- /** Return the exact currently resolvable ancestry from `agent` upward. */
701
- liveLineage(agent) {
702
- const lineage = [agent];
703
- const seen = new Set([agent.id]);
704
- let parentSession = agent.session.header.parentSession;
705
- while (parentSession !== undefined) {
706
- const parent = this.ctx.agents.get(parentSession);
707
- if (parent === undefined || seen.has(parent.id))
708
- break;
709
- lineage.push(parent);
710
- seen.add(parent.id);
711
- parentSession = parent.session.header.parentSession;
712
- }
713
- return lineage;
714
- }
715
- /** Return the teardown that closed continuable admission for this agent's lineage. */
624
+ /** Return the subagent tree teardown that closed admission for this exact Agent. */
716
625
  closingTeardownFor(agent) {
717
626
  if (this.draining)
718
627
  return 'manager';
719
- const lineage = this.liveLineage(agent);
720
- for (const [root, members] of this.closingScopes) {
721
- if (members.has(agent) || lineage.includes(root))
722
- return root;
723
- }
724
- return undefined;
628
+ const root = this.rootOf(agent);
629
+ return this.closingRoots.has(root) ? root : undefined;
725
630
  }
726
- /** Resolve a root's pool once; descendants inherit their resident parent's pool directly. */
631
+ /** Share one capacity pool across every instance in a subagent tree. */
727
632
  rootPool(parent) {
728
633
  let pool = this.rootPools.get(parent);
729
634
  if (pool === undefined) {
@@ -733,9 +638,8 @@ export class SubagentManager {
733
638
  return pool;
734
639
  }
735
640
  /** Perform one tracked materialization through publication or rollback. */
736
- async materializeTracked(inputs, parentLineage, pool, releaseSlot) {
737
- const { provider, parent } = inputs;
738
- let childId = inputs.childId;
641
+ async materializeTracked(inputs, releaseSlot) {
642
+ const { provider, root } = inputs;
739
643
  inputs.signal.throwIfAborted();
740
644
  let resource;
741
645
  if (inputs.kind === 'external') {
@@ -746,9 +650,8 @@ export class SubagentManager {
746
650
  const run = await inputs.start(controller.signal);
747
651
  resource = { kind: 'external', run, controller };
748
652
  void run.result.catch(() => undefined);
749
- childId = run.id;
750
653
  try {
751
- this.assertChildIdAvailable(childId);
654
+ this.assertChildIdAvailable(run.id);
752
655
  }
753
656
  catch (error) {
754
657
  await run.dispose();
@@ -760,7 +663,7 @@ export class SubagentManager {
760
663
  }
761
664
  }
762
665
  else {
763
- const { create } = inputs;
666
+ const { create, childId } = inputs;
764
667
  let structured;
765
668
  const setup = async (childCtx, child) => {
766
669
  // Only fresh creation appends the descriptor and delegated policy after
@@ -770,7 +673,7 @@ export class SubagentManager {
770
673
  const approvalPolicy = create?.delegatedPolicies.approvalPolicy
771
674
  ?? childCtx.get('approval')?.effectivePolicy(child.session);
772
675
  if (inputs.composition.agentPreset !== undefined) {
773
- await applyChildComposition(childCtx, parent, inputs.composition, approvalPolicy);
676
+ await applyChildComposition(childCtx, create?.parent, inputs.composition, approvalPolicy);
774
677
  }
775
678
  if (create !== undefined) {
776
679
  if (inputs.composition.agentPreset !== undefined) {
@@ -784,25 +687,22 @@ export class SubagentManager {
784
687
  await workingDirectory.set(child, create.cwd, inputs.signal);
785
688
  }
786
689
  if (inputs.composition.agentPreset === undefined) {
787
- await applyChildComposition(childCtx, parent, inputs.composition, approvalPolicy);
690
+ await applyChildComposition(childCtx, create?.parent, inputs.composition, approvalPolicy);
788
691
  }
789
692
  if (inputs.outputSchema !== undefined) {
790
- structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () => this.resident.get(childId)?.ownedChildren.size === 0
791
- && child.inbox.nextStep.length === 0
693
+ structured = attachStructuredRuntime(childCtx, inputs.outputSchema, () => child.inbox.nextStep.length === 0
792
694
  && child.inbox.nextTurn.length === 0);
793
695
  }
794
696
  };
795
697
  const handle = create === undefined
796
698
  ? await this.ownerCtx.agents.resume({
797
699
  resumeSessionId: childId,
798
- parentAgent: parent,
799
700
  agentOptions: inputs.agentOptions,
800
701
  signal: inputs.signal,
801
702
  setup,
802
703
  })
803
704
  : await this.ownerCtx.agents.create({
804
705
  sessionId: childId,
805
- parentAgent: parent,
806
706
  meta: create.meta,
807
707
  ...(create.seed === undefined ? {} : { seed: create.seed }),
808
708
  inheritedEventCount: create.inheritedEventCount,
@@ -820,29 +720,25 @@ export class SubagentManager {
820
720
  omitClosingText: false,
821
721
  };
822
722
  }
823
- const observer = this.host.observeActivation(provider, childId, parent);
723
+ const childId = resource.kind === 'local' ? resource.handle.agent.id : resource.run.id;
824
724
  const activation = {
825
- pool,
826
725
  releaseSlot,
827
726
  childId,
727
+ runId: SubagentRunId(randomUUID()),
728
+ provider,
828
729
  ...resource,
829
- parent,
730
+ root,
731
+ ancestors: inputs.ancestors,
830
732
  delivery: inputs.delivery ?? 'parent',
831
733
  result: Promise.withResolvers(),
832
734
  closing: undefined,
833
- ancestry: new WeakSet(resource.kind === 'external' ? parentLineage : [resource.handle.agent, ...parentLineage]),
834
- ownedChildren: new Set(),
835
- observer,
836
- announced: false,
735
+ accepted: false,
837
736
  };
838
737
  void activation.result.promise.catch(() => undefined);
839
738
  this.resident.set(childId, activation);
840
739
  try {
841
- this.assertAdmitting(parent);
740
+ this.assertAdmitting(root);
842
741
  inputs.signal.throwIfAborted();
843
- if (this.ctx.agents.get(parent.id) !== parent)
844
- throw new SubagentError('subagent parent is no longer live', 'UNAUTHORIZED');
845
- this.acquireOwnership(parent, childId);
846
742
  const wakeSettlement = () => { this.wake(activation); };
847
743
  if (activation.kind === 'local') {
848
744
  observeLocalFailure(activation, wakeSettlement);
@@ -862,7 +758,9 @@ export class SubagentManager {
862
758
  activation.omitClosingText = false;
863
759
  });
864
760
  }
865
- observer.start(activationAgent(activation));
761
+ // Listeners may read the resident Agent or drain it immediately. Publish
762
+ // after residency and inbox listeners, as the last rollback-protected step.
763
+ this.host.emitLifecycle('subagent/start', this.runInfo(activation), root);
866
764
  }
867
765
  catch (error) {
868
766
  /* v8 ignore next -- rollback failure must not mask the admission failure
@@ -872,14 +770,19 @@ export class SubagentManager {
872
770
  }
873
771
  return activation;
874
772
  }
875
- /**
876
- * Begin settlement observation after initial acceptance is recorded.
877
- * @param activation - accepted activation to observe.
878
- */
879
- announce(activation) {
880
- if (activation.announced)
881
- return;
882
- activation.announced = true;
773
+ /** Identity shared by one residency epoch's start/end pair, including cold resumes. */
774
+ runInfo(activation) {
775
+ return {
776
+ runId: activation.runId,
777
+ provider: activation.provider,
778
+ parentSessionId: activation.ancestors.at(-1),
779
+ id: activation.childId,
780
+ local: activation.kind === 'local',
781
+ };
782
+ }
783
+ /** Record acceptance and begin settlement observation before returning the receipt. */
784
+ accept(activation) {
785
+ activation.accepted = true;
883
786
  if (activation.kind === 'local') {
884
787
  this.watchLocalSettlement(activation);
885
788
  }
@@ -897,29 +800,10 @@ export class SubagentManager {
897
800
  finally {
898
801
  this.resident.delete(activation.childId);
899
802
  activation.releaseSlot();
900
- this.releaseOwnership(activation.childId);
901
803
  activation.result.reject(error);
902
804
  }
903
805
  });
904
806
  }
905
- /** Register the child in a continuation-managed parent's owned set. */
906
- acquireOwnership(parent, childId) {
907
- const parentActivation = this.resident.get(parent.id);
908
- if (parentActivation === undefined)
909
- return;
910
- if (parentActivation.closing !== undefined) {
911
- throw new SubagentError(`subagent parent "${parent.id}" is being disposed; the child was not established`, 'ACTIVATION_CLOSING');
912
- }
913
- parentActivation.ownedChildren.add(childId);
914
- this.wake(parentActivation);
915
- }
916
- /** Remove one child from its live owner's set and let that owner re-check settlement. */
917
- releaseOwnership(childId) {
918
- for (const candidate of this.resident.values()) {
919
- if (candidate.ownedChildren.delete(childId))
920
- this.wake(candidate);
921
- }
922
- }
923
807
  /** Let a settlement watcher re-check residency after relevant state changes. */
924
808
  wake(activation) {
925
809
  if (activation.kind === 'external')
@@ -985,20 +869,18 @@ export class SubagentManager {
985
869
  reportTeardownFailure(activation, error) {
986
870
  this.ctx.logger.warn(`subagent "${activation.childId}" activation teardown failed: ${failureMessage(error)}`);
987
871
  }
988
- /** Check pending input and owned children before closing admission. */
872
+ /** Check pending input before closing admission. */
989
873
  settlementState(activation, observation) {
990
874
  if (activation.closing !== undefined)
991
875
  return 'closed';
992
876
  if (activation.poke !== observation)
993
877
  return 'retry';
994
878
  const inbox = activation.handle.agent.inbox;
995
- if (activation.ownedChildren.size > 0)
996
- return 'wait';
997
879
  if (activation.failureAt === undefined && (inbox.nextTurn.length > 0 || inbox.nextStep.length > 0))
998
880
  return 'wait';
999
881
  return 'ready';
1000
882
  }
1001
- /** Propagate stop synchronously and await descendant startup rollback before release. */
883
+ /** Stop and release one instance without affecting its creation descendants. */
1002
884
  async finishDisposal(activation, stop) {
1003
885
  this.wake(activation);
1004
886
  const { childId } = activation;
@@ -1024,27 +906,6 @@ export class SubagentManager {
1024
906
  if (stop) {
1025
907
  activation.handle.agent.cancel({ kind: 'parent' });
1026
908
  const idle = activation.handle.agent.whenIdle();
1027
- const children = [...activation.ownedChildren]
1028
- .map(child => this.resident.get(child))
1029
- .filter((child) => child !== undefined);
1030
- const pending = [...this.materializations].filter(item => item.lineage.includes(activation.handle.agent));
1031
- const childDisposals = children.map(child => this.dispose(child));
1032
- for (const item of pending)
1033
- item.controller.abort();
1034
- childDisposals.push(...pending.map(item => item.settled));
1035
- const childFailures = await Promise.all(childDisposals.map(async (disposal) => {
1036
- try {
1037
- await disposal;
1038
- return undefined;
1039
- }
1040
- catch (error) {
1041
- return error;
1042
- }
1043
- }));
1044
- const reasons = childFailures.filter(reason => reason !== undefined);
1045
- if (reasons.length > 0) {
1046
- failures.push(new SubagentError(`subagent "${childId}" child teardown failed: ${reasons.map(reason => failureMessage(reason)).join('; ')}`, 'ACTIVATION_TEARDOWN_FAILED'));
1047
- }
1048
909
  await idle;
1049
910
  await this.flushFinalState(activation);
1050
911
  }
@@ -1076,8 +937,13 @@ export class SubagentManager {
1076
937
  activation.releaseSlot();
1077
938
  const terminal = failure === undefined ? result : { output: [], stopReason: 'error' };
1078
939
  this.notifySettlement(activation, result);
1079
- this.releaseOwnership(childId);
1080
- activation.observer.settle(terminal);
940
+ // The sole end publication follows resource release and parent notification,
941
+ // before result delivery. Cleanup failure changes the event, not the captured result.
942
+ this.host.emitLifecycle('subagent/end', {
943
+ ...this.runInfo(activation),
944
+ stopReason: terminal.stopReason,
945
+ ...terminal.output.length === 0 ? {} : { lastAssistantMessage: terminal.output },
946
+ }, activation.root);
1081
947
  if (resultFailure === undefined)
1082
948
  activation.result.resolve(result);
1083
949
  else
@@ -1087,11 +953,12 @@ export class SubagentManager {
1087
953
  }
1088
954
  /** Deliver the captured execution result to the durable direct parent after cleanup. */
1089
955
  notifySettlement(activation, terminal) {
1090
- if (!activation.announced || activation.delivery === 'caller')
956
+ if (!activation.accepted || activation.delivery === 'caller')
1091
957
  return;
1092
958
  try {
1093
- const parent = this.ctx.agents.get(activation.parent.id);
1094
- if (parent !== activation.parent)
959
+ const parentId = activation.ancestors.at(-1);
960
+ const parent = this.ctx.agents.get(parentId);
961
+ if (parent === undefined || this.rootOf(parent) !== activation.root)
1095
962
  return;
1096
963
  const message = createSettlementMessage(activation.childId, terminal, activation.kind === 'local', activation.kind === 'local' && activation.omitClosingText);
1097
964
  if (this.closingTeardownFor(parent) !== undefined) {
@@ -1123,140 +990,112 @@ export class SubagentManager {
1123
990
  dispose: () => this.dispose(activation),
1124
991
  };
1125
992
  }
1126
- /** Route one parent-originated delivery through residency and cold resume. */
1127
- async deliverToChild(parent, childId, content, options) {
1128
- this.assertAdmitting(parent);
1129
- const releaseHold = this.holdOwnership(parent, childId);
1130
- try {
1131
- return await this.deliverFollowup(parent, childId, content, options);
1132
- }
1133
- catch (error) {
1134
- releaseHold();
1135
- throw error;
1136
- }
1137
- }
1138
- /** The delivery loop behind {@link deliverToChild}, run under the parent hold. */
1139
- async deliverFollowup(parent, childId, content, options) {
993
+ /** Serialize target restoration and inbox admission; closure is awaited outside the lock. */
994
+ async deliverToMember(root, childId, content, options) {
1140
995
  while (true) {
1141
- const live = await this.locks.run(childId, async () => {
996
+ this.assertAdmitting(root);
997
+ options.signal.throwIfAborted();
998
+ const result = await this.locks.run(childId, async () => {
999
+ if (childId === root.id) {
1000
+ if (contentHasImage(content))
1001
+ await this.assertImageCapable(root, options.signal);
1002
+ options.signal.throwIfAborted();
1003
+ options.authorize?.();
1004
+ this.assertAdmitting(root);
1005
+ const message = createUserMessage({ content, source: options.source });
1006
+ this.sendWaking(root, message, options.delivery);
1007
+ options.onAccepted?.();
1008
+ return { messageId: message.id };
1009
+ }
1142
1010
  const activation = this.resident.get(childId);
1143
- if (activation === undefined)
1144
- return this.coldResume(parent, childId, content, options);
1145
- requireLocalActivation(activation);
1146
- const disposal = activation.closing;
1147
- /* v8 ignore next 3 -- the send-versus-dispose cutoff needs a delivery to
1148
- * observe the transaction inside the same critical section that opened it. */
1149
- if (disposal !== undefined) {
1150
- return disposal.then(() => undefined, () => undefined);
1011
+ if (activation === undefined) {
1012
+ const restored = await this.restoreTarget(root, childId, options.signal);
1013
+ return { messageId: await this.submitMaterialized(restored, content, options, root) };
1151
1014
  }
1152
- if (contentHasImage(content)) {
1015
+ if (activation.root !== root)
1016
+ throw new SubagentError('target belongs to another subagent tree', 'UNAUTHORIZED');
1017
+ requireLocalActivation(activation);
1018
+ if (activation.closing === undefined && contentHasImage(content)) {
1153
1019
  await this.assertImageCapable(requireLocalActivation(activation).handle.agent, options.signal);
1154
- if (activation.closing !== undefined) {
1155
- await Promise.allSettled([activation.closing]);
1156
- return undefined;
1157
- }
1158
1020
  }
1159
- const messageId = this.submitAdmitted(activation, content, options, parent);
1160
- this.announce(activation);
1161
- return messageId;
1021
+ if (activation.closing !== undefined)
1022
+ return { closing: activation.closing };
1023
+ const messageId = this.submitAdmitted(activation, content, options, root);
1024
+ return { messageId };
1162
1025
  });
1163
- /* v8 ignore start -- only a delivery that lost the disposal cutoff retries. */
1164
- if (live !== undefined)
1165
- return live;
1166
- this.assertAdmitting(parent);
1167
- options.signal.throwIfAborted();
1168
- /* v8 ignore stop */
1169
- }
1170
- }
1171
- /** Deliver one resident continuable child's message to its live direct parent. */
1172
- sendToParent(activation, sender, content) {
1173
- /* v8 ignore next 6 -- only synchronous re-entrant teardown can open this
1174
- * transaction between exact-agent authorization and this no-await span. */
1175
- if (activation.closing !== undefined) {
1176
- throw new SubagentError(`subagent "${sender.id}" activation is being disposed; the message was not delivered`, 'ACTIVATION_CLOSING');
1177
- }
1178
- const parent = this.ctx.agents.get(activation.parent.id);
1179
- if (parent !== activation.parent) {
1180
- throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE');
1026
+ if ('messageId' in result)
1027
+ return result.messageId;
1028
+ await Promise.allSettled([result.closing]);
1181
1029
  }
1182
- const message = createAgentMessage(sender, content);
1183
- this.sendAgentMessage(parent, message);
1184
- activation.omitClosingText = true;
1185
- return message.id;
1186
1030
  }
1187
- /** Send one Agent message while translating only the target's own rejection. */
1188
- sendAgentMessage(parent, message) {
1031
+ /** Read continuation history, preserving cancellation and reporting unavailable storage. */
1032
+ async observeContinuation(childId, signal) {
1033
+ const query = this.requireSessionQuery();
1189
1034
  try {
1190
- this.sendWaking(parent, message, 'steer');
1035
+ return await query.observeSession(childId, { signal });
1191
1036
  }
1192
1037
  catch (error) {
1193
- throw new SubagentError('direct parent is not live; the message was not delivered', 'PARENT_UNAVAILABLE', { cause: error });
1038
+ signal.throwIfAborted();
1039
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1194
1040
  }
1195
1041
  }
1196
- /**
1197
- * Cold-resume a persisted child and submit the waiting turn. The descriptor
1198
- * supplies every reconstruction input; no subagent provider is dispatched.
1199
- */
1200
- async coldResume(parent, childId, content, options) {
1201
- const env_1 = { stack: [], error: void 0, hasError: false };
1202
- try {
1203
- const query = this.requireSessionQuery();
1204
- let observation;
1042
+ /** Reconstruct only the target under its subagent tree root; the caller owns acceptance or rollback. */
1043
+ async restoreTarget(root, childId, signal) {
1044
+ return this.trackStartup(root, signal, async (signal) => {
1045
+ const env_3 = { stack: [], error: void 0, hasError: false };
1205
1046
  try {
1206
- observation = await query.observeSession(childId, {
1207
- signal: options.signal,
1208
- });
1209
- }
1210
- catch (error) {
1211
- options.signal.throwIfAborted();
1212
- throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1213
- }
1214
- const source = __addDisposableResource(env_1, observation, false);
1215
- this.assertAdmitting(parent);
1216
- this.authorizeLineage(parent, childId, source.header.parentSession);
1217
- const ownEvents = source.events.slice(source.inheritedEventCount);
1218
- const descriptor = foldSubagentDescriptor(ownEvents);
1219
- if (descriptor === undefined || descriptor.mode !== 'continuable') {
1220
- throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
1221
- }
1222
- let activation;
1223
- try {
1224
- const independentlySelected = hasIndependentPresetSelection(ownEvents);
1225
- const agentPreset = independentlySelected ? source.projections?.values.agentPreset : undefined;
1226
- if (independentlySelected && typeof agentPreset !== 'string') {
1227
- throw new SubagentError(`subagent "${childId}" has no agent preset projection for restoration`, 'NOT_RESUMABLE');
1047
+ const source = __addDisposableResource(env_3, await this.observeContinuation(childId, signal), false);
1048
+ this.assertAdmitting(root);
1049
+ const ownEvents = source.events.slice(source.inheritedEventCount);
1050
+ const descriptor = foldSubagentDescriptor(ownEvents);
1051
+ if (descriptor === undefined || descriptor.mode !== 'continuable') {
1052
+ throw new SubagentError(`subagent "${childId}" has no supported continuation state and cannot be resumed; choose a different target`, 'NOT_RESUMABLE');
1228
1053
  }
1229
- activation = await this.materialize({
1230
- kind: 'local',
1231
- childId,
1232
- provider: descriptor.provider,
1233
- parent,
1234
- agentOptions: {
1235
- ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
1236
- ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
1237
- ...descriptor.agentReasoningEffort !== undefined
1238
- ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
1239
- : {},
1240
- },
1241
- composition: { agentPreset: agentPreset ?? undefined, persona: descriptor.persona, toolFilter: descriptor.toolFilter },
1242
- signal: options.signal,
1243
- });
1054
+ const { ancestors } = await this.resolveCreationAncestry(source, signal, root);
1055
+ let activation;
1056
+ try {
1057
+ const projectedPreset = source.projections?.values.agentPreset;
1058
+ if (projectedPreset === undefined && source.events.some(event => event.type === 'agent-preset/selected')) {
1059
+ throw new SubagentError(`subagent "${childId}" has no agent preset projection for restoration`, 'NOT_RESUMABLE');
1060
+ }
1061
+ const agentPreset = projectedPreset ?? source.header.agentPreset;
1062
+ activation = await this.materialize({
1063
+ kind: 'local',
1064
+ childId,
1065
+ provider: descriptor.provider,
1066
+ root,
1067
+ ancestors,
1068
+ agentOptions: {
1069
+ ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {},
1070
+ ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {},
1071
+ ...descriptor.agentReasoningEffort !== undefined
1072
+ ? { reasoningEffort: ReasoningEffortId(descriptor.agentReasoningEffort) }
1073
+ : {},
1074
+ },
1075
+ composition: {
1076
+ agentPreset: agentPreset ?? undefined,
1077
+ persona: descriptor.persona,
1078
+ toolFilter: descriptor.toolFilter,
1079
+ },
1080
+ signal,
1081
+ });
1082
+ }
1083
+ catch (error) {
1084
+ signal.throwIfAborted();
1085
+ if (error instanceof SubagentError)
1086
+ throw error;
1087
+ throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1088
+ }
1089
+ return activation;
1244
1090
  }
1245
- catch (error) {
1246
- options.signal.throwIfAborted();
1247
- if (error instanceof SubagentError)
1248
- throw error;
1249
- throw new SubagentError(`subagent "${childId}" is unavailable`, 'NOT_RESUMABLE', { cause: error });
1091
+ catch (e_3) {
1092
+ env_3.error = e_3;
1093
+ env_3.hasError = true;
1250
1094
  }
1251
- return await this.submitMaterialized(activation, content, options, parent);
1252
- }
1253
- catch (e_1) {
1254
- env_1.error = e_1;
1255
- env_1.hasError = true;
1256
- }
1257
- finally {
1258
- __disposeResources(env_1);
1259
- }
1095
+ finally {
1096
+ __disposeResources(env_3);
1097
+ }
1098
+ });
1260
1099
  }
1261
1100
  /** Admit a materialized child, commit its creation fact, and release it on failure. */
1262
1101
  async submitMaterialized(activation, content, options, parent, commit) {
@@ -1269,7 +1108,7 @@ export class SubagentManager {
1269
1108
  }
1270
1109
  const messageId = this.submitAdmitted(activation, content, options, parent);
1271
1110
  commit?.();
1272
- this.announce(activation);
1111
+ this.accept(activation);
1273
1112
  return messageId;
1274
1113
  }
1275
1114
  catch (error) {
@@ -1284,19 +1123,17 @@ export class SubagentManager {
1284
1123
  }
1285
1124
  /** Build and submit one message across the final synchronous admission cutoff. */
1286
1125
  submitAdmitted(activation, content, options, parent) {
1287
- const message = options.source === undefined
1288
- ? createAgentMessage(parent, content)
1289
- : createUserMessage({ content, source: options.source });
1126
+ const message = createUserMessage({ content, source: options.source });
1290
1127
  options.signal.throwIfAborted();
1128
+ options.authorize?.();
1291
1129
  this.assertAdmitting(parent);
1292
- this.authorizeLineage(parent, activation.childId, activation.parent.id);
1293
- this.acquireOwnership(parent, activation.childId);
1294
1130
  try {
1295
1131
  this.deliver(activation, message, options.delivery);
1296
1132
  }
1297
1133
  finally {
1298
1134
  this.wake(activation);
1299
1135
  }
1136
+ options.onAccepted?.();
1300
1137
  return message.id;
1301
1138
  }
1302
1139
  /** Refuse image content for a child whose fixed model accepts text only. */
@@ -1331,15 +1168,4 @@ export class SubagentManager {
1331
1168
  }
1332
1169
  }
1333
1170
  export default SubagentManager;
1334
- /** Creation-time selection precedes the own descriptor; historical forks can select after it. */
1335
- function hasIndependentPresetSelection(events) {
1336
- for (const event of events) {
1337
- if (event.type === 'subagent/descriptor')
1338
- return false;
1339
- if (event.type === 'agent-preset/selected')
1340
- return true;
1341
- }
1342
- /* v8 ignore next -- coldResume rejects logs without a supported own descriptor before this lookup. */
1343
- return false;
1344
- }
1345
1171
  //# sourceMappingURL=manager.js.map