@cotal-ai/manager 0.45.0 → 0.47.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.
Files changed (43) hide show
  1. package/dist/commands.d.ts +1 -0
  2. package/dist/commands.d.ts.map +1 -1
  3. package/dist/commands.js +9 -9
  4. package/dist/commands.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/launch.js +2 -2
  10. package/dist/launch.js.map +1 -1
  11. package/dist/manager-service-contract.d.ts +21 -1
  12. package/dist/manager-service-contract.d.ts.map +1 -1
  13. package/dist/manager-service-contract.js +126 -3
  14. package/dist/manager-service-contract.js.map +1 -1
  15. package/dist/manager.d.ts +47 -7
  16. package/dist/manager.d.ts.map +1 -1
  17. package/dist/manager.js +263 -62
  18. package/dist/manager.js.map +1 -1
  19. package/dist/remote-register.d.ts +4 -0
  20. package/dist/remote-register.d.ts.map +1 -1
  21. package/dist/remote-register.js +7 -5
  22. package/dist/remote-register.js.map +1 -1
  23. package/dist/run-hosting.d.ts +114 -0
  24. package/dist/run-hosting.d.ts.map +1 -0
  25. package/dist/run-hosting.js +462 -0
  26. package/dist/run-hosting.js.map +1 -0
  27. package/dist/runtime/custodial-pty.d.ts +16 -0
  28. package/dist/runtime/custodial-pty.d.ts.map +1 -0
  29. package/dist/runtime/custodial-pty.js +40 -0
  30. package/dist/runtime/custodial-pty.js.map +1 -0
  31. package/dist/runtime/index.d.ts +3 -1
  32. package/dist/runtime/index.d.ts.map +1 -1
  33. package/dist/runtime/index.js +11 -2
  34. package/dist/runtime/index.js.map +1 -1
  35. package/dist/runtime/pty.d.ts +10 -6
  36. package/dist/runtime/pty.d.ts.map +1 -1
  37. package/dist/runtime/pty.js +12 -5
  38. package/dist/runtime/pty.js.map +1 -1
  39. package/dist/static-lifecycle.d.ts +35 -1
  40. package/dist/static-lifecycle.d.ts.map +1 -1
  41. package/dist/static-lifecycle.js +65 -0
  42. package/dist/static-lifecycle.js.map +1 -1
  43. package/package.json +10 -9
package/dist/manager.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash, randomUUID, randomBytes } from "node:crypto";
3
3
  import { hostname } from "node:os";
4
- import { connect, credsAuthenticator } from "@nats-io/transport-node";
4
+ import { credsAuthenticator } from "@nats-io/transport-node";
5
5
  import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
6
6
  import { join, dirname, resolve } from "node:path";
7
- import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } from "@cotal-ai/core";
7
+ import { CotalEndpoint, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, mintCreds, mintLifecycleUid, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, parseShareSelection, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, epRequestSubject, epCallerReplyFilter, parseEpSubject, controlServiceSubject, eventChannelPrincipal, } 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, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveOnPath, saveManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
- import { createRuntime, } from "./runtime/index.js";
9
+ import { createRuntime, requireRuntimeAdopt, } from "./runtime/index.js";
10
10
  import { AttachEndpoint } from "./attach-endpoint.js";
11
11
  import { makeManagerEndpointEvictionEvidence, makeManagerEndpointEvictor } from "./endpoint-evict.js";
12
12
  import { makeManagerHolderLivenessProbe } from "./holder-liveness.js";
@@ -14,6 +14,7 @@ import { GateReconcileRefused, reconcileEndpointGate } from "./reconcile-gate.js
14
14
  import { launchSpecForRun, materializePersona, launchAgentToStartOpts, parseLaunchSpec, persistLaunchSpec } from "./launch.js";
15
15
  import { authorizeLaunch, authorizeNamedControl } from "./authorize.js";
16
16
  import { controlShutdown } from "./control-shutdown.js";
17
+ import { RunHosting } from "./run-hosting.js";
17
18
  import { controlSession } from "./control-session.js";
18
19
  import { parseResumeCommitArgs, parseResumeControlArgs, parseResumeFinalizeArgs } from "./resume.js";
19
20
  // Unit B (the static §13.1 lifecycle executor): the shared grammar/stores from core plus the
@@ -30,7 +31,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
30
31
  // the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
31
32
  import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, registerServiceInstance, deregisterServiceInstance, authorizeServeGrant, writeServiceStatus, SERVICE_READY, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, renderLifecycleBlocked, } from "@cotal-ai/core";
32
33
  import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
33
- import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, } from "./static-lifecycle.js";
34
+ import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, observeStaticSlot, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
34
35
  /** Concurrency ceiling — the manager refuses to hold more than this many live + in-flight +
35
36
  * cooling slots at once (P4a). Bounds a fork-bomb: spawn is a full agent process per call. */
36
37
  const MAX_AGENTS = 50;
@@ -136,6 +137,8 @@ const FREE_SLOT_CAUSE_TEXT = {
136
137
  "process-exit": "its own process exited and this manager did not stop it",
137
138
  "pi-crash-loop": "this manager retired it after a Pi crash loop",
138
139
  "pi-recovery-failed": "this manager retired it after Pi session recovery failed",
140
+ "supervise-crash-loop": "this manager retired it after its supervise restart budget was spent",
141
+ "supervise-recovery-failed": "this manager retired it after a supervised restart failed",
139
142
  "session-bind-failed": "this manager stopped it: its host session could not be bound at launch",
140
143
  "resume-session-rebind-failed": "this manager stopped it: its host session could not be rebound on resume",
141
144
  };
@@ -319,6 +322,10 @@ export class Manager {
319
322
  * manager reads its OWN `epgate.<e>.<iid>` epoch over this connection before a terminal commit
320
323
  * and skips a superseded commit (the fast-fail belt paired with the (b) barrier-revoke fence). */
321
324
  goalWriter;
325
+ /** The workflow-run host (SPEC 14.3): the drives this incarnation holds, each on its own
326
+ * per-run credential and connection. Absent under a remote authority, which mints no driver
327
+ * credentials, so the `run-*` family refuses there rather than connecting on a weaker identity. */
328
+ runHosting;
322
329
  /** P2 item 2 must-5 (b): the STABLE goal-writer identity (auth mode) — minted once at
323
330
  * registration alongside the serve identity; a renewal re-mints the SAME nkey with a fresh
324
331
  * bounded exp and re-stages its distinct credId into the §13.1 revocation family. The current
@@ -455,6 +462,17 @@ export class Manager {
455
462
  resumeDurableCommitToken;
456
463
  resumedAgentNames = new Set();
457
464
  remoteAuthority;
465
+ /** Every broker dial this manager makes, through ONE transport decision.
466
+ *
467
+ * `this.servers` is whatever the mesh record held (`cotal supervise` reads it from there), so it
468
+ * can be a `ws://`/`wss://` broker published through an HTTPS edge. The raw node transport
469
+ * refuses such a URL instead of dialing it, so the scheme picks the dialer here rather than at
470
+ * thirteen call sites. `dialerFor` also owns the websocket transport's options, which is why
471
+ * callers below still pass their own auth options unchanged. */
472
+ dial(opts) {
473
+ const servers = this.servers ?? DEFAULT_SERVER;
474
+ return dialerFor(servers)({ ...opts, servers });
475
+ }
458
476
  constructor(opts) {
459
477
  this.space = opts.space;
460
478
  this.servers = opts.servers;
@@ -495,6 +513,10 @@ export class Manager {
495
513
  get runtimeKind() {
496
514
  return this.runtime.kind;
497
515
  }
516
+ /** Reattach this manager's runtime to a durable handle. Refuses by name when adopt is absent. */
517
+ adoptRuntimeHandle(reference) {
518
+ return requireRuntimeAdopt(this.runtime, reference);
519
+ }
498
520
  /** The console page URL (manager-hosted, loopback). */
499
521
  get consoleUrl() {
500
522
  return this.attach.consoleUrl();
@@ -699,6 +721,25 @@ export class Manager {
699
721
  // BEFORE spawn-as-action begins accepting (the goalReconcileDone gate) — a fresh incarnation
700
722
  // never drops a goal a dead predecessor accepted. Never fatal; the gate opens either way.
701
723
  await this.reconcileGoalIndex();
724
+ // SPEC 14.3: the manager hosts workflow runs. Stood up AFTER registration (it names this
725
+ // instance's coordinates) and reconciled AFTER the goal index, for the same reason: a fresh
726
+ // incarnation takes back every run a dead predecessor was driving before it accepts new ones.
727
+ // The serve surface is already live by here, so the family itself holds the gate: `runHost()`
728
+ // refuses `run-start`/`run-resume` as `unavailable` until the host exists and `RunHosting`
729
+ // refuses them until its reconcile has returned. A user-auth mesh stands no host up at all
730
+ // (`runHost()` names why); a remote-authority manager holds no signer to mint with.
731
+ if (!this.remoteAuthority && !this.userMode) {
732
+ this.runHosting = new RunHosting({
733
+ space: this.space,
734
+ servers: this.servers,
735
+ endpoint: MANAGER_ENDPOINT,
736
+ instanceId: this.managerInstanceId,
737
+ holder: { id: this.ep.ref().id, lifecycleUid: this.managerLifecycleUid },
738
+ auth: this.auth,
739
+ log: (line) => console.error(line),
740
+ });
741
+ await this.runHosting.reconcile();
742
+ }
702
743
  // Plane-3 (durable backstop) is NOT the manager's job — the manager only manages agent lifecycle.
703
744
  // The server-side delivery daemon hosts the fan-out writer + trusted reader, owns the durable
704
745
  // membership registry, and serves the runtime durable join/leave/list ops (on `ctl.delivery`). The
@@ -835,6 +876,9 @@ export class Manager {
835
876
  // scoped executor. Without this the standing session-ledger connection dies at its TTL and
836
877
  // `attach` stops establishing sessions until a restart. The connection's authenticator presents
837
878
  // the refreshed credential on its next (re)connect.
879
+ // SPEC 14.6: every hosted drive's per-run `run-driver` credential is the manager's to renew
880
+ // for the same nkey; a run parked in a pause for days must not die at the credential's TTL.
881
+ await this.runHosting?.renew();
838
882
  if (this.sessionLedgerConn && this.sessionLedgerCreds && this.auth) {
839
883
  const sw = this.sessionLedgerConn;
840
884
  try {
@@ -1121,8 +1165,6 @@ export class Manager {
1121
1165
  async awaitHandleExit(handle) {
1122
1166
  if (!handle.waitForExit)
1123
1167
  throw new Error(`runtime "${handle.kind}" cannot prove child exit (AgentHandle.waitForExit is not implemented)`);
1124
- if (handle.status() === "exited")
1125
- return;
1126
1168
  await withTimeout(handle.waitForExit(), this.preserveStopTimeoutMs, `child did not exit within ${this.preserveStopTimeoutMs}ms`);
1127
1169
  if (handle.status() !== "exited")
1128
1170
  throw new Error(`runtime "${handle.kind}" reported exit completion but status is still running`);
@@ -1336,6 +1378,10 @@ export class Manager {
1336
1378
  // in-flight command can write a status back onto the record it just removed.
1337
1379
  const registered = this.serviceServe !== undefined;
1338
1380
  await this.stopServiceServe();
1381
+ // The drives AFTER the serve loop (no new `run-start` can land) and BEFORE deregistration: a
1382
+ // released run's status write is the last thing this incarnation says about it.
1383
+ await this.runHosting?.stop();
1384
+ this.runHosting = undefined;
1339
1385
  if (registered)
1340
1386
  await this.deregisterServiceOnStop();
1341
1387
  await this.stopGoalWriter();
@@ -1759,6 +1805,35 @@ export class Manager {
1759
1805
  * cross-owner persona writes - an operator redefines via config, not the wire), where the ctl
1760
1806
  * admin tier allowed operator cross-owner redefine; (2) launch is owner-equality-only, above.
1761
1807
  * Both are least-privilege reductions, never widenings. */
1808
+ /** The run host, or one of three refusals. A remote-authority manager holds no space signer, so
1809
+ * it cannot mint the per-run driver credential SPEC 14.6 requires, and hosting on any other
1810
+ * identity would be the fallback this tree does not take: `unimplemented`, for good. A
1811
+ * user-auth mesh is `unimplemented` too, for a different reason it names: a hosted run's seats
1812
+ * are spawned, turned and despawned by a caller derived from the run id under the static
1813
+ * owner, which the user-mode spawn door refuses (no `u_` owner), so a program would fail at
1814
+ * its first seat; no path to `--local` is offered there since a user bearer holds no run rows
1815
+ * either. An ordinary manager whose host is not standing yet is still booting (the serve
1816
+ * surface comes up before the host): `unavailable`, retry. The three are told apart, since
1817
+ * the first sentence steers a caller to `--local` and the others must not. */
1818
+ runHost() {
1819
+ if (this.runHosting)
1820
+ return this.runHosting;
1821
+ if (this.remoteAuthority)
1822
+ throw new EpEnvelopeError("unimplemented", "this manager does not host workflow runs: a remote-authority manager mints no run-driver credentials (SPEC 14.6); drive the run from a terminal with `cotal run start --local --file <program>`");
1823
+ if (this.userMode)
1824
+ throw new EpEnvelopeError("unimplemented", `user-auth space "${this.space}" hosts no workflow runs yet: a hosted run's seats would be spawned under the static owner, which a user mesh refuses; run programs on a static-auth mesh`);
1825
+ throw new EpEnvelopeError("unavailable", "the manager is still booting its workflow-run host; retry shortly (SPEC 14.3)");
1826
+ }
1827
+ /** The answerer a `run-answer` records (SPEC 14.5): the caller as this manager knows them. A
1828
+ * managed seat is named by its persona name; any other authenticated caller (an operator
1829
+ * instrument, a logged-in user) by its principal. Never the request body's word. */
1830
+ runAnswerer(ctx) {
1831
+ const caller = principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
1832
+ for (const a of this.agents.values())
1833
+ if (this.managedPrincipal(a) === caller)
1834
+ return a.name;
1835
+ return caller;
1836
+ }
1762
1837
  managerServiceDefs() {
1763
1838
  const args = (ctx) => (ctx.request.args ?? {});
1764
1839
  const callerOf = (ctx) => principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
@@ -1794,8 +1869,34 @@ export class Manager {
1794
1869
  inspect: (ctx) => this.serveGated(ctx, async () => {
1795
1870
  const name = String(args(ctx).name ?? "").trim();
1796
1871
  const row = this.list(await this.psOwnerFilter(callerOf(ctx), false)).find((x) => x.name === name);
1797
- if (!row)
1872
+ if (!row) {
1873
+ // The live hit above stays entirely local. Only a miss widens into durable static state,
1874
+ // where `not-found` is honest only after the slot read itself succeeds and returns absent.
1875
+ // User-mode lifecycles have no manager-local mgrslot row; their per-name projection remains
1876
+ // the live map until the auth service exposes an equivalent mediated reader.
1877
+ if (this.auth && !this.userMode) {
1878
+ const recordsKv = this.goalWriter?.ctx.kv;
1879
+ if (!recordsKv)
1880
+ throw new EpEnvelopeError("unavailable", `the durable static slot store is not ready while inspecting "${name}"`, [
1881
+ { kind: STATIC_SLOT_READ_FAILED_DETAIL, name, record: "slot", operation: "read" },
1882
+ ]);
1883
+ let observed;
1884
+ try {
1885
+ observed = await observeStaticSlot(recordsKv, DEV_OWNER, name, this.managerInstanceId);
1886
+ }
1887
+ catch (error) {
1888
+ throw new EpEnvelopeError("unavailable", `the durable static slot state for "${name}" could not be read: ${error.message}`, [
1889
+ { kind: STATIC_SLOT_READ_FAILED_DETAIL, name, record: error instanceof StaticSlotReadError ? error.record : "slot-or-head", operation: "read" },
1890
+ ]);
1891
+ }
1892
+ // A retired row affirms that no agent exists now, so it keeps the old `not-found`
1893
+ // result. The changed refusal is reserved for durable NONTERMINAL state that contradicts
1894
+ // the live-map miss and needs operator attention.
1895
+ if (observed !== undefined && observed.slotPhase !== "retired")
1896
+ throw new EpEnvelopeError("failed-precondition", `no live agent "${name}"; durable state contradicts absence ${renderStaticSlotObservation(observed)}`, [observed]);
1897
+ }
1798
1898
  throw new EpEnvelopeError("not-found", `no agent "${name}"`);
1899
+ }
1799
1900
  return row;
1800
1901
  }),
1801
1902
  models: (ctx) => this.serveGated(ctx, async () => {
@@ -1873,6 +1974,13 @@ export class Manager {
1873
1974
  resumePreserved: (ctx) => adminGated(ctx, async () => unwrap(await this.opResumePreserved(args(ctx)))),
1874
1975
  commitResume: (ctx) => adminGated(ctx, async () => unwrap(await this.opCommitResume(args(ctx)))),
1875
1976
  finalizeResume: (ctx) => adminGated(ctx, async () => unwrap(await this.opFinalizeResume(args(ctx)))),
1977
+ // The workflow-run family (SPEC 14.3): the manager hosts the driver. Reach is the broker's
1978
+ // (`run` capability / privileged instrument rows); the serve gate is the maintenance fence.
1979
+ runStart: (ctx) => this.serveGated(ctx, () => this.runHost().start(args(ctx))),
1980
+ runResume: (ctx) => this.serveGated(ctx, () => this.runHost().resume(args(ctx))),
1981
+ runAnswer: (ctx) => this.serveGated(ctx, () => this.runHost().answer(args(ctx), this.runAnswerer(ctx))),
1982
+ runStatus: (ctx) => this.serveGated(ctx, () => this.runHost().status(args(ctx))),
1983
+ runPs: (ctx) => this.serveGated(ctx, () => this.runHost().list(args(ctx))),
1876
1984
  preparePreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("preparePreservation", args(ctx)))),
1877
1985
  commitPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("commitPreservation", args(ctx)))),
1878
1986
  abortPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("abortPreservation", args(ctx)))),
@@ -2101,7 +2209,7 @@ export class Manager {
2101
2209
  // vocabulary as static capabilities; the broker maps them to the ctl tiers. `role:<r>` tokens
2102
2210
  // pass through too (a persona may hold delegable roles) — the ledger's envelope walk still
2103
2211
  // attenuates every one of these against the spawner chain.
2104
- const scope = (opts.capabilities ?? []).filter((c) => c === "spawn" || c === "admin" || /^role:[A-Za-z0-9_-]+$/.test(c));
2212
+ const scope = (opts.capabilities ?? []).filter((c) => c === "spawn" || c === "run" || c === "admin" || /^role:[A-Za-z0-9_-]+$/.test(c));
2105
2213
  // The manager's ONE store (injected for a hosted composition, workstation FS locally). A hosted
2106
2214
  // user-mode spawn reads the callout material from it — the same store the auth-store kinds
2107
2215
  // (callout/issuer/…) were migrated onto — so this is no longer a local-only path.
@@ -2441,7 +2549,7 @@ export class Manager {
2441
2549
  const creds = await mintCreds(this.auth, newIdentity(), "retirement-requester", {
2442
2550
  retirementRequester: { ...caller, target: { owner: target.owner, actor: target.actor, lifecycleUid: a.lifecycleUid } },
2443
2551
  });
2444
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, authenticator: credsAuthenticator(new TextEncoder().encode(creds)), maxReconnectAttempts: 0 });
2552
+ const nc = await this.dial({ authenticator: credsAuthenticator(new TextEncoder().encode(creds)), maxReconnectAttempts: 0 });
2445
2553
  try {
2446
2554
  // §13.2 nonce: >=128 bits of CSPRNG entropy, base64url (the `endpoint-invoke` idiom).
2447
2555
  const nonce = randomBytes(24).toString("base64url");
@@ -2635,9 +2743,10 @@ export class Manager {
2635
2743
  }
2636
2744
  throw new Error(`replacement did not prove session ${expected} (${last})`);
2637
2745
  }
2638
- /** Restart one continuation-capable managed process in place. Identity, lifecycle, credentials,
2639
- * durables, children, and the manager row remain owned; only the process handle/control endpoint
2640
- * change. A fourth crash inside two minutes is a loop and falls through to normal retirement. */
2746
+ /** Restart one managed process in place. Identity, lifecycle, credentials, durables, children,
2747
+ * and the manager row remain owned; only the process handle/control endpoint change. Budget
2748
+ * comes from `restart.policy` when spawn carried `supervise`; otherwise the Pi session-recovery
2749
+ * constants. Spending the budget falls through to normal retirement. */
2641
2750
  recoverManagedSession(a) {
2642
2751
  const restart = a.restart;
2643
2752
  if (!restart || !restart.armed || restart.recovering || a.terminalizing)
@@ -2646,12 +2755,15 @@ export class Manager {
2646
2755
  if (!release)
2647
2756
  return; // preservation owns the cut once the lifecycle fence closes
2648
2757
  const now = Date.now();
2649
- restart.crashes = restart.crashes.filter((at) => now - at < SESSION_RESTART_WINDOW_MS);
2758
+ const limit = restart.policy?.restarts ?? SESSION_RESTART_LIMIT;
2759
+ const windowMs = restart.policy?.windowMs ?? SESSION_RESTART_WINDOW_MS;
2760
+ const supervised = restart.policy !== undefined;
2761
+ restart.crashes = restart.crashes.filter((at) => now - at < windowMs);
2650
2762
  restart.crashes.push(now);
2651
- if (restart.crashes.length > SESSION_RESTART_LIMIT) {
2652
- console.error(`! ${a.name}: Pi crash loop (${restart.crashes.length} crashes in ${SESSION_RESTART_WINDOW_MS / 1000}s) - retiring the managed seat`);
2763
+ if (restart.crashes.length > limit) {
2764
+ console.error(`! ${a.name}: ${supervised ? "supervised" : "Pi"} crash loop (${restart.crashes.length} crashes in ${windowMs / 1000}s) - retiring the managed seat`);
2653
2765
  restart.armed = false;
2654
- this.freeSlot(a, true, "pi-crash-loop");
2766
+ this.freeSlot(a, true, supervised ? "supervise-crash-loop" : "pi-crash-loop");
2655
2767
  this.reapChildrenOf(this.managedPrincipal(a));
2656
2768
  release();
2657
2769
  return;
@@ -2660,21 +2772,40 @@ export class Manager {
2660
2772
  void (async () => {
2661
2773
  let replacement;
2662
2774
  try {
2663
- const sessionId = this.readManagedSession(a);
2664
2775
  const connector = await this.resolveConnector(a.agent);
2665
- if (!connector.supportsSessionContinuation)
2666
- throw new Error(`connector ${connector.name} no longer declares same-session continuation`);
2667
- const opts = {
2668
- ...restart.opts,
2669
- resume: undefined,
2670
- prompt: undefined,
2671
- continueSession: sessionId,
2672
- };
2776
+ const continueSession = connector.supportsSessionContinuation ? this.readManagedSession(a) : undefined;
2777
+ const opts = continueSession !== undefined
2778
+ ? { ...restart.opts, resume: undefined, prompt: undefined, continueSession }
2779
+ : { ...restart.opts, resume: undefined, prompt: undefined };
2673
2780
  const spec = connector.buildLaunch(opts);
2781
+ const wanted = this.managedPrincipal(a);
2782
+ const joinedAfter = this.ep.getRoster()
2783
+ .filter((p) => p.card.id === wanted && p.lifecycleUid === a.lifecycleUid)
2784
+ .reduce((max, p) => Math.max(max, p.ts), 0) + 1;
2674
2785
  const handle = this.runtime.spawn(a.name, spec, a.launch.cwd);
2675
2786
  replacement = handle;
2676
2787
  restart.sessionStatePath = spec.sessionStatePath ?? restart.sessionStatePath;
2677
- await this.awaitRecoveredSession(a, sessionId, handle, spec.control);
2788
+ if (continueSession !== undefined)
2789
+ await this.awaitRecoveredSession(a, continueSession, handle, spec.control);
2790
+ else {
2791
+ const previousHandle = a.handle;
2792
+ const previousControl = a.control;
2793
+ a.handle = handle;
2794
+ a.control = spec.control;
2795
+ try {
2796
+ const readiness = await this.awaitReadiness(a, connector.readinessTimeoutMs ?? this.readinessTimeoutMs, {
2797
+ reapOnExit: false,
2798
+ joinedAfter,
2799
+ });
2800
+ if (!readiness.ok)
2801
+ throw new Error(readiness.detail);
2802
+ }
2803
+ catch (error) {
2804
+ a.handle = previousHandle;
2805
+ a.control = previousControl;
2806
+ throw error;
2807
+ }
2808
+ }
2678
2809
  if (this.agents.get(a.name) !== a || a.terminalizing) {
2679
2810
  try {
2680
2811
  handle.stop({ graceful: false });
@@ -2687,7 +2818,10 @@ export class Manager {
2687
2818
  replacement = undefined;
2688
2819
  restart.opts = opts;
2689
2820
  restart.recovering = false;
2690
- console.error(`! ${a.name}: recovered Pi session ${sessionId} after crash (${restart.crashes.length}/${SESSION_RESTART_LIMIT})`);
2821
+ if (continueSession !== undefined)
2822
+ console.error(`! ${a.name}: recovered Pi session ${continueSession} after crash (${restart.crashes.length}/${limit})`);
2823
+ else
2824
+ console.error(`! ${a.name}: restarted under the same lifecycle after crash (${restart.crashes.length}/${limit})`);
2691
2825
  this.watchExit(a);
2692
2826
  }
2693
2827
  catch (error) {
@@ -2698,14 +2832,14 @@ export class Manager {
2698
2832
  tail = this.tail(await (replacement ?? a.handle).attach().backlog());
2699
2833
  }
2700
2834
  catch { /* runtime has no readable tail */ }
2701
- console.error(`! ${a.name}: Pi session recovery failed: ${error.message}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
2702
- // The replacement may be alive but unable to prove the expected session. Stop it BEFORE
2835
+ console.error(`! ${a.name}: ${supervised ? "supervised restart" : "Pi session recovery"} failed: ${error.message}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
2836
+ // The replacement may be alive but unable to prove readiness. Stop it BEFORE
2703
2837
  // retiring credentials/durables; otherwise an untracked process survives under torn auth.
2704
2838
  try {
2705
2839
  replacement?.stop({ graceful: false });
2706
2840
  }
2707
2841
  catch { /* terminal cleanup continues */ }
2708
- this.freeSlot(a, true, "pi-recovery-failed");
2842
+ this.freeSlot(a, true, supervised ? "supervise-recovery-failed" : "pi-recovery-failed");
2709
2843
  this.reapChildrenOf(this.managedPrincipal(a));
2710
2844
  }
2711
2845
  finally {
@@ -2714,13 +2848,23 @@ export class Manager {
2714
2848
  })();
2715
2849
  }
2716
2850
  /** A managed agent's process exited on its own (crash, /exit, finished). Continuation-capable Pi
2717
- * seats restart in place after readiness; every other exit follows the existing terminal path. */
2851
+ * seats restart in place after readiness; a spawn carrying `supervise` restarts any connector
2852
+ * the same way, without classifying a session-state file. Every other exit follows the existing
2853
+ * terminal path. */
2718
2854
  onAgentExit(a) {
2719
2855
  // Preservation owns the child-stop snapshot. Exit watchers must neither delete that snapshot nor
2720
2856
  // trigger normal deprovision/reap while the cut is being formed.
2721
2857
  if (this.maintenanceState !== "active")
2722
2858
  return;
2859
+ // A replacement is proving readiness under this row. Its own wait owns a failed
2860
+ // relaunch; treating that exit as a seat death would free the slot mid-recovery.
2861
+ if (a.restart?.recovering && !a.terminalizing)
2862
+ return;
2723
2863
  if (a.restart?.armed && !a.terminalizing) {
2864
+ if (a.restart.policy !== undefined) {
2865
+ this.recoverManagedSession(a);
2866
+ return;
2867
+ }
2724
2868
  try {
2725
2869
  if (this.readManagedSessionState(a).status === "running") {
2726
2870
  this.recoverManagedSession(a);
@@ -2808,6 +2952,21 @@ export class Manager {
2808
2952
  // scalar/array (the CLI never does). Core doesn't interpret the keys; the connector validates them.
2809
2953
  if (args.launchOptions !== undefined && (typeof args.launchOptions !== "object" || args.launchOptions === null || Array.isArray(args.launchOptions)))
2810
2954
  return Promise.resolve({ ok: false, error: "launchOptions: expected a key:value mapping" });
2955
+ let supervise;
2956
+ if (args.supervise !== undefined) {
2957
+ const raw = args.supervise;
2958
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
2959
+ return Promise.resolve({ ok: false, error: "supervise: expected { restarts, windowMs }" });
2960
+ const rec = raw;
2961
+ const extra = Object.keys(rec).filter((k) => k !== "restarts" && k !== "windowMs");
2962
+ if (extra.length > 0)
2963
+ return Promise.resolve({ ok: false, error: `supervise: unknown key ${extra[0]}; it takes restarts and windowMs` });
2964
+ if (typeof rec.restarts !== "number" || !Number.isInteger(rec.restarts) || rec.restarts < 1)
2965
+ return Promise.resolve({ ok: false, error: "supervise.restarts: expected a positive integer" });
2966
+ if (typeof rec.windowMs !== "number" || !Number.isInteger(rec.windowMs) || rec.windowMs < 1)
2967
+ return Promise.resolve({ ok: false, error: "supervise.windowMs: expected a positive integer" });
2968
+ supervise = { restarts: rec.restarts, windowMs: rec.windowMs };
2969
+ }
2811
2970
  // ACL overrides arrive as string arrays or not at all — a malformed value is a bad request,
2812
2971
  // not something to coerce (no fallbacks).
2813
2972
  const strList = (v, flag) => {
@@ -2844,6 +3003,7 @@ export class Manager {
2844
3003
  allowSubscribe,
2845
3004
  allowPublish,
2846
3005
  shareTools: args.shareTools !== undefined ? String(args.shareTools) : undefined,
3006
+ ...(supervise !== undefined ? { supervise } : {}),
2847
3007
  }, caller, hooks);
2848
3008
  }
2849
3009
  /** Resolve a connector by agent type. Library composition (installedExtensions off) → a registry
@@ -3165,6 +3325,16 @@ export class Manager {
3165
3325
  // reject-before-side-effects window as the harness preflight above; buildLaunch stays the backstop.
3166
3326
  if (opts.resume && !connector.supportsResume)
3167
3327
  return { ok: false, error: `${agent} connector does not support resuming an existing session (resume)` };
3328
+ // A restart policy this host cannot honour is refused at accept, never accepted and ignored.
3329
+ // External runtimes (tmux/cmux/orca/herdr) attach to a process they do not own and stream no
3330
+ // exit, so a name cannot be respawned in place. User-mode seats have no static slot that
3331
+ // keeps the incarnation owned across a process death, so the same refusal applies there.
3332
+ if (opts.supervise !== undefined) {
3333
+ if (this.runtime.kind !== "pty")
3334
+ return { ok: false, error: `supervise is a restart policy this host cannot enforce: runtime "${this.runtime.kind}" cannot respawn a name in place` };
3335
+ if (this.userMode)
3336
+ return { ok: false, error: "supervise is a restart policy this host cannot enforce: a user-mode seat has no static slot to keep the incarnation owned across a process death" };
3337
+ }
3168
3338
  // Resolve the launch profile: IDENTITY (free-form `name:`) + role + read/post ACL + capabilities
3169
3339
  // + model/variant. Either from a fully-resolved manifest launch object (`opts.resolved`, whose `config`
3170
3340
  // is a materialized transient persona — the file is NOT the access authority), or from the
@@ -3577,8 +3747,17 @@ export class Manager {
3577
3747
  ? Object.keys(opts.launchOptions).sort()
3578
3748
  : undefined,
3579
3749
  },
3580
- ...(connector.supportsSessionContinuation
3581
- ? { restart: { opts: launchOpts, sessionStatePath: spec.sessionStatePath, crashes: [], recovering: false, armed: false } }
3750
+ ...(connector.supportsSessionContinuation || opts.supervise !== undefined
3751
+ ? {
3752
+ restart: {
3753
+ opts: launchOpts,
3754
+ sessionStatePath: spec.sessionStatePath,
3755
+ crashes: [],
3756
+ recovering: false,
3757
+ armed: false,
3758
+ ...(opts.supervise !== undefined ? { policy: opts.supervise } : {}),
3759
+ },
3760
+ }
3582
3761
  : {}),
3583
3762
  };
3584
3763
  // Unit B: the DURABLE slot takes the `active` phase before the in-memory row takes the
@@ -3621,16 +3800,21 @@ export class Manager {
3621
3800
  return { ok: false, error: readiness.detail };
3622
3801
  }
3623
3802
  if (managed.restart) {
3624
- try {
3625
- await this.armSessionRecovery(managed);
3626
- managed.launch.sessionId = this.readManagedSession(managed);
3803
+ if (connector.supportsSessionContinuation) {
3804
+ try {
3805
+ await this.armSessionRecovery(managed);
3806
+ managed.launch.sessionId = this.readManagedSession(managed);
3807
+ }
3808
+ catch (error) {
3809
+ const detail = `${managed.name} joined, but its exact host session could not be bound for supervised recovery: ${error.message}`;
3810
+ this.stopHandle(managed, false);
3811
+ this.freeSlot(managed, true, "session-bind-failed");
3812
+ await hooks?.onOutcome?.({ kind: "failed", data: { error: detail } });
3813
+ return { ok: false, error: detail };
3814
+ }
3627
3815
  }
3628
- catch (error) {
3629
- const detail = `${managed.name} joined, but its exact host session could not be bound for supervised recovery: ${error.message}`;
3630
- this.stopHandle(managed, false);
3631
- this.freeSlot(managed, true, "session-bind-failed");
3632
- await hooks?.onOutcome?.({ kind: "failed", data: { error: detail } });
3633
- return { ok: false, error: detail };
3816
+ else {
3817
+ managed.restart.armed = true;
3634
3818
  }
3635
3819
  }
3636
3820
  this.watchExit(managed);
@@ -4184,7 +4368,7 @@ export class Manager {
4184
4368
  * `"presence"` event is only a wake; the roster is re-read as the source of truth (subscribe-then-check
4185
4369
  * catches a join/exit that landed before we subscribed). Runtimes that stream no exit signal (external surfaces,
4186
4370
  * whose `attach()` throws) race presence-vs-backstop only — better than the old "assume up". */
4187
- async awaitReadiness(a, readinessTimeoutMs) {
4371
+ async awaitReadiness(a, readinessTimeoutMs, opts = {}) {
4188
4372
  let session;
4189
4373
  try {
4190
4374
  session = a.handle.attach();
@@ -4206,16 +4390,25 @@ export class Manager {
4206
4390
  // claims. The manager threads the uid into EVERY mode's launch (open included), so the child
4207
4391
  // adopts it over a self-mint and publishes it in presence; the uid is absent only from a peer
4208
4392
  // the manager never launched (a pure operator/daemon connection that never registers).
4209
- const joined = () => this.ep.getRoster().some((p) => p.card.id === wanted && p.status !== "offline" && p.lifecycleUid === a.lifecycleUid);
4393
+ // A supervised restart keeps the same principal+uid, so a SIGKILL'd child's still-live
4394
+ // presence row would otherwise satisfy this equality. `joinedAfter` is one millisecond
4395
+ // past that row's last heartbeat: only a later heartbeat counts as THIS replacement joining.
4396
+ const joined = () => this.ep.getRoster().some((p) => p.card.id === wanted
4397
+ && p.status !== "offline"
4398
+ && p.lifecycleUid === a.lifecycleUid
4399
+ && (opts.joinedAfter === undefined || p.ts >= opts.joinedAfter));
4210
4400
  return await new Promise((resolve) => {
4211
4401
  let done = false;
4212
4402
  let timer;
4213
4403
  let unsubExit = () => { };
4404
+ let joinedPoll;
4214
4405
  const finish = (r) => {
4215
4406
  if (done)
4216
4407
  return;
4217
4408
  done = true;
4218
4409
  clearTimeout(timer);
4410
+ if (joinedPoll !== undefined)
4411
+ clearInterval(joinedPoll);
4219
4412
  this.ep.off("presence", onPresence);
4220
4413
  unsubExit();
4221
4414
  resolve(r);
@@ -4224,6 +4417,11 @@ export class Manager {
4224
4417
  if (joined())
4225
4418
  finish({ ok: true });
4226
4419
  };
4420
+ if (opts.joinedAfter !== undefined) {
4421
+ // Heartbeats bump roster.ts without emitting "presence" (same status/uid/activity).
4422
+ // A supervised restart needs that bump, so poll joined() rather than waiting on the event.
4423
+ joinedPoll = setInterval(onPresence, 50);
4424
+ }
4227
4425
  // Process exit → failed. Clear the backstop FIRST (synchronously) so it can't resolve UNCERTAIN while
4228
4426
  // the backlog reads async — the process is known dead, that's a failure, not an unknown. Reap through
4229
4427
  // onAgentExit so a child the launcher spawned in the window is reaped too.
@@ -4255,8 +4453,14 @@ export class Manager {
4255
4453
  // └──────────────────────────────────────────────────────────────────────────────────────┘
4256
4454
  const deliberate = a.terminalizing === true;
4257
4455
  void (async () => {
4258
- const tail = this.tail(await s.backlog());
4259
- this.onAgentExit(a);
4456
+ // waitForExit may close the attach stream before this snapshot
4457
+ let tail = "";
4458
+ try {
4459
+ tail = this.tail(await s.backlog());
4460
+ }
4461
+ catch { }
4462
+ if (opts.reapOnExit !== false)
4463
+ this.onAgentExit(a);
4260
4464
  // A DELIBERATE STOP IS NOT A LAUNCH FAILURE. The despawn path owns this goal's terminal
4261
4465
  // and commits `cancel`; reporting `failed` here races it and, when it wins, tells the
4262
4466
  // caller the agent died on launch when in fact an operator cancelled it. The process
@@ -4379,7 +4583,7 @@ export class Manager {
4379
4583
  const creds = await mintCreds(this.auth, identity, "lifecycle-executor", {
4380
4584
  lifecycleExecutor: { owner: pin.owner, actor: pin.actor, lifecycleUid: pin.lifecycleUid, alias: pin.alias },
4381
4585
  });
4382
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4586
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4383
4587
  try {
4384
4588
  const kvm = new Kvm(nc);
4385
4589
  const recordsKv = await kvm.open(recordsBucket(this.space));
@@ -4404,7 +4608,7 @@ export class Manager {
4404
4608
  : undefined);
4405
4609
  if (!creds)
4406
4610
  throw new Error("withEndpointServeExecutor: no scoped executor authority (an open mesh must use the bare path)");
4407
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4611
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4408
4612
  try {
4409
4613
  const kvm = new Kvm(nc);
4410
4614
  return await fn({ recordsKv: await kvm.open(recordsBucket(this.space)), authKv: await kvm.open(epAuthBucket(this.space)), nc });
@@ -4420,7 +4624,7 @@ export class Manager {
4420
4624
  async withOpenServeConnection(fn) {
4421
4625
  if (this.auth)
4422
4626
  throw new Error("withOpenServeConnection: an auth mesh must use the scoped endpoint-serve executor");
4423
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, maxReconnectAttempts: 0 });
4627
+ const nc = await this.dial({ maxReconnectAttempts: 0 });
4424
4628
  try {
4425
4629
  const kvm = new Kvm(nc);
4426
4630
  // An open mesh may be a RAW broker (no `cotal up` provisioning ran), and `Kvm.open` binds
@@ -4439,6 +4643,7 @@ export class Manager {
4439
4643
  return {
4440
4644
  instanceId: this.managerInstanceId,
4441
4645
  runtime: this.runtime.kind,
4646
+ custody: process.platform === "linux" && this.runtime.kind === "pty" ? "custodied" : "legacy",
4442
4647
  agentCount: this.agents.size,
4443
4648
  uptimeMs: Date.now() - this.startedAtMs,
4444
4649
  connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
@@ -4519,8 +4724,7 @@ export class Manager {
4519
4724
  creds: remote.serveCreds,
4520
4725
  };
4521
4726
  const enc = new TextEncoder();
4522
- const nc = await connect({
4523
- servers: this.servers ?? DEFAULT_SERVER,
4727
+ const nc = await this.dial({
4524
4728
  authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce),
4525
4729
  inboxPrefix: `_INBOX_${state.identity.id}`,
4526
4730
  maxReconnectAttempts: -1,
@@ -4553,7 +4757,7 @@ export class Manager {
4553
4757
  {
4554
4758
  // Open mesh: the bare connection holds the rights (there is no credential system to mint from).
4555
4759
  const provCreds = auth ? await mintCreds(auth, newIdentity(), "provisioner") : undefined;
4556
- const provNc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4760
+ const provNc = await this.dial({ ...standaloneConnectOpts({ creds: provCreds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
4557
4761
  try {
4558
4762
  // P2 item 2: the manager now WRITES goal facts (EPF) + progress events (EPE), so the §13.12
4559
4763
  // endpoint streams must exist. Nothing provisioned them before spawn-as-action (no endpoint
@@ -4698,8 +4902,7 @@ export class Manager {
4698
4902
  // registered surface for its whole incarnation.
4699
4903
  const state = { handle: undefined, nc: undefined, identity: serveIdentity, grant, creds };
4700
4904
  const enc = new TextEncoder();
4701
- const nc = await connect({
4702
- servers: this.servers ?? DEFAULT_SERVER,
4905
+ const nc = await this.dial({
4703
4906
  // Open mesh: a bare serve connection (no credential exists; the broker enforces nothing).
4704
4907
  ...(creds !== undefined ? { authenticator: (nonce) => credsAuthenticator(enc.encode(state.creds))(nonce) } : {}),
4705
4908
  inboxPrefix: `_INBOX_${serveIdentity.id}`,
@@ -4794,8 +4997,7 @@ export class Manager {
4794
4997
  // The mutable holder captured by the authenticator (mirrors the serve connection): a half-TTL
4795
4998
  // renewal updates `gw.creds` and the next (re)connect presents the refreshed credential.
4796
4999
  const gw = { nc: undefined, ctx: undefined, creds: (this.auth || this.remoteAuthority) ? this.goalWriterCreds : undefined, identity };
4797
- const nc = await connect({
4798
- servers: this.servers ?? DEFAULT_SERVER,
5000
+ const nc = await this.dial({
4799
5001
  ...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(gw.creds))(nonce) } : {}),
4800
5002
  inboxPrefix: `_INBOX_${identity.id}`,
4801
5003
  maxReconnectAttempts: -1,
@@ -4900,8 +5102,7 @@ export class Manager {
4900
5102
  // The mutable holder captured by the authenticator (mirrors the goal-writer): a half-TTL renewal
4901
5103
  // updates `sw.creds` and the next (re)connect presents the refreshed credential.
4902
5104
  const sw = { nc: undefined, creds: (this.auth || this.remoteAuthority) ? this.sessionLedgerCreds : undefined };
4903
- const nc = await connect({
4904
- servers: this.servers ?? DEFAULT_SERVER,
5105
+ const nc = await this.dial({
4905
5106
  ...((this.auth || this.remoteAuthority) ? { authenticator: (nonce) => credsAuthenticator(enc.encode(sw.creds))(nonce) } : {}),
4906
5107
  inboxPrefix: `_INBOX_${identity.id}`,
4907
5108
  maxReconnectAttempts: -1,
@@ -5072,7 +5273,7 @@ export class Manager {
5072
5273
  // FAIL LOUD: there is deliberately no shared connection to fall back to. Serving a session
5073
5274
  // without its own credential is exactly the standing-writer shape this design removes.
5074
5275
  const opts = (this.auth || this.remoteAuthority) ? standaloneConnectOpts({ creds: cred.creds, /* not yet wired to a recorded transport */ tls: false }) : {};
5075
- return connect({ servers: this.servers ?? DEFAULT_SERVER, ...opts, maxReconnectAttempts: -1 });
5276
+ return this.dial({ ...opts, maxReconnectAttempts: -1 });
5076
5277
  },
5077
5278
  revoke: async (credentialId) => {
5078
5279
  if (!this.auth && !this.remoteAuthority)
@@ -5127,8 +5328,8 @@ export class Manager {
5127
5328
  try {
5128
5329
  let entries = [];
5129
5330
  const nc = this.auth
5130
- ? await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds: await mintCreds(this.auth, newIdentity(), "provisioner"), /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 })
5131
- : await connect({ servers: this.servers ?? DEFAULT_SERVER, maxReconnectAttempts: 0 });
5331
+ ? await this.dial({ ...standaloneConnectOpts({ creds: await mintCreds(this.auth, newIdentity(), "provisioner"), /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 })
5332
+ : await this.dial({ maxReconnectAttempts: 0 });
5132
5333
  try {
5133
5334
  const kvm = new Kvm(nc);
5134
5335
  await ensureAuthorityStores(await jetstreamManager(nc), kvm, this.space);
@@ -6090,7 +6291,7 @@ export class Manager {
6090
6291
  return;
6091
6292
  const identity = newIdentity();
6092
6293
  const creds = await mintCreds(this.auth, identity, "provisioner");
6093
- const nc = await connect({ servers: this.servers ?? DEFAULT_SERVER, ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
6294
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds, /* not yet wired to a recorded transport */ tls: false }), maxReconnectAttempts: 0 });
6094
6295
  const slotRows = [];
6095
6296
  try {
6096
6297
  const jsm = await jetstreamManager(nc);