@qilin-agent/experimental-agent-team 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,12 +1,12 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import z from "@qilin-agent/schemastery";
3
+ import { HarnessError, createUserMessage } from "@qilin-agent/llm";
4
+ import { steerHostSubagentPrompt } from "@qilin-agent/subagent/internal";
2
5
  import { Remote, RemoteError, TypertRemoteService } from "@qilin-agent/typert-protocol";
3
6
  import { inspect } from "node:util";
4
- import { HarnessError, createUserMessage } from "@qilin-agent/llm";
5
- import { randomUUID } from "node:crypto";
7
+ import { z as z$1 } from "zod";
6
8
  import { brandString } from "@qilin-agent/brand";
7
- import { steerHostSubagentPrompt } from "@qilin-agent/subagent/internal";
8
9
  import { foldSubagentDescriptor } from "@qilin-agent/subagent";
9
- import { z as z$1 } from "zod";
10
10
  //#region lib/types/error.js
11
11
  /** Typed Agent Teams failures. */
12
12
  /** Stable failure raised by the Team domain. */
@@ -165,6 +165,7 @@ var TeamJournal = class {
165
165
  /** Owns the single Team runtime cancellation fact and disposal timeout. */
166
166
  var TeamRuntimeLifecycle = class {
167
167
  disposalTimeoutMs;
168
+ operations = /* @__PURE__ */ new Set();
168
169
  controller = new AbortController();
169
170
  /**
170
171
  * @param disposalTimeoutMs - maximum wait for one disposal settlement operation.
@@ -202,6 +203,27 @@ var TeamRuntimeLifecycle = class {
202
203
  this.controller.abort(new TeamError("Agent Teams service disposed", "TEAM_DISPOSED"));
203
204
  }
204
205
  /**
206
+ * Track an admitted send or complete creation transaction until it settles.
207
+ * @param operation - asynchronous work owned by this runtime.
208
+ * @returns the same operation.
209
+ */
210
+ track(operation) {
211
+ this.operations.add(operation);
212
+ operation.then(() => {
213
+ this.operations.delete(operation);
214
+ }, () => {
215
+ this.operations.delete(operation);
216
+ });
217
+ return operation;
218
+ }
219
+ /**
220
+ * Capture outstanding operations after closing admission.
221
+ * @returns admitted work still awaiting settlement.
222
+ */
223
+ pending() {
224
+ return [...this.operations];
225
+ }
226
+ /**
205
227
  * Await admitted operations and retain failures other than runtime cancellation.
206
228
  * @param operations - admitted operations captured after the admission cutoff.
207
229
  * @param failures - aggregate destination for unexpected rejection or timeout.
@@ -235,54 +257,6 @@ var TeamRuntimeLifecycle = class {
235
257
  }
236
258
  };
237
259
  //#endregion
238
- //#region lib/types/persisted.js
239
- /** Short-lived read-handle access to persisted Team member Sessions. */
240
- /**
241
- * Read one stored session's header and complete event log through a
242
- * short-lived read handle, closing the handle before returning.
243
- * @param persistence - the durable session store.
244
- * @param id - the stored session to read.
245
- * @param signal - cancellation observed by open and read.
246
- * @returns the stored header and every committed event.
247
- */
248
- async function readPersistedSession(persistence, id, signal) {
249
- const handle = await persistence.open(id, "read", { signal });
250
- try {
251
- const { events } = await handle.read(0, void 0, { signal });
252
- return {
253
- header: handle.header,
254
- inheritedEventCount: handle.inheritedEventCount,
255
- events
256
- };
257
- } finally {
258
- await handle.close();
259
- }
260
- }
261
- //#endregion
262
- //#region lib/types/session-message.js
263
- /** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
264
- /** Fold the durable inbox suffix into the messages still awaiting a claim. */
265
- function pendingInboxMessages(events) {
266
- const inbox = {
267
- "next-turn": [],
268
- "next-step": []
269
- };
270
- for (const event of events) {
271
- if (event.type !== "agent/inbox/spliced") continue;
272
- inbox[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
273
- }
274
- return [...inbox["next-turn"], ...inbox["next-step"]];
275
- }
276
- /**
277
- * Test whether one message is model-visible or still durably pending.
278
- * @param events - one Session's non-inherited event suffix.
279
- * @param predicate - identity check for the accepted message.
280
- * @returns whether history or the current inbox contains a match.
281
- */
282
- function messageAccepted(events, predicate) {
283
- return events.some((event) => event.type === "user/message" && predicate(event.data)) || pendingInboxMessages(events).some(predicate);
284
- }
285
- //#endregion
286
260
  //#region lib/types/types.js
287
261
  /** Public Agent Teams identities, durable records, and service request values. */
288
262
  /**
@@ -310,1148 +284,927 @@ function TeamMessageId(id) {
310
284
  return id;
311
285
  }
312
286
  //#endregion
313
- //#region lib/types/validation.js
314
- /** Input normalization shared by Team roster and task commands. */
287
+ //#region lib/types/task-graph.js
288
+ /** Complete dependency validation for current Team task snapshots. */
289
+ /** Package-private task dependency failure retained for command error mapping. */
290
+ var TeamTaskGraphError = class extends Error {
291
+ violation;
292
+ /**
293
+ * @param message - concrete invalid dependency relation.
294
+ * @param violation - stable relation category used by Team commands.
295
+ */
296
+ constructor(message, violation) {
297
+ super(message);
298
+ this.violation = violation;
299
+ this.name = "TeamTaskGraphError";
300
+ }
301
+ };
315
302
  /**
316
- * Normalize one required human-authored string.
317
- * @param value - raw input value.
318
- * @param field - diagnostic field name.
319
- * @param maxLength - maximum normalized character count.
320
- * @returns trimmed non-empty text.
303
+ * Validate the complete active task graph after replacing one candidate snapshot.
304
+ * @param current - current task snapshots before the candidate event.
305
+ * @param candidate - new or next-revision task snapshot.
306
+ * @throws {TeamTaskGraphError} when an active dependency is missing, duplicated, self-referential, or cyclic.
321
307
  */
322
- function requiredText(value, field, maxLength) {
323
- const text = value.trim();
324
- if (text.length === 0) throw new TeamError(`${field} must be non-empty`, "TEAM_INVALID_ARGUMENT");
325
- if (text.length > maxLength) throw new TeamError(`${field} exceeds ${maxLength} characters`, "TEAM_INVALID_ARGUMENT");
326
- return text;
308
+ function assertTaskGraphCandidate(current, candidate) {
309
+ const tasks = new Map(current.map((task) => [task.id, task]));
310
+ tasks.set(candidate.id, candidate);
311
+ for (const task of tasks.values()) {
312
+ if (task.status === "deleted") continue;
313
+ const seen = /* @__PURE__ */ new Set();
314
+ for (const blockerId of task.blockedBy) {
315
+ if (blockerId === task.id) throw new TeamTaskGraphError(`team task "${task.id}" cannot block itself`, "cycle");
316
+ if (seen.has(blockerId)) throw new TeamTaskGraphError(`team task "${task.id}" repeats blocker "${blockerId}"`, "duplicate");
317
+ const blocker = tasks.get(blockerId);
318
+ if (blocker === void 0 || blocker.status === "deleted") throw new TeamTaskGraphError(`blocker task "${blockerId}" for "${task.id}" is missing or deleted`, "missing");
319
+ seen.add(blockerId);
320
+ }
321
+ }
322
+ const visiting = /* @__PURE__ */ new Set();
323
+ const visited = /* @__PURE__ */ new Set();
324
+ const visit = (id) => {
325
+ if (visiting.has(id)) throw new TeamTaskGraphError(`task dependency cycle includes "${id}"`, "cycle");
326
+ if (visited.has(id)) return;
327
+ const task = tasks.get(id);
328
+ if (task === void 0 || task.status === "deleted") return;
329
+ visiting.add(id);
330
+ for (const blockerId of task.blockedBy) visit(blockerId);
331
+ visiting.delete(id);
332
+ visited.add(id);
333
+ };
334
+ for (const task of tasks.values()) visit(task.id);
327
335
  }
336
+ //#endregion
337
+ //#region lib/types/task-view.js
338
+ /** Pure task-view derivation shared by the task board and the client projection. */
328
339
  /**
329
- * Normalize one workspace-relative path prefix without treating it as a lock.
330
- * @param value - user-authored path prefix.
331
- * @returns normalized slash-separated prefix.
340
+ * Whether two normalized file or directory prefixes overlap on path components.
341
+ * @param left - normalized write scope.
342
+ * @param right - normalized write scope.
343
+ * @returns whether either scope contains the other.
332
344
  */
333
- function writeScope(value) {
334
- const normalized = value.replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
335
- const segments = normalized.split("/");
336
- if (normalized.length === 0 || normalized.startsWith("/") || /^[a-z]:/iu.test(normalized) || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new TeamError(`invalid workspace-relative write scope ${JSON.stringify(value)}`, "TEAM_INVALID_WRITE_SCOPE");
337
- return normalized;
345
+ function scopesOverlap(left, right) {
346
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
338
347
  }
339
- //#endregion
340
- //#region lib/types/roster.js
341
- /** Team membership, continuable-child provisioning, and roster-owned teardown. */
342
- const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
343
348
  /**
344
- * Resolve one active Team member by model-facing name, including the Lead pseudo-row.
345
- * @param root - exact live Team Lead.
346
- * @param state - current Team state.
347
- * @param rawName - candidate member name.
348
- * @returns resolved durable id and normalized name.
349
+ * Whether every current blocker of one task completed.
350
+ * @param state - Team state supplying sibling tasks.
351
+ * @param task - durable task snapshot to test.
352
+ * @returns whether the task has no incomplete blockers.
349
353
  */
350
- function resolveActiveMember(root, state, rawName) {
351
- const name = rawName.trim();
352
- if (name === "lead") return {
353
- id: root.id,
354
- name
355
- };
356
- const member = state.members.find((candidate) => candidate.name === name);
357
- if (member === void 0 || member.phase !== "active") throw new TeamError(`active teammate "${name}" not found`, "TEAM_MEMBER_NOT_FOUND");
354
+ function taskReady(state, task) {
355
+ return task.blockedBy.every((id) => state.tasks.find((candidate) => candidate.id === id)?.status === "completed");
356
+ }
357
+ /**
358
+ * Derive one task view with owner name, readiness, and advisory write overlaps.
359
+ * A committing caller may pass its pre-append state because `task` supplies the
360
+ * new value explicitly; owner names, blocker readiness, and other task scopes
361
+ * do not change when that snapshot is appended.
362
+ * @param state - Team state supplying members and sibling tasks.
363
+ * @param task - durable task snapshot to view.
364
+ * @returns a detached task view.
365
+ */
366
+ function projectTaskView(state, task) {
367
+ const ownerName = task.ownerId === void 0 ? void 0 : task.ownerId === brandString(state.id) ? "lead" : state.members.find((member) => member.id === task.ownerId)?.name;
368
+ const warnings = /* @__PURE__ */ new Set();
369
+ for (const other of state.tasks) {
370
+ if (other.id === task.id || other.status !== "in_progress") continue;
371
+ if (task.writeScopes.some((left) => other.writeScopes.some((right) => scopesOverlap(left, right)))) warnings.add(`write scopes overlap with ${other.id}`);
372
+ }
358
373
  return {
359
- id: member.id,
360
- name
374
+ id: task.id,
375
+ revision: task.revision,
376
+ subject: task.subject,
377
+ description: task.description,
378
+ status: task.status,
379
+ blockedBy: [...task.blockedBy],
380
+ writeScopes: [...task.writeScopes],
381
+ ...ownerName === void 0 ? {} : { ownerName },
382
+ ready: task.status === "pending" && taskReady(state, task),
383
+ writeScopeWarnings: [...warnings]
361
384
  };
362
385
  }
363
- /** Owns Team identities and the lifecycle of rostered continuable children. */
364
- var TeamRoster = class {
365
- ctx;
366
- journal;
367
- lifecycle;
368
- maxMembers;
369
- inFlightCreations = /* @__PURE__ */ new Set();
370
- /**
371
- * @param ctx - Team service context with Agent, Session, persistence, and subagent services.
372
- * @param journal - authoritative Lead-log transaction owner.
373
- * @param lifecycle - shared Team runtime admission cutoff.
374
- * @param maxMembers - maximum immutable roster entries per Team.
375
- */
376
- constructor(ctx, journal, lifecycle, maxMembers) {
377
- this.ctx = ctx;
378
- this.journal = journal;
379
- this.lifecycle = lifecycle;
380
- this.maxMembers = maxMembers;
381
- }
382
- /**
383
- * Resolve one exact live Agent's Team role.
384
- * @param agent - exact live Agent used as the authority credential.
385
- * @returns its root, Team identity, role, and model-facing name.
386
- */
387
- membership(agent) {
388
- const membership = this.tryMembership(agent);
389
- if (membership === void 0) throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, "TEAM_NOT_MEMBER");
390
- return membership;
391
- }
392
- /**
393
- * Resolve a caller without throwing for scoped installation and lifecycle observers.
394
- * @param agent - candidate exact live Agent.
395
- * @returns Team membership, or undefined for non-Team subagents and stale identities.
396
- */
397
- tryMembership(agent) {
398
- if (this.ctx.agents.get(agent.id) !== agent) return void 0;
399
- try {
400
- const parentId = agent.session.header.parentSession;
401
- if (parentId !== void 0) {
402
- const root = this.ctx.agents.get(parentId);
403
- if (root !== void 0) {
404
- const member = this.journal.state(root).members.find((candidate) => candidate.id === agent.id);
405
- if (member?.phase === "active" || member?.phase === "provisioning") return {
406
- root,
407
- id: TeamId(root.id),
408
- role: "teammate",
409
- name: member.name
410
- };
411
- if (this.subagentDescriptor(agent)) return void 0;
412
- return {
413
- root: agent,
414
- id: TeamId(agent.id),
415
- role: "lead",
416
- name: "lead"
417
- };
418
- }
419
- }
420
- if (this.subagentDescriptor(agent)) return void 0;
421
- return {
422
- root: agent,
423
- id: TeamId(agent.id),
424
- role: "lead",
425
- name: "lead"
426
- };
427
- } catch {
428
- return;
429
- }
430
- }
431
- /**
432
- * List the runtime-enriched roster visible to one Team member.
433
- * @param membership - exact caller membership resolved by this roster.
434
- * @returns Lead and teammate rows in creation order.
435
- */
436
- list(membership) {
437
- const { root } = membership;
438
- const state = this.journal.state(root);
439
- const result = [{
440
- id: root.id,
441
- name: "lead",
442
- role: "lead",
443
- status: availability(root),
444
- ...root.options.model === void 0 ? {} : { model: root.options.model },
445
- diagnostics: []
446
- }];
447
- for (const member of state.members) {
448
- const live = this.ctx.agents.get(member.id);
449
- const model = live?.options.model ?? root.options.model;
450
- result.push({
451
- id: member.id,
452
- name: member.name,
453
- role: "teammate",
454
- status: member.phase === "failed" ? "failed" : member.phase === "provisioning" ? "provisioning" : availability(live),
455
- description: member.description,
456
- provider: member.provider,
457
- context: member.context,
458
- ...model === void 0 ? {} : { model },
459
- diagnostics: member.error === void 0 ? [] : [member.error]
460
- });
461
- }
462
- return result;
463
- }
464
- /**
465
- * Create one named, continuable direct child of the Team Lead.
466
- * @param caller - exact live Lead Agent.
467
- * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
468
- * @returns the active roster row.
469
- */
470
- async spawn(caller, request) {
471
- if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
472
- const operation = this.spawnAdmitted(caller, request);
473
- this.inFlightCreations.add(operation);
474
- try {
475
- return await operation;
476
- } finally {
477
- this.inFlightCreations.delete(operation);
478
- }
479
- }
480
- /**
481
- * Return admitted creation operations captured for ordered disposal.
482
- * @returns detached snapshot ordered only by Set insertion.
483
- */
484
- pendingCreations() {
485
- return [...this.inFlightCreations];
486
- }
487
- /**
488
- * Reconcile provisioning state when one Team member Session starts.
489
- * @param agent - newly started exact live Agent.
490
- * @param signal - shared runtime cancellation.
491
- */
492
- async recoverFor(agent, signal) {
493
- signal.throwIfAborted();
494
- const membership = this.tryMembership(agent);
495
- if (membership?.role === "lead") await this.reconcileProvisioning(membership.root, signal);
496
- }
497
- /**
498
- * Interrupt one live teammate turn without clearing its pending inbox.
499
- * @param caller - exact live Lead Agent.
500
- * @param targetName - durable teammate name.
501
- * @returns the target status sampled before cancellation.
502
- */
503
- interrupt(caller, targetName) {
504
- const membership = this.membership(caller);
505
- if (membership.role !== "lead") throw new TeamError("only the Team Lead can interrupt teammates", "TEAM_LEAD_REQUIRED");
506
- const state = this.journal.state(membership.root);
507
- const target = resolveActiveMember(membership.root, state, targetName);
508
- if (target.id === membership.root.id) throw new TeamError("the Team Lead cannot interrupt itself", "TEAM_INVALID_TARGET");
509
- const live = this.ctx.agents.get(target.id);
510
- if (live === void 0) return { previousStatus: "inactive" };
511
- const previousStatus = availability(live);
512
- this.ctx.subagents.interrupt(target.id, {
513
- kind: "ancestor",
514
- agent: caller
515
- });
516
- return { previousStatus };
517
- }
518
- /**
519
- * Group exact live roster children by their current Lead for runtime teardown.
520
- * @returns each live Lead and the roster child ids currently in the Agent registry.
521
- */
522
- liveChildrenByRoot() {
523
- const teams = /* @__PURE__ */ new Map();
524
- for (const agent of this.ctx.agents.list()) {
525
- const rootId = agent.session.header.parentSession;
526
- if (rootId === void 0) continue;
527
- const root = this.ctx.agents.get(rootId);
528
- if (root === void 0 || !this.journal.state(root).members.some((member) => member.id === agent.id)) continue;
529
- const children = teams.get(root) ?? [];
530
- children.push(agent.id);
531
- teams.set(root, children);
532
- }
533
- return teams;
534
- }
535
- /**
536
- * Release exact teammate Activations through the continuation lifecycle owner.
537
- * @param root - exact live Team Lead authorizing release.
538
- * @param childIds - selected roster child ids.
539
- */
540
- async stopTeammates(root, childIds) {
541
- await this.lifecycle.withTimeout(this.ctx.subagents.drainContinuableChildren(root, childIds));
542
- }
543
- /** Perform one creation admitted before the Team runtime disposal cutoff. */
544
- async spawnAdmitted(caller, request) {
545
- const membership = this.membership(caller);
546
- if (membership.role !== "lead") throw new TeamError("only the Team Lead can create teammates", "TEAM_LEAD_REQUIRED");
547
- const signal = AbortSignal.any([request.signal, this.lifecycle.signal]);
548
- signal.throwIfAborted();
549
- const root = membership.root;
550
- const name = this.memberName(request.name);
551
- const description = requiredText(request.description, "description", 200);
552
- const childId = brandString(randomUUID());
553
- const member = {
554
- id: childId,
555
- name,
556
- description,
557
- provider: requiredText(request.provider, "provider", 200),
558
- context: request.context,
559
- phase: "provisioning"
560
- };
561
- await this.journal.transact(root.id, async () => {
562
- const state = this.journal.state(root);
563
- if (state.members.some((member) => member.name === name)) throw new TeamError(`teammate name "${name}" was already used in this Team`, "TEAM_MEMBER_NAME_TAKEN");
564
- if (state.members.length >= this.maxMembers) throw new TeamError(`Team member limit ${this.maxMembers} reached`, "TEAM_MEMBER_LIMIT");
565
- await this.journal.appendAndFlush(root, "team/member", {
566
- version: 2,
567
- teamId: TeamId(root.id),
568
- member
569
- });
570
- });
571
- let started;
572
- try {
573
- started = await this.ctx.subagents.startContinuable({
574
- childId,
575
- provider: request.provider,
576
- label: description,
577
- request: {
578
- prompt: request.prompt,
579
- parent: root
580
- },
581
- signal
582
- });
583
- await this.checkpointInitialPrompt(childId, started.messageId, signal);
584
- } catch (error) {
585
- const failed = {
586
- ...member,
587
- phase: "failed",
588
- error: errorMessage(error)
589
- };
590
- try {
591
- const phase = await this.settleProvisioning(root, failed);
592
- await this.stopTeammates(root, [childId]);
593
- if (phase === "active") throw new TeamError(`teammate "${name}" became active while its creator reported failure`, "TEAM_PROVISIONING_CONFLICT", { cause: error });
594
- } catch (recordError) {
595
- throw new AggregateError([error, recordError], "teammate creation and durable failure recording both failed");
596
- }
597
- throw error;
598
- }
599
- const active = {
600
- ...member,
601
- phase: "active"
602
- };
603
- if (await this.settleProvisioning(root, active) === "failed") {
604
- const conflict = new TeamError(`teammate "${name}" was reconciled as failed while creation was in progress`, "TEAM_PROVISIONING_CONFLICT");
605
- try {
606
- await this.stopTeammates(root, [childId]);
607
- } catch (cleanupError) {
608
- /* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */
609
- throw new AggregateError([conflict, cleanupError], "provisioning conflict cleanup failed");
610
- }
611
- throw conflict;
612
- }
613
- return { member: this.memberView(active) };
614
- }
615
- /** Flush the accepted initial inbox item before the Lead can commit `active`. */
616
- async checkpointInitialPrompt(childId, messageId, signal) {
617
- while (true) {
618
- signal.throwIfAborted();
619
- const session = this.ctx.sessions.get(childId);
620
- if (session === void 0) {
621
- const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal);
622
- if (messageAccepted(stored.events.slice(stored.inheritedEventCount), (message) => message.id === messageId)) return;
623
- throw new TeamError(`teammate "${childId}" initial prompt was not durably accepted`, "TEAM_PROVISIONING_CONFLICT");
624
- }
625
- const progress = Promise.withResolvers();
626
- progress.promise.catch(() => void 0);
627
- const stopEvent = this.ctx.on("session/event", (candidate) => {
628
- if (candidate === session) progress.resolve();
629
- });
630
- const stopDisposed = this.ctx.on("session/disposed", (candidate) => {
631
- if (candidate === session) progress.resolve();
632
- });
633
- const onAbort = () => {
634
- const reason = signal.reason;
635
- progress.reject(reason instanceof Error ? reason : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, "TEAM_DISPOSED"));
636
- };
637
- signal.addEventListener("abort", onAbort, { once: true });
638
- try {
639
- signal.throwIfAborted();
640
- await this.ctx.sessions.flush(session);
641
- if (messageAccepted(session.snapshotEvents(session.inheritedEventCount), (message) => message.id === messageId)) return;
642
- if (this.ctx.sessions.get(childId) !== session) continue;
643
- await progress.promise;
644
- } finally {
645
- signal.removeEventListener("abort", onAbort);
646
- stopDisposed();
647
- stopEvent();
648
- }
649
- }
386
+ //#endregion
387
+ //#region lib/types/projection.js
388
+ /** Team state projected incrementally from committed Session events, with a durable-only client view. */
389
+ const nonNegativeSafeInteger = z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
390
+ const positiveSafeInteger = nonNegativeSafeInteger.min(1);
391
+ const sessionIdSchema = z$1.string().min(1).transform((value) => brandString(value));
392
+ const teamIdSchema = z$1.string().min(1).transform((value) => TeamId(value));
393
+ const numericTaskIdPattern = /^task-(\d+)$/u;
394
+ const teamTaskIdSchema = z$1.string().min(1).refine((value) => {
395
+ const match = numericTaskIdPattern.exec(value);
396
+ return match === null || Number.isSafeInteger(Number(match[1]));
397
+ }, { message: "numeric task id suffix must be a safe integer" }).transform((value) => TeamTaskId(value));
398
+ const teamMessageIdSchema = z$1.string().min(1).transform((value) => TeamMessageId(value));
399
+ const coreContentBlockTypes = new Set([
400
+ "text",
401
+ "reasoning",
402
+ "image",
403
+ "tool-call",
404
+ "tool-result"
405
+ ]);
406
+ const imageAttachmentSchema = z$1.object({
407
+ attachmentId: z$1.string().min(1),
408
+ mediaType: z$1.enum([
409
+ "image/png",
410
+ "image/jpeg",
411
+ "image/webp",
412
+ "image/gif"
413
+ ]),
414
+ bytes: nonNegativeSafeInteger,
415
+ width: positiveSafeInteger,
416
+ height: positiveSafeInteger,
417
+ name: z$1.string().optional()
418
+ }).strict();
419
+ const contentBlockSchema = z$1.lazy(() => z$1.union([
420
+ z$1.object({
421
+ type: z$1.literal("text"),
422
+ text: z$1.string()
423
+ }).strict(),
424
+ z$1.object({
425
+ type: z$1.literal("reasoning"),
426
+ text: z$1.string()
427
+ }).strict(),
428
+ z$1.object({
429
+ type: z$1.literal("image"),
430
+ attachment: imageAttachmentSchema
431
+ }).strict(),
432
+ z$1.object({
433
+ type: z$1.literal("tool-call"),
434
+ id: z$1.string().min(1),
435
+ name: z$1.string(),
436
+ arguments: z$1.string()
437
+ }).strict(),
438
+ z$1.custom((value) => {
439
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
440
+ const type = value.type;
441
+ return typeof type === "string" && type.length > 0 && !coreContentBlockTypes.has(type);
442
+ })
443
+ ]));
444
+ const teamMemberSnapshotSchema = z$1.object({
445
+ id: sessionIdSchema,
446
+ name: z$1.string(),
447
+ description: z$1.string(),
448
+ provider: z$1.string(),
449
+ context: z$1.enum(["fresh", "fork"]),
450
+ phase: z$1.enum([
451
+ "provisioning",
452
+ "active",
453
+ "failed"
454
+ ]),
455
+ error: z$1.string().optional()
456
+ }).strict();
457
+ const teamTaskSnapshotSchema = z$1.object({
458
+ id: teamTaskIdSchema,
459
+ revision: positiveSafeInteger,
460
+ subject: z$1.string(),
461
+ description: z$1.string(),
462
+ status: z$1.enum([
463
+ "pending",
464
+ "in_progress",
465
+ "completed",
466
+ "deleted"
467
+ ]),
468
+ ownerId: sessionIdSchema.optional(),
469
+ blockedBy: z$1.array(teamTaskIdSchema),
470
+ writeScopes: z$1.array(z$1.string())
471
+ }).strict();
472
+ const teamMessageSnapshotSchema = z$1.object({
473
+ id: teamMessageIdSchema,
474
+ senderId: sessionIdSchema,
475
+ senderName: z$1.string(),
476
+ targetId: sessionIdSchema,
477
+ content: z$1.array(contentBlockSchema)
478
+ }).strict();
479
+ const teamEventSelectorSchema = z$1.object({
480
+ version: nonNegativeSafeInteger,
481
+ teamId: teamIdSchema
482
+ }).loose();
483
+ const teamMemberEventSchema = z$1.object({
484
+ version: z$1.literal(2),
485
+ teamId: teamIdSchema,
486
+ member: teamMemberSnapshotSchema
487
+ }).strict();
488
+ const teamTaskEventSchema = z$1.object({
489
+ version: z$1.literal(2),
490
+ teamId: teamIdSchema,
491
+ task: teamTaskSnapshotSchema
492
+ }).strict();
493
+ const teamMessageQueuedEventSchema = z$1.object({
494
+ version: z$1.literal(2),
495
+ teamId: teamIdSchema,
496
+ message: teamMessageSnapshotSchema
497
+ }).strict();
498
+ const teamMessageDeliveredEventSchema = z$1.object({
499
+ version: z$1.literal(2),
500
+ teamId: teamIdSchema,
501
+ messageId: teamMessageIdSchema,
502
+ targetId: sessionIdSchema
503
+ }).strict();
504
+ /**
505
+ * Construct empty state for one Team identity.
506
+ * @param rootId - root Session identity.
507
+ * @returns empty Team state.
508
+ */
509
+ function emptyTeamState(rootId) {
510
+ return {
511
+ id: TeamId(rootId),
512
+ members: [],
513
+ tasks: [],
514
+ messages: [],
515
+ delivered: [],
516
+ nextTaskNumber: 1
517
+ };
518
+ }
519
+ const teamProjectionEntrySchema = z$1.object({
520
+ id: teamIdSchema,
521
+ members: z$1.array(teamMemberSnapshotSchema),
522
+ tasks: z$1.array(teamTaskSnapshotSchema),
523
+ messages: z$1.array(teamMessageSnapshotSchema),
524
+ delivered: z$1.array(teamMessageIdSchema),
525
+ nextTaskNumber: positiveSafeInteger,
526
+ failure: z$1.string().optional()
527
+ }).strict();
528
+ /**
529
+ * Test whether a Session event belongs to the Team domain.
530
+ * @param event - candidate Session event.
531
+ * @returns whether the event has a Team-owned type.
532
+ */
533
+ function isTeamEvent(event) {
534
+ return event.type === "team/member" || event.type === "team/task" || event.type === "team/message/queued" || event.type === "team/message/delivered";
535
+ }
536
+ /** Decode one persisted Team value and retain the schema failure as its cause. */
537
+ function parsePersisted(type, schema, value) {
538
+ try {
539
+ return schema.parse(value);
540
+ } catch (error) {
541
+ throw new Error(`persisted Agent Teams ${type} payload is invalid`, { cause: error });
650
542
  }
651
- /** Settle provisioning-only members from their independently durable child Sessions. */
652
- async reconcileProvisioning(root, signal) {
653
- const provisioning = this.journal.state(root).members.filter((member) => member.phase === "provisioning");
654
- for (const member of provisioning) {
655
- signal.throwIfAborted();
656
- if (this.ctx.agents.get(member.id) !== void 0) continue;
657
- let phase = "failed";
658
- let failure = "provisioning did not leave a resumable child Session";
659
- try {
660
- const loaded = await readPersistedSession(this.ctx.sessionPersistence, member.id, signal);
661
- const suffix = loaded.events.slice(loaded.inheritedEventCount);
662
- const descriptor = foldSubagentDescriptor(suffix);
663
- const acceptedInitialPrompt = messageAccepted(suffix, (message) => message.source.kind === "user");
664
- if (loaded.header.parentSession === root.id && descriptor?.mode === "continuable" && descriptor.provider === member.provider && acceptedInitialPrompt) phase = "active";
665
- else failure = "persisted child Session does not match the provisioned continuation";
666
- } catch (error) {
667
- failure = `child Session recovery failed: ${errorMessage(error)}`;
668
- }
669
- signal.throwIfAborted();
670
- await this.journal.transact(root.id, async () => {
671
- signal.throwIfAborted();
672
- const current = this.journal.state(root).members.find((candidate) => candidate.id === member.id);
673
- if (current?.phase !== "provisioning") return;
674
- const settled = {
675
- ...current,
676
- phase,
677
- ...phase === "failed" ? { error: failure } : {}
678
- };
679
- await this.journal.appendAndFlush(root, "team/member", {
680
- version: 2,
681
- teamId: TeamId(root.id),
682
- member: settled
683
- });
684
- });
685
- }
543
+ }
544
+ /** Decode the complete current-version payload selected by one Team event type. */
545
+ function parseCurrentTeamEvent(event) {
546
+ switch (event.type) {
547
+ case "team/member": return {
548
+ ...event,
549
+ data: parsePersisted(event.type, teamMemberEventSchema, event.data)
550
+ };
551
+ case "team/task": return {
552
+ ...event,
553
+ data: parsePersisted(event.type, teamTaskEventSchema, event.data)
554
+ };
555
+ case "team/message/queued": return {
556
+ ...event,
557
+ data: parsePersisted(event.type, teamMessageQueuedEventSchema, event.data)
558
+ };
559
+ case "team/message/delivered": return {
560
+ ...event,
561
+ data: parsePersisted(event.type, teamMessageDeliveredEventSchema, event.data)
562
+ };
563
+ /* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
564
+ default: return event;
686
565
  }
687
- /** Build one runtime member row after successful creation. */
688
- memberView(member) {
689
- const live = this.ctx.agents.get(member.id);
566
+ }
567
+ function applyProjectionEvent(state, event) {
568
+ if (state.failure !== void 0) return state;
569
+ if (!isTeamEvent(event)) return state;
570
+ try {
571
+ const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data);
572
+ if (selector.teamId !== state.id) return state;
573
+ if (selector.version !== 2) throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`);
574
+ return applyCurrentTeamEvent(state, parseCurrentTeamEvent(event));
575
+ } catch (error) {
576
+ /* v8 ignore next -- the owned Team transition throws Error instances. */
690
577
  return {
691
- id: member.id,
692
- name: member.name,
693
- role: "teammate",
694
- status: availability(live),
695
- description: member.description,
696
- provider: member.provider,
697
- context: member.context,
698
- ...live?.options.model === void 0 ? {} : { model: live.options.model },
699
- diagnostics: []
578
+ ...state,
579
+ failure: error instanceof Error ? error.message : String(error)
700
580
  };
701
581
  }
702
- /** Validate a never-reused model-facing teammate name. */
703
- memberName(value) {
704
- if (!MEMBER_NAME.test(value) || value.length > 64 || value === "lead") throw new TeamError("teammate name must be lower-kebab-case, at most 64 characters, and not \"lead\"", "TEAM_INVALID_MEMBER_NAME");
705
- return value;
706
- }
707
- /** Append one terminal provisioning edge unless recovery already settled it. */
708
- async settleProvisioning(root, terminal) {
709
- return this.journal.transact(root.id, async () => {
710
- const current = this.journal.state(root).members.find((member) => member.id === terminal.id);
711
- /* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
712
- if (current === void 0) throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, "TEAM_PROVISIONING_CONFLICT");
713
- if (current.phase !== "provisioning") return current.phase;
714
- await this.journal.appendAndFlush(root, "team/member", {
715
- version: 2,
716
- teamId: TeamId(root.id),
717
- member: terminal
718
- });
719
- return terminal.phase === "active" ? "active" : "failed";
720
- });
721
- }
722
- /** Whether a Session's own suffix identifies a provider-owned subagent child. */
723
- subagentDescriptor(agent) {
724
- return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== void 0;
725
- }
726
- };
727
- /** Turn availability is independent of whether the Agent is loaded. */
728
- function availability(agent) {
729
- return agent?.status === "running" ? "running" : "inactive";
730
582
  }
731
- //#endregion
732
- //#region lib/types/mailbox.js
733
- /** Durable Team mailbox admission, target-local dispatch, acknowledgement, and recovery. */
734
- /** Owns every process-local state transition for the durable Team mailbox. */
735
- var TeamMailbox = class {
736
- ctx;
737
- journal;
738
- roster;
739
- lifecycle;
740
- maxPendingMessagesPerMember;
741
- maxMessageBytes;
742
- dispatchTails = /* @__PURE__ */ new Map();
743
- inFlightMessages = /* @__PURE__ */ new Set();
744
- inFlightDispatches = /* @__PURE__ */ new Set();
745
- /**
746
- * @param ctx - Team service context with Agent, Session, persistence, and subagent services.
747
- * @param journal - authoritative Lead-log transaction owner.
748
- * @param roster - Team membership and member-name resolver.
749
- * @param lifecycle - shared Team runtime admission cutoff.
750
- * @param maxPendingMessagesPerMember - per-target queued-minus-delivered limit.
751
- * @param maxMessageBytes - maximum complete sender-framed delivery size.
752
- */
753
- constructor(ctx, journal, roster, lifecycle, maxPendingMessagesPerMember, maxMessageBytes) {
754
- this.ctx = ctx;
755
- this.journal = journal;
756
- this.roster = roster;
757
- this.lifecycle = lifecycle;
758
- this.maxPendingMessagesPerMember = maxPendingMessagesPerMember;
759
- this.maxMessageBytes = maxMessageBytes;
760
- }
761
- /**
762
- * Queue one durable peer message, then attempt immediate delivery.
763
- * @param caller - exact live sending Team member.
764
- * @param request - target name, content, and pre-queue cancellation.
765
- * @returns durable message identity and immediate-delivery observation.
766
- */
767
- async send(caller, request) {
768
- if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
769
- const operation = this.sendAdmitted(caller, {
770
- ...request,
771
- signal: AbortSignal.any([request.signal, this.lifecycle.signal])
772
- });
773
- return await this.trackDispatch(operation);
774
- }
775
- /**
776
- * Observe target-side durable receipts and checkpoint their Lead-log acknowledgement.
777
- * @param session - exact target Session receiving the event.
778
- * @param event - newly appended Session event.
779
- */
780
- observeSessionEvent(session, event) {
781
- if (this.lifecycle.disposed || event.type !== "user/message" || event.data.source.kind !== "team-message") return;
782
- const source = event.data.source;
783
- const acknowledgement = Promise.resolve().then(async () => {
784
- const root = this.ctx.agents.get(brandString(source.teamId));
785
- if (root !== void 0) await this.checkpointDelivered(root, session, source.messageId);
786
- }).catch((error) => {
787
- this.ctx.logger.warn(`Team message "${source.messageId}" acknowledgement failed: ${errorMessage(error)}`);
788
- });
789
- this.trackDispatch(acknowledgement);
790
- }
791
- /**
792
- * Retry durable pending messages relevant to one started Team member.
793
- * @param agent - newly started exact live Agent.
794
- * @param signal - shared runtime cancellation.
795
- */
796
- async recoverFor(agent, signal) {
797
- signal.throwIfAborted();
798
- const membership = this.roster.tryMembership(agent);
799
- if (membership === void 0) return;
800
- const state = this.journal.state(membership.root);
801
- const messages = state.messages.filter((message) => !state.delivered.includes(message.id) && (membership.role === "lead" || message.targetId === agent.id));
802
- for (const message of messages) {
803
- signal.throwIfAborted();
804
- await this.tryDispatch(membership.root, message, signal);
805
- }
806
- }
807
- /**
808
- * Return admitted dispatch and acknowledgement operations captured for disposal.
809
- * @returns detached snapshot ordered only by Set insertion.
810
- */
811
- pendingDispatches() {
812
- return [...this.inFlightDispatches];
813
- }
814
- /** Queue and dispatch one mailbox item admitted before the disposal cutoff. */
815
- async sendAdmitted(caller, request) {
816
- const membership = this.roster.membership(caller);
817
- request.signal.throwIfAborted();
818
- const root = membership.root;
819
- const content = structuredClone(request.content);
820
- const queued = await this.journal.transact(root.id, async () => {
821
- request.signal.throwIfAborted();
822
- const state = this.journal.state(root);
823
- const target = resolveActiveMember(root, state, request.target);
824
- if (target.id === caller.id) throw new TeamError("a Team member cannot message itself", "TEAM_SELF_MESSAGE");
825
- const pendingForTarget = state.messages.filter((candidate) => candidate.targetId === target.id && !state.delivered.includes(candidate.id)).length;
826
- if (pendingForTarget >= this.maxPendingMessagesPerMember) throw new TeamError(`teammate "${target.name}" has ${pendingForTarget} pending messages`, "TEAM_MAILBOX_FULL");
827
- const queued = {
828
- id: TeamMessageId(`team-message-${randomUUID()}`),
829
- senderId: caller.id,
830
- senderName: membership.name,
831
- targetId: target.id,
832
- content
583
+ function replaceAt(items, index, item) {
584
+ const next = [...items];
585
+ if (index < 0) next.push(item);
586
+ else next[index] = item;
587
+ return next;
588
+ }
589
+ function applyCurrentTeamEvent(state, event) {
590
+ switch (event.type) {
591
+ case "team/member": {
592
+ const member = event.data.member;
593
+ const index = state.members.findIndex((candidate) => candidate.id === member.id);
594
+ const prior = state.members[index];
595
+ const named = state.members.find((candidate) => candidate.name === member.name);
596
+ if (named !== void 0 && named.id !== member.id) throw new Error(`teammate name "${member.name}" is reused by another member`);
597
+ if (prior === void 0) {
598
+ if (member.phase !== "provisioning") throw new Error(`teammate "${member.name}" must begin provisioning`);
599
+ } else {
600
+ if (prior.name !== member.name || prior.provider !== member.provider || prior.context !== member.context) throw new Error(`teammate "${member.id}" changed immutable identity fields`);
601
+ if (prior.phase !== "provisioning" || member.phase === "provisioning") throw new Error(`teammate "${member.name}" has an invalid ${prior.phase} -> ${member.phase} transition`);
602
+ }
603
+ return {
604
+ ...state,
605
+ members: replaceAt(state.members, index, member)
833
606
  };
834
- if (Buffer.byteLength(JSON.stringify(this.deliveryContent(queued)), "utf8") > this.maxMessageBytes) throw new TeamError(`team message exceeds ${this.maxMessageBytes} bytes`, "TEAM_MESSAGE_TOO_LARGE");
835
- await this.journal.appendAndFlush(root, "team/message/queued", {
836
- version: 2,
837
- teamId: TeamId(root.id),
838
- message: queued
839
- });
607
+ }
608
+ case "team/task": {
609
+ const task = event.data.task;
610
+ const index = state.tasks.findIndex((candidate) => candidate.id === task.id);
611
+ const prior = state.tasks[index];
612
+ if (prior === void 0 && task.revision !== 1) throw new Error(`team task "${task.id}" must begin at revision 1`);
613
+ if (prior !== void 0 && task.revision !== prior.revision + 1) throw new Error(`team task "${task.id}" revision is not contiguous`);
614
+ assertTaskGraphCandidate(state.tasks, task);
615
+ let nextTaskNumber = state.nextTaskNumber;
616
+ const match = numericTaskIdPattern.exec(task.id);
617
+ if (match !== null) {
618
+ const number = Number(match[1]);
619
+ nextTaskNumber = Math.max(nextTaskNumber, number === Number.MAX_SAFE_INTEGER ? number : number + 1);
620
+ }
840
621
  return {
841
- message: queued,
842
- dispatch: this.tryDispatch(root, queued, request.signal)
622
+ ...state,
623
+ tasks: replaceAt(state.tasks, index, task),
624
+ nextTaskNumber
843
625
  };
844
- });
845
- const accepted = await queued.dispatch;
846
- return {
847
- messageId: queued.message.id,
848
- status: accepted ? "accepted" : "queued"
849
- };
850
- }
851
- /** Attempt one queued message exactly once in this process at a time. */
852
- tryDispatch(root, message, signal) {
853
- if (this.lifecycle.disposed) return Promise.resolve(false);
854
- if (this.inFlightMessages.has(message.id)) return Promise.resolve(false);
855
- this.inFlightMessages.add(message.id);
856
- const operation = this.trackDispatch(this.tryDispatchAdmitted(root, message, AbortSignal.any([signal, this.lifecycle.signal])));
857
- const forget = () => {
858
- this.inFlightMessages.delete(message.id);
859
- };
860
- operation.then(forget, forget);
861
- return operation;
862
- }
863
- /** Track one dispatch transaction through delivery admission or contained failure. */
864
- trackDispatch(operation) {
865
- this.inFlightDispatches.add(operation);
866
- operation.then(() => {
867
- this.inFlightDispatches.delete(operation);
868
- }, () => {
869
- this.inFlightDispatches.delete(operation);
870
- });
871
- return operation;
872
- }
873
- /** Attempt one queued message admitted before the service lifecycle cutoff. */
874
- async tryDispatchAdmitted(root, message, signal) {
875
- return await this.serializeDispatch(message, () => this.dispatchThrough(root, message, signal));
876
- }
877
- /** Serialize delivery admission for one durable target in queued order. */
878
- async serializeDispatch(message, operation) {
879
- const targetId = message.targetId;
880
- /* v8 ignore next -- dispatch tails absorb rejection, so the recovery callback is a fail-safe backstop. */
881
- const run = (this.dispatchTails.get(targetId) ?? Promise.resolve()).then(operation, operation);
882
- /* v8 ignore next -- dispatchOnce contains delivery failures and serializeDispatch itself does not throw. */
883
- const tail = run.then(() => void 0, () => void 0);
884
- this.dispatchTails.set(targetId, tail);
885
- try {
886
- return await run;
887
- } finally {
888
- if (this.dispatchTails.get(targetId) === tail) this.dispatchTails.delete(targetId);
889
626
  }
890
- }
891
- /** Deliver every pending target message through `message` in durable queue order. */
892
- async dispatchThrough(root, message, signal) {
893
- const state = this.journal.state(root);
894
- const pending = state.messages.filter((candidate) => candidate.targetId === message.targetId && !state.delivered.includes(candidate.id));
895
- const requested = pending.findIndex((candidate) => candidate.id === message.id);
896
- if (requested < 0) return state.delivered.includes(message.id);
897
- for (const candidate of pending.slice(0, requested + 1)) {
898
- const ownsInFlight = !this.inFlightMessages.has(candidate.id);
899
- if (ownsInFlight) this.inFlightMessages.add(candidate.id);
900
- try {
901
- if (!await this.dispatchOnce(root, candidate, signal)) return false;
902
- } finally {
903
- if (ownsInFlight) this.inFlightMessages.delete(candidate.id);
904
- }
627
+ case "team/message/queued": {
628
+ const message = event.data.message;
629
+ if (state.messages.some((candidate) => candidate.id === message.id)) throw new Error(`team message "${message.id}" was queued twice`);
630
+ return {
631
+ ...state,
632
+ messages: [...state.messages, message]
633
+ };
905
634
  }
906
- return true;
907
- }
908
- /** Attempt one queued delivery after target-local ordering admits it. */
909
- async dispatchOnce(root, message, signal) {
910
- try {
911
- const target = message.targetId === root.id ? root : this.ctx.agents.get(message.targetId);
912
- if (target !== void 0 && this.targetRecorded(target.session, message.id)) return await this.checkpointDelivered(root, target.session, message.id);
913
- const source = {
914
- kind: "team-message",
915
- teamId: TeamId(root.id),
916
- messageId: message.id,
917
- senderId: message.senderId,
918
- senderName: message.senderName
635
+ case "team/message/delivered": {
636
+ const queued = state.messages.find((message) => message.id === event.data.messageId);
637
+ if (queued === void 0) throw new Error(`team message "${event.data.messageId}" was delivered before queueing`);
638
+ if (queued.targetId !== event.data.targetId) throw new Error(`team message "${event.data.messageId}" target changed`);
639
+ if (state.delivered.includes(event.data.messageId)) throw new Error(`team message "${event.data.messageId}" was delivered twice`);
640
+ return {
641
+ ...state,
642
+ delivered: [...state.delivered, event.data.messageId]
919
643
  };
920
- const content = this.deliveryContent(message);
921
- if (message.targetId === root.id) {
922
- const input = createUserMessage({
923
- content,
924
- source
925
- });
926
- root.steer(input);
927
- return await this.checkpointDelivered(root, root.session, message.id);
928
- }
929
- if (target === void 0) {
930
- const recorded = await this.persistedTargetRecorded(message.targetId, message.id, signal);
931
- if (recorded === void 0) return false;
932
- if (recorded) {
933
- await this.markDelivered(root, message.id, message.targetId);
934
- return true;
935
- }
936
- }
937
- await steerHostSubagentPrompt(this.ctx.subagents, root, message.targetId, content, source, signal);
938
- return target === void 0 ? true : await this.checkpointDelivered(root, target.session, message.id);
939
- } catch (error) {
940
- this.ctx.logger.warn(`team message "${message.id}" remains queued: ${errorMessage(error)}`);
941
- return false;
942
644
  }
645
+ /* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
646
+ default: return state;
943
647
  }
944
- /** Flush one live target receipt before the Lead records its delivered edge. */
945
- async checkpointDelivered(root, target, messageId) {
946
- await this.ctx.sessions.flush(target);
947
- if (!this.targetRecorded(target, messageId)) return false;
948
- await this.markDelivered(root, messageId, target.id);
949
- return true;
950
- }
951
- /** Record delivery unless the acknowledgement already exists. */
952
- async markDelivered(root, messageId, targetId) {
953
- await this.journal.transact(root.id, async () => {
954
- const state = this.journal.state(root);
955
- if (state.delivered.includes(messageId)) return;
956
- const queued = state.messages.find((message) => message.id === messageId);
957
- if (queued === void 0 || queued.targetId !== targetId) return;
958
- await this.journal.appendAndFlush(root, "team/message/delivered", {
959
- version: 2,
960
- teamId: TeamId(root.id),
961
- messageId,
962
- targetId
963
- });
964
- });
965
- }
966
- /** Whether a target Session already contains the durable message identity. */
967
- targetRecorded(session, messageId) {
968
- return messageAccepted(session.snapshotEvents(session.inheritedEventCount), (message) => message.source.kind === "team-message" && message.source.messageId === messageId);
648
+ }
649
+ const teamMemberProjectionSchema = z$1.object({
650
+ id: sessionIdSchema,
651
+ name: z$1.string(),
652
+ role: z$1.enum(["lead", "teammate"]),
653
+ phase: z$1.enum([
654
+ "provisioning",
655
+ "active",
656
+ "failed"
657
+ ]),
658
+ error: z$1.string().optional()
659
+ }).strict();
660
+ const teamTaskViewSchema = z$1.object({
661
+ id: teamTaskIdSchema,
662
+ revision: positiveSafeInteger,
663
+ subject: z$1.string(),
664
+ description: z$1.string(),
665
+ status: z$1.enum([
666
+ "pending",
667
+ "in_progress",
668
+ "completed",
669
+ "deleted"
670
+ ]),
671
+ blockedBy: z$1.array(teamTaskIdSchema),
672
+ writeScopes: z$1.array(z$1.string()),
673
+ ownerName: z$1.string().optional(),
674
+ ready: z$1.boolean(),
675
+ writeScopeWarnings: z$1.array(z$1.string())
676
+ }).strict();
677
+ const teamProjectionSchema = z$1.object({
678
+ members: z$1.array(teamMemberProjectionSchema),
679
+ tasks: z$1.array(teamTaskViewSchema),
680
+ failure: z$1.string().optional()
681
+ }).strict();
682
+ /** Client views keyed by the member and task collections they were derived from. */
683
+ const teamProjectionViews = /* @__PURE__ */ new WeakMap();
684
+ function buildTeamProjection(state) {
685
+ const members = [{
686
+ id: brandString(state.id),
687
+ name: "lead",
688
+ role: "lead",
689
+ phase: "active"
690
+ }];
691
+ for (const member of state.members) members.push({
692
+ id: member.id,
693
+ name: member.name,
694
+ role: "teammate",
695
+ phase: member.phase,
696
+ ...member.error === void 0 ? {} : { error: member.error }
697
+ });
698
+ return {
699
+ members,
700
+ tasks: state.tasks.filter((task) => task.status !== "deleted").map((task) => projectTaskView(state, task)),
701
+ ...state.failure === void 0 ? {} : { failure: state.failure }
702
+ };
703
+ }
704
+ /**
705
+ * Durable client view of one Team state. Mailbox-only state changes reuse the
706
+ * previous view reference, so the live drive publishes nothing for them.
707
+ * A failure is terminal: later events retain the failed state reference and
708
+ * do not republish its view.
709
+ * @param state - current Team state.
710
+ * @returns the roster and non-deleted task board, plus any projection failure.
711
+ */
712
+ function teamProjectionView(state) {
713
+ if (state.failure !== void 0) return buildTeamProjection(state);
714
+ let byTasks = teamProjectionViews.get(state.members);
715
+ if (byTasks === void 0) {
716
+ byTasks = /* @__PURE__ */ new WeakMap();
717
+ teamProjectionViews.set(state.members, byTasks);
969
718
  }
970
- /** Frame peer content with stable sender and message identity for the receiving model. */
971
- deliveryContent(message) {
972
- return [{
973
- type: "text",
974
- text: `Team message ${message.id} from ${message.senderName}:`
975
- }, ...structuredClone(message.content)];
719
+ let view = byTasks.get(state.tasks);
720
+ if (view === void 0) {
721
+ view = buildTeamProjection(state);
722
+ byTasks.set(state.tasks, view);
976
723
  }
977
- /** Read an inactive target's durable log before cold resume; uncertainty keeps the mailbox queued. */
978
- async persistedTargetRecorded(targetId, messageId, signal) {
979
- try {
980
- const stored = await readPersistedSession(this.ctx.sessionPersistence, targetId, signal);
981
- return messageAccepted(stored.events.slice(stored.inheritedEventCount), (message) => message.source.kind === "team-message" && message.source.messageId === messageId);
982
- } catch (error) {
983
- this.ctx.logger.warn(`cannot read Team message target "${targetId}": ${errorMessage(error)}`);
984
- return;
985
- }
724
+ return view;
725
+ }
726
+ /** Team projection selected by the projected Session identity; the wire view carries durable roster and task state only. */
727
+ const teamProjectionDefinition = {
728
+ key: "agentTeam",
729
+ stateVersion: 4,
730
+ stateSchema: teamProjectionEntrySchema,
731
+ init: (header) => emptyTeamState(header.id),
732
+ apply: applyProjectionEvent,
733
+ wire: {
734
+ viewSchema: teamProjectionSchema,
735
+ view: teamProjectionView
986
736
  }
987
737
  };
988
738
  //#endregion
989
- //#region lib/types/task-graph.js
990
- /** Complete dependency validation for current Team task snapshots. */
991
- /** Package-private task dependency failure retained for command error mapping. */
992
- var TeamTaskGraphError = class extends Error {
993
- violation;
994
- /**
995
- * @param message - concrete invalid dependency relation.
996
- * @param violation - stable relation category used by Team commands.
997
- */
998
- constructor(message, violation) {
999
- super(message);
1000
- this.violation = violation;
1001
- this.name = "TeamTaskGraphError";
1002
- }
1003
- };
739
+ //#region lib/types/remote.js
740
+ /** Wire failure mapping for the Team service's browser write face. */
1004
741
  /**
1005
- * Validate the complete active task graph after replacing one candidate snapshot.
1006
- * @param current - current task snapshots before the candidate event.
1007
- * @param candidate - new or next-revision task snapshot.
1008
- * @throws {TeamTaskGraphError} when an active dependency is missing, duplicated, self-referential, or cyclic.
742
+ * Map one Team-domain failure onto the shared Remote failure vocabulary so the
743
+ * browser can branch on a stable code. Unknown values pass through unchanged
744
+ * and surface as `gateway/internal`.
745
+ * @param error - the value thrown by the Team domain operation.
746
+ * @returns the value to rethrow from the wire method.
1009
747
  */
1010
- function assertTaskGraphCandidate(current, candidate) {
1011
- const tasks = new Map(current.map((task) => [task.id, task]));
1012
- tasks.set(candidate.id, candidate);
1013
- for (const task of tasks.values()) {
1014
- if (task.status === "deleted") continue;
1015
- const seen = /* @__PURE__ */ new Set();
1016
- for (const blockerId of task.blockedBy) {
1017
- if (blockerId === task.id) throw new TeamTaskGraphError(`team task "${task.id}" cannot block itself`, "cycle");
1018
- if (seen.has(blockerId)) throw new TeamTaskGraphError(`team task "${task.id}" repeats blocker "${blockerId}"`, "duplicate");
1019
- const blocker = tasks.get(blockerId);
1020
- if (blocker === void 0 || blocker.status === "deleted") throw new TeamTaskGraphError(`blocker task "${blockerId}" for "${task.id}" is missing or deleted`, "missing");
1021
- seen.add(blockerId);
1022
- }
748
+ function teamWireError(error) {
749
+ if (!(error instanceof TeamError)) return error;
750
+ if (error.code === "TEAM_TASK_STALE_REVISION") return new RemoteError("agent-team/stale-revision", error.message, {});
751
+ if (error.code === "TEAM_NOT_MEMBER") return new RemoteError("agent-team/not-a-member", error.message, {});
752
+ return new RemoteError("agent-team/rejected", error.message, { code: error.code });
753
+ }
754
+ //#endregion
755
+ //#region lib/types/persisted.js
756
+ /** Short-lived read-handle access to persisted Team member Sessions. */
757
+ /**
758
+ * Read one stored session's header and complete event log through a
759
+ * short-lived read handle, closing the handle before returning.
760
+ * @param persistence - the durable session store.
761
+ * @param id - the stored session to read.
762
+ * @param signal - cancellation observed by open and read.
763
+ * @returns the stored header and every committed event.
764
+ */
765
+ async function readPersistedSession(persistence, id, signal) {
766
+ const handle = await persistence.open(id, "read", { signal });
767
+ try {
768
+ const { events } = await handle.read(0, void 0, { signal });
769
+ return {
770
+ header: handle.header,
771
+ inheritedEventCount: handle.inheritedEventCount,
772
+ events
773
+ };
774
+ } finally {
775
+ await handle.close();
1023
776
  }
1024
- const visiting = /* @__PURE__ */ new Set();
1025
- const visited = /* @__PURE__ */ new Set();
1026
- const visit = (id) => {
1027
- if (visiting.has(id)) throw new TeamTaskGraphError(`task dependency cycle includes "${id}"`, "cycle");
1028
- if (visited.has(id)) return;
1029
- const task = tasks.get(id);
1030
- if (task === void 0 || task.status === "deleted") return;
1031
- visiting.add(id);
1032
- for (const blockerId of task.blockedBy) visit(blockerId);
1033
- visiting.delete(id);
1034
- visited.add(id);
1035
- };
1036
- for (const task of tasks.values()) visit(task.id);
1037
777
  }
1038
778
  //#endregion
1039
- //#region lib/types/task-view.js
1040
- /** Pure task-view derivation shared by the task board and the client projection. */
779
+ //#region lib/types/session-message.js
780
+ /** Durable Session-message acceptance checks shared by provisioning and mailbox recovery. */
781
+ /** Fold the durable inbox suffix into the messages still awaiting a claim. */
782
+ function pendingInboxMessages(events) {
783
+ const inbox = {
784
+ "next-turn": [],
785
+ "next-step": []
786
+ };
787
+ for (const event of events) {
788
+ if (event.type !== "agent/inbox/spliced") continue;
789
+ inbox[event.data.target].splice(event.data.start, event.data.removedCount ?? 0, ...event.data.inserted);
790
+ }
791
+ return [...inbox["next-turn"], ...inbox["next-step"]];
792
+ }
1041
793
  /**
1042
- * Whether two normalized file or directory prefixes overlap on path components.
1043
- * @param left - normalized write scope.
1044
- * @param right - normalized write scope.
1045
- * @returns whether either scope contains the other.
794
+ * Test whether one message is model-visible or still durably pending.
795
+ * @param events - one Session's non-inherited event suffix.
796
+ * @param predicate - identity check for the accepted message.
797
+ * @returns whether history or the current inbox contains a match.
1046
798
  */
1047
- function scopesOverlap(left, right) {
1048
- return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
799
+ function messageAccepted(events, predicate) {
800
+ return events.some((event) => event.type === "user/message" && predicate(event.data)) || pendingInboxMessages(events).some(predicate);
1049
801
  }
802
+ //#endregion
803
+ //#region lib/types/validation.js
804
+ /** Input normalization shared by Team roster and task commands. */
1050
805
  /**
1051
- * Whether every current blocker of one task completed.
1052
- * @param state - Team state supplying sibling tasks.
1053
- * @param task - durable task snapshot to test.
1054
- * @returns whether the task has no incomplete blockers.
806
+ * Normalize one required human-authored string.
807
+ * @param value - raw input value.
808
+ * @param field - diagnostic field name.
809
+ * @param maxLength - maximum normalized character count.
810
+ * @returns trimmed non-empty text.
1055
811
  */
1056
- function taskReady(state, task) {
1057
- return task.blockedBy.every((id) => state.tasks.find((candidate) => candidate.id === id)?.status === "completed");
812
+ function requiredText(value, field, maxLength) {
813
+ const text = value.trim();
814
+ if (text.length === 0) throw new TeamError(`${field} must be non-empty`, "TEAM_INVALID_ARGUMENT");
815
+ if (text.length > maxLength) throw new TeamError(`${field} exceeds ${maxLength} characters`, "TEAM_INVALID_ARGUMENT");
816
+ return text;
1058
817
  }
1059
818
  /**
1060
- * Derive one task view with owner name, readiness, and advisory write overlaps.
1061
- * A committing caller may pass its pre-append state because `task` supplies the
1062
- * new value explicitly; owner names, blocker readiness, and other task scopes
1063
- * do not change when that snapshot is appended.
1064
- * @param state - Team state supplying members and sibling tasks.
1065
- * @param task - durable task snapshot to view.
1066
- * @returns a detached task view.
819
+ * Normalize one workspace-relative path prefix without treating it as a lock.
820
+ * @param value - user-authored path prefix.
821
+ * @returns normalized slash-separated prefix.
1067
822
  */
1068
- function projectTaskView(state, task) {
1069
- const ownerName = task.ownerId === void 0 ? void 0 : task.ownerId === brandString(state.id) ? "lead" : state.members.find((member) => member.id === task.ownerId)?.name;
1070
- const warnings = /* @__PURE__ */ new Set();
1071
- for (const other of state.tasks) {
1072
- if (other.id === task.id || other.status !== "in_progress") continue;
1073
- if (task.writeScopes.some((left) => other.writeScopes.some((right) => scopesOverlap(left, right)))) warnings.add(`write scopes overlap with ${other.id}`);
1074
- }
1075
- return {
1076
- id: task.id,
1077
- revision: task.revision,
1078
- subject: task.subject,
1079
- description: task.description,
1080
- status: task.status,
1081
- blockedBy: [...task.blockedBy],
1082
- writeScopes: [...task.writeScopes],
1083
- ...ownerName === void 0 ? {} : { ownerName },
1084
- ready: task.status === "pending" && taskReady(state, task),
1085
- writeScopeWarnings: [...warnings]
1086
- };
823
+ function writeScope(value) {
824
+ const normalized = value.replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
825
+ const segments = normalized.split("/");
826
+ if (normalized.length === 0 || normalized.startsWith("/") || /^[a-z]:/iu.test(normalized) || segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new TeamError(`invalid workspace-relative write scope ${JSON.stringify(value)}`, "TEAM_INVALID_WRITE_SCOPE");
827
+ return normalized;
1087
828
  }
1088
829
  //#endregion
1089
- //#region lib/types/projection.js
1090
- /** Team state projected incrementally from committed Session events, with a durable-only client view. */
1091
- const nonNegativeSafeInteger = z$1.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
1092
- const positiveSafeInteger = nonNegativeSafeInteger.min(1);
1093
- const sessionIdSchema = z$1.string().min(1).transform((value) => brandString(value));
1094
- const teamIdSchema = z$1.string().min(1).transform((value) => TeamId(value));
1095
- const numericTaskIdPattern = /^task-(\d+)$/u;
1096
- const teamTaskIdSchema = z$1.string().min(1).refine((value) => {
1097
- const match = numericTaskIdPattern.exec(value);
1098
- return match === null || Number.isSafeInteger(Number(match[1]));
1099
- }, { message: "numeric task id suffix must be a safe integer" }).transform((value) => TeamTaskId(value));
1100
- const teamMessageIdSchema = z$1.string().min(1).transform((value) => TeamMessageId(value));
1101
- const coreContentBlockTypes = new Set([
1102
- "text",
1103
- "reasoning",
1104
- "image",
1105
- "tool-call",
1106
- "tool-result"
1107
- ]);
1108
- const imageAttachmentSchema = z$1.object({
1109
- attachmentId: z$1.string().min(1),
1110
- mediaType: z$1.enum([
1111
- "image/png",
1112
- "image/jpeg",
1113
- "image/webp",
1114
- "image/gif"
1115
- ]),
1116
- bytes: nonNegativeSafeInteger,
1117
- width: positiveSafeInteger,
1118
- height: positiveSafeInteger,
1119
- name: z$1.string().optional()
1120
- }).strict();
1121
- const contentBlockSchema = z$1.lazy(() => z$1.union([
1122
- z$1.object({
1123
- type: z$1.literal("text"),
1124
- text: z$1.string()
1125
- }).strict(),
1126
- z$1.object({
1127
- type: z$1.literal("reasoning"),
1128
- text: z$1.string()
1129
- }).strict(),
1130
- z$1.object({
1131
- type: z$1.literal("image"),
1132
- attachment: imageAttachmentSchema
1133
- }).strict(),
1134
- z$1.object({
1135
- type: z$1.literal("tool-call"),
1136
- id: z$1.string().min(1),
1137
- name: z$1.string(),
1138
- arguments: z$1.string()
1139
- }).strict(),
1140
- z$1.custom((value) => {
1141
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1142
- const type = value.type;
1143
- return typeof type === "string" && type.length > 0 && !coreContentBlockTypes.has(type);
1144
- })
1145
- ]));
1146
- const teamMemberSnapshotSchema = z$1.object({
1147
- id: sessionIdSchema,
1148
- name: z$1.string(),
1149
- description: z$1.string(),
1150
- provider: z$1.string(),
1151
- context: z$1.enum(["fresh", "fork"]),
1152
- phase: z$1.enum([
1153
- "provisioning",
1154
- "active",
1155
- "failed"
1156
- ]),
1157
- error: z$1.string().optional()
1158
- }).strict();
1159
- const teamTaskSnapshotSchema = z$1.object({
1160
- id: teamTaskIdSchema,
1161
- revision: positiveSafeInteger,
1162
- subject: z$1.string(),
1163
- description: z$1.string(),
1164
- status: z$1.enum([
1165
- "pending",
1166
- "in_progress",
1167
- "completed",
1168
- "deleted"
1169
- ]),
1170
- ownerId: sessionIdSchema.optional(),
1171
- blockedBy: z$1.array(teamTaskIdSchema),
1172
- writeScopes: z$1.array(z$1.string())
1173
- }).strict();
1174
- const teamMessageSnapshotSchema = z$1.object({
1175
- id: teamMessageIdSchema,
1176
- senderId: sessionIdSchema,
1177
- senderName: z$1.string(),
1178
- targetId: sessionIdSchema,
1179
- content: z$1.array(contentBlockSchema)
1180
- }).strict();
1181
- const teamEventSelectorSchema = z$1.object({
1182
- version: nonNegativeSafeInteger,
1183
- teamId: teamIdSchema
1184
- }).loose();
1185
- const teamMemberEventSchema = z$1.object({
1186
- version: z$1.literal(2),
1187
- teamId: teamIdSchema,
1188
- member: teamMemberSnapshotSchema
1189
- }).strict();
1190
- const teamTaskEventSchema = z$1.object({
1191
- version: z$1.literal(2),
1192
- teamId: teamIdSchema,
1193
- task: teamTaskSnapshotSchema
1194
- }).strict();
1195
- const teamMessageQueuedEventSchema = z$1.object({
1196
- version: z$1.literal(2),
1197
- teamId: teamIdSchema,
1198
- message: teamMessageSnapshotSchema
1199
- }).strict();
1200
- const teamMessageDeliveredEventSchema = z$1.object({
1201
- version: z$1.literal(2),
1202
- teamId: teamIdSchema,
1203
- messageId: teamMessageIdSchema,
1204
- targetId: sessionIdSchema
1205
- }).strict();
830
+ //#region lib/types/roster.js
831
+ /** Team membership, continuable-child provisioning, and roster-owned teardown. */
832
+ const MEMBER_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1206
833
  /**
1207
- * Construct empty state for one Team identity.
1208
- * @param rootId - root Session identity.
1209
- * @returns empty Team state.
834
+ * Resolve one active Team member by model-facing name, including the Lead pseudo-row.
835
+ * @param root - exact live Team Lead.
836
+ * @param state - current Team state.
837
+ * @param rawName - candidate member name.
838
+ * @returns resolved durable id and normalized name.
1210
839
  */
1211
- function emptyTeamState(rootId) {
840
+ function resolveActiveMember(root, state, rawName) {
841
+ const name = rawName.trim();
842
+ if (name === "lead") return {
843
+ id: root.id,
844
+ name
845
+ };
846
+ const member = state.members.find((candidate) => candidate.name === name);
847
+ if (member === void 0 || member.phase !== "active") throw new TeamError(`active teammate "${name}" not found`, "TEAM_MEMBER_NOT_FOUND");
1212
848
  return {
1213
- id: TeamId(rootId),
1214
- members: [],
1215
- tasks: [],
1216
- messages: [],
1217
- delivered: [],
1218
- nextTaskNumber: 1
849
+ id: member.id,
850
+ name
1219
851
  };
1220
852
  }
1221
- const teamProjectionEntrySchema = z$1.object({
1222
- id: teamIdSchema,
1223
- members: z$1.array(teamMemberSnapshotSchema),
1224
- tasks: z$1.array(teamTaskSnapshotSchema),
1225
- messages: z$1.array(teamMessageSnapshotSchema),
1226
- delivered: z$1.array(teamMessageIdSchema),
1227
- nextTaskNumber: positiveSafeInteger,
1228
- failure: z$1.string().optional()
1229
- }).strict();
1230
- /**
1231
- * Test whether a Session event belongs to the Team domain.
1232
- * @param event - candidate Session event.
1233
- * @returns whether the event has a Team-owned type.
1234
- */
1235
- function isTeamEvent(event) {
1236
- return event.type === "team/member" || event.type === "team/task" || event.type === "team/message/queued" || event.type === "team/message/delivered";
1237
- }
1238
- /** Decode one persisted Team value and retain the schema failure as its cause. */
1239
- function parsePersisted(type, schema, value) {
1240
- try {
1241
- return schema.parse(value);
1242
- } catch (error) {
1243
- throw new Error(`persisted Agent Teams ${type} payload is invalid`, { cause: error });
1244
- }
1245
- }
1246
- /** Decode the complete current-version payload selected by one Team event type. */
1247
- function parseCurrentTeamEvent(event) {
1248
- switch (event.type) {
1249
- case "team/member": return {
1250
- ...event,
1251
- data: parsePersisted(event.type, teamMemberEventSchema, event.data)
1252
- };
1253
- case "team/task": return {
1254
- ...event,
1255
- data: parsePersisted(event.type, teamTaskEventSchema, event.data)
1256
- };
1257
- case "team/message/queued": return {
1258
- ...event,
1259
- data: parsePersisted(event.type, teamMessageQueuedEventSchema, event.data)
1260
- };
1261
- case "team/message/delivered": return {
1262
- ...event,
1263
- data: parsePersisted(event.type, teamMessageDeliveredEventSchema, event.data)
1264
- };
1265
- /* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
1266
- default: return event;
853
+ /** Owns Team identities and the lifecycle of rostered continuable children. */
854
+ var TeamRoster = class {
855
+ ctx;
856
+ journal;
857
+ lifecycle;
858
+ maxMembers;
859
+ /**
860
+ * @param ctx - Team service context with Agent, Session, persistence, and subagent services.
861
+ * @param journal - authoritative Lead-log transaction owner.
862
+ * @param lifecycle - shared Team runtime admission cutoff.
863
+ * @param maxMembers - maximum immutable roster entries per Team.
864
+ */
865
+ constructor(ctx, journal, lifecycle, maxMembers) {
866
+ this.ctx = ctx;
867
+ this.journal = journal;
868
+ this.lifecycle = lifecycle;
869
+ this.maxMembers = maxMembers;
1267
870
  }
1268
- }
1269
- function applyProjectionEvent(state, event) {
1270
- if (state.failure !== void 0) return state;
1271
- if (!isTeamEvent(event)) return state;
1272
- try {
1273
- const selector = parsePersisted(event.type, teamEventSelectorSchema, event.data);
1274
- if (selector.teamId !== state.id) return state;
1275
- if (selector.version !== 2) throw new Error(`unsupported Agent Teams event version ${String(selector.version)}`);
1276
- return applyCurrentTeamEvent(state, parseCurrentTeamEvent(event));
1277
- } catch (error) {
1278
- /* v8 ignore next -- the owned Team transition throws Error instances. */
1279
- return {
1280
- ...state,
1281
- failure: error instanceof Error ? error.message : String(error)
1282
- };
871
+ /**
872
+ * Resolve one exact live Agent's Team role.
873
+ * @param agent - exact live Agent used as the authority credential.
874
+ * @returns its root, Team identity, role, and model-facing name.
875
+ */
876
+ membership(agent) {
877
+ const membership = this.tryMembership(agent);
878
+ if (membership === void 0) throw new TeamError(`agent "${agent.id}" is not a member of an active Agent Team`, "TEAM_NOT_MEMBER");
879
+ return membership;
1283
880
  }
1284
- }
1285
- function replaceAt(items, index, item) {
1286
- const next = [...items];
1287
- if (index < 0) next.push(item);
1288
- else next[index] = item;
1289
- return next;
1290
- }
1291
- function applyCurrentTeamEvent(state, event) {
1292
- switch (event.type) {
1293
- case "team/member": {
1294
- const member = event.data.member;
1295
- const index = state.members.findIndex((candidate) => candidate.id === member.id);
1296
- const prior = state.members[index];
1297
- const named = state.members.find((candidate) => candidate.name === member.name);
1298
- if (named !== void 0 && named.id !== member.id) throw new Error(`teammate name "${member.name}" is reused by another member`);
1299
- if (prior === void 0) {
1300
- if (member.phase !== "provisioning") throw new Error(`teammate "${member.name}" must begin provisioning`);
1301
- } else {
1302
- if (prior.name !== member.name || prior.provider !== member.provider || prior.context !== member.context) throw new Error(`teammate "${member.id}" changed immutable identity fields`);
1303
- if (prior.phase !== "provisioning" || member.phase === "provisioning") throw new Error(`teammate "${member.name}" has an invalid ${prior.phase} -> ${member.phase} transition`);
881
+ /**
882
+ * Resolve a caller without throwing for scoped installation and lifecycle observers.
883
+ * @param agent - candidate exact live Agent.
884
+ * @returns Team membership, or undefined for non-Team subagents and stale identities.
885
+ */
886
+ tryMembership(agent) {
887
+ if (this.ctx.agents.get(agent.id) !== agent) return void 0;
888
+ try {
889
+ const parentId = agent.session.header.parentSession;
890
+ if (parentId !== void 0) {
891
+ const root = this.ctx.agents.get(parentId);
892
+ if (root !== void 0) {
893
+ const member = this.journal.state(root).members.find((candidate) => candidate.id === agent.id);
894
+ if (member?.phase === "active" || member?.phase === "provisioning") return {
895
+ root,
896
+ id: TeamId(root.id),
897
+ role: "teammate",
898
+ name: member.name
899
+ };
900
+ if (this.subagentDescriptor(agent)) return void 0;
901
+ return {
902
+ root: agent,
903
+ id: TeamId(agent.id),
904
+ role: "lead",
905
+ name: "lead"
906
+ };
907
+ }
1304
908
  }
909
+ if (this.subagentDescriptor(agent)) return void 0;
1305
910
  return {
1306
- ...state,
1307
- members: replaceAt(state.members, index, member)
911
+ root: agent,
912
+ id: TeamId(agent.id),
913
+ role: "lead",
914
+ name: "lead"
1308
915
  };
916
+ } catch {
917
+ return;
1309
918
  }
1310
- case "team/task": {
1311
- const task = event.data.task;
1312
- const index = state.tasks.findIndex((candidate) => candidate.id === task.id);
1313
- const prior = state.tasks[index];
1314
- if (prior === void 0 && task.revision !== 1) throw new Error(`team task "${task.id}" must begin at revision 1`);
1315
- if (prior !== void 0 && task.revision !== prior.revision + 1) throw new Error(`team task "${task.id}" revision is not contiguous`);
1316
- assertTaskGraphCandidate(state.tasks, task);
1317
- let nextTaskNumber = state.nextTaskNumber;
1318
- const match = numericTaskIdPattern.exec(task.id);
1319
- if (match !== null) {
1320
- const number = Number(match[1]);
1321
- nextTaskNumber = Math.max(nextTaskNumber, number === Number.MAX_SAFE_INTEGER ? number : number + 1);
919
+ }
920
+ /**
921
+ * List the runtime-enriched roster visible to one Team member.
922
+ * @param membership - exact caller membership resolved by this roster.
923
+ * @returns Lead and teammate rows in creation order.
924
+ */
925
+ list(membership) {
926
+ const { root } = membership;
927
+ const state = this.journal.state(root);
928
+ const result = [{
929
+ id: root.id,
930
+ name: "lead",
931
+ role: "lead",
932
+ status: availability(root),
933
+ ...root.options.model === void 0 ? {} : { model: root.options.model },
934
+ diagnostics: []
935
+ }];
936
+ for (const member of state.members) {
937
+ const live = this.ctx.agents.get(member.id);
938
+ const model = live?.options.model ?? root.options.model;
939
+ result.push({
940
+ id: member.id,
941
+ name: member.name,
942
+ role: "teammate",
943
+ status: member.phase === "failed" ? "failed" : member.phase === "provisioning" ? "provisioning" : availability(live),
944
+ description: member.description,
945
+ provider: member.provider,
946
+ context: member.context,
947
+ ...model === void 0 ? {} : { model },
948
+ diagnostics: member.error === void 0 ? [] : [member.error]
949
+ });
950
+ }
951
+ return result;
952
+ }
953
+ /**
954
+ * Create one named, continuable direct child of the Team Lead.
955
+ * @param caller - exact live Lead Agent.
956
+ * @param request - immutable name, description, prompt, context mode, provider, and cancellation.
957
+ * @returns the active roster row.
958
+ */
959
+ async spawn(caller, request) {
960
+ if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
961
+ return await this.lifecycle.track(this.spawnAdmitted(caller, request));
962
+ }
963
+ /**
964
+ * Reconcile provisioning state when one Team member Session starts.
965
+ * @param agent - newly started exact live Agent.
966
+ * @param signal - shared runtime cancellation.
967
+ */
968
+ async recoverFor(agent, signal) {
969
+ signal.throwIfAborted();
970
+ const membership = this.tryMembership(agent);
971
+ if (membership?.role === "lead") await this.reconcileProvisioning(membership.root, signal);
972
+ }
973
+ /**
974
+ * Interrupt one live teammate turn without clearing its pending inbox.
975
+ * @param caller - exact live Lead Agent.
976
+ * @param targetName - durable teammate name.
977
+ * @returns the target status sampled before cancellation.
978
+ */
979
+ interrupt(caller, targetName) {
980
+ const membership = this.membership(caller);
981
+ if (membership.role !== "lead") throw new TeamError("only the Team Lead can interrupt teammates", "TEAM_LEAD_REQUIRED");
982
+ const state = this.journal.state(membership.root);
983
+ const target = resolveActiveMember(membership.root, state, targetName);
984
+ if (target.id === membership.root.id) throw new TeamError("the Team Lead cannot interrupt itself", "TEAM_INVALID_TARGET");
985
+ const live = this.ctx.agents.get(target.id);
986
+ if (live === void 0) return { previousStatus: "inactive" };
987
+ const previousStatus = availability(live);
988
+ this.ctx.subagents.interrupt(target.id, {
989
+ kind: "ancestor",
990
+ agent: caller
991
+ });
992
+ return { previousStatus };
993
+ }
994
+ /**
995
+ * Group exact live roster children by their current Lead for runtime teardown.
996
+ * @returns each live Lead and the roster child ids currently in the Agent registry.
997
+ */
998
+ liveChildrenByRoot() {
999
+ const teams = /* @__PURE__ */ new Map();
1000
+ for (const agent of this.ctx.agents.list()) {
1001
+ const rootId = agent.session.header.parentSession;
1002
+ if (rootId === void 0) continue;
1003
+ const root = this.ctx.agents.get(rootId);
1004
+ if (root === void 0 || !this.journal.state(root).members.some((member) => member.id === agent.id)) continue;
1005
+ const children = teams.get(root) ?? [];
1006
+ children.push(agent.id);
1007
+ teams.set(root, children);
1008
+ }
1009
+ return teams;
1010
+ }
1011
+ /**
1012
+ * Release exact teammate Activations through the continuation lifecycle owner.
1013
+ * @param root - exact live Team Lead authorizing release.
1014
+ * @param childIds - selected roster child ids.
1015
+ */
1016
+ async stopTeammates(root, childIds) {
1017
+ await this.lifecycle.withTimeout(this.ctx.subagents.drainChildren(root, childIds));
1018
+ }
1019
+ /** Perform one creation admitted before the Team runtime disposal cutoff. */
1020
+ async spawnAdmitted(caller, request) {
1021
+ const membership = this.membership(caller);
1022
+ if (membership.role !== "lead") throw new TeamError("only the Team Lead can create teammates", "TEAM_LEAD_REQUIRED");
1023
+ const signal = AbortSignal.any([request.signal, this.lifecycle.signal]);
1024
+ signal.throwIfAborted();
1025
+ const root = membership.root;
1026
+ const name = this.memberName(request.name);
1027
+ const description = requiredText(request.description, "description", 200);
1028
+ const childId = brandString(randomUUID());
1029
+ const member = {
1030
+ id: childId,
1031
+ name,
1032
+ description,
1033
+ provider: requiredText(request.provider, "provider", 200),
1034
+ context: request.context,
1035
+ phase: "provisioning"
1036
+ };
1037
+ await this.journal.transact(root.id, async () => {
1038
+ const state = this.journal.state(root);
1039
+ if (state.members.some((member) => member.name === name)) throw new TeamError(`teammate name "${name}" was already used in this Team`, "TEAM_MEMBER_NAME_TAKEN");
1040
+ if (state.members.length >= this.maxMembers) throw new TeamError(`Team member limit ${this.maxMembers} reached`, "TEAM_MEMBER_LIMIT");
1041
+ await this.journal.appendAndFlush(root, "team/member", {
1042
+ version: 2,
1043
+ teamId: TeamId(root.id),
1044
+ member
1045
+ });
1046
+ });
1047
+ let started;
1048
+ try {
1049
+ started = await this.ctx.subagents.startActivation({
1050
+ childId,
1051
+ provider: request.provider,
1052
+ label: description,
1053
+ request: {
1054
+ prompt: request.prompt,
1055
+ parent: root
1056
+ },
1057
+ signal,
1058
+ delivery: "parent"
1059
+ });
1060
+ if (started.messageId === void 0) throw new TeamError("the spawned teammate accepted no initial prompt", "TEAM_PROMPT_NOT_ACCEPTED");
1061
+ await this.checkpointInitialPrompt(childId, started.messageId, signal);
1062
+ } catch (error) {
1063
+ const failed = {
1064
+ ...member,
1065
+ phase: "failed",
1066
+ error: errorMessage(error)
1067
+ };
1068
+ try {
1069
+ const phase = await this.settleProvisioning(root, failed);
1070
+ await this.stopTeammates(root, [childId]);
1071
+ if (phase === "active") throw new TeamError(`teammate "${name}" became active while its creator reported failure`, "TEAM_PROVISIONING_CONFLICT", { cause: error });
1072
+ } catch (recordError) {
1073
+ throw new AggregateError([error, recordError], "teammate creation and durable failure recording both failed");
1074
+ }
1075
+ throw error;
1076
+ }
1077
+ const active = {
1078
+ ...member,
1079
+ phase: "active"
1080
+ };
1081
+ if (await this.settleProvisioning(root, active) === "failed") {
1082
+ const conflict = new TeamError(`teammate "${name}" was reconciled as failed while creation was in progress`, "TEAM_PROVISIONING_CONFLICT");
1083
+ try {
1084
+ await this.stopTeammates(root, [childId]);
1085
+ } catch (cleanupError) {
1086
+ /* v8 ignore next -- requires the independently tested HMR settlement conflict and cleanup failure together. */
1087
+ throw new AggregateError([conflict, cleanupError], "provisioning conflict cleanup failed");
1322
1088
  }
1323
- return {
1324
- ...state,
1325
- tasks: replaceAt(state.tasks, index, task),
1326
- nextTaskNumber
1327
- };
1089
+ throw conflict;
1328
1090
  }
1329
- case "team/message/queued": {
1330
- const message = event.data.message;
1331
- if (state.messages.some((candidate) => candidate.id === message.id)) throw new Error(`team message "${message.id}" was queued twice`);
1332
- return {
1333
- ...state,
1334
- messages: [...state.messages, message]
1091
+ return { member: this.memberView(active) };
1092
+ }
1093
+ /** Flush the accepted initial inbox item before the Lead can commit `active`. */
1094
+ async checkpointInitialPrompt(childId, messageId, signal) {
1095
+ while (true) {
1096
+ signal.throwIfAborted();
1097
+ const session = this.ctx.sessions.get(childId);
1098
+ if (session === void 0) {
1099
+ const stored = await readPersistedSession(this.ctx.sessionPersistence, childId, signal);
1100
+ if (messageAccepted(stored.events.slice(stored.inheritedEventCount), (message) => message.id === messageId)) return;
1101
+ throw new TeamError(`teammate "${childId}" initial prompt was not durably accepted`, "TEAM_PROVISIONING_CONFLICT");
1102
+ }
1103
+ const progress = Promise.withResolvers();
1104
+ progress.promise.catch(() => void 0);
1105
+ const stopEvent = this.ctx.on("session/event", (candidate) => {
1106
+ if (candidate === session) progress.resolve();
1107
+ });
1108
+ const stopDisposed = this.ctx.on("session/disposed", (candidate) => {
1109
+ if (candidate === session) progress.resolve();
1110
+ });
1111
+ const onAbort = () => {
1112
+ const reason = signal.reason;
1113
+ progress.reject(reason instanceof Error ? reason : new TeamError(`teammate creation aborted: ${errorMessage(reason)}`, "TEAM_DISPOSED"));
1335
1114
  };
1115
+ signal.addEventListener("abort", onAbort, { once: true });
1116
+ try {
1117
+ signal.throwIfAborted();
1118
+ await this.ctx.sessions.flush(session);
1119
+ if (messageAccepted(session.snapshotEvents(session.inheritedEventCount), (message) => message.id === messageId)) return;
1120
+ if (this.ctx.sessions.get(childId) !== session) continue;
1121
+ await progress.promise;
1122
+ } finally {
1123
+ signal.removeEventListener("abort", onAbort);
1124
+ stopDisposed();
1125
+ stopEvent();
1126
+ }
1336
1127
  }
1337
- case "team/message/delivered": {
1338
- const queued = state.messages.find((message) => message.id === event.data.messageId);
1339
- if (queued === void 0) throw new Error(`team message "${event.data.messageId}" was delivered before queueing`);
1340
- if (queued.targetId !== event.data.targetId) throw new Error(`team message "${event.data.messageId}" target changed`);
1341
- if (state.delivered.includes(event.data.messageId)) throw new Error(`team message "${event.data.messageId}" was delivered twice`);
1342
- return {
1343
- ...state,
1344
- delivered: [...state.delivered, event.data.messageId]
1345
- };
1128
+ }
1129
+ /** Settle provisioning-only members from their independently durable child Sessions. */
1130
+ async reconcileProvisioning(root, signal) {
1131
+ const provisioning = this.journal.state(root).members.filter((member) => member.phase === "provisioning");
1132
+ for (const member of provisioning) {
1133
+ signal.throwIfAborted();
1134
+ if (this.ctx.agents.get(member.id) !== void 0) continue;
1135
+ let phase = "failed";
1136
+ let failure = "provisioning did not leave a resumable child Session";
1137
+ try {
1138
+ const loaded = await readPersistedSession(this.ctx.sessionPersistence, member.id, signal);
1139
+ const suffix = loaded.events.slice(loaded.inheritedEventCount);
1140
+ const descriptor = foldSubagentDescriptor(suffix);
1141
+ const acceptedInitialPrompt = messageAccepted(suffix, (message) => message.source.kind === "user");
1142
+ if (loaded.header.parentSession === root.id && descriptor?.mode === "continuable" && descriptor.provider === member.provider && acceptedInitialPrompt) phase = "active";
1143
+ else failure = "persisted child Session does not match the provisioned continuation";
1144
+ } catch (error) {
1145
+ failure = `child Session recovery failed: ${errorMessage(error)}`;
1146
+ }
1147
+ signal.throwIfAborted();
1148
+ await this.journal.transact(root.id, async () => {
1149
+ signal.throwIfAborted();
1150
+ const current = this.journal.state(root).members.find((candidate) => candidate.id === member.id);
1151
+ if (current?.phase !== "provisioning") return;
1152
+ const settled = {
1153
+ ...current,
1154
+ phase,
1155
+ ...phase === "failed" ? { error: failure } : {}
1156
+ };
1157
+ await this.journal.appendAndFlush(root, "team/member", {
1158
+ version: 2,
1159
+ teamId: TeamId(root.id),
1160
+ member: settled
1161
+ });
1162
+ });
1346
1163
  }
1347
- /* v8 ignore next 2 -- TeamEventType is closed and every member is handled above. */
1348
- default: return state;
1349
1164
  }
1350
- }
1351
- const teamMemberProjectionSchema = z$1.object({
1352
- id: sessionIdSchema,
1353
- name: z$1.string(),
1354
- role: z$1.enum(["lead", "teammate"]),
1355
- phase: z$1.enum([
1356
- "provisioning",
1357
- "active",
1358
- "failed"
1359
- ]),
1360
- error: z$1.string().optional()
1361
- }).strict();
1362
- const teamTaskViewSchema = z$1.object({
1363
- id: teamTaskIdSchema,
1364
- revision: positiveSafeInteger,
1365
- subject: z$1.string(),
1366
- description: z$1.string(),
1367
- status: z$1.enum([
1368
- "pending",
1369
- "in_progress",
1370
- "completed",
1371
- "deleted"
1372
- ]),
1373
- blockedBy: z$1.array(teamTaskIdSchema),
1374
- writeScopes: z$1.array(z$1.string()),
1375
- ownerName: z$1.string().optional(),
1376
- ready: z$1.boolean(),
1377
- writeScopeWarnings: z$1.array(z$1.string())
1378
- }).strict();
1379
- const teamProjectionSchema = z$1.object({
1380
- members: z$1.array(teamMemberProjectionSchema),
1381
- tasks: z$1.array(teamTaskViewSchema),
1382
- failure: z$1.string().optional()
1383
- }).strict();
1384
- /** Client views keyed by the member and task collections they were derived from. */
1385
- const teamProjectionViews = /* @__PURE__ */ new WeakMap();
1386
- function buildTeamProjection(state) {
1387
- const members = [{
1388
- id: brandString(state.id),
1389
- name: "lead",
1390
- role: "lead",
1391
- phase: "active"
1392
- }];
1393
- for (const member of state.members) members.push({
1394
- id: member.id,
1395
- name: member.name,
1396
- role: "teammate",
1397
- phase: member.phase,
1398
- ...member.error === void 0 ? {} : { error: member.error }
1399
- });
1400
- return {
1401
- members,
1402
- tasks: state.tasks.filter((task) => task.status !== "deleted").map((task) => projectTaskView(state, task)),
1403
- ...state.failure === void 0 ? {} : { failure: state.failure }
1404
- };
1405
- }
1406
- /**
1407
- * Durable client view of one Team state. Mailbox-only state changes reuse the
1408
- * previous view reference, so the live drive publishes nothing for them.
1409
- * A failure is terminal: later events retain the failed state reference and
1410
- * do not republish its view.
1411
- * @param state - current Team state.
1412
- * @returns the roster and non-deleted task board, plus any projection failure.
1413
- */
1414
- function teamProjectionView(state) {
1415
- if (state.failure !== void 0) return buildTeamProjection(state);
1416
- let byTasks = teamProjectionViews.get(state.members);
1417
- if (byTasks === void 0) {
1418
- byTasks = /* @__PURE__ */ new WeakMap();
1419
- teamProjectionViews.set(state.members, byTasks);
1165
+ /** Build one runtime member row after successful creation. */
1166
+ memberView(member) {
1167
+ const live = this.ctx.agents.get(member.id);
1168
+ return {
1169
+ id: member.id,
1170
+ name: member.name,
1171
+ role: "teammate",
1172
+ status: availability(live),
1173
+ description: member.description,
1174
+ provider: member.provider,
1175
+ context: member.context,
1176
+ ...live?.options.model === void 0 ? {} : { model: live.options.model },
1177
+ diagnostics: []
1178
+ };
1420
1179
  }
1421
- let view = byTasks.get(state.tasks);
1422
- if (view === void 0) {
1423
- view = buildTeamProjection(state);
1424
- byTasks.set(state.tasks, view);
1180
+ /** Validate a never-reused model-facing teammate name. */
1181
+ memberName(value) {
1182
+ if (!MEMBER_NAME.test(value) || value.length > 64 || value === "lead") throw new TeamError("teammate name must be lower-kebab-case, at most 64 characters, and not \"lead\"", "TEAM_INVALID_MEMBER_NAME");
1183
+ return value;
1425
1184
  }
1426
- return view;
1427
- }
1428
- /** Team projection selected by the projected Session identity; the wire view carries durable roster and task state only. */
1429
- const teamProjectionDefinition = {
1430
- key: "agentTeam",
1431
- stateVersion: 4,
1432
- stateSchema: teamProjectionEntrySchema,
1433
- init: (header) => emptyTeamState(header.id),
1434
- apply: applyProjectionEvent,
1435
- wire: {
1436
- viewSchema: teamProjectionSchema,
1437
- view: teamProjectionView
1185
+ /** Append one terminal provisioning edge unless recovery already settled it. */
1186
+ async settleProvisioning(root, terminal) {
1187
+ return this.journal.transact(root.id, async () => {
1188
+ const current = this.journal.state(root).members.find((member) => member.id === terminal.id);
1189
+ /* v8 ignore next 3 -- the append-only provisioning event is committed by this operation before settlement. */
1190
+ if (current === void 0) throw new TeamError(`provisioned teammate "${terminal.id}" disappeared`, "TEAM_PROVISIONING_CONFLICT");
1191
+ if (current.phase !== "provisioning") return current.phase;
1192
+ await this.journal.appendAndFlush(root, "team/member", {
1193
+ version: 2,
1194
+ teamId: TeamId(root.id),
1195
+ member: terminal
1196
+ });
1197
+ return terminal.phase === "active" ? "active" : "failed";
1198
+ });
1199
+ }
1200
+ /** Whether a Session's own suffix identifies a provider-owned subagent child. */
1201
+ subagentDescriptor(agent) {
1202
+ return foldSubagentDescriptor(agent.session.snapshotEvents(agent.session.inheritedEventCount)) !== void 0;
1438
1203
  }
1439
1204
  };
1440
- //#endregion
1441
- //#region lib/types/remote.js
1442
- /** Wire failure mapping for the Team service's browser write face. */
1443
- /**
1444
- * Map one Team-domain failure onto the shared Remote failure vocabulary so the
1445
- * browser can branch on a stable code. Unknown values pass through unchanged
1446
- * and surface as `gateway/internal`.
1447
- * @param error - the value thrown by the Team domain operation.
1448
- * @returns the value to rethrow from the wire method.
1449
- */
1450
- function teamWireError(error) {
1451
- if (!(error instanceof TeamError)) return error;
1452
- if (error.code === "TEAM_TASK_STALE_REVISION") return new RemoteError("agent-team/stale-revision", error.message, {});
1453
- if (error.code === "TEAM_NOT_MEMBER") return new RemoteError("agent-team/not-a-member", error.message, {});
1454
- return new RemoteError("agent-team/rejected", error.message, { code: error.code });
1205
+ /** Turn availability is independent of whether the Agent is loaded. */
1206
+ function availability(agent) {
1207
+ return agent?.status === "running" ? "running" : "inactive";
1455
1208
  }
1456
1209
  //#endregion
1457
1210
  //#region lib/types/task-board.js
@@ -1681,7 +1434,7 @@ var TeamTaskBoard = class {
1681
1434
  };
1682
1435
  //#endregion
1683
1436
  //#region lib/types/index.js
1684
- /** Agent Teams service façade over roster, mailbox, task, and runtime lifecycle owners. */
1437
+ /** Agent Teams service façade over roster, messaging, task, and runtime lifecycle owners. */
1685
1438
  var __runInitializers = function(thisArg, initializers, value) {
1686
1439
  var useValue = arguments.length > 2;
1687
1440
  for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
@@ -1722,7 +1475,6 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
1722
1475
  };
1723
1476
  const DEFAULT_MAX_MEMBERS = 16;
1724
1477
  const DEFAULT_MAX_TASKS = 256;
1725
- const DEFAULT_MAX_PENDING_MESSAGES = 64;
1726
1478
  const DEFAULT_MAX_MESSAGE_BYTES = 65536;
1727
1479
  const DEFAULT_DISPOSAL_TIMEOUT_MS = 5e3;
1728
1480
  /** Validate one positive safe-integer deployment limit. */
@@ -1785,7 +1537,6 @@ let TeamService = (() => {
1785
1537
  static Config = z.object({
1786
1538
  maxMembers: z.number().step(1).min(1).default(DEFAULT_MAX_MEMBERS),
1787
1539
  maxTasks: z.number().step(1).min(1).default(DEFAULT_MAX_TASKS),
1788
- maxPendingMessagesPerMember: z.number().step(1).min(1).default(DEFAULT_MAX_PENDING_MESSAGES),
1789
1540
  maxMessageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_BYTES),
1790
1541
  disposalTimeoutMs: z.number().step(1).min(1).default(DEFAULT_DISPOSAL_TIMEOUT_MS)
1791
1542
  });
@@ -1795,14 +1546,12 @@ let TeamService = (() => {
1795
1546
  lifecycle;
1796
1547
  journal;
1797
1548
  roster;
1798
- mailbox;
1799
1549
  tasks;
1800
1550
  constructor(ctx, config = {}) {
1801
1551
  super(ctx, "agentTeams");
1802
1552
  this.config = {
1803
1553
  maxMembers: positiveLimit("maxMembers", config.maxMembers ?? DEFAULT_MAX_MEMBERS),
1804
1554
  maxTasks: positiveLimit("maxTasks", config.maxTasks ?? DEFAULT_MAX_TASKS),
1805
- maxPendingMessagesPerMember: positiveLimit("maxPendingMessagesPerMember", config.maxPendingMessagesPerMember ?? DEFAULT_MAX_PENDING_MESSAGES),
1806
1555
  maxMessageBytes: positiveLimit("maxMessageBytes", config.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES),
1807
1556
  disposalTimeoutMs: positiveLimit("disposalTimeoutMs", config.disposalTimeoutMs ?? DEFAULT_DISPOSAL_TIMEOUT_MS)
1808
1557
  };
@@ -1812,11 +1561,7 @@ let TeamService = (() => {
1812
1561
  this.activity.notify(TeamId(root.id));
1813
1562
  });
1814
1563
  this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers);
1815
- this.mailbox = new TeamMailbox(ctx, this.journal, this.roster, this.lifecycle, this.config.maxPendingMessagesPerMember, this.config.maxMessageBytes);
1816
1564
  this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks);
1817
- ctx.on("session/event", (session, event) => {
1818
- this.mailbox.observeSessionEvent(session, event);
1819
- });
1820
1565
  ctx.on("agent/created", ({ agent }) => {
1821
1566
  this.scheduleRecovery(agent);
1822
1567
  });
@@ -1862,13 +1607,50 @@ let TeamService = (() => {
1862
1607
  return await this.roster.spawn(caller, request);
1863
1608
  }
1864
1609
  /**
1865
- * Queue one durable peer message, then attempt immediate delivery.
1610
+ * Steer one peer message into the target inbox or reject the attempt.
1866
1611
  * @param caller - exact live sending Team member.
1867
- * @param request - target name, content, and pre-queue cancellation.
1868
- * @returns durable message identity and immediate-delivery observation.
1612
+ * @param request - target name, content, and cancellation before acceptance.
1613
+ * @returns accepted inbox identity; acceptance follows normal Agent persistence and does not await model processing.
1869
1614
  */
1870
1615
  async sendMessage(caller, request) {
1871
- return await this.mailbox.send(caller, request);
1616
+ if (this.lifecycle.disposed) throw new TeamError("Agent Teams service is disposing", "TEAM_DISPOSED");
1617
+ return await this.lifecycle.track(this.sendAdmitted(caller, {
1618
+ ...request,
1619
+ signal: AbortSignal.any([request.signal, this.lifecycle.signal])
1620
+ }));
1621
+ }
1622
+ /**
1623
+ * Admit one peer message and steer it into the target's inbox.
1624
+ * @param caller - exact live sending Team member.
1625
+ * @param request - target name, content, and signal already joined with runtime cancellation.
1626
+ * @returns the durable message identity carried by the delivered source.
1627
+ */
1628
+ async sendAdmitted(caller, request) {
1629
+ const membership = this.roster.membership(caller);
1630
+ request.signal.throwIfAborted();
1631
+ const { root } = membership;
1632
+ const target = resolveActiveMember(root, this.journal.state(root), request.target);
1633
+ if (target.id === caller.id) throw new TeamError("a Team member cannot message itself", "TEAM_SELF_MESSAGE");
1634
+ const messageId = TeamMessageId(`team-message-${randomUUID()}`);
1635
+ const source = {
1636
+ kind: "team-message",
1637
+ teamId: TeamId(root.id),
1638
+ messageId,
1639
+ senderId: caller.id,
1640
+ senderName: membership.name
1641
+ };
1642
+ const content = [{
1643
+ type: "text",
1644
+ text: `Team message ${messageId} from ${membership.name}:`
1645
+ }, ...structuredClone(request.content)];
1646
+ if (Buffer.byteLength(JSON.stringify(content), "utf8") > this.config.maxMessageBytes) throw new TeamError(`team message exceeds ${this.config.maxMessageBytes} bytes`, "TEAM_MESSAGE_TOO_LARGE");
1647
+ if (target.id === root.id) root.steer(createUserMessage({
1648
+ content,
1649
+ source
1650
+ }));
1651
+ else await steerHostSubagentPrompt(this.ctx.subagents, root, target.id, content, source, request.signal);
1652
+ this.activity.notify(membership.id);
1653
+ return { messageId };
1872
1654
  }
1873
1655
  /**
1874
1656
  * Create one unowned pending task in the Team Lead log.
@@ -1977,18 +1759,16 @@ let TeamService = (() => {
1977
1759
  });
1978
1760
  });
1979
1761
  }
1980
- /** Reconcile roster provisioning before retrying that member's pending mailbox. */
1762
+ /** Reconcile roster provisioning when one Team member Session starts. */
1981
1763
  async recoverFor(agent) {
1982
1764
  await this.roster.recoverFor(agent, this.lifecycle.signal);
1983
- await this.mailbox.recoverFor(agent, this.lifecycle.signal);
1984
1765
  }
1985
1766
  /** Stop Team-owned live branches and release every waiter before service disposal completes. */
1986
1767
  async disposeRuntime() {
1987
1768
  this.lifecycle.close();
1988
1769
  this.activity.close();
1989
1770
  const failures = [];
1990
- await this.lifecycle.settle(this.roster.pendingCreations(), failures);
1991
- await this.lifecycle.settle(this.mailbox.pendingDispatches(), failures);
1771
+ await this.lifecycle.settle(this.lifecycle.pending(), failures);
1992
1772
  for (const [root, childIds] of this.roster.liveChildrenByRoot()) try {
1993
1773
  await this.roster.stopTeammates(root, childIds);
1994
1774
  } catch (error) {