@cotal-ai/manager 0.60.0 → 0.62.0

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/dist/manager.js CHANGED
@@ -4,11 +4,11 @@ import { hostname } from "node:os";
4
4
  import { credsAuthenticator } from "@nats-io/transport-node";
5
5
  import { existsSync, lstatSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
6
6
  import { isAbsolute, join, dirname, resolve } from "node:path";
7
- import { CotalEndpoint, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
7
+ import { CotalEndpoint, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
8
8
  import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, connectorInstallHint, DEFAULT_CONNECTOR, defaultAgentType, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, createManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
9
  import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
10
10
  import { AttachEndpoint } from "./attach-endpoint.js";
11
- import { makeManagerEndpointEvictionEvidence, makeManagerEndpointEvictor, makeManagerEndpointHolderEvictor } from "./endpoint-evict.js";
11
+ import { DELIVERY_ADMIN_BOOT_WAIT_MS, isUnansweredDeliveryAdmin, makeManagerEndpointEvictionEvidence, makeManagerEndpointEvictor, makeManagerEndpointHolderEvictor, untilDeliveryAdminAnswers } from "./endpoint-evict.js";
12
12
  import { makeManagerHolderLivenessProbe } from "./holder-liveness.js";
13
13
  import { GateReconcileRefused, reconcileEndpointGate } from "./reconcile-gate.js";
14
14
  import { launchSpecForRun, materializePersona, launchAgentToStartOpts, parseLaunchSpec, persistLaunchSpec, readContinuityAssignment, writeContinuityAssignment } from "./launch.js";
@@ -70,6 +70,9 @@ const DELIVERY_ADMIN_RELOAD_TIMEOUT_MS = 15_000;
70
70
  /** A hard preservation stop should settle quickly. The manager still waits and reports a partial
71
71
  * cut rather than pretending a child is gone. Held in ManagerOptions so fake runtimes can shorten it. */
72
72
  const PRESERVE_STOP_TIMEOUT_MS = 10_000;
73
+ /** How long a resume waits for a reaped orphan's principal to leave presence. Presence rows age
74
+ * out after their 6s TTL, so three TTLs covers a killed seat that never published its leave. */
75
+ const ORPHAN_PRESENCE_CLEAR_MS = 20_000;
73
76
  /** How much of an `input` text is written to a seat's pty at a time. Comfortably under the 4096-byte
74
77
  * kernel input buffer, so a slice is never re-split at a boundary the manager did not choose: the
75
78
  * tail of an oversized write stays queued and swallows the submit key that follows it (issue #1649).
@@ -122,6 +125,17 @@ function retireOpId(lifecycleUid) {
122
125
  * dash) — what {@link Manager.psOwnerFilter} returns for an unparseable caller so a malformed
123
126
  * principal fail-closes to an empty `ps` instead of an unbounded one. */
124
127
  const NO_OWNER_MATCHES = "-no-owner-";
128
+ /** A caught value's text for a refusal. A host-supplied store or callback may reject with any value,
129
+ * including one whose `message` is a Symbol or whose `message` getter or `toString` throws, and the
130
+ * refusal must still be returned, so the coercion to text runs inside the guard. */
131
+ function rejectionText(e) {
132
+ try {
133
+ return String(e?.message ?? e);
134
+ }
135
+ catch {
136
+ return "an unreadable rejection";
137
+ }
138
+ }
125
139
  /** Run the agent's bearer argv once, pre-launch — the end-to-end auth preflight (state dir, daemon,
126
140
  * ledger row, secret). Its stderr is the provider command's operator-exact sentence; surface it
127
141
  * verbatim as the spawn refusal. */
@@ -237,6 +251,14 @@ function isAbsentDeliveryAdmin(msg) {
237
251
  return false;
238
252
  return /no responders|\b503\b/i.test(msg);
239
253
  }
254
+ /** The store challenge went UNANSWERED while a delivery daemon exists: the rail timed out, or it had
255
+ * no responder while the lease row names a holder. Both are what a daemon that is starting or
256
+ * re-acquiring its lease looks like, so manager boot waits them out (#871); a renewal pass still
257
+ * refuses on the first one. A reply that does not decode was answered and is never this. */
258
+ class DeliveryAdminUnanswered extends Error {
259
+ }
260
+ /** How long one store challenge waits for the delivery daemon's answer. */
261
+ const STORE_CHALLENGE_TIMEOUT_MS = 5_000;
240
262
  /**
241
263
  * The agent supervisor: a long-lived mesh node that owns agent process lifecycle.
242
264
  * It serves control requests on the "manager" service and spawns/kills agents
@@ -275,6 +297,11 @@ export class Manager {
275
297
  * the launch settles, so no other spawn can take it, but the ceiling counts that seat through
276
298
  * `agents` and, if it frees young, its cooling stamp, never through the reservation (#906). */
277
299
  reservedLive = new Set();
300
+ /** Every numbered name {@link uniqueName} has issued. A suffix carries nothing but arrival order,
301
+ * so reissuing a freed one would give a later principal the name an earlier one posted under, and
302
+ * a reader of that name could not tell the two apart (#382). Never pruned in-process: one string
303
+ * per numbered spawn. */
304
+ issuedNumberedNames = new Set();
278
305
  /** Expiry stamps (`startedAt + MIN_LIFETIME`) for slots that freed while still young — a
279
306
  * count-only, lazily-pruned recycle floor (P4c). Pruned + summed into the ceiling gate. */
280
307
  cooling = [];
@@ -437,6 +464,10 @@ export class Manager {
437
464
  /** The complete boot task. stop() joins it after fencing static reconciliation, so a registration
438
465
  * already past an earlier shutdown check cannot finish after stop() returns. */
439
466
  startTask;
467
+ /** The one stop this manager runs, with the agent policy it was asked for. A stop takes seats out
468
+ * of `agents` before they have exited, so a second teardown would find none and report success
469
+ * while the first still waits on a live seat. */
470
+ stopTask;
440
471
  /** Held as an instance field so broker-owning smokes can compress the schedule without changing
441
472
  * production semantics. It is never a fence expiry: the durable non-retired row stays authoritative. */
442
473
  staticReconcileRetryDelaysMs = STATIC_RECONCILE_RETRY_DELAYS_MS;
@@ -474,7 +505,8 @@ export class Manager {
474
505
  * for release would otherwise move the row out from under a same-cached-revision CAS delete. */
475
506
  leaseRenewTask;
476
507
  /** Set at the top of `stop()`. `renewLease` returns at once once this is set, and its `gone` arm
477
- * does not re-acquire: a manager that is stopping never puts its own key back. */
508
+ * does not re-acquire: a manager that is stopping never puts its own key back. It also closes the
509
+ * lifecycle fence ({@link beginLifecycle}), so no spawn can launch a seat the stop never sees. */
478
510
  leaseStopping = false;
479
511
  /** The class-2 renewal owner's half-TTL schedule (D5 slice 5); armed only on auth meshes. */
480
512
  credRenewTimer;
@@ -585,9 +617,12 @@ export class Manager {
585
617
  get runtimeKind() {
586
618
  return this.runtime.kind;
587
619
  }
588
- /** Whether a default process stop can drop manager-local custody without taking agents with it. */
589
- get canSpareAgents() {
590
- return this.runtime.kind !== "pty" || this.runtime.supportsRelease === true;
620
+ /** What a default process stop does with this manager's seats. `release` drops manager-local
621
+ * custody and leaves every seat running. `stop` means this runtime keeps its PTYs inside the
622
+ * manager process, so those seats close with it: the stop stops and deprovisions them, and still
623
+ * releases every seat that can outlive it. */
624
+ get spareSeats() {
625
+ return this.runtime.kind !== "pty" || this.runtime.supportsRelease === true ? "release" : "stop";
591
626
  }
592
627
  /** Reattach this manager's runtime to a durable handle. Refuses by name when adopt is absent. */
593
628
  adoptRuntimeHandle(reference) {
@@ -890,8 +925,10 @@ export class Manager {
890
925
  // `reloadCreds` adoption on the delivery-admin rail, and persist the audit record doctor renders.
891
926
  if (this.auth) {
892
927
  // #1634: a space may be served by several managers, so ownership of the daemon's credential
893
- // renewal is decided here rather than by being the only manager able to start.
894
- await this.claimDaemonRenewalOwnership();
928
+ // renewal is decided here rather than by being the only manager able to start. #871: a daemon
929
+ // that is still binding or re-acquiring its lease leaves this challenge unanswered for a few
930
+ // seconds, so boot waits that out instead of exiting over a daemon about to answer.
931
+ await this.claimDaemonRenewalOwnership(DELIVERY_ADMIN_BOOT_WAIT_MS);
895
932
  // ARMED BEFORE the first remint, not after. That first `renewDaemonCreds()` re-signs through
896
933
  // the SecretStore, and a slow or contended store makes the one pass outlast the lease's
897
934
  // MANAGER_LEASE_TTL_MS. Arming afterwards leaves the whole first remint unprotected: the lease
@@ -1025,16 +1062,17 @@ export class Manager {
1025
1062
  * daemon"), and only a row that is absent or holderless settles "absent", the state start()
1026
1063
  * treats as not-yet-bound. An unreadable row (a failed read, or a key that exists but does not
1027
1064
  * parse) is a genuine unknown and refuses fail-closed, like a hung rail. */
1028
- async daemonStoreRelation() {
1065
+ async daemonStoreRelation(timeoutMs) {
1029
1066
  let reply;
1030
1067
  try {
1031
- reply = await this.ep.requestDeliveryAdmin("reloadStoreIdentity", {}, 5_000);
1068
+ reply = await this.ep.requestDeliveryAdmin("reloadStoreIdentity", {}, timeoutMs);
1032
1069
  }
1033
1070
  catch (e) {
1034
1071
  const msg = e.message;
1035
1072
  if (isAbsentDeliveryAdmin(msg))
1036
- return this.absentByLeaseRow();
1037
- throw new Error(`could not challenge the delivery daemon's SecretStore: ${msg}`);
1073
+ return this.absentByLeaseRow(isUnansweredDeliveryAdmin(e));
1074
+ const failure = `could not challenge the delivery daemon's SecretStore: ${msg}`;
1075
+ throw isUnansweredDeliveryAdmin(e) ? new DeliveryAdminUnanswered(failure) : new Error(failure);
1038
1076
  }
1039
1077
  if (!reply.ok)
1040
1078
  throw new Error(reply.error ?? "delivery daemon refused to name the SecretStore it reloads from");
@@ -1089,7 +1127,7 @@ export class Manager {
1089
1127
  *
1090
1128
  * `deliveryLeaseHolder` swallows its read failures into `undefined`, which would fold case 2
1091
1129
  * into case 3 — so this reads the entry directly and lets the read's own failure refuse. */
1092
- async absentByLeaseRow() {
1130
+ async absentByLeaseRow(unanswered) {
1093
1131
  let entry;
1094
1132
  try {
1095
1133
  entry = await this.ep.readDeliveryLeaseEntry(Manager.DELIVERY_SHARD);
@@ -1099,9 +1137,11 @@ export class Manager {
1099
1137
  `(${e.message}) - absence is undetermined, nothing reminted`);
1100
1138
  }
1101
1139
  const holder = entry?.info.holder;
1102
- if (holder !== undefined && holder !== "")
1103
- throw new Error(`the delivery-admin rail reported no responder while the delivery lease names a holder ` +
1104
- `(${holder}) - absence cannot be read off the rail alone, and nothing reminted`);
1140
+ if (holder !== undefined && holder !== "") {
1141
+ const refusal = `the delivery-admin rail reported no responder while the delivery lease names a holder ` +
1142
+ `(${holder}) - absence cannot be read off the rail alone, and nothing reminted`;
1143
+ throw unanswered ? new DeliveryAdminUnanswered(refusal) : new Error(refusal);
1144
+ }
1105
1145
  return "absent";
1106
1146
  }
1107
1147
  /** Keep this manager's renewal lease alive between credential passes (#1634). The manager bucket
@@ -1135,7 +1175,7 @@ export class Manager {
1135
1175
  * request, which is the #773 refusal the challenge exists to prevent.
1136
1176
  *
1137
1177
  * A manager failing either test serves the space normally and skips only the daemon remint. */
1138
- async claimDaemonRenewalOwnership() {
1178
+ async claimDaemonRenewalOwnership(waitMs = 0) {
1139
1179
  // EVERY refusal outcome drops a held lease, not only "divergent". The refusal paths THROW
1140
1180
  // (#1694: a non-holder answer, an unbindable answerer, an unreadable or holder-naming row on a
1141
1181
  // no-responder rail), and a thrown refusal caught by renewDaemonCreds would otherwise leave a
@@ -1144,7 +1184,9 @@ export class Manager {
1144
1184
  // Deciding here, inside the claim, is what keeps the release on the refusal path itself.
1145
1185
  let relation;
1146
1186
  try {
1147
- relation = await this.daemonStoreRelation();
1187
+ // Only the read-only challenge is retried, so a wait that ends mid-attempt abandons a read,
1188
+ // never the lease work below.
1189
+ relation = await untilDeliveryAdminAnswers(waitMs, (requestMs) => this.daemonStoreRelation(requestMs(STORE_CHALLENGE_TIMEOUT_MS)), (reason, delayMs) => console.error(`! delivery-admin rail did not answer the store challenge (${reason}); retrying in ${delayMs / 1000}s`), (e) => e instanceof DeliveryAdminUnanswered);
1148
1190
  }
1149
1191
  catch (e) {
1150
1192
  if (this.daemonRenewalOwner)
@@ -1522,9 +1564,9 @@ export class Manager {
1522
1564
  }
1523
1565
  }
1524
1566
  /** Admit one lifecycle/control operation while active. The synchronous increment is the fence:
1525
- * preserveState flips state before its first await, so work is either counted or rejected. */
1567
+ * preserveState and stop close it before their first await, so work is either counted or rejected. */
1526
1568
  beginLifecycle(resumeOperation = false) {
1527
- if (this.maintenanceState !== "active" || (this.resumeRequired && !resumeOperation))
1569
+ if (this.leaseStopping || this.maintenanceState !== "active" || (this.resumeRequired && !resumeOperation))
1528
1570
  return undefined;
1529
1571
  this.lifecycleInFlight++;
1530
1572
  let released = false;
@@ -1558,6 +1600,8 @@ export class Manager {
1558
1600
  await new Promise((resolve) => this.lifecycleDrainWaiters.push(resolve));
1559
1601
  }
1560
1602
  maintenanceError() {
1603
+ if (this.leaseStopping)
1604
+ return "manager is stopping; new lifecycle/control work is fenced";
1561
1605
  if (this.resumeRequired)
1562
1606
  return `manager is waiting for resume attempt ${this.resumeAttemptId}; ordinary lifecycle/control work is fenced`;
1563
1607
  return `manager is in ${this.maintenanceState} mode; new lifecycle/control work is fenced`;
@@ -1895,15 +1939,15 @@ export class Manager {
1895
1939
  ...(backfillFloor !== undefined ? { backfillFloor } : {}),
1896
1940
  };
1897
1941
  }
1898
- /** Tear down every managed agent's footprint on an explicit destructive {@link stop}. A manager exit is
1942
+ /** Tear down every managed agent's footprint on an explicit destructive {@link stop}, or only the
1943
+ * in-process seats a default stop cannot detach. A manager exit is
1899
1944
  * a mass agent-exit, and without this its agents' footprints (creds files + `dm_`/`dlv_` durables + ACL
1900
1945
  * rows) would orphan exactly as the per-agent exit path prevents. Hard-stop each child (an exit has no
1901
1946
  * time for the graceful grace window) and AWAIT its deprovision — bounded per agent (`withTimeout`) and
1902
1947
  * best-effort (`allSettled` + a loud log), so one slow/failed teardown can neither hang nor abort exit.
1903
1948
  * The creds file is dropped even if the broker teardown fails (see {@link deprovision}). Deliberately
1904
1949
  * touches NEITHER the lease NOR the endpoints — the caller owns those. */
1905
- async teardownManagedAgents() {
1906
- const managed = [...this.agents.values()];
1950
+ async teardownManagedAgents(managed = [...this.agents.values()]) {
1907
1951
  const failures = [];
1908
1952
  for (const a of managed) {
1909
1953
  // Free the slot + hard-stop each; `stopHandle` is best-effort (never throws — see it), so one bad
@@ -1934,16 +1978,11 @@ export class Manager {
1934
1978
  if (failures.length)
1935
1979
  throw new Error(`manager shutdown could not prove every seat exited: ${failures.join("; ")}`);
1936
1980
  }
1937
- /** Drop only this manager's local custody handles. Validate the complete snapshot before releasing
1938
- * any handle so a non-detachable runtime cannot leave a partially detached manager behind. */
1981
+ /** Drop this manager's local custody of every seat that can outlive it. A pty seat whose handle
1982
+ * cannot be released lives in this process and closes with it, so it is left for {@link stop} to
1983
+ * stop instead. */
1939
1984
  releaseManagedAgents() {
1940
- const managed = [...this.agents.values()];
1941
- const blocked = managed.filter((a) => a.handle.kind === "pty" && typeof a.handle.release !== "function");
1942
- if (blocked.length) {
1943
- return [
1944
- `manager shutdown cannot detach ${blocked.map((a) => `${a.name} (${a.handle.kind})`).join(", ")}: runtime handle does not support release`,
1945
- ];
1946
- }
1985
+ const managed = [...this.agents.values()].filter((a) => !(a.handle.kind === "pty" && typeof a.handle.release !== "function"));
1947
1986
  const failures = [];
1948
1987
  for (const a of managed) {
1949
1988
  try {
@@ -1981,7 +2020,16 @@ export class Manager {
1981
2020
  if (failures.length)
1982
2021
  throw new Error(`manager preservation shutdown incomplete: ${failures.join("; ")}`);
1983
2022
  }
1984
- async stop(options = {}) {
2023
+ /** Every call joins the first stop and settles with it. A call that asks for a different
2024
+ * `withAgents` is refused, since the running stop has already applied its own policy. */
2025
+ stop(options = {}) {
2026
+ const withAgents = options.withAgents === true;
2027
+ this.stopTask ??= { withAgents, done: this.stopOnce(withAgents) };
2028
+ if (this.stopTask.withAgents === withAgents)
2029
+ return this.stopTask.done;
2030
+ return Promise.reject(new Error(`manager is already stopping ${this.stopTask.withAgents ? "with" : "without"} its agents`));
2031
+ }
2032
+ async stopOnce(withAgents) {
1985
2033
  this.leaseStopping = true;
1986
2034
  this.staticReconcileStopping = true;
1987
2035
  const starting = this.startTask;
@@ -2007,14 +2055,22 @@ export class Manager {
2007
2055
  // still move the key's revision after we clear the timer, and releasing against a cached
2008
2056
  // revision it has since moved is a CAS the broker correctly refuses, silently, forever.
2009
2057
  await this.leaseRenewTask?.catch(() => { });
2058
+ // A spawn admitted before the fence closed may still be launching. Its seat must be in the
2059
+ // snapshot below, or it would outlive a stop that reported success.
2060
+ await this.awaitLifecycleDrain();
2010
2061
  let releaseFailures = [];
2011
2062
  let seatFailure;
2012
2063
  try {
2013
2064
  if (this.maintenanceState === "active" && !this.resumeRequired) {
2014
- if (options.withAgents)
2065
+ if (withAgents)
2015
2066
  await this.teardownManagedAgents();
2016
- else
2067
+ else {
2068
+ // An in-process seat cannot be detached from the process that is exiting, so the default
2069
+ // stop stops and deprovisions it as `withAgents` does instead of orphaning its footprint.
2070
+ const inProcess = [...this.agents.values()].filter((a) => a.handle.kind === "pty" && typeof a.handle.release !== "function");
2017
2071
  releaseFailures = this.releaseManagedAgents();
2072
+ await this.teardownManagedAgents(inProcess);
2073
+ }
2018
2074
  }
2019
2075
  else {
2020
2076
  // A signal after a partial preservation must never fall back into destructive teardown.
@@ -2654,6 +2710,13 @@ export class Manager {
2654
2710
  // A retired row affirms that no agent exists now, so it keeps the old `not-found`
2655
2711
  // result. The changed refusal is reserved for durable NONTERMINAL state that contradicts
2656
2712
  // the live-map miss and needs operator attention.
2713
+ //
2714
+ // A sibling's nonterminal row is not a contradiction: the name belongs to another
2715
+ // manager instance in this space. In a multi-manager space the class queue hands this
2716
+ // read to either instance, so a plain `not-found` here read the same as a name that
2717
+ // exists nowhere and a caller had no way to tell the two apart (#443).
2718
+ if (observed?.ownerInstanceId !== undefined)
2719
+ throw new EpEnvelopeError("failed-precondition", `the durable slot for "${name}" belongs to manager instance ${observed.ownerInstanceId}, not ${this.managerInstanceId} which answered; only that instance can act on it ${renderStaticSlotObservation(observed)}`, [observed]);
2657
2720
  if (observed !== undefined && observed.slotPhase !== "retired")
2658
2721
  throw new EpEnvelopeError("failed-precondition", `no live agent "${name}"; durable state contradicts absence ${renderStaticSlotObservation(observed)}`, [observed]);
2659
2722
  }
@@ -3018,6 +3081,19 @@ export class Manager {
3018
3081
  * dead auth plane. Every failure is returned as the refusal sentence, with the grant + files
3019
3082
  * rolled back. */
3020
3083
  async provisionUserAgent(name, opts) {
3084
+ if (opts.delegatedIntent) {
3085
+ // startAgentActive refused a delegated launch on a manager without this callback.
3086
+ const execute = this.remoteAuthority.executeDelegatedUserIntent;
3087
+ const { intentId, owner } = opts.delegatedIntent;
3088
+ // The hosted arm with the intent's execution in place of the holder's own enrollment: the
3089
+ // spawning owner is the intent's, so its existing owner check refuses any other.
3090
+ return this.enrollUserAgent(name, { ...opts, specOwner: owner }, async ({ target }) => {
3091
+ const result = await execute({ intentId, operation: "launch", target: { actor: target.actor, tokenHash: target.tokenHash } });
3092
+ if (result.operation !== "launch" || result.intentId !== intentId)
3093
+ throw new Error(`the host answered delegated intent ${intentId} with another execution`);
3094
+ return result.material;
3095
+ });
3096
+ }
3021
3097
  if (this.remoteAuthority?.enrollManagedAgent)
3022
3098
  return this.enrollUserAgent(name, opts, this.remoteAuthority.enrollManagedAgent);
3023
3099
  const spawnerPr = opts.spawner ? parsePrincipalKey(opts.spawner) : null;
@@ -3176,6 +3252,8 @@ export class Manager {
3176
3252
  // Re-key the family onto the HOST's uid (SPEC 13.1 name-disjointness on the FS): the child's
3177
3253
  // credential paths must embed the uid its broker footprint carries, never the provisional one.
3178
3254
  files = agentLifecycleSecretFilePaths(this.workspaceRoot, this.space, name, material.lifecycleUid);
3255
+ // From here the host holds a grant at that uid.
3256
+ opts.onEnrolled?.({ owner: material.owner, lifecycleUid: material.lifecycleUid, files });
3179
3257
  if (files.actorToken !== staged.actorToken) {
3180
3258
  await secrets.put(agentSecretKeyForFile(files.actorToken, this.space), actorToken);
3181
3259
  await materializeSecretToFile(secrets, agentSecretKeyForFile(files.actorToken, this.space), files.actorToken);
@@ -3210,14 +3288,25 @@ export class Manager {
3210
3288
  // Shred every local secret this attempt materialized. The HOST's rows are the host's to
3211
3289
  // reconcile: a participant holds no writer, so it must not pretend to roll back a grant or a
3212
3290
  // durable it never authored. A retry with the same requestId re-enters the same enrollment.
3291
+ // A failed removal joins the refusal instead of throwing, so every removal is attempted and
3292
+ // the refusal still reaches the caller.
3293
+ const unshredded = [];
3294
+ const shred = async (what, remove) => {
3295
+ try {
3296
+ await remove();
3297
+ }
3298
+ catch (err) {
3299
+ unshredded.push(`${what}: ${rejectionText(err)}`);
3300
+ }
3301
+ };
3213
3302
  for (const family of files.actorToken === staged.actorToken ? [staged] : [staged, files]) {
3214
- await secrets.delete(agentSecretKeyForFile(family.actorToken, this.space)).catch(() => { });
3215
- await secrets.delete(agentSecretKeyForFile(family.sentinelCreds, this.space)).catch(() => { });
3216
- rmSync(family.actorToken, { force: true });
3217
- rmSync(family.sentinelCreds, { force: true });
3218
- rmSync(family.health, { force: true });
3303
+ for (const path of [family.actorToken, family.sentinelCreds])
3304
+ await shred(`secret ${path}`, () => secrets.delete(agentSecretKeyForFile(path, this.space)));
3305
+ for (const path of [family.actorToken, family.sentinelCreds, family.health])
3306
+ await shred(`file ${path}`, () => rmSync(path, { force: true }));
3219
3307
  }
3220
- return { error: `agent auth preflight failed for "${name}": ${e.message}` };
3308
+ const leftover = unshredded.length ? `; cleanup failed: ${unshredded.join("; ")}` : "";
3309
+ return { error: `agent auth preflight failed for "${name}": ${rejectionText(e)}${leftover}` };
3221
3310
  }
3222
3311
  }
3223
3312
  /**
@@ -3325,7 +3414,7 @@ export class Manager {
3325
3414
  if (this.userMode) {
3326
3415
  const p = parsePrincipalKey(a.id);
3327
3416
  if (p)
3328
- this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: p.owner, actor: p.actor, agentId: a.id, userOwner: a.userOwner, secretPaths: a.secretPaths, launch: { allowSubscribe: a.launch.allowSubscribe }, startedAt: Date.now() });
3417
+ this.retiring.set(a.name, { opId: retireOpId(a.lifecycleUid), lifecycleUid: a.lifecycleUid, owner: p.owner, actor: p.actor, agentId: a.id, userOwner: a.userOwner, delegated: a.delegated, secretPaths: a.secretPaths, launch: { allowSubscribe: a.launch.allowSubscribe }, startedAt: Date.now() });
3329
3418
  }
3330
3419
  else if (this.auth) {
3331
3420
  // Unit B: a STATIC lifecycle now also holds its name pending its own terminal (the F1
@@ -3376,6 +3465,14 @@ export class Manager {
3376
3465
  /** The actual footprint teardown (wrapped by {@link deprovision}'s single-flight). */
3377
3466
  async driveDeprovision(a) {
3378
3467
  if (this.remoteAuthority) {
3468
+ if (a.delegated) {
3469
+ // The agent is its user's: only that user's retirement intent retires it, so the name stays
3470
+ // held until retireDelegatedAgent confirms the host retired this UID.
3471
+ const held = this.retiring.get(a.name);
3472
+ if (held?.lifecycleUid === a.lifecycleUid)
3473
+ held.lastError = `"${a.name}" was launched under a delegated user intent and retires only through its user's retirement intent`;
3474
+ return;
3475
+ }
3379
3476
  // A hosted composition owns grant revocation and resumable footprint release. It calls this
3380
3477
  // prerequisite before the terminal rail. The callback must preserve this UID and its pending
3381
3478
  // state until the host decides terminal retirement, never map deprovisioning to suspension.
@@ -4031,10 +4128,16 @@ export class Manager {
4031
4128
  * still refused downstream exactly as before. Offline rows do NOT occupy — a properly retired
4032
4129
  * name stays reusable. A row this manager itself confirmed retired (its alias, principal, and
4033
4130
  * lifecycleUid all matching a {@link confirmedRetiredPredecessors} entry) does not occupy
4034
- * either, even while its advisory presence record ages out. */
4131
+ * either, even while its advisory presence record ages out. A numbered name is never reissued
4132
+ * ({@link issuedNumberedNames}), even after its holder retired. The requested base is exempt
4133
+ * from that history: it is the persona's own name, which stays reusable even when it once came
4134
+ * out of another persona's numbering. */
4035
4135
  uniqueName(base) {
4036
4136
  const live = this.liveRosterNames();
4037
- return firstFreeName(base, (n) => this.nameInUse(n, live));
4137
+ const name = firstFreeName(base, (n) => this.nameInUse(n, live) || (n !== base && this.issuedNumberedNames.has(n)));
4138
+ if (name !== base)
4139
+ this.issuedNumberedNames.add(name);
4140
+ return name;
4038
4141
  }
4039
4142
  /** Spawn a teammate by persona ref (`name` loads `.cotal/agents/<name>.md`; the peer presents
4040
4143
  * under that file's own `name:`), as if a peer asked via the control plane. Used to pre-spawn the
@@ -4434,6 +4537,14 @@ export class Manager {
4434
4537
  }
4435
4538
  }
4436
4539
  async startAgentActive(opts, spawner, hooks) {
4540
+ if (opts.delegatedIntent) {
4541
+ if (!this.remoteAuthority?.executeDelegatedUserIntent)
4542
+ return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
4543
+ if (opts.supervise)
4544
+ return { ok: false, error: `"${opts.name}" names a delegated user intent, and a delegated agent is never restarted (SPEC 13.16)` };
4545
+ // A delegated launch's spawner is the user's principal, never the composition root's call.
4546
+ spawner = opts.delegatedIntent.parent;
4547
+ }
4437
4548
  // The spawn argument is a persona REF — a filename in `.cotal/agents` (the unique spawn KEY), or
4438
4549
  // a path via `--config`. It is NOT the mesh identity: the identity comes from inside the file
4439
4550
  // (`name:`), so a persona can be filed descriptively (review-critic.md) yet present under a
@@ -4642,6 +4753,27 @@ export class Manager {
4642
4753
  `An operator adds \`capabilities: [spawn]\` to ${configPath}; a peer-defined persona ` +
4643
4754
  `(cotal_persona) cannot declare capabilities by design — ask an operator to grant it.`,
4644
4755
  };
4756
+ // #581: the operator's per-role model allowlist, judged on the EFFECTIVE role and model resolved
4757
+ // above, before any reserve or mint. This is where the launch is performed, so no caller can
4758
+ // skip it, and a definition pinned to a stale model (or to none) is refused here instead of
4759
+ // serving on it. Read per spawn, so a policy edit reaches the next spawn without a restart.
4760
+ let policyRefusal;
4761
+ try {
4762
+ policyRefusal = modelPolicyRefusal(loadCotalConfig(this.workspaceRoot), {
4763
+ persona: opts.resolved ? `manifest agent "${ref}"` : `persona "${ref}" (${configPath})`,
4764
+ role, model, variant,
4765
+ // A manifest launch carries its own declared model/variant in these options
4766
+ // (launchAgentToStartOpts), so there they are the definition's field, never a flag.
4767
+ modelFlag: !opts.resolved && opts.model !== undefined,
4768
+ variantFlag: !opts.resolved && opts.variant !== undefined,
4769
+ launchOptions,
4770
+ });
4771
+ }
4772
+ catch (e) {
4773
+ return { ok: false, error: e.message };
4774
+ }
4775
+ if (policyRefusal)
4776
+ return { ok: false, error: policyRefusal };
4645
4777
  // The alias-reuse gate (#29 piece 3): a name whose previous agent is still retiring REFUSES
4646
4778
  // legibly (never a silent suffix), and the refusal re-drives the FULL durable teardown so
4647
4779
  // "retry the spawn" is also the nudge. It routes through `deprovision` (not `requestRetirement`
@@ -4652,7 +4784,7 @@ export class Manager {
4652
4784
  return { ok: false, error: `the name "${identityName}" is still reconciling at manager startup; its prior lifecycle terminal owns this alias until it completes. Retry shortly.` };
4653
4785
  const held = this.retiring.get(identityName);
4654
4786
  if (held !== undefined) {
4655
- void this.deprovision({ id: held.agentId, name: identityName, lifecycleUid: held.lifecycleUid, userOwner: held.userOwner, secretPaths: held.secretPaths, launch: held.launch }).catch(() => { });
4787
+ void this.deprovision({ id: held.agentId, name: identityName, lifecycleUid: held.lifecycleUid, userOwner: held.userOwner, delegated: held.delegated, secretPaths: held.secretPaths, launch: held.launch }).catch(() => { });
4656
4788
  const err = lifecycleBlocked("failed-precondition", `the name "${identityName}" is reserved pending retirement: its previous agent's despawn started that lifecycle's teardown (footprint + standing-authority revoke + auth-side retirement), and the name frees only when all of it completes${held.lastError !== undefined ? ` (last attempt: ${held.lastError})` : ""}. NEXT: wait a moment and retry this spawn (retrying re-drives the whole teardown), or pick another name.`, { blockedOp: "retirement", headState: "retiring", opId: held.opId, remedy: "retry" });
4657
4789
  return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
4658
4790
  }
@@ -4844,6 +4976,15 @@ export class Manager {
4844
4976
  capabilities,
4845
4977
  label: ref,
4846
4978
  lifecycleUid,
4979
+ delegatedIntent: opts.delegatedIntent,
4980
+ // Hold the name at the UID the host enrolled for the intent's user before any later local
4981
+ // step runs, so no later failure, its refusal included, releases a lifecycle that only that
4982
+ // user's retirement intent retires.
4983
+ onEnrolled: opts.delegatedIntent
4984
+ ? (enrolled) => {
4985
+ provisioned = { id: principalKey(enrolled.owner, name).key, name, lifecycleUid: enrolled.lifecycleUid, userOwner: enrolled.owner, delegated: true, secretPaths: enrolled.files };
4986
+ }
4987
+ : undefined,
4847
4988
  });
4848
4989
  if ("error" in prep) {
4849
4990
  this.reserved.delete(name);
@@ -4857,7 +4998,7 @@ export class Manager {
4857
4998
  lifecycleUid = prep.lifecycleUid;
4858
4999
  userLaunch = prep.launch;
4859
5000
  userOwner = prep.owner;
4860
- provisioned = { id: principalKey(prep.owner, name).key, name, lifecycleUid, userOwner: prep.owner, secretPaths: prep.files, ...(custody ? { runtime: custody } : {}) };
5001
+ provisioned = { id: principalKey(prep.owner, name).key, name, lifecycleUid, userOwner: prep.owner, ...(opts.delegatedIntent ? { delegated: true } : {}), secretPaths: prep.files, ...(custody ? { runtime: custody } : {}) };
4861
5002
  }
4862
5003
  else if (this.auth) {
4863
5004
  // Unit B (§13.1): reserve + activate this incarnation's DURABLE identity BEFORE any
@@ -4983,6 +5124,7 @@ export class Manager {
4983
5124
  // recorded truth teardown/preservation/health consume, never re-derived by name.
4984
5125
  secretPaths: provisioned?.secretPaths,
4985
5126
  ...(userLaunch ? { userOwner } : { seed: identity.seed }),
5127
+ ...(opts.delegatedIntent ? { delegated: true } : {}),
4986
5128
  ...(issued ? { issued } : {}),
4987
5129
  spawner: spawner ?? this.ep.ref().id,
4988
5130
  authorityParent: userLaunch && spawner && parsePrincipalKey(spawner) ? spawner : undefined,
@@ -5021,7 +5163,8 @@ export class Manager {
5021
5163
  ? Object.keys(opts.launchOptions).sort()
5022
5164
  : undefined,
5023
5165
  },
5024
- ...(connector.supportsSessionContinuation || opts.supervise !== undefined
5166
+ // A delegated agent is never restarted (SPEC 13.16), so its slot arms no recovery.
5167
+ ...((connector.supportsSessionContinuation || opts.supervise !== undefined) && !opts.delegatedIntent
5025
5168
  ? {
5026
5169
  restart: {
5027
5170
  opts: launchOpts,
@@ -5139,6 +5282,11 @@ export class Manager {
5139
5282
  // orphan down (detached, fail-loud) so a failed spawn leaves no creds/durables behind (#159 B).
5140
5283
  if (provisioned) {
5141
5284
  const orphan = provisioned;
5285
+ // The host enrolled this UID for the intent's user, so only that user's retirement intent
5286
+ // retires it, and its name stays held for retireDelegatedAgent.
5287
+ const p = orphan.delegated ? parsePrincipalKey(orphan.id) : null;
5288
+ if (p)
5289
+ this.retiring.set(orphan.name, { opId: retireOpId(orphan.lifecycleUid), lifecycleUid: orphan.lifecycleUid, owner: p.owner, actor: p.actor, agentId: orphan.id, userOwner: orphan.userOwner, delegated: true, secretPaths: orphan.secretPaths, startedAt: Date.now() });
5142
5290
  this.trackDeprovision(orphan, "(orphaned spawn)");
5143
5291
  }
5144
5292
  }
@@ -5159,6 +5307,7 @@ export class Manager {
5159
5307
  return { ok: false, agents: [], error: `resume inventory belongs to space "${inventory.space}", not "${this.space}"` };
5160
5308
  const seen = new Set();
5161
5309
  const principals = new Set();
5310
+ const orphans = [];
5162
5311
  await this.ep.waitForPresenceSnapshot();
5163
5312
  const liveRoster = this.ep.getRoster().filter((presence) => presence.status !== "offline");
5164
5313
  const occupancy = this.occupancy();
@@ -5180,6 +5329,7 @@ export class Manager {
5180
5329
  name: entry.name,
5181
5330
  lifecycleUid: held.lifecycleUid,
5182
5331
  userOwner: held.userOwner,
5332
+ delegated: held.delegated,
5183
5333
  secretPaths: held.secretPaths,
5184
5334
  }).catch(() => { });
5185
5335
  return {
@@ -5210,8 +5360,12 @@ export class Manager {
5210
5360
  presence.card.name !== entry.name ||
5211
5361
  entry.identity.lifecycleUid === confirmedPredecessor.lifecycleUid;
5212
5362
  });
5213
- if (principalIsLive)
5214
- return { ok: false, agents: [], error: `retained principal "${principal}" is already live and this runtime cannot authoritatively adopt it` };
5363
+ if (principalIsLive) {
5364
+ const orphan = await this.orphanedResumeSeat(entry, principal, liveRoster);
5365
+ if (!orphan)
5366
+ return { ok: false, agents: [], error: `retained principal "${principal}" is already live and this runtime cannot authoritatively adopt it` };
5367
+ orphans.push({ principal, reference: orphan });
5368
+ }
5215
5369
  if (this.agents.has(entry.name) || this.reserved.has(entry.name))
5216
5370
  return { ok: false, agents: [], error: `retained agent "${entry.name}" is already managed or reserved` };
5217
5371
  }
@@ -5231,6 +5385,13 @@ export class Manager {
5231
5385
  agents: preflight,
5232
5386
  error: `${preflightFailures.length} retained agent${preflightFailures.length === 1 ? "" : "s"} failed preflight`,
5233
5387
  };
5388
+ // Only now, with the whole inventory through preflight, is an orphan reaped: a refused batch
5389
+ // leaves every seat as it found it.
5390
+ for (const { principal, reference } of orphans) {
5391
+ const refusal = await this.reapOrphanedResumeSeat(principal, reference);
5392
+ if (refusal)
5393
+ return { ok: false, agents: [], error: refusal };
5394
+ }
5234
5395
  const agents = [];
5235
5396
  for (let i = 0; i < inventory.agents.length; i++) {
5236
5397
  const entry = inventory.agents[i];
@@ -5430,6 +5591,46 @@ export class Manager {
5430
5591
  throw new Error(`retained user principal ${entry.identity.owner}.${entry.identity.actor} could not be reused: ${e.message}`);
5431
5592
  }
5432
5593
  }
5594
+ /** The seat reference behind a live retained principal, when that seat is provably one a lost
5595
+ * manager of this logical instance launched for this exact incarnation. The resume binds the
5596
+ * launched handle's reference to the incarnation's active slot before it waits for readiness, and
5597
+ * the coordinator journals `resume-active` only after that wait, so a coordinator crash between the
5598
+ * two leaves `resume-intent` behind a live seat. Undefined for everything else, which stays refused:
5599
+ * another incarnation's presence, a slot another instance owns, a runtime that cannot reap the
5600
+ * reference, and user or open meshes, which keep no slot. */
5601
+ async orphanedResumeSeat(entry, principal, liveRoster) {
5602
+ if (!this.auth || this.userMode || entry.identity.mode !== "static")
5603
+ return undefined;
5604
+ if (typeof this.runtime.reap !== "function")
5605
+ return undefined;
5606
+ if (liveRoster.some((p) => p.card.id === principal && p.lifecycleUid !== entry.identity.lifecycleUid))
5607
+ return undefined;
5608
+ const slot = await this.readStaticReconcileSlot(entry.name);
5609
+ if (slot?.phase !== "active" || slot.actor !== entry.identity.id || slot.lifecycleUid !== entry.identity.lifecycleUid)
5610
+ return undefined;
5611
+ if (slot.ownerInstanceId !== undefined && slot.ownerInstanceId !== this.managerInstanceId)
5612
+ return undefined;
5613
+ return slot.runtime?.kind === this.runtime.kind ? slot.runtime : undefined;
5614
+ }
5615
+ /** Reap a seat {@link orphanedResumeSeat} found, then wait for its principal to leave presence so
5616
+ * the relaunch's readiness cannot match the dead seat's row. Returns the refusal when the reap
5617
+ * proves nothing or the principal stays live, which means another process holds it. */
5618
+ async reapOrphanedResumeSeat(principal, reference) {
5619
+ try {
5620
+ const evidence = await requireRuntimeReap(this.runtime, reference);
5621
+ console.error(`resume: reaped orphaned seat ${reference.kind}:${reference.id} of retained principal "${principal}" (${evidence.detail})`);
5622
+ }
5623
+ catch (e) {
5624
+ return `retained principal "${principal}" is already live and its recorded seat ${reference.kind}:${reference.id} could not be reaped: ${e.message}`;
5625
+ }
5626
+ const deadline = Date.now() + ORPHAN_PRESENCE_CLEAR_MS;
5627
+ while (this.ep.getRoster().some((p) => p.card.id === principal && p.status !== "offline")) {
5628
+ if (Date.now() >= deadline)
5629
+ return `retained principal "${principal}" is still live after its recorded seat ${reference.kind}:${reference.id} was reaped, so another process holds it and this runtime cannot authoritatively adopt it`;
5630
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
5631
+ }
5632
+ return undefined;
5633
+ }
5433
5634
  /** Validate/relaunch one retained inventory entry. Called only through resumePreserved so all
5434
5635
  * records pass the same preflight before the first child is exposed. */
5435
5636
  async resumePreservedAgent(entry, preflightOnly = false, batchReserved = false, prepared) {
@@ -5986,6 +6187,53 @@ export class Manager {
5986
6187
  void this.cancelAgentGoal(a.name, graceful ? "graceful" : "terminate"); // M4: cancel a live spawn goal
5987
6188
  return { ok: true, data: { name: a.name, stopped: true, graceful } };
5988
6189
  }
6190
+ /** Retire one agent this manager launched under a delegated user intent, through the host
6191
+ * (SPEC 13.16). It sends the user's retirement intent for the UID of the running slot or its
6192
+ * retirement hold, and only on `retired: true` for that exact target stops the slot and frees the
6193
+ * name; every other answer leaves the slot running or the name held. It never calls the holder's
6194
+ * own prepare or requester. */
6195
+ async retireDelegatedAgent(name, intentId) {
6196
+ const execute = this.remoteAuthority?.executeDelegatedUserIntent;
6197
+ if (!execute)
6198
+ return { ok: false, error: `this manager has no host execution for a delegated user intent; "${name}" was not retired` };
6199
+ const release = this.beginLifecycle();
6200
+ if (!release)
6201
+ return { ok: false, error: this.maintenanceError() };
6202
+ try {
6203
+ const live = this.agents.get(name);
6204
+ const held = live ? undefined : this.retiring.get(name);
6205
+ const principal = live?.delegated ? parsePrincipalKey(live.id) : held?.delegated ? held : null;
6206
+ const lifecycleUid = live?.lifecycleUid ?? held?.lifecycleUid;
6207
+ if (!principal || !lifecycleUid)
6208
+ return { ok: false, error: `no agent named "${name}" was launched on this manager under a delegated user intent` };
6209
+ const target = { owner: principal.owner, actor: principal.actor, lifecycleUid };
6210
+ try {
6211
+ const result = await execute({ intentId, operation: "retire", target });
6212
+ if (result.operation !== "retire" || result.intentId !== intentId || result.opId !== retireOpId(target.lifecycleUid) ||
6213
+ result.target.owner !== target.owner || result.target.actor !== target.actor || result.target.lifecycleUid !== target.lifecycleUid)
6214
+ throw new Error(`the host answered delegated intent ${intentId} with another execution`);
6215
+ if (result.retired !== true)
6216
+ return { ok: false, error: `the host did not confirm the delegated retirement of "${name}" at ${target.lifecycleUid}; "${name}" is unchanged` };
6217
+ }
6218
+ catch (e) {
6219
+ const error = `the delegated retirement under intent ${intentId} did not complete (${rejectionText(e)})`;
6220
+ const hold = this.retiring.get(name);
6221
+ if (hold?.lifecycleUid === target.lifecycleUid)
6222
+ hold.lastError = error;
6223
+ return { ok: false, error: `${error}; "${name}" is unchanged` };
6224
+ }
6225
+ // The host's retirement closed this UID's provider handle before its barrier, so the stop only
6226
+ // reaps the slot, and its hold clears with the confirmation.
6227
+ const slot = this.agents.get(name);
6228
+ if (slot?.lifecycleUid === target.lifecycleUid)
6229
+ this.despawnAuthorized(slot, true, false, `delegated retirement ${intentId}`);
6230
+ this.confirmRetirement({ id: principalKey(target.owner, target.actor).key, name, lifecycleUid: target.lifecycleUid });
6231
+ return { ok: true, data: { name, lifecycleUid: target.lifecycleUid, retired: true } };
6232
+ }
6233
+ finally {
6234
+ release();
6235
+ }
6236
+ }
5989
6237
  /** Resolve a v0.4 TARGET triple (owner, actor, lifecycleUid — broker-validated subject/body
5990
6238
  * agreement, currency re-checked by the serve boundary's resolver) to the live managed agent it
5991
6239
  * names. Static agents key `(DEV_OWNER, nkey)`; user-mode agents store the principal dot-form
@@ -6167,7 +6415,7 @@ export class Manager {
6167
6415
  try {
6168
6416
  report = await reconcileEndpointGate({
6169
6417
  kv: authKv, space: this.space, endpoint: MANAGER_ENDPOINT, instanceId,
6170
- probeHolder: makeManagerHolderLivenessProbe({ space: this.space, servers, auth, log }),
6418
+ probeHolder: makeManagerHolderLivenessProbe({ space: this.space, servers, auth, log, unreachableWaitMs: DELIVERY_ADMIN_BOOT_WAIT_MS }),
6171
6419
  evictHolders: makeManagerEndpointHolderEvictor({ space: this.space, servers, auth, log }),
6172
6420
  log,
6173
6421
  recordsKv,
@@ -6442,7 +6690,7 @@ export class Manager {
6442
6690
  // error text — a crash-restart never silently skips eviction (no-fallbacks).
6443
6691
  const barrier = endpointRegistrationBarrier(authKv, this.space, {
6444
6692
  endpoint: MANAGER_ENDPOINT, instanceId: iid, opId: registrationOpId,
6445
- ...(auth ? { evict: makeManagerEndpointEvictor({ space: this.space, servers: this.servers ?? DEFAULT_SERVER, auth, log: (line) => console.error(line) }) } : {}),
6693
+ ...(auth ? { evict: makeManagerEndpointEvictor({ space: this.space, servers: this.servers ?? DEFAULT_SERVER, auth, log: (line) => console.error(line), unreachableWaitMs: DELIVERY_ADMIN_BOOT_WAIT_MS }) } : {}),
6446
6694
  });
6447
6695
  const spec = { endpoint: MANAGER_ENDPOINT, owner: DEV_OWNER, clusterDigests: [artifacts.closureDigest], protocol: { v: 1 } };
6448
6696
  const { registrationRevision } = await registerServiceInstance(recordsKv, {