@ours.network/fleet 1.1.0-nightly.3 → 1.1.0-nightly.30

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.
Files changed (208) hide show
  1. package/README.md +244 -40
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +4 -4
  8. package/dist/application/role-creation-service.js +19 -1
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +93 -40
  14. package/dist/application/task-room-service.js +579 -173
  15. package/dist/briefing.js +85 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +217 -51
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +38 -3
  22. package/dist/config.js +240 -28
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +7 -4
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +213 -42
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +181 -0
  32. package/dist/fleet-command-audit.js +738 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/harness/agent-session.d.ts +5 -0
  35. package/dist/harness/claude-code-session.d.ts +2 -0
  36. package/dist/harness/claude-code-session.js +6 -1
  37. package/dist/harness/claude-code.js +2 -0
  38. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  39. package/dist/harness/codex-app-server-proxy.js +348 -14
  40. package/dist/harness/codex-session.d.ts +11 -2
  41. package/dist/harness/codex-session.js +22 -2
  42. package/dist/harness/codex.d.ts +5 -1
  43. package/dist/harness/codex.js +110 -10
  44. package/dist/harness/types.d.ts +1 -1
  45. package/dist/index.d.ts +4 -3
  46. package/dist/index.js +3 -2
  47. package/dist/init-guidance.d.ts +1 -1
  48. package/dist/init-guidance.js +4 -3
  49. package/dist/init-wizard.d.ts +98 -0
  50. package/dist/init-wizard.js +631 -0
  51. package/dist/lifecycle-summary.d.ts +134 -0
  52. package/dist/lifecycle-summary.js +179 -0
  53. package/dist/loops/config.d.ts +10 -1
  54. package/dist/loops/config.js +38 -5
  55. package/dist/model-recovery.d.ts +1 -1
  56. package/dist/ops.js +2 -17
  57. package/dist/owner-channel/channel.d.ts +75 -3
  58. package/dist/owner-channel/channel.js +531 -123
  59. package/dist/owner-channel/commands.d.ts +28 -6
  60. package/dist/owner-channel/commands.js +112 -36
  61. package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
  62. package/dist/owner-channel/lifecycle-outbox.js +87 -0
  63. package/dist/owner-channel/ours-client.d.ts +19 -0
  64. package/dist/owner-channel/ours-client.js +3 -0
  65. package/dist/owner-channel/state.d.ts +2 -0
  66. package/dist/owner-channel/state.js +8 -2
  67. package/dist/permissions.js +1 -1
  68. package/dist/preset-bootstrap.d.ts +10 -0
  69. package/dist/preset-bootstrap.js +85 -0
  70. package/dist/preset-migration.d.ts +42 -0
  71. package/dist/preset-migration.js +450 -0
  72. package/dist/reports/artifact.d.ts +10 -0
  73. package/dist/reports/artifact.js +32 -0
  74. package/dist/reports/index.d.ts +5 -0
  75. package/dist/reports/index.js +5 -0
  76. package/dist/reports/render.d.ts +6 -0
  77. package/dist/reports/render.js +240 -0
  78. package/dist/reports/service.d.ts +42 -0
  79. package/dist/reports/service.js +57 -0
  80. package/dist/reports/tasks.d.ts +52 -0
  81. package/dist/reports/tasks.js +177 -0
  82. package/dist/reports/types.d.ts +315 -0
  83. package/dist/reports/types.js +6 -0
  84. package/dist/resolved-plan.js +13 -1
  85. package/dist/rooms-tasks/cli.d.ts +2 -0
  86. package/dist/rooms-tasks/cli.js +519 -261
  87. package/dist/rooms-tasks/close.d.ts +9 -1
  88. package/dist/rooms-tasks/close.js +23 -7
  89. package/dist/rooms-tasks/config.js +16 -29
  90. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  91. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  92. package/dist/rooms-tasks/deletion.d.ts +51 -0
  93. package/dist/rooms-tasks/deletion.js +217 -0
  94. package/dist/rooms-tasks/external-worker.d.ts +9 -0
  95. package/dist/rooms-tasks/external-worker.js +52 -9
  96. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  97. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  98. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  99. package/dist/rooms-tasks/member-overrides.js +204 -0
  100. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  101. package/dist/rooms-tasks/member-startup.js +9 -2
  102. package/dist/rooms-tasks/provision.js +146 -56
  103. package/dist/rooms-tasks/room-state.d.ts +1 -0
  104. package/dist/rooms-tasks/room-state.js +12 -1
  105. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  106. package/dist/rooms-tasks/task-room-name.js +61 -0
  107. package/dist/rooms-tasks/task-state.d.ts +115 -3
  108. package/dist/rooms-tasks/task-state.js +361 -11
  109. package/dist/rooms-tasks/templates.d.ts +4 -2
  110. package/dist/rooms-tasks/templates.js +42 -78
  111. package/dist/rooms-tasks/terminal.d.ts +3 -0
  112. package/dist/rooms-tasks/terminal.js +10 -4
  113. package/dist/rooms-tasks/types.d.ts +94 -8
  114. package/dist/rooms-tasks/types.js +14 -1
  115. package/dist/runner.d.ts +12 -2
  116. package/dist/runner.js +167 -56
  117. package/dist/session/acp.d.ts +9 -1
  118. package/dist/session/acp.js +35 -9
  119. package/dist/session/arbiter.d.ts +2 -1
  120. package/dist/session/arbiter.js +2 -0
  121. package/dist/session/codex-app-server-transport.d.ts +53 -0
  122. package/dist/session/codex-app-server-transport.js +184 -0
  123. package/dist/session/codex-app-server.d.ts +122 -0
  124. package/dist/session/codex-app-server.js +1031 -0
  125. package/dist/session/control.d.ts +14 -1
  126. package/dist/session/control.js +51 -0
  127. package/dist/session/conversation-types.d.ts +4 -4
  128. package/dist/session/types.d.ts +24 -6
  129. package/dist/session/types.js +28 -0
  130. package/dist/spawn.d.ts +9 -0
  131. package/dist/spawn.js +77 -9
  132. package/dist/temp-lifecycle.d.ts +1 -1
  133. package/dist/web/fleet-config-service.d.ts +1 -0
  134. package/dist/web/fleet-config-service.js +27 -8
  135. package/dist/web/runtime.js +11 -6
  136. package/dist/web/server.js +28 -1
  137. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  138. package/dist/web-app/index.html +1 -1
  139. package/package.json +5 -4
  140. package/presets/brain-catalog.json +17 -0
  141. package/presets/fleet/agent_templates/Critic.yaml +5 -0
  142. package/presets/fleet/agent_templates/Developer.yaml +5 -0
  143. package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
  144. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  145. package/presets/fleet/brains/claude-default.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  150. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  155. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  160. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  193. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  194. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
  195. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
  196. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
  197. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
  198. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
  199. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
  200. package/presets/fleet/roles/Coordinator.yaml +30 -0
  201. package/presets/fleet/roles/Critic.yaml +28 -0
  202. package/presets/fleet/roles/Developer.yaml +28 -0
  203. package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
  204. package/presets/fleet/room_templates/pair.yaml +13 -0
  205. package/presets/fleet/room_templates/single.yaml +9 -0
  206. package/presets/fleet/room_templates/team.yaml +16 -0
  207. package/presets/fleet.yaml +7 -0
  208. package/presets/manifest.json +6 -0
package/dist/runner.js CHANGED
@@ -6,13 +6,15 @@ import { agentDir, stateRoot } from './paths.js';
6
6
  import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
7
7
  import { getAdapter } from './harness/registry.js';
8
8
  import { createMonitor, probeIdentityPresence, } from './monitor.js';
9
+ import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
10
+ import { recoverAgentIdentity } from './agent-recovery-gate.js';
9
11
  import { realExec } from './exec.js';
10
12
  import { resolveIsolation } from './isolation/policy.js';
11
13
  import { selectIsolationBackend } from './isolation/registry.js';
12
14
  import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
13
15
  import { resolveLaunchRuntime } from './isolation/runtime.js';
14
16
  import { controlRequest, RoleControlServer } from './session/control.js';
15
- import { ACP_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus } from './session/types.js';
17
+ import { ACP_CANCEL_DEADLINE_EXCEEDED, CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, classifyShellStatus, } from './session/types.js';
16
18
  import { effectiveModelForRole, modelRecoveryHeld, reconcileModelRecovery, recordModelFailure, classifyFailureText, } from './model-recovery.js';
17
19
  import { rotateWorklog } from './worklog.js';
18
20
  import { reconcilePermanentRoleIdentities } from './creation.js';
@@ -24,6 +26,15 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
24
26
  import { effectivePermissionMode } from './permissions.js';
25
27
  import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
26
28
  import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
29
+ import { FleetCommandAuditStore } from './fleet-command-audit.js';
30
+ export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
31
+ export class SupervisorRecycleRequiredError extends Error {
32
+ code = SUPERVISOR_RECYCLE_REQUIRED;
33
+ constructor() {
34
+ super('owner channel requires a fresh supervisor process after unproven client quiescence');
35
+ this.name = 'SupervisorRecycleRequiredError';
36
+ }
37
+ }
27
38
  const defaultDeps = () => ({
28
39
  exec: realExec,
29
40
  cpuDelegated: () => cpuControllerDelegated(),
@@ -38,10 +49,12 @@ const defaultDeps = () => ({
38
49
  now: () => Date.now(),
39
50
  log: line => process.stderr.write(line + '\n'),
40
51
  fetch: (url, init) => globalThis.fetch(url, init),
52
+ probeGeneration: env => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env),
41
53
  createMonitor: opts => createMonitor(opts),
42
54
  createOwnerChannel: opts => new OwnerChannel(opts),
43
55
  startAgentSession: (adapter, options) => adapter.start(options),
44
56
  createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
57
+ createLoopManager: (...args) => new ScheduledLoopManager(...args),
45
58
  acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
46
59
  reportOwnerStartupFailure: async (stateDir) => {
47
60
  const response = await controlRequest(stateDir, {
@@ -58,6 +71,32 @@ const defaultDeps = () => ({
58
71
  });
59
72
  const MONITOR_OWNER_FILE = '.monitor-owner';
60
73
  const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
74
+ function localFleetAuditor(stateDir, caller, log) {
75
+ const store = new FleetCommandAuditStore(join(stateDir, '.fleet-command-audit.json'));
76
+ return {
77
+ async begin(requestId, argv) {
78
+ let attempt = store.begin(requestId, caller, argv);
79
+ if (attempt.invocation === 'sending') {
80
+ log(`[${caller}] fleet proxy command ${attempt.correlationId} `
81
+ + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
82
+ attempt = store.invocation(attempt.correlationId, caller, 'delivered');
83
+ }
84
+ return attempt;
85
+ },
86
+ async finish(input) {
87
+ let attempt = store.finish(input.correlationId, caller, {
88
+ class: input.class, effect: input.effect,
89
+ ...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
90
+ ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
91
+ ...(input.presentations ? { presentations: input.presentations } : {}),
92
+ });
93
+ if (attempt.outcome?.delivery === 'sending')
94
+ attempt = store.outcome(attempt.correlationId, caller, 'delivered');
95
+ return attempt;
96
+ },
97
+ async present() { },
98
+ };
99
+ }
61
100
  /** Environment injected only into the managed harness process. */
62
101
  export function managedFleetProxyEnv(role, stateDir) {
63
102
  const roleEnv = { ...(role.env ?? {}) };
@@ -453,6 +492,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
453
492
  const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
454
493
  const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
455
494
  const sessionBackend = role.session ?? 'acp';
495
+ const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
456
496
  let launch = adapter.agentSession.prepareLaunch(role, prep);
457
497
  // Isolation is additive: only roles that declare `isolation:` are wrapped.
458
498
  let wrappedArgv = launch.argv;
@@ -532,6 +572,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
532
572
  name, identity: role.identity, agentDir: dir, cfg: role.monitor,
533
573
  deps: resolvedMonitorDeps,
534
574
  }) : null;
575
+ const daemonObserver = new DaemonGenerationObserver();
576
+ const initialDaemon = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
577
+ let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
535
578
  if (monitor)
536
579
  await monitor.prime({ resetCursor: resetMonitorCursor });
537
580
  rmSync(exitFile, { force: true });
@@ -540,13 +583,13 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
540
583
  let control;
541
584
  let unsubscribeRecovery;
542
585
  let monitorLoop;
543
- let acpStartupComplete = false;
586
+ let sessionStartupComplete = false;
544
587
  let sessionClosed = false;
545
588
  let ownerChannel;
546
589
  let ownerBinder;
547
- const pendingFleetSpawnNotices = [];
548
590
  let loopManager;
549
591
  let arbiter;
592
+ let recoveryController;
550
593
  let reloadLoopConfig;
551
594
  let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
552
595
  loop.name, loop.definitionHash, loop.promptHash,
@@ -571,7 +614,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
571
614
  unsubscribeRecovery = agentSession.subscribe(event => {
572
615
  if (event.kind !== 'error' || !event.text)
573
616
  return;
574
- const evidence = classifyFailureText(event.text, 'acp', new Date(deps.now()).toISOString());
617
+ const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
575
618
  if (evidence)
576
619
  resolvedMonitorDeps.onFailureEvidence?.(evidence);
577
620
  });
@@ -596,33 +639,50 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
596
639
  + `${error?.message ?? String(error)}`);
597
640
  }
598
641
  }
599
- control = deps.createControlServer(dir, arbiter, deps.log);
600
642
  try {
643
+ if (role.owner_channel)
644
+ ownerChannel = deps.createOwnerChannel({
645
+ role: name,
646
+ harness: role.harness,
647
+ config: role.owner_channel,
648
+ session: arbiter,
649
+ stateDir: dir,
650
+ env: role.env,
651
+ log: deps.log,
652
+ ...(ownerBinder ? { binderLease: ownerBinder } : {}),
653
+ ...(configPath ? { configPath } : {}),
654
+ });
655
+ control = deps.createControlServer(dir, arbiter, deps.log);
656
+ control.setFleetAuditor(ownerChannel ? {
657
+ begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
658
+ finish: input => ownerChannel.finishFleetCommandAudit(input),
659
+ present: presentations => ownerChannel.notifyFleetLifecycle(presentations),
660
+ } : localFleetAuditor(dir, name, deps.log));
601
661
  await control.start();
602
662
  }
603
663
  catch (error) {
664
+ await ownerChannel?.close().catch(() => undefined);
604
665
  ownerBinder?.release();
605
666
  await agentSession.close();
606
667
  unsubscribeRecovery?.();
607
668
  throw error;
608
669
  }
609
- control.setFleetSpawner(async (requested) => {
610
- const event = await executeManagedSpawn(role, configPath, requested, deps.log);
611
- if (!role.owner_channel)
612
- return event;
613
- if (ownerChannel?.notifyFleetSpawn) {
614
- try {
615
- await ownerChannel.notifyFleetSpawn(event);
616
- }
617
- catch (error) {
618
- deps.log(`[${name}] spawned-agent owner notice failed: `
619
- + `${error?.message ?? String(error)}`);
670
+ if (!role.roomMemberStartup)
671
+ control.setFleetSpawner(async (requested) => {
672
+ const event = await executeManagedSpawn(role, configPath, requested, deps.log);
673
+ // Room-member launches are collected with their Task/Room transaction so
674
+ // causal delivery is Task → Room → Agents → Room active → Task active.
675
+ if (!requested.roomMemberStartup && role.owner_channel && ownerChannel?.notifyFleetSpawn) {
676
+ try {
677
+ await ownerChannel.notifyFleetSpawn(event);
678
+ }
679
+ catch (error) {
680
+ deps.log(`[${name}] Agent lifecycle notice delivery failed: `
681
+ + `${error?.message ?? String(error)}`);
682
+ }
620
683
  }
621
- }
622
- else
623
- pendingFleetSpawnNotices.push(event);
624
- return event;
625
- });
684
+ return event;
685
+ });
626
686
  resolvedMonitorDeps.delivery = {
627
687
  // A wake is only delivered when its turn TERMINATES successfully. A
628
688
  // refusal or a cancellation reached the agent and was not acted on, so
@@ -633,7 +693,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
633
693
  // steer into the live turn instead; after it completes, honor the
634
694
  // configured interrupt policy normally.
635
695
  const policy = options?.interrupt;
636
- const interrupt = policy === true && acpStartupComplete;
696
+ const interrupt = policy === true && sessionStartupComplete;
637
697
  const promptOptions = {
638
698
  interrupt, steer: true,
639
699
  ...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
@@ -641,7 +701,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
641
701
  };
642
702
  // Startup is already a protected boundary: as with immediate mode,
643
703
  // steer rather than waiting on/cancelling the runner-owned first turn.
644
- const result = policy === 'after_tool' && acpStartupComplete
704
+ const result = policy === 'after_tool' && sessionStartupComplete
645
705
  ? await arbiter.submitPromptAfterTool(text, promptOptions)
646
706
  : await arbiter.submitPrompt(text, promptOptions);
647
707
  const steered = result.accepted
@@ -702,32 +762,21 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
702
762
  exit: {
703
763
  version: 1,
704
764
  class: 'program-exit',
705
- detail: `ACP startup triggered model recovery (${modelRecovery})`,
765
+ detail: `${sessionLabel} startup triggered model recovery (${modelRecovery})`,
706
766
  },
707
767
  rotated: false,
708
768
  mode,
709
769
  modelRecovery,
710
770
  };
711
771
  }
712
- throw new Error(`[${name}] ACP startup prompt ${started.outcome}` +
772
+ throw new Error(`[${name}] ${sessionLabel} startup prompt ${started.outcome}` +
713
773
  `${started.detail ? `: ${started.detail}` : ''}`);
714
774
  }
715
775
  if (interruptedForWake)
716
- deps.log(`[${name}] ACP startup prompt cancelled by ${started.cancellationSource}; `
776
+ deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
717
777
  + 'keeping temporary supervisor alive');
718
- acpStartupComplete = true;
719
- if (role.owner_channel) {
720
- ownerChannel = deps.createOwnerChannel({
721
- role: name,
722
- harness: role.harness,
723
- config: role.owner_channel,
724
- session: arbiter,
725
- stateDir: dir,
726
- env: role.env,
727
- log: deps.log,
728
- ...(ownerBinder ? { binderLease: ownerBinder } : {}),
729
- ...(configPath ? { configPath } : {}),
730
- });
778
+ sessionStartupComplete = true;
779
+ if (ownerChannel) {
731
780
  try {
732
781
  await ownerChannel.start();
733
782
  }
@@ -743,18 +792,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
743
792
  throw new Error(`[${name}] owner channel failed to start: `
744
793
  + `${error?.message ?? String(error)}`);
745
794
  }
746
- for (const event of pendingFleetSpawnNotices.splice(0)) {
747
- try {
748
- await ownerChannel.notifyFleetSpawn?.(event);
749
- }
750
- catch (error) {
751
- deps.log(`[${name}] deferred spawned-agent owner notice failed: `
752
- + `${error?.message ?? String(error)}`);
753
- }
754
- }
755
795
  }
756
- if (ownerChannel)
796
+ if (ownerChannel) {
757
797
  control.setOwnerChannel(ownerChannel);
798
+ }
758
799
  reloadLoopConfig = async () => {
759
800
  const nextRole = findRole(loadConfig(configPath), name);
760
801
  const definitions = nextRole.loops ?? [];
@@ -764,7 +805,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
764
805
  if (generation === loopGeneration && (loopManager || !definitions.length))
765
806
  return { changed: false, loops: definitions.length };
766
807
  if (!loopManager && definitions.length) {
767
- loopManager = new ScheduledLoopManager(name, definitions, dir, arbiter, {
808
+ loopManager = deps.createLoopManager(name, definitions, dir, arbiter, {
768
809
  now: deps.now,
769
810
  setTimer: (callback, ms) => setTimeout(callback, ms),
770
811
  clearTimer: timer => clearTimeout(timer),
@@ -780,10 +821,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
780
821
  deps.log(`[${name}] scheduled loops reloaded (${definitions.length} definitions)`);
781
822
  return { changed: true, loops: definitions.length };
782
823
  };
783
- control.setConfigReloader(reloadLoopConfig);
824
+ // Temporary agents are immutable launch snapshots: never re-resolve mutable Fleet YAML.
825
+ if (!temp)
826
+ control.setConfigReloader(reloadLoopConfig);
784
827
  if (role.loops?.length) {
785
828
  try {
786
- loopManager = new ScheduledLoopManager(name, role.loops, dir, arbiter, {
829
+ loopManager = deps.createLoopManager(name, role.loops, dir, arbiter, {
787
830
  now: deps.now,
788
831
  setTimer: (callback, ms) => setTimeout(callback, ms),
789
832
  clearTimer: timer => clearTimeout(timer),
@@ -793,6 +836,17 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
793
836
  loopManager.start();
794
837
  }
795
838
  catch (error) {
839
+ if (temp) {
840
+ monitor?.stop();
841
+ if (monitorLoop)
842
+ await monitorLoop;
843
+ await control.close();
844
+ ownerBinder?.release();
845
+ await agentSession.close();
846
+ unsubscribeRecovery?.();
847
+ throw new Error(`[${name}] configured temporary loop manager failed to start: `
848
+ + `${error?.message ?? String(error)}`);
849
+ }
796
850
  deps.log(`[${name}] scheduled loop manager unavailable: ${error?.name ?? 'Error'}`);
797
851
  }
798
852
  }
@@ -801,16 +855,49 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
801
855
  // The monitor loop lives exactly as long as the session: it starts once the
802
856
  // pane pid is known and is stopped when that pid dies (task dies with runner).
803
857
  monitorLoop ??= monitor?.run(pid);
858
+ recoveryController = new RoleRecoveryController({
859
+ role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
860
+ log: deps.log,
861
+ recoverAgent: async () => {
862
+ const evidence = await recoverAgentIdentity(arbiter, role.identity);
863
+ return evidence.ok ? { ok: true } : { ok: false, reason: evidence.reason };
864
+ },
865
+ recoverOwner: async (epoch) => {
866
+ if (!ownerChannel?.recover)
867
+ return { ok: true };
868
+ try {
869
+ await ownerChannel.recover(epoch);
870
+ return { ok: true };
871
+ }
872
+ catch (error) {
873
+ return { ok: false, reason: error instanceof Error
874
+ ? `OWNER_${error.name.toUpperCase()}` : 'OWNER_UNKNOWN_ERROR' };
875
+ }
876
+ },
877
+ });
804
878
  const start = deps.now();
805
879
  let nextLoopReloadAt = deps.now() + 30_000;
806
880
  let nextIdentityPollAt = deps.now();
881
+ let nextDaemonProbeAt = deps.now();
807
882
  let lastReloadError = '';
808
883
  let identityObserved = false;
809
884
  let identityAbsentSince;
810
885
  let retirementReason;
886
+ let supervisorRecycleRequired = false;
811
887
  while (sessionHandle.isAlive()) {
812
888
  await deps.sleep(temp ? 500 : 2000);
813
889
  const now = deps.now();
890
+ if (now >= nextDaemonProbeAt) {
891
+ nextDaemonProbeAt = now + 2_000;
892
+ const observation = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
893
+ if (observation.kind === 'lost')
894
+ recoveryController.noteLoss(observation.reason);
895
+ if (observation.kind === 'changed' || observation.kind === 'available'
896
+ || (observation.kind === 'baseline' && sessionStartedWithoutDaemonBaseline)) {
897
+ sessionStartedWithoutDaemonBaseline = false;
898
+ void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
899
+ }
900
+ }
814
901
  if (temp && deps.shouldStop?.()) {
815
902
  retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
816
903
  deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
@@ -861,6 +948,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
861
948
  control?.setLoopManager(undefined);
862
949
  await loopManager.stop();
863
950
  }
951
+ // Let already-settled path operations publish their aggregate result before
952
+ // the shutdown fence invalidates the epoch. This never waits on I/O.
953
+ await Promise.resolve();
954
+ await Promise.resolve();
955
+ recoveryController?.cancel();
864
956
  control?.setConfigReloader(undefined);
865
957
  // Close the authenticated control route before releasing the binder lease;
866
958
  // otherwise the predecessor can unlink the replacement's new socket.
@@ -871,7 +963,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
871
963
  }
872
964
  if (ownerChannel)
873
965
  await ownerChannel.close();
874
- ownerBinder?.release();
966
+ if (ownerChannel?.binderReleaseSafe?.() === false) {
967
+ deps.log(`[${name}] owner binder retained because client quiescence was not proven at shutdown`);
968
+ supervisorRecycleRequired = true;
969
+ }
970
+ else
971
+ ownerBinder?.release();
875
972
  if (monitor) {
876
973
  monitor.stop();
877
974
  await monitorLoop;
@@ -895,7 +992,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
895
992
  rotated = true;
896
993
  deps.log(`[${name}] ${why} -> rotated session-id; next start is FRESH`);
897
994
  };
898
- if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED))
995
+ if (exitRecord.detail.includes(ACP_CANCEL_DEADLINE_EXCEEDED)
996
+ || exitRecord.detail.includes(CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED))
899
997
  // This is a deliberate adapter reclamation, not evidence that resume state
900
998
  // is poisoned. Preserve the context even when the resumed generation hits
901
999
  // the same bound immediately; runSupervised still counts the fast exit and
@@ -920,6 +1018,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
920
1018
  }
921
1019
  else
922
1020
  deps.log(`[${name}] ${exitRecord.detail} (${elapsed.toFixed(0)}s) -> next start RESUMES context`);
1021
+ if (supervisorRecycleRequired)
1022
+ throw new SupervisorRecycleRequiredError();
923
1023
  return {
924
1024
  elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
925
1025
  ...(retirementReason ? { retirementReason } : {}),
@@ -996,6 +1096,16 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
996
1096
  result = await attempt(name, { configPath: opts.configPath, allowResumeRotation: !ledger.resumeDiscarded }, deps);
997
1097
  }
998
1098
  catch (e) {
1099
+ if (e instanceof SupervisorRecycleRequiredError) {
1100
+ writeRestartLedger(dir, {
1101
+ ...ledger,
1102
+ lastReason: SUPERVISOR_RECYCLE_REQUIRED,
1103
+ nextDelayMs: 0,
1104
+ updatedAt: stamp(),
1105
+ });
1106
+ deps.log(`[${name}] supervisor recycle required: ${SUPERVISOR_RECYCLE_REQUIRED}`);
1107
+ throw e;
1108
+ }
999
1109
  // A session that could not even start is an immediate failure like any
1000
1110
  // other; it must count, or an unstartable role loops forever.
1001
1111
  result = {
@@ -1089,7 +1199,7 @@ function fastFailSecsFor(name, configPath) {
1089
1199
  }
1090
1200
  }
1091
1201
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
1092
- export async function runTemp(name, deps = {}) {
1202
+ export async function runTemp(name, deps = {}, attempt = runOnce) {
1093
1203
  const dir = agentDir(name, true);
1094
1204
  await markTempSupervisorActive(dir);
1095
1205
  let signal;
@@ -1100,7 +1210,7 @@ export async function runTemp(name, deps = {}) {
1100
1210
  let result;
1101
1211
  let failure;
1102
1212
  try {
1103
- result = await runOnce(name, { temp: true }, {
1213
+ result = await attempt(name, { temp: true }, {
1104
1214
  ...deps,
1105
1215
  shouldStop: () => Boolean(signal) || (deps.shouldStop?.() ?? false),
1106
1216
  });
@@ -1114,6 +1224,7 @@ export async function runTemp(name, deps = {}) {
1114
1224
  const requested = requestedTempStopReason(dir);
1115
1225
  const reason = requested
1116
1226
  ?? result?.retirementReason
1227
+ ?? (failure instanceof SupervisorRecycleRequiredError ? 'supervisor-recycle' : undefined)
1117
1228
  ?? (signal ? 'supervisor-signal' : failure ? 'startup-failure' : 'session-ended');
1118
1229
  // A service-manager stop can make the child connection close before the
1119
1230
  // runner reaches its normal loop. That is still a successful requested
@@ -28,6 +28,8 @@ export declare function promptContentBlocks(text: string, origin?: PromptOrigin)
28
28
  export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
29
29
  export interface AcpSessionOptions {
30
30
  name: string;
31
+ /** Harness identity used only for honest optional capability reporting. */
32
+ harness?: string;
31
33
  argv: string[];
32
34
  cwd: string;
33
35
  env: Record<string, string>;
@@ -36,6 +38,11 @@ export interface AcpSessionOptions {
36
38
  permissions: CommonPermissions;
37
39
  /** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
38
40
  modeId?: string;
41
+ /** Ordered explicit Brain choices that must be applied before readiness. */
42
+ configSelections?: Array<{
43
+ configId: string;
44
+ value: string;
45
+ }>;
39
46
  /** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
40
47
  permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
41
48
  /** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
@@ -84,6 +91,7 @@ export declare function classifyStopReason(stopReason: string | undefined): Turn
84
91
  export declare class AcpSession implements AgentSession {
85
92
  private readonly options;
86
93
  readonly backend: "acp";
94
+ readonly capabilities: import("./types.js").AgentSessionCapabilities;
87
95
  readonly pid: number;
88
96
  private readonly child;
89
97
  private readonly events;
@@ -110,7 +118,7 @@ export declare class AcpSession implements AgentSession {
110
118
  private queueDepth;
111
119
  private exit;
112
120
  private steeringSupported;
113
- private capabilities?;
121
+ private agentCapabilities?;
114
122
  private runtimeModel?;
115
123
  private reasoningEffort?;
116
124
  private controllerCount;
@@ -7,7 +7,7 @@ import * as acp from '@agentclientprotocol/sdk';
7
7
  import { normalizeSessionUpdate } from './conversation-normalizer.js';
8
8
  import { ConversationEventStore } from './conversation-store.js';
9
9
  import { SessionEvents } from './events.js';
10
- import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, turnResult, } from './types.js';
10
+ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, sessionBackendCapabilities, turnResult, } from './types.js';
11
11
  const CANCEL_SETTLE_GRACE_MS = 15_000;
12
12
  const CANCEL_TERMINATE_GRACE_MS = 5_000;
13
13
  /** A permission no human answered is eventually a decision nobody made. */
@@ -206,6 +206,7 @@ export function classifyStopReason(stopReason) {
206
206
  export class AcpSession {
207
207
  options;
208
208
  backend = 'acp';
209
+ capabilities;
209
210
  pid;
210
211
  child;
211
212
  events;
@@ -232,7 +233,7 @@ export class AcpSession {
232
233
  queueDepth = 0;
233
234
  exit = null;
234
235
  steeringSupported = false;
235
- capabilities;
236
+ agentCapabilities;
236
237
  runtimeModel;
237
238
  reasoningEffort;
238
239
  controllerCount = 0;
@@ -265,6 +266,7 @@ export class AcpSession {
265
266
  this.child = child;
266
267
  this.connection = connection;
267
268
  this.pid = child.pid ?? -1;
269
+ this.capabilities = sessionBackendCapabilities('acp', options.harness);
268
270
  this.events = new SessionEvents(join(options.stateDir, '.session-events.jsonl'));
269
271
  this.conversation = new ConversationEventStore(join(options.stateDir, '.conversation'), {
270
272
  roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
@@ -994,7 +996,7 @@ export class AcpSession {
994
996
  for (const [permissionId, pending] of [...this.pendingPermissions])
995
997
  this.settlePendingAutomatically(permissionId, pending, 'cancelled', undefined, 'the session closed while this request was pending');
996
998
  this.releaseAllTools();
997
- if (this.sessionId && this.capabilities?.sessionCapabilities?.close != null) {
999
+ if (this.sessionId && this.agentCapabilities?.sessionCapabilities?.close != null) {
998
1000
  await this.connection.agent.request(acp.methods.agent.session.close, { sessionId: this.sessionId }).catch(() => undefined);
999
1001
  }
1000
1002
  this.connection.close();
@@ -1020,23 +1022,25 @@ export class AcpSession {
1020
1022
  });
1021
1023
  if (initialized.protocolVersion !== acp.PROTOCOL_VERSION)
1022
1024
  throw new Error(`ACP protocol mismatch: agent selected ${initialized.protocolVersion}, client supports ${acp.PROTOCOL_VERSION}`);
1023
- this.capabilities = initialized.agentCapabilities;
1025
+ this.agentCapabilities = initialized.agentCapabilities;
1024
1026
  const steering = initialized._meta?.steering;
1025
1027
  this.steeringSupported = steering !== null && typeof steering === 'object'
1026
1028
  && steering.supported === true;
1027
1029
  const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
1028
1030
  ? readFileSync(this.sessionFile, 'utf8').trim()
1029
1031
  : '';
1030
- if (persisted && this.capabilities?.sessionCapabilities?.resume != null) {
1032
+ let advertisedConfigOptions;
1033
+ if (persisted && this.agentCapabilities?.sessionCapabilities?.resume != null) {
1031
1034
  const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
1032
1035
  sessionId: persisted,
1033
1036
  cwd: this.options.cwd,
1034
1037
  mcpServers: this.declaredMcpServers(),
1035
1038
  });
1036
- this.captureRuntimeMetadata(resumed.configOptions);
1039
+ advertisedConfigOptions = resumed.configOptions;
1040
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1037
1041
  this.sessionId = persisted;
1038
1042
  }
1039
- else if (persisted && this.capabilities?.loadSession) {
1043
+ else if (persisted && this.agentCapabilities?.loadSession) {
1040
1044
  // `session/load` replays prior history as ordinary updates before the
1041
1045
  // response; those records carry `agent_replay` provenance, never `agent`.
1042
1046
  this.replaying = true;
@@ -1046,7 +1050,8 @@ export class AcpSession {
1046
1050
  cwd: this.options.cwd,
1047
1051
  mcpServers: this.declaredMcpServers(),
1048
1052
  });
1049
- this.captureRuntimeMetadata(loaded.configOptions);
1053
+ advertisedConfigOptions = loaded.configOptions;
1054
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1050
1055
  }
1051
1056
  finally {
1052
1057
  this.replaying = false;
@@ -1060,8 +1065,29 @@ export class AcpSession {
1060
1065
  ...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
1061
1066
  });
1062
1067
  this.sessionId = created.sessionId;
1063
- this.captureRuntimeMetadata(created.configOptions);
1068
+ advertisedConfigOptions = created.configOptions;
1069
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1070
+ }
1071
+ for (const selection of this.options.configSelections ?? []) {
1072
+ if (!advertisedConfigOptions?.some(option => option.id === selection.configId))
1073
+ throw new Error(`ACP agent did not advertise required session config option '${selection.configId}'`);
1074
+ let configured;
1075
+ try {
1076
+ configured = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId: selection.configId, value: selection.value });
1077
+ }
1078
+ catch (error) {
1079
+ throw new Error(`ACP agent refused required session config option '${selection.configId}' value '${selection.value}': `
1080
+ + (error instanceof Error ? error.message : String(error)));
1081
+ }
1082
+ advertisedConfigOptions = configured.configOptions;
1083
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1084
+ const applied = advertisedConfigOptions.find(option => option.id === selection.configId);
1085
+ if (applied?.currentValue !== selection.value)
1086
+ throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
1064
1087
  }
1088
+ // Do not persist a session until every required Brain choice is live. A
1089
+ // failed startup closes the ACP session; persisting its id first would make
1090
+ // a later resume repeatedly target that invalid session.
1065
1091
  writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
1066
1092
  // Deliver the configured permission mode whichever way the session came up
1067
1093
  // (new, resume or load) — the launch flag never reaches an ACP agent. A
@@ -17,8 +17,9 @@ export type ScheduledAttempt = {
17
17
  */
18
18
  export declare class RoleTurnArbiter implements AgentSession {
19
19
  private readonly session;
20
- readonly backend: "acp";
20
+ readonly backend: import("../config.js").SessionBackendId;
21
21
  readonly pid: number;
22
+ readonly capabilities: import("./types.js").AgentSessionCapabilities | undefined;
22
23
  private tail;
23
24
  private unsettled;
24
25
  private stopping;
@@ -25,6 +25,7 @@ export class RoleTurnArbiter {
25
25
  session;
26
26
  backend;
27
27
  pid;
28
+ capabilities;
28
29
  tail = Promise.resolve();
29
30
  unsettled = 0;
30
31
  stopping = false;
@@ -36,6 +37,7 @@ export class RoleTurnArbiter {
36
37
  this.session = session;
37
38
  this.backend = session.backend;
38
39
  this.pid = session.pid;
40
+ this.capabilities = session.capabilities;
39
41
  }
40
42
  /**
41
43
  * Waiters race the tail against their own generation's retirement, so a