@cotal-ai/manager 0.90.1 → 0.92.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 (60) hide show
  1. package/dist/chat-session.d.ts +165 -0
  2. package/dist/chat-session.d.ts.map +1 -0
  3. package/dist/chat-session.js +281 -0
  4. package/dist/chat-session.js.map +1 -0
  5. package/dist/commands.d.ts +3 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +384 -163
  8. package/dist/commands.js.map +1 -1
  9. package/dist/endpoint-evict.d.ts +2 -0
  10. package/dist/endpoint-evict.d.ts.map +1 -1
  11. package/dist/endpoint-evict.js +1 -0
  12. package/dist/endpoint-evict.js.map +1 -1
  13. package/dist/holder-liveness.d.ts +1 -0
  14. package/dist/holder-liveness.d.ts.map +1 -1
  15. package/dist/holder-liveness.js.map +1 -1
  16. package/dist/index.d.ts +6 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/manager-service-contract.d.ts +61 -1
  21. package/dist/manager-service-contract.d.ts.map +1 -1
  22. package/dist/manager-service-contract.js +225 -4
  23. package/dist/manager-service-contract.js.map +1 -1
  24. package/dist/manager.d.ts +106 -3
  25. package/dist/manager.d.ts.map +1 -1
  26. package/dist/manager.js +526 -49
  27. package/dist/manager.js.map +1 -1
  28. package/dist/operator-deregister.d.ts +4 -0
  29. package/dist/operator-deregister.d.ts.map +1 -0
  30. package/dist/operator-deregister.js +75 -0
  31. package/dist/operator-deregister.js.map +1 -0
  32. package/dist/remote-authority.d.ts +4 -2
  33. package/dist/remote-authority.d.ts.map +1 -1
  34. package/dist/remote-authority.js +39 -0
  35. package/dist/remote-authority.js.map +1 -1
  36. package/dist/run-hosting.d.ts +34 -1
  37. package/dist/run-hosting.d.ts.map +1 -1
  38. package/dist/run-hosting.js +369 -15
  39. package/dist/run-hosting.js.map +1 -1
  40. package/dist/service-adapter.d.ts +12 -0
  41. package/dist/service-adapter.d.ts.map +1 -0
  42. package/dist/service-adapter.js +174 -0
  43. package/dist/service-adapter.js.map +1 -0
  44. package/dist/service-child.d.ts +37 -0
  45. package/dist/service-child.d.ts.map +1 -0
  46. package/dist/service-child.js +195 -0
  47. package/dist/service-child.js.map +1 -0
  48. package/dist/service-observation.d.ts +19 -0
  49. package/dist/service-observation.d.ts.map +1 -0
  50. package/dist/service-observation.js +97 -0
  51. package/dist/service-observation.js.map +1 -0
  52. package/dist/static-lifecycle.d.ts +8 -6
  53. package/dist/static-lifecycle.d.ts.map +1 -1
  54. package/dist/static-lifecycle.js +19 -8
  55. package/dist/static-lifecycle.js.map +1 -1
  56. package/dist/vitals.d.ts +37 -0
  57. package/dist/vitals.d.ts.map +1 -0
  58. package/dist/vitals.js +117 -0
  59. package/dist/vitals.js.map +1 -0
  60. package/package.json +13 -12
package/dist/manager.js CHANGED
@@ -5,6 +5,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
5
5
  import { isAbsolute, join, dirname, resolve } from "node:path";
6
6
  import Headless from "@xterm/headless";
7
7
  import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, MANAGER_LEASE_TTL_MS, awaitLeaseExpiry, leaseRemainingMs, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, injectedSecretStoreIdentity, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, identityFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, digestIdToken, } from "@cotal-ai/core";
8
+ import { loadLaunchPersonaFile, PersonaSourceError } from "@cotal-ai/workspace";
8
9
  import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimIdentityRecord, claimManagerSiblingIdentities, connectorInstallHint, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, probeLiveness, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, spaceKey, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
10
  import { createRuntime, exitProof, isCustodialRuntime, pollProvedExit, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, watchProvedExit, } from "./runtime/index.js";
10
11
  import { AttachEndpoint } from "./attach-endpoint.js";
@@ -32,7 +33,9 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
32
33
  // the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
33
34
  import { provisionEndpointGateOpen, endpointRegistrationBarrier, serveIssuanceGateKv, commitSiblingIssuance, markLedgerRowRevoked, epcredRowKey, epgateKey, parseEndpointGate, readEndpointGateGeneration, registerServingInstance, deregisterServiceInstance, serveEndpoint, bindGoal, createGoal, transitionGoal, commitGoalResult, requestGoalCancel, goalAlreadyTerminal, settleGoalUncertain, readGoalResult, readGoalStatus, readGoalSpec, recordGoalIndex, readGoalIndex, clearGoalIndex, listGoalIndex, mintCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointSpec, expireCheckpoint, GOAL_TERMINAL_STATES, goalRefOf, goalProgressTopic, epeSubject, submissionFingerprint, EpEnvelopeError, lifecycleBlocked, lifecycleBlockedFrom, renderLifecycleBlocked, } from "@cotal-ai/core";
34
35
  import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
36
+ import { measureManagerVitals } from "./vitals.js";
35
37
  import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, walkStaticSlots, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
38
+ import { CHAT_ALIAS_PREFIX, CHAT_EXPIRY_CONCURRENCY, ChatSessionExpiry, chatOpenLists, chatRetireRefusal, drawChatAlias, isChatAlias, readChatOpenInput, readChatRetireInput, sealChatAdmission, } from "./chat-session.js";
36
39
  // Capture the loaded package version before an installation can replace its files.
37
40
  const MANAGER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
38
41
  /** Minimum slot lifetime for rate-flooring (P4c). A slot freed (by despawn OR natural exit/reap)
@@ -415,6 +418,7 @@ export class Manager {
415
418
  * {@link provisionEndpointGateOpen} idempotent (no core barrier change) and gives verified
416
419
  * eviction a stable target (the predecessor's connections under this principal). Set in start(). */
417
420
  managerServeIdentity;
421
+ onServiceIdentity;
418
422
  /** The manager's v0.4 service-endpoint serve state — the serve handle +
419
423
  * its dedicated connection, the STABLE serve identity (renewals re-mint the same nkey), the
420
424
  * branded serve grant, and the CURRENT credential (the connection's authenticator reads it on
@@ -525,6 +529,15 @@ export class Manager {
525
529
  /** Static aliases whose startup exact-op terminal is still in flight. Scoped per alias: control
526
530
  * serves during the sweep, but no caller can reuse or attach an alias the sweep still owns. */
527
531
  reconcilingAliases = new Set();
532
+ /** In-flight static chat-session terminals, by lifecycle UID, so every caller that retires one
533
+ * session joins one terminal instead of racing its CAS sequence. */
534
+ chatRetirements = new Map();
535
+ /** The expiry side of every chat session this process admitted and nothing retired sooner. */
536
+ chatExpiry = new ChatSessionExpiry((session) => this.expireStaticChatSession(session), {
537
+ retryDelaysMs: STATIC_RECONCILE_RETRY_DELAYS_MS,
538
+ concurrency: CHAT_EXPIRY_CONCURRENCY,
539
+ log: (line) => console.error(line),
540
+ });
528
541
  /** Current failed/recovered static reconciliation coordinates. This is an operator snapshot, not
529
542
  * lifecycle authority: every re-drive re-reads broker KV and re-runs planStaticSlotResume. */
530
543
  staticReconcileItems = new Map();
@@ -641,11 +654,16 @@ export class Manager {
641
654
  return dialerFor(servers)({ ...opts, servers });
642
655
  }
643
656
  constructor(opts) {
657
+ if (opts.onServiceIdentity !== undefined && typeof opts.onServiceIdentity !== "function")
658
+ throw new Error("manager onServiceIdentity must be a trusted embedding callback");
644
659
  if (opts.execution !== undefined && opts.execution !== "runtime" && opts.execution !== "none")
645
660
  throw new Error(`unknown manager execution capability ${JSON.stringify(opts.execution)}`);
646
- if (opts.execution === "none" && (!opts.pooled || !opts.remoteAuthority || opts.runtime !== undefined))
647
- throw new Error("execution: none requires pooled signerless remote authority and no runtime option");
648
- if (opts.execution === "none" && ["renewExecutor", "renewStandingBundle", "mintSessionServing", "mintRetirementRequester", "prepareAgentRetirement", "validateRetainedAgent", "scanGoalIndex", "authorizeAdmin"].some((key) => typeof opts.remoteAuthority?.[key] !== "function"))
661
+ if (opts.execution === "none" && ((opts.pooled !== undefined && typeof opts.pooled !== "boolean") ||
662
+ (opts.remoteAuthority !== undefined && (!opts.remoteAuthority || typeof opts.remoteAuthority !== "object" || Array.isArray(opts.remoteAuthority)))))
663
+ throw new Error("execution: none requires boolean pooled and a non-null remote authority object when supplied");
664
+ if (opts.execution === "none" && (opts.runtime !== undefined || (!!opts.pooled !== !!opts.remoteAuthority)))
665
+ throw new Error("execution: none requires pooled signerless remote authority for remote composition, or genuine local authority, and no runtime option");
666
+ if (opts.execution === "none" && opts.remoteAuthority && ["renewExecutor", "renewStandingBundle", "mintSessionServing", "mintRetirementRequester", "prepareAgentRetirement", "validateRetainedAgent", "scanGoalIndex", "authorizeAdmin"].some((key) => typeof opts.remoteAuthority?.[key] !== "function"))
649
667
  throw new Error("execution: none requires complete closed remote authority callbacks before construction");
650
668
  if (opts.pooled && (!opts.remoteAuthority || (opts.execution !== "none" && (opts.runtime === undefined || opts.runtime === "auto" || opts.runtime === "pty"))))
651
669
  throw new Error("pooled control requires signerless remote authority and an explicit non-PTY runtime; local custodial execution is forbidden");
@@ -655,6 +673,7 @@ export class Manager {
655
673
  this.servers = opts.servers;
656
674
  this.name = opts.name ?? "manager";
657
675
  this.workspaceRoot = opts.workspaceRoot ?? findCotalRoot();
676
+ this.onServiceIdentity = opts.onServiceIdentity;
658
677
  this.eventsRequired = opts.eventsRequired === true;
659
678
  this.maxSessions = opts.maxSessions;
660
679
  this.remoteAuthority = opts.remoteAuthority;
@@ -670,6 +689,11 @@ export class Manager {
670
689
  this.secretStoreIdentity = injectedSecretStoreIdentity(opts.secretStore);
671
690
  this.installedExtensions = opts.installedExtensions ?? false;
672
691
  this.executionState = opts.execution === "none" ? { kind: "none" } : { kind: "runtime" };
692
+ if (!this.remoteAuthority) {
693
+ const local = loadManagerInstanceIdentity(this.workspaceRoot, this.space);
694
+ if (local && existsSync(this.executionRecordPath(local.instanceId)))
695
+ this.claimExecutionRecord(local.instanceId);
696
+ }
673
697
  if (this.executionState.kind === "runtime")
674
698
  this.runtime = createRuntime(opts.runtime ?? "auto", `cotal-${this.space}`);
675
699
  if (opts.pooled && this.executionState.kind === "runtime" && isCustodialRuntime(this.requiredRuntime))
@@ -707,7 +731,7 @@ export class Manager {
707
731
  return this.executionState.kind;
708
732
  }
709
733
  executionRefusal() {
710
- return "execution: none forbids seat launch and workflow hosting on this control instance; select the participant Manager with an instance-pinned manager-caller view";
734
+ return "execution: none forbids seat launch, workflow hosting, model inspection and caller-driven writes on this control instance; select the participant Manager with an instance-pinned manager-caller view";
711
735
  }
712
736
  /** A defensive last barrier, not a substitute Runtime. Entry admission runs before effects. */
713
737
  get requiredRuntime() {
@@ -730,6 +754,37 @@ export class Manager {
730
754
  throw new EpEnvelopeError("unimplemented", this.executionRefusal());
731
755
  return requireRuntimeAdopt(this.requiredRuntime, reference);
732
756
  }
757
+ /** Snapshot this embedding's own registration and stop policy. It is not a fresh serving
758
+ * response, registration-leader read or authorization for another process to act. Wire callers
759
+ * still use status through its fresh owner/admin visibility gate. No credential or URL leaves. */
760
+ localServiceObservation() {
761
+ const state = this.serviceServe;
762
+ if (!state)
763
+ return undefined;
764
+ const identity = this.serviceIdentityObservation();
765
+ if (state.grant.endpoint !== identity.endpoint || state.grant.instanceId !== identity.instanceId || state.grant.owner !== identity.principal.owner)
766
+ throw new Error("the manager's local service observation does not match its owned serve incarnation");
767
+ const stopImpact = this.managerStatusData(undefined).stopImpact;
768
+ if (!stopImpact)
769
+ throw new Error("the manager did not expose its own native stop policy");
770
+ // The local receipt keeps its revision-32 shape: an older controller's closed reader refuses an
771
+ // extra key. The preserved-exit check reads the binding through `localPreservation` instead.
772
+ const { preservation: _preservation, ...ordinary } = stopImpact;
773
+ return { registration: { ...identity, epoch: state.grant.epoch }, stopImpact: ordinary };
774
+ }
775
+ /** The attempt and inventory digest this embedding holds while maintenance is not `active`, for
776
+ * its own native child only. Absent when active or before an inventory exists. */
777
+ localPreservation() {
778
+ return this.managerStatusData(undefined).stopImpact?.preservation;
779
+ }
780
+ serviceIdentityObservation() {
781
+ if (!this.managerInstanceId || !this.managerServeIdentity)
782
+ throw new Error("manager native service identity has not been selected");
783
+ const principal = this.remoteAuthority
784
+ ? { owner: this.remoteAuthority.owner, actor: this.remoteAuthority.actors.serve, uid: this.managerLifecycleUid }
785
+ : { owner: DEV_OWNER, actor: this.managerServeIdentity.id, uid: this.managerLifecycleUid };
786
+ return Object.freeze({ endpoint: MANAGER_ENDPOINT, instanceId: this.managerInstanceId, principal: Object.freeze(principal) });
787
+ }
733
788
  /** The console page URL (manager-hosted, loopback). */
734
789
  get consoleUrl() {
735
790
  return this.attach.consoleUrl();
@@ -831,12 +886,64 @@ export class Manager {
831
886
  this.startTask = wrapped;
832
887
  return wrapped;
833
888
  }
889
+ executionRecordPath(instanceId) {
890
+ return join(this.workspaceRoot, ".cotal", `manager-execution.${spaceKey(this.space)}.${instanceId}.json`);
891
+ }
892
+ claimExecutionRecord(instanceId) {
893
+ const record = claimIdentityRecord(this.executionRecordPath(instanceId), "manager execution capability", (raw) => {
894
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
895
+ return undefined;
896
+ const r = raw;
897
+ return Object.keys(r).sort().join(",") === "execution,instanceId,space,v" && r.v === 1 && r.space === this.space &&
898
+ r.instanceId === instanceId && (r.execution === "none" || r.execution === "runtime") ? r : undefined;
899
+ }, () => ({ v: 1, space: this.space, instanceId, execution: this.execution }));
900
+ if (record.execution !== this.execution)
901
+ throw new Error("manager execution capability differs from its retained instance record; execution cannot change across restart");
902
+ }
834
903
  async admitExecutionState() {
904
+ if (!this.remoteAuthority) {
905
+ let local = loadManagerInstanceIdentity(this.workspaceRoot, this.space);
906
+ if (this.execution === "none") {
907
+ const recorded = loadMeshes().find((m) => m.space === this.space);
908
+ const auth = hasUserAuthState(this.workspaceRoot, this.space) && recorded?.mode === "user"
909
+ ? await getSpaceAuth(this.secrets, this.space) : undefined;
910
+ if (!auth)
911
+ throw new Error("execution: none requires a genuine hosting user-auth root and its user-mode mesh record");
912
+ if (local && !existsSync(this.executionRecordPath(local.instanceId)))
913
+ throw new Error("execution: none refuses an existing unclassified manager instance; keep its runtime and retained state, or register a fresh control instance");
914
+ if (!local) {
915
+ const proposed = { instanceId: mintLifecycleUid(), serveIdentity: newIdentity() };
916
+ local = createManagerInstanceIdentity(this.workspaceRoot, this.space, proposed);
917
+ if (local.instanceId !== proposed.instanceId)
918
+ throw new Error("execution: none refuses a concurrently claimed unclassified manager identity");
919
+ }
920
+ const localInstanceId = local.instanceId;
921
+ this.claimExecutionRecord(localInstanceId);
922
+ const nc = await this.dial({ ...standaloneConnectOpts({ creds: await mintCreds(auth, newIdentity(), "provisioner"), tls: false }), maxReconnectAttempts: 0 });
923
+ try {
924
+ const kvm = new Kvm(nc);
925
+ await ensureAuthorityStores(await jetstreamManager(nc), kvm, this.space);
926
+ const entries = await listGoalIndex(await kvm.open(recordsBucket(this.space)), MANAGER_ENDPOINT);
927
+ if (entries.some((entry) => entry.iid === localInstanceId && (entry.allocated !== undefined || entry.note !== undefined)))
928
+ throw new Error("execution: none refuses retained seat or turn inventory before recovery; keep the execution-owning Manager");
929
+ }
930
+ finally {
931
+ await nc.drain().catch(() => nc.close());
932
+ }
933
+ }
934
+ else {
935
+ local ??= createManagerInstanceIdentity(this.workspaceRoot, this.space, { instanceId: mintLifecycleUid(), serveIdentity: newIdentity() });
936
+ this.claimExecutionRecord(local.instanceId);
937
+ }
938
+ return;
939
+ }
835
940
  if (this.remoteAuthority) {
941
+ if (this.execution === "none" && (await localTrustOfSpace(this.secrets, this.space)).present)
942
+ throw new Error("remote manager-service authority cannot be combined with local space signing trust - choose one authority path, never a fallback");
836
943
  // Execution is fixed for a registered instance. An older instance with no capability
837
944
  // record might own retained seats/runs, so it cannot be converted to none by omission.
838
945
  const remote = this.remoteAuthority;
839
- const path = join(this.workspaceRoot, ".cotal", `manager-execution.${spaceKey(this.space)}.${remote.instanceId}.json`);
946
+ const path = this.executionRecordPath(remote.instanceId);
840
947
  if (this.execution === "none" && !existsSync(path) && remote.serveGrant.epoch !== 0)
841
948
  throw new Error("execution: none refuses an existing unclassified manager instance; keep its runtime and retained state, or register a fresh control instance");
842
949
  if (this.execution === "none") {
@@ -844,17 +951,8 @@ export class Manager {
844
951
  if (entries.some((entry) => entry.iid === remote.instanceId && (entry.allocated !== undefined || entry.note !== undefined)))
845
952
  throw new Error("execution: none refuses retained seat or turn inventory before recovery; keep the execution-owning Manager");
846
953
  }
847
- if (this.execution === "none" || existsSync(path)) {
848
- const record = claimIdentityRecord(path, "manager execution capability", (raw) => {
849
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
850
- return undefined;
851
- const r = raw;
852
- return Object.keys(r).sort().join(",") === "execution,instanceId,space,v" && r.v === 1 && r.space === this.space &&
853
- r.instanceId === remote.instanceId && (r.execution === "none" || r.execution === "runtime") ? r : undefined;
854
- }, () => ({ v: 1, space: this.space, instanceId: remote.instanceId, execution: this.execution }));
855
- if (record.execution !== this.execution)
856
- throw new Error("manager execution capability differs from its retained instance record; execution cannot change across restart");
857
- }
954
+ if (this.execution === "none" || existsSync(path))
955
+ this.claimExecutionRecord(remote.instanceId);
858
956
  }
859
957
  }
860
958
  async runStart() {
@@ -931,6 +1029,7 @@ export class Manager {
931
1029
  }
932
1030
  }
933
1031
  }
1032
+ await this.onServiceIdentity?.(this.serviceIdentityObservation());
934
1033
  let creds;
935
1034
  let id;
936
1035
  if (this.remoteAuthority) {
@@ -1143,7 +1242,9 @@ export class Manager {
1143
1242
  auth: undefined,
1144
1243
  log: (line) => console.error(line),
1145
1244
  admitRun: remoteRuns.admitRun,
1245
+ submitRun: remoteRuns.submitRun,
1146
1246
  revokeRun: remoteRuns.revokeRun,
1247
+ cancelRun: remoteRuns.cancelRun,
1147
1248
  issueAttempt: remoteRuns.issueAttempt,
1148
1249
  issueOperator: remoteRuns.issueOperator,
1149
1250
  renewRun: remoteRuns.renewRun,
@@ -1164,7 +1265,7 @@ export class Manager {
1164
1265
  }
1165
1266
  // A carried resume (#1499) needs this instance's transfer reader: minted from the space's signing
1166
1267
  // seed, issued by the host to a remote manager, or none on an open mesh.
1167
- if (!this.remoteAuthority || this.remoteAuthority.mintTransferReader) {
1268
+ if (this.execution !== "none" && (!this.remoteAuthority || this.remoteAuthority.mintTransferReader)) {
1168
1269
  this.transcripts = new TranscriptReceiver({
1169
1270
  space: this.space,
1170
1271
  instanceId: this.managerInstanceId,
@@ -1754,6 +1855,8 @@ export class Manager {
1754
1855
  /** Fence and build the inventory without stopping a child. The coordinator must durably persist
1755
1856
  * this exact plan before calling commitPreservation with the same attempt id. */
1756
1857
  preparePreservation(attemptId) {
1858
+ if (this.execution === "none")
1859
+ return Promise.reject(new EpEnvelopeError("unimplemented", this.executionRefusal()));
1757
1860
  if (!attemptId.trim())
1758
1861
  return Promise.reject(new Error("preservation attemptId is required"));
1759
1862
  if (this.preservationAttemptId && this.preservationAttemptId !== attemptId)
@@ -1857,6 +1960,8 @@ export class Manager {
1857
1960
  /** Stop children only after the coordinator has persisted the prepared inventory. Same-attempt
1858
1961
  * retries are idempotent; a different attempt is refused. */
1859
1962
  commitPreservation(attemptId) {
1963
+ if (this.execution === "none")
1964
+ return Promise.reject(new EpEnvelopeError("unimplemented", this.executionRefusal()));
1860
1965
  if (!this.preservationAttemptId || this.preservationAttemptId !== attemptId)
1861
1966
  return Promise.reject(new Error(`preservation attempt ${attemptId} was not prepared by this manager`));
1862
1967
  if (!this.preservationInventory)
@@ -1882,6 +1987,8 @@ export class Manager {
1882
1987
  /** Recover an abandoned prepare before any child stop. Once commit begins, preservation is
1883
1988
  * irreversible and remains fenced until the coordinator records failure/recourse. */
1884
1989
  abortPreservation(attemptId) {
1990
+ if (this.execution === "none")
1991
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
1885
1992
  if (this.preservationAttemptId !== attemptId)
1886
1993
  throw new Error(`preservation attempt ${attemptId} is not the active manager attempt`);
1887
1994
  if (this.preparationTask || this.lifecycleInFlight > 0)
@@ -2144,11 +2251,14 @@ export class Manager {
2144
2251
  if (failures.length)
2145
2252
  throw new Error(`manager shutdown could not prove every seat exited: ${failures.join("; ")}`);
2146
2253
  }
2254
+ isProcessBoundSeat(agent) {
2255
+ return agent.handle.kind === "pty" && typeof agent.handle.release !== "function";
2256
+ }
2147
2257
  /** Drop this manager's local custody of every seat that can outlive it. A pty seat whose handle
2148
2258
  * cannot be released lives in this process and closes with it, so it is left for {@link stop} to
2149
2259
  * stop instead. */
2150
2260
  releaseManagedAgents() {
2151
- const managed = [...this.agents.values()].filter((a) => !(a.handle.kind === "pty" && typeof a.handle.release !== "function"));
2261
+ const managed = [...this.agents.values()].filter((a) => !this.isProcessBoundSeat(a));
2152
2262
  const failures = [];
2153
2263
  for (const a of managed) {
2154
2264
  try {
@@ -2248,6 +2358,7 @@ export class Manager {
2248
2358
  this.leaseStopping = true;
2249
2359
  this.transcripts?.stop();
2250
2360
  this.staticReconcileStopping = true;
2361
+ this.chatExpiry.stop();
2251
2362
  const starting = this.startTask;
2252
2363
  for (const item of this.staticReconcileItems.values()) {
2253
2364
  if (item.timer)
@@ -2286,7 +2397,7 @@ export class Manager {
2286
2397
  else {
2287
2398
  // An in-process seat cannot be detached from the process that is exiting, so the default
2288
2399
  // stop stops and deprovisions it as `withAgents` does instead of orphaning its footprint.
2289
- const inProcess = [...this.agents.values()].filter((a) => a.handle.kind === "pty" && typeof a.handle.release !== "function");
2400
+ const inProcess = [...this.agents.values()].filter((a) => this.isProcessBoundSeat(a));
2290
2401
  releaseFailures = this.releaseManagedAgents();
2291
2402
  await this.teardownManagedAgents(inProcess);
2292
2403
  }
@@ -2310,6 +2421,13 @@ export class Manager {
2310
2421
  failures.push(`${name}: ${rejectionText(e)}`);
2311
2422
  }
2312
2423
  };
2424
+ // The expiry that would end a chat session stops with this process, and nothing can adopt one,
2425
+ // so an active stop retires every session this process admitted, under either agent policy, as
2426
+ // it does an in-process seat. A preserving stop leaves them to the next start's sweep.
2427
+ if (this.maintenanceState === "active" && !this.resumeRequired)
2428
+ await step("retire chat sessions", () => this.retireChatSessionsOnStop());
2429
+ else
2430
+ this.reportChatSessionsLeftAtStop(this.resumeRequired ? "this manager is waiting for a resume" : "this manager is preserving its state");
2313
2431
  // A seat that may still be alive keeps this instance's leases and registration (see
2314
2432
  // teardownManagedAgents): they lapse on their TTL instead of being handed to a successor.
2315
2433
  if (seatFailure === undefined) {
@@ -2555,6 +2673,8 @@ export class Manager {
2555
2673
  return { kind: "held", revision: current.revision };
2556
2674
  }
2557
2675
  async opFinalizeResume(rawArgs) {
2676
+ if (this.execution === "none")
2677
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2558
2678
  {
2559
2679
  let args;
2560
2680
  try {
@@ -2605,6 +2725,8 @@ export class Manager {
2605
2725
  }
2606
2726
  }
2607
2727
  async opCommitResume(rawArgs) {
2728
+ if (this.execution === "none")
2729
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2608
2730
  {
2609
2731
  let attemptId;
2610
2732
  try {
@@ -2638,11 +2760,11 @@ export class Manager {
2638
2760
  }
2639
2761
  }
2640
2762
  async opResumePreserved(rawArgs) {
2763
+ if (this.execution === "none")
2764
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2641
2765
  {
2642
2766
  try {
2643
2767
  const args = parseResumeControlArgs(rawArgs);
2644
- if (this.execution === "none" && args.inventory.agents.length !== 0)
2645
- return { ok: false, error: this.executionRefusal() };
2646
2768
  const inventoryDigest = createHash("sha256").update(JSON.stringify(args.inventory)).digest("hex");
2647
2769
  if (!this.resumeAttemptId)
2648
2770
  return { ok: false, error: "resumePreserved requires a manager started with --resume-attempt" };
@@ -2675,6 +2797,8 @@ export class Manager {
2675
2797
  }
2676
2798
  }
2677
2799
  async opPreservationCtl(op, rawArgs) {
2800
+ if (this.execution === "none")
2801
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2678
2802
  if (this.resumeRequired)
2679
2803
  return { ok: false, error: this.maintenanceError() };
2680
2804
  const attemptId = String(rawArgs?.attemptId ?? "").trim();
@@ -2726,8 +2850,9 @@ export class Manager {
2726
2850
  async serveGated(ctx, fn) {
2727
2851
  const caller = principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key;
2728
2852
  const admission = this.admitControl(caller);
2853
+ // Both fences refuse before the handler runs, so nothing ran (SPEC 13.3).
2729
2854
  if (admission.refusal !== undefined)
2730
- throw new EpEnvelopeError(admission.fence === "membership" ? "permission-denied" : "unavailable", admission.refusal);
2855
+ throw new EpEnvelopeError(admission.fence === "membership" ? "permission-denied" : "unavailable", admission.refusal, undefined, "not-executed");
2731
2856
  try {
2732
2857
  return await fn();
2733
2858
  }
@@ -2735,6 +2860,29 @@ export class Manager {
2735
2860
  admission.release();
2736
2861
  }
2737
2862
  }
2863
+ /** `status` only: {@link serveGated}, except that a fully preserved manager, and an active one
2864
+ * launched for a resume that has not committed it yet, still answer it. A preserved-exit stop's
2865
+ * caller-attributed preview needs the preserved manager's stop impact, and a resumed service's
2866
+ * readiness reads the same status; the maintenance fence refuses every other command in both
2867
+ * states. The read is counted in `lifecycleInFlight` like any admitted operation and released in
2868
+ * `finally`, so a drain waits at most for one status snapshot; the membership gate still
2869
+ * applies. `preserving` and a stopping manager stay fenced. Status writes nothing. */
2870
+ async serveObservation(ctx, fn) {
2871
+ const preserved = this.maintenanceState === "preserved" && !this.resumeRequired;
2872
+ const resumePending = this.maintenanceState === "active" && this.resumeRequired;
2873
+ if (this.leaseStopping || !preserved && !resumePending)
2874
+ return this.serveGated(ctx, fn);
2875
+ const membership = this.lifecycleMembershipRefusal(principalKey(ctx.subject.caller.owner, ctx.subject.caller.actor).key);
2876
+ if (membership)
2877
+ throw new EpEnvelopeError("permission-denied", membership, undefined, "not-executed");
2878
+ this.lifecycleInFlight++;
2879
+ try {
2880
+ return await fn();
2881
+ }
2882
+ finally {
2883
+ this.releaseLifecycle();
2884
+ }
2885
+ }
2738
2886
  /** The ep door's ADMIN flag for a caller. Static mesh: `true` — the
2739
2887
  * admin-grade rows (any-mode despawn/attach, the `manager.admin` family, `launch`) are minted
2740
2888
  * only into operator instruments (§13.2: `any` is operator-policy-mintable; the agent/spawn
@@ -2898,20 +3046,23 @@ export class Manager {
2898
3046
  // serveGated BYPASS (those ops must run while the maintenance fence holds) but not the gate.
2899
3047
  const adminGated = async (ctx, fn) => {
2900
3048
  if (!(await this.epAdminReach(ctx.subject.caller)))
2901
- throw new EpEnvelopeError("permission-denied", `${ctx.subject.command} is operator reach; the caller's current ledger grant does not carry "admin" (SPEC 13.2)`);
3049
+ throw new EpEnvelopeError("permission-denied", `${ctx.subject.command} is operator reach; the caller's current ledger grant does not carry "admin" (SPEC 13.2)`, undefined, "not-executed");
2902
3050
  return fn();
2903
3051
  };
2904
3052
  const targetAgent = (ctx) => {
3053
+ if (this.execution === "none")
3054
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2905
3055
  const t = ctx.request.target; // targeted commands only: the serve boundary enforced body-target presence + fresh currency
2906
3056
  const a = this.findManagedByTarget(t);
2907
3057
  if (!a)
2908
- throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} (lifecycle ${t.lifecycleUid}) is not a live managed agent of this manager`);
3058
+ throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} (lifecycle ${t.lifecycleUid}) is not a live managed agent of this manager`, undefined, "not-executed");
2909
3059
  return a;
2910
3060
  };
2911
3061
  return managerCommandDefs({
3062
+ vitals: (ctx) => this.serveGated(ctx, () => measureManagerVitals()),
2912
3063
  // A remote Manager files every seat under its own owner, so only a user mesh's shared local
2913
3064
  // Manager narrows the count, and status never waits on a remote admin check.
2914
- status: (ctx) => this.serveGated(ctx, async () => this.managerStatusData(this.remoteAuthority ? undefined : await this.psOwnerFilter(ctx.subject.caller, false))),
3065
+ status: (ctx) => this.serveObservation(ctx, async () => this.managerStatusData(this.remoteAuthority ? undefined : await this.psOwnerFilter(ctx.subject.caller, false))),
2915
3066
  ps: (ctx) => this.serveGated(ctx, async () => this.list(await this.psOwnerFilter(ctx.subject.caller, false))),
2916
3067
  inspect: (ctx) => this.serveGated(ctx, async () => {
2917
3068
  const name = String(args(ctx).name ?? "").trim();
@@ -2966,7 +3117,11 @@ export class Manager {
2966
3117
  ]);
2967
3118
  let observed;
2968
3119
  try {
2969
- observed = await listStaticSlotObservations(recordsKv, DEV_OWNER, this.managerInstanceId);
3120
+ const query = args(ctx);
3121
+ observed = await listStaticSlotObservations(recordsKv, DEV_OWNER, this.managerInstanceId, {
3122
+ ...(query.names === undefined ? {} : { names: query.names }),
3123
+ ...(query.includeRetired === true ? { includeRetired: true } : {}),
3124
+ });
2970
3125
  }
2971
3126
  catch (error) {
2972
3127
  throw new EpEnvelopeError("unavailable", `the durable static slot state could not be scanned: ${rejectionText(error)}`, [
@@ -2995,9 +3150,11 @@ export class Manager {
2995
3150
  }),
2996
3151
  resolveCwd: (ctx) => this.serveGated(ctx, () => this.resolveSpawnCwd(args(ctx).cwd)),
2997
3152
  resolveAgent: (ctx) => this.serveGated(ctx, async () => {
3153
+ if (this.execution === "none")
3154
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
2998
3155
  const persona = await this.spawnPersona(args(ctx), callerOf(ctx), ctx.subject.caller);
2999
3156
  if ("error" in persona)
3000
- throw new EpEnvelopeError(persona.error.startsWith("no persona") ? "not-found" : "failed-precondition", persona.error);
3157
+ throw new EpEnvelopeError(persona.code ?? (persona.error.startsWith("no persona") ? "not-found" : "failed-precondition"), persona.error);
3001
3158
  return { agent: persona.agent };
3002
3159
  }),
3003
3160
  // `spawn` is an ACTION - accept a goal + reply the acceptance floor payload, drive
@@ -3011,14 +3168,14 @@ export class Manager {
3011
3168
  const a = targetAgent(ctx);
3012
3169
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
3013
3170
  if (denied)
3014
- throw new EpEnvelopeError("permission-denied", denied);
3171
+ throw new EpEnvelopeError("permission-denied", denied, undefined, "not-executed");
3015
3172
  return unwrap(this.despawnAuthorized(a, args(ctx).graceful !== false, true, callerOf(ctx)));
3016
3173
  }),
3017
3174
  attach: (ctx) => this.serveGated(ctx, async () => {
3018
3175
  const a = targetAgent(ctx);
3019
3176
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
3020
3177
  if (denied)
3021
- throw new EpEnvelopeError("permission-denied", denied);
3178
+ throw new EpEnvelopeError("permission-denied", denied, undefined, "not-executed");
3022
3179
  return unwrap(await this.attachAuthorized(a, ctx.subject.caller));
3023
3180
  }),
3024
3181
  // C3 `input`: the SAME authorization as `attach`, deliberately written out rather than
@@ -3028,7 +3185,7 @@ export class Manager {
3028
3185
  const a = targetAgent(ctx);
3029
3186
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
3030
3187
  if (denied)
3031
- throw new EpEnvelopeError("permission-denied", denied);
3188
+ throw new EpEnvelopeError("permission-denied", denied, undefined, "not-executed");
3032
3189
  return this.inputAuthorized(a, args(ctx));
3033
3190
  }),
3034
3191
  // The turn relay (§8 durable actions): `turn` shares despawn/input's reach policy — the
@@ -3038,7 +3195,7 @@ export class Manager {
3038
3195
  const a = targetAgent(ctx);
3039
3196
  const denied = await this.authorizeNamed(a, callerOf(ctx), await this.epAnyModeAdmin(ctx), ctx.subject.caller);
3040
3197
  if (denied)
3041
- throw new EpEnvelopeError("permission-denied", denied);
3198
+ throw new EpEnvelopeError("permission-denied", denied, undefined, "not-executed");
3042
3199
  return this.serveTurnGoal(ctx, a);
3043
3200
  }),
3044
3201
  // The seat's own half of the relay (`manager.self`, stop-self's tier): a seat pulls the
@@ -3050,6 +3207,10 @@ export class Manager {
3050
3207
  cancel: (ctx) => this.serveGated(ctx, () => this.serveTurnCancel(ctx)),
3051
3208
  stopSelf: (ctx) => this.serveGated(ctx, () => unwrap(this.opStopSelf(callerOf(ctx), args(ctx)))),
3052
3209
  definePersona: (ctx) => this.serveGated(ctx, () => unwrap(this.opDefinePersona(args(ctx), callerOf(ctx), false))),
3210
+ // Reach is the broker's: only operator instruments carry these rows. The serve gate is the
3211
+ // maintenance fence, so neither runs while a shutdown or resume holds control.
3212
+ chatSessionOpen: (ctx) => this.serveGated(ctx, () => this.openStaticChatSession(args(ctx))),
3213
+ chatSessionRetire: (ctx) => this.serveGated(ctx, () => this.retireStaticChatSession(args(ctx))),
3053
3214
  listPersonas: (ctx) => this.serveGated(ctx, () => unwrap(this.opListPersonas(callerOf(ctx), false))),
3054
3215
  showPersona: (ctx) => this.serveGated(ctx, () => {
3055
3216
  const r = this.opShowPersona(args(ctx), callerOf(ctx), false);
@@ -3081,7 +3242,14 @@ export class Manager {
3081
3242
  // The workflow-run family (SPEC 14.3): the manager hosts the driver. Reach is the broker's
3082
3243
  // (`run` capability / privileged instrument rows); the serve gate is the maintenance fence.
3083
3244
  runStart: (ctx) => this.serveGated(ctx, () => this.runHost().start(ctx, args(ctx))),
3245
+ runStartKeyed: (ctx) => this.serveGated(ctx, () => this.runHost().startKeyed(ctx, args(ctx))),
3246
+ runSubmissionStatus: (ctx) => this.serveGated(ctx, () => this.runHost().submissionStatus(ctx, String(args(ctx).submissionId))),
3084
3247
  runResume: (ctx) => this.serveGated(ctx, () => this.runHost().resume(args(ctx), ctx)),
3248
+ runCancel: (ctx) => {
3249
+ const host = this.runHost();
3250
+ host.validateCancellation(ctx, args(ctx));
3251
+ return this.serveGated(ctx, () => host.cancel(ctx, args(ctx)));
3252
+ },
3085
3253
  runAnswer: (ctx) => this.serveGated(ctx, () => {
3086
3254
  const input = args(ctx);
3087
3255
  if (input.amend === true)
@@ -3094,6 +3262,8 @@ export class Manager {
3094
3262
  commitPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("commitPreservation", args(ctx)))),
3095
3263
  abortPreservation: (ctx) => adminGated(ctx, async () => unwrap(await this.opPreservationCtl("abortPreservation", args(ctx)))),
3096
3264
  transcriptReceive: (ctx) => this.serveGated(ctx, () => adminGated(ctx, () => {
3265
+ if (this.execution === "none")
3266
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
3097
3267
  if (!this.transcripts)
3098
3268
  throw new EpEnvelopeError("failed-precondition", "transcript-receive: this manager does not receive carried transcripts (its remote authority issues no transfer reader)");
3099
3269
  return this.transcripts.receive(args(ctx));
@@ -3231,6 +3401,8 @@ export class Manager {
3231
3401
  * hitting another agent. Non-managed callers (human CLI, the manager itself, observers) find
3232
3402
  * no match and get a loud error, not a silent no-op. */
3233
3403
  opStopSelf(callerId, args) {
3404
+ if (this.execution === "none")
3405
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
3234
3406
  const target = [...this.agents.values()].find((a) => this.managedPrincipal(a) === callerId);
3235
3407
  if (!target)
3236
3408
  return { ok: false, error: `self-stop: caller ${callerId} is not a managed agent` };
@@ -3390,6 +3562,9 @@ export class Manager {
3390
3562
  parent: spawnerPr ? opts.spawner : undefined,
3391
3563
  label: opts.label,
3392
3564
  lifecycleUid: opts.lifecycleUid,
3565
+ // This manager attests its own persisted logical instance id: the same id a static slot
3566
+ // records as its owner. Recovery binds a managed row to a manager only through this.
3567
+ custody: { kind: "local-manager", instanceId: this.managerInstanceId },
3393
3568
  });
3394
3569
  // Durables + ACL row, LIFECYCLE-keyed (SPEC 13.1) — the same onboarding as static agents minus
3395
3570
  // the mint (a user agent's credential is its bearer, minted by the callout per connect from the
@@ -4631,6 +4806,8 @@ export class Manager {
4631
4806
  /** Resolve a placed spawn directory on this manager's host. Delegated execution refuses before
4632
4807
  * filesystem lookup, since its seats cannot use host cwd. Creates nothing and never substitutes the workspace root. */
4633
4808
  resolveSpawnCwd(value) {
4809
+ if (this.execution === "none")
4810
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
4634
4811
  if (this.execution === "runtime" && this.requiredRuntime.spawnDelegated)
4635
4812
  throw new EpEnvelopeError("failed-precondition", `runtime "${this.requiredRuntime.kind}" starts seats outside this host, so it cannot resolve a directory on this host's filesystem (cwd); the seat runs in its runtime resource's own directory`);
4636
4813
  const asked = typeof value === "string" ? value : String(value ?? "");
@@ -4786,6 +4963,8 @@ export class Manager {
4786
4963
  * local model-list API reports `supported:false` rather than blocking the manager. A connector that
4787
4964
  * fails to import shows an `error:` row (from manifest enumeration) and never blocks the others. */
4788
4965
  async opModels(args) {
4966
+ if (this.execution === "none")
4967
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
4789
4968
  const requested = String(args.agent ?? "").trim();
4790
4969
  const refresh = args.refresh === true;
4791
4970
  const one = async (connector) => {
@@ -4796,7 +4975,7 @@ export class Manager {
4796
4975
  return { agent: connector.name, supported: true, models: [], error: harness.refusal };
4797
4976
  try {
4798
4977
  const catalog = await connector.listModels({ refresh });
4799
- return { agent: connector.name, supported: true, ...catalog };
4978
+ return { agent: connector.name, supported: catalog.unavailable === undefined, ...catalog };
4800
4979
  }
4801
4980
  catch (e) {
4802
4981
  return { agent: connector.name, supported: true, models: [], error: rejectionText(e) };
@@ -4883,6 +5062,7 @@ export class Manager {
4883
5062
  spec = parseLaunchSpec(args.spec, `inline launch spec (run ${runId})`);
4884
5063
  if (spec.runId !== runId)
4885
5064
  return { ok: false, error: `inline launch spec runId "${spec.runId}" does not match requested "${runId}"` };
5065
+ hooks?.onEffect?.();
4886
5066
  persistLaunchSpec(this.workspaceRoot, spec);
4887
5067
  }
4888
5068
  catch (e) {
@@ -4914,6 +5094,7 @@ export class Manager {
4914
5094
  }
4915
5095
  let configPath;
4916
5096
  try {
5097
+ hooks?.onEffect?.();
4917
5098
  configPath = materializePersona(this.workspaceRoot, runId, la);
4918
5099
  }
4919
5100
  catch (e) {
@@ -4995,10 +5176,10 @@ export class Manager {
4995
5176
  let def;
4996
5177
  if (!opts.resolved) {
4997
5178
  try {
4998
- def = loadAgentFile(configPath);
5179
+ def = loadLaunchPersonaFile(configPath);
4999
5180
  }
5000
5181
  catch (e) {
5001
- return { error: rejectionText(e) };
5182
+ return { error: rejectionText(e), ...(e instanceof PersonaSourceError ? { code: "bad-request" } : {}) };
5002
5183
  }
5003
5184
  // Shared operator files remain launchable. Owned content follows the catalog's visibility
5004
5185
  // decision before connector loading or any allocation. A hosted run acts for its admission;
@@ -5032,7 +5213,7 @@ export class Manager {
5032
5213
  }
5033
5214
  const persona = await this.spawnPersona(opts, spawner, authenticatedCaller);
5034
5215
  if ("error" in persona)
5035
- return { ok: false, error: persona.error };
5216
+ return { ok: false, error: persona.error, ...(persona.code ? { code: persona.code } : {}) };
5036
5217
  const { ref, configPath, def, agent } = persona;
5037
5218
  // Materialize the requested connector up front — the ONE async step in the spawn path (a lazy
5038
5219
  // `cotal ext` manifest import on the published binary). It runs BEFORE the capacity/reserve span
@@ -5044,7 +5225,12 @@ export class Manager {
5044
5225
  connector = await this.resolveConnector(agent);
5045
5226
  }
5046
5227
  catch (e) {
5047
- return { ok: false, error: rejectionText(e) };
5228
+ const error = rejectionText(e);
5229
+ // An action refusal here precedes capacity, allocation and onAccepted. Preserve that proof
5230
+ // through serveSpawnGoal without changing the direct startAgent ControlReply contract.
5231
+ if (hooks?.onAccepted)
5232
+ throw new EpEnvelopeError("failed-precondition", error, undefined, "not-executed");
5233
+ return { ok: false, error };
5048
5234
  }
5049
5235
  const readinessTimeoutMs = connector.readinessTimeoutMs ?? this.readinessTimeoutMs;
5050
5236
  if (!Number.isSafeInteger(readinessTimeoutMs) || readinessTimeoutMs <= 0)
@@ -5265,6 +5451,7 @@ export class Manager {
5265
5451
  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.` };
5266
5452
  const held = this.retiring.get(identityName);
5267
5453
  if (held !== undefined) {
5454
+ hooks?.onEffect?.();
5268
5455
  void this.deprovision(held.target).catch(() => { });
5269
5456
  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", opId: held.opId, remedy: "retry" });
5270
5457
  return { ok: false, error: renderLifecycleBlocked(err.message, err), details: err.details };
@@ -5328,6 +5515,11 @@ export class Manager {
5328
5515
  else {
5329
5516
  name = this.uniqueName(identityName);
5330
5517
  }
5518
+ // The `chat_` prefix belongs to static chat sessions, whose aliases only `chat-session-open`
5519
+ // draws. Checked once the name is fixed by either branch above, so a persona name, a numbered
5520
+ // collision, a manifest name and a `--name` override all meet it, and before the reservation.
5521
+ if (name.startsWith(CHAT_ALIAS_PREFIX))
5522
+ return { ok: false, error: `the name "${name}" starts with "${CHAT_ALIAS_PREFIX}", which is reserved for chat sessions; pick another name` };
5331
5523
  // Set once the agent's footprint (durables + creds, or the user-mode grant + secret files)
5332
5524
  // exists; cleared when a live slot takes ownership. If it survives to `finally`, the spawn threw
5333
5525
  // AFTER provisioning (buildLaunch / runtime.spawn) — the orphan-rollback tears it down. Carries
@@ -5343,6 +5535,7 @@ export class Manager {
5343
5535
  let seatHome;
5344
5536
  // Taken right before the try so its finally is the only release; a refusal belongs above the
5345
5537
  // allocation, where there is no name to give back.
5538
+ hooks?.onEffect?.();
5346
5539
  this.reserved.add(name);
5347
5540
  try {
5348
5541
  // A stable nkey identity assigned at spawn: the public key is the agent's card.id (threaded via
@@ -5824,7 +6017,7 @@ export class Manager {
5824
6017
  * principal without provisioning. A later runtime launch failure is reported per-agent, but malformed
5825
6018
  * or missing inventory material can never produce a partially resumed set. */
5826
6019
  async resumePreserved(inventory) {
5827
- if (this.execution === "none" && (!Array.isArray(inventory.agents) || inventory.agents.length !== 0))
6020
+ if (this.execution === "none")
5828
6021
  return { ok: false, agents: [], error: this.executionRefusal() };
5829
6022
  const release = this.beginLifecycle(true);
5830
6023
  if (!release)
@@ -6957,6 +7150,7 @@ export class Manager {
6957
7150
  managerStatusData(ownerFilter) {
6958
7151
  return {
6959
7152
  instanceId: this.managerInstanceId,
7153
+ lifecycleUid: this.managerLifecycleUid,
6960
7154
  host: hostname(),
6961
7155
  version: MANAGER_VERSION,
6962
7156
  execution: this.execution,
@@ -6969,6 +7163,34 @@ export class Manager {
6969
7163
  connectors: this.connectorStatuses.map((row) => ({ ...row, binaries: { ...row.binaries } })),
6970
7164
  classSpawn: this.classSpawnEnabled(),
6971
7165
  staticReconciliation: this.staticReconciliationStatus(),
7166
+ stopImpact: {
7167
+ agentScope: ownerFilter === undefined ? "manager" : "owner-filtered",
7168
+ observedAt: new Date().toISOString(),
7169
+ maintenance: this.maintenanceState,
7170
+ resumeRequired: this.resumeRequired,
7171
+ stopping: this.leaseStopping,
7172
+ lifecycleInFlight: this.lifecycleInFlight,
7173
+ reservedLaunches: this.reserved.size,
7174
+ retirementsPending: this.retiring.size,
7175
+ deprovisioningInFlight: this.deprovisioningFlight.size,
7176
+ staticReconciliationInFlight: this.staticReconcileSweepsInFlight,
7177
+ agents: [...this.agents.values()]
7178
+ .filter((a) => ownerFilter === undefined || a.userOwner === ownerFilter)
7179
+ .map((a) => ({
7180
+ name: a.name,
7181
+ id: a.id,
7182
+ lifecycleUid: a.lifecycleUid,
7183
+ mode: a.handle.kind,
7184
+ defaultAction: this.maintenanceState !== "active" || this.resumeRequired
7185
+ ? "stop-retaining-state" : this.isProcessBoundSeat(a) ? "stop" : "release-custody",
7186
+ withAgentsAction: this.maintenanceState !== "active" || this.resumeRequired ? "stop-retaining-state" : "stop",
7187
+ })),
7188
+ // The same digest form `opResumePreserved` binds, so a preserved-exit stop and the later
7189
+ // resume name one inventory.
7190
+ ...(this.maintenanceState !== "active" && this.preservationAttemptId && this.preservationInventory
7191
+ ? { preservation: { attemptId: this.preservationAttemptId, inventorySha256: createHash("sha256").update(JSON.stringify(this.preservationInventory)).digest("hex") } }
7192
+ : {}),
7193
+ },
6972
7194
  };
6973
7195
  }
6974
7196
  /** Render the current operator snapshot. Recovered rows live until the next sweep starts, then
@@ -7973,15 +8195,16 @@ export class Manager {
7973
8195
  * Returns the acceptance floor payload {name, owner, actor, uid, goalId, fingerprint, executor}
7974
8196
  * (the ALLOCATED identity). goalId = the request id (env.id). */
7975
8197
  async serveSpawnGoal(ctx, run) {
8198
+ // These four refuse before anything is accepted or allocated, so they state it (SPEC 13.3).
7976
8199
  if (this.execution === "none")
7977
- throw new EpEnvelopeError("unimplemented", this.executionRefusal());
8200
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal(), undefined, "not-executed");
7978
8201
  const gw = this.goalWriter;
7979
8202
  if (!gw)
7980
- throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing; spawn-as-action cannot accept (SPEC 13.6)");
8203
+ throw new EpEnvelopeError("unavailable", "the manager goal-writer connection is not standing; spawn-as-action cannot accept (SPEC 13.6)", undefined, "not-executed");
7981
8204
  // Refuse to accept until the boot reconcile of inherited goals completes, so a fresh
7982
8205
  // acceptance never races the sweep (settling a live goal mid-flight would steal its real terminal).
7983
8206
  if (!this.goalReconcileDone)
7984
- throw new EpEnvelopeError("unavailable", "the manager is still reconciling accepted goals at boot; retry shortly (SPEC 13.6)");
8207
+ throw new EpEnvelopeError("unavailable", "the manager is still reconciling accepted goals at boot; retry shortly (SPEC 13.6)", undefined, "not-executed");
7985
8208
  const goalId = ctx.request.id;
7986
8209
  const { fingerprint } = submissionFingerprint(ctx.request, ctx.subject);
7987
8210
  const ref = goalRefOf(ctx.subject, goalId);
@@ -8002,7 +8225,7 @@ export class Manager {
8002
8225
  const accepted = prior ?? ended;
8003
8226
  if (accepted !== undefined) {
8004
8227
  if (accepted.fingerprint !== fingerprint)
8005
- throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" was accepted under a different submission; one goalId never carries two specs (SPEC 13.6)`);
8228
+ throw new EpEnvelopeError("failed-precondition", `goal "${goalId}" was accepted under a different submission; one goalId never carries two specs (SPEC 13.6)`, undefined, "not-executed");
8006
8229
  return prior ?? this.cachedSpawnAcceptance(ref, goalId, fingerprint, executor);
8007
8230
  }
8008
8231
  let resolveAccept;
@@ -8029,6 +8252,8 @@ export class Manager {
8029
8252
  // (#357): it had entered no terminal, so the fallback wrote one for it, using its own abort
8030
8253
  // message as the caller-visible outcome.
8031
8254
  let ownsGoal = false;
8255
+ // Set by onEffect. A refusal that arrives with it still false changed nothing.
8256
+ let effectStarted = false;
8032
8257
  // The terminal commits OFF-handler on the goal-writer connection (manager-only authority; a
8033
8258
  // caller cannot publish it). TWO COMPOSED FENCES (defense in depth): (a) reads THIS
8034
8259
  // incarnation's OWN gate epoch and REFUSES a superseded commit (the currency belt), and (b)
@@ -8148,12 +8373,15 @@ export class Manager {
8148
8373
  // it and commits `cancel`. This only stops the non-ok reply below from manufacturing a
8149
8374
  // `failed` that would race that `cancel` (first-terminal-fact-wins).
8150
8375
  onTerminalDeferred: () => { terminalEntered = true; },
8376
+ onEffect: () => { effectStarted = true; },
8151
8377
  });
8152
8378
  bg.then((reply) => {
8153
8379
  // Refused BEFORE onAccepted (hard-pin collision, capacity, persona-not-found) — no goal bound.
8380
+ // Only a refusal before the first effect states `not-executed`; a later one, such as the
8381
+ // alias gate that re-drives a teardown, leaves the outcome unknown.
8154
8382
  if (acceptance === undefined) {
8155
8383
  const details = reply.details;
8156
- rejectAccept(new EpEnvelopeError("failed-precondition", reply.error ?? "spawn refused at accept", details));
8384
+ rejectAccept(new EpEnvelopeError("failed-precondition", reply.error ?? "spawn refused at accept", details, effectStarted ? undefined : "not-executed"));
8157
8385
  return;
8158
8386
  }
8159
8387
  // H1: `run` CATCHES its own body (a throw in buildLaunch/runtime.spawn) and RESOLVES `{ok:false}`
@@ -8415,6 +8643,8 @@ export class Manager {
8415
8643
  * `succeeded` carrying the TurnResult. A yield AFTER the deadline drives the deadline terminal
8416
8644
  * instead and refuses — the expiry outcome stands, never a late success over it. */
8417
8645
  async serveTurnYield(c, raw) {
8646
+ if (this.execution === "none")
8647
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
8418
8648
  // Same boot window as the pull, and checked first because it is the earlier boot phase: before
8419
8649
  // the index is rebuilt a predecessor's accepted turn is not in `pendingTurns`, and `not-found`
8420
8650
  // would tell the seat to drop work it is holding.
@@ -8503,6 +8733,8 @@ export class Manager {
8503
8733
  * whose outcome cannot be read back stays out and converges through the goal index at the
8504
8734
  * next boot, as a failed deadline commit does. */
8505
8735
  async serveTurnCancel(ctx) {
8736
+ if (this.execution === "none")
8737
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
8506
8738
  if (!this.goalReconcileDone)
8507
8739
  throw new EpEnvelopeError("unavailable", "the manager is still reconciling accepted goals at boot; the pending-turn index is not rebuilt yet, retry (SPEC 13.6)");
8508
8740
  const gw = this.goalWriter;
@@ -9141,6 +9373,229 @@ export class Manager {
9141
9373
  throw e;
9142
9374
  }
9143
9375
  }
9376
+ /** `chat-session-open`: admit one static chat session. The manager draws the `chat_` alias and a
9377
+ * fresh nkey identity, then runs the same native steps a static spawn does: the slot intent and
9378
+ * the shared activation saga, the provisioner's bind-only DM and DELIVER durables plus the ACL
9379
+ * row, and the agent-profile mint. The leaf carries the spawn's three lists under the spawn's
9380
+ * rules (`resolveReadAcl`, a default-deny post list, the foreign event-channel refusal) and
9381
+ * nothing else: no role (so no TASK queue), no capabilities, no endpoint rows, no issuance. It
9382
+ * expires at `ttl` with no renewal, because no managed row exists to renew it.
9383
+ *
9384
+ * The session is registered with the tracker before the first durable write and scheduled for
9385
+ * its expiry before the reply is sent, or after a rollback that did not complete, so this
9386
+ * process retires it at the expiry even when the reply is lost or the client never retires.
9387
+ *
9388
+ * The credential is ledgered before it leaves this process (slot credentialId, then the `cred.`
9389
+ * row), so the static terminal marks the row revoked and verify-evicts the session's principal;
9390
+ * the broker keeps accepting the leaf until its expiry. The direct ledger write is deliberate:
9391
+ * the leaf is not an issuance, so the issuance seam, which writes the same row inside its
9392
+ * finalization, never runs for it, and without the row the terminal's eviction set would be
9393
+ * empty. The credential leaves only sealed to `sealTo`, bound to the space, this instance and
9394
+ * the reply's fields, so a reply printed anywhere carries no usable credential. A failure at
9395
+ * any step drives the terminal before the refusal is returned, and the refusal says whether it
9396
+ * completed. */
9397
+ async openStaticChatSession(raw) {
9398
+ if (!this.auth || this.userMode)
9399
+ throw new EpEnvelopeError("unimplemented", "chat-session-open admits static chat sessions only, and this manager is not a static-auth manager", undefined, "not-executed");
9400
+ const input = readChatOpenInput(raw);
9401
+ if ("error" in input)
9402
+ throw new EpEnvelopeError("bad-request", input.error, undefined, "not-executed");
9403
+ const lists = chatOpenLists(input);
9404
+ if ("error" in lists)
9405
+ throw new EpEnvelopeError("bad-request", lists.error, undefined, "not-executed");
9406
+ const alias = drawChatAlias();
9407
+ const identity = newIdentity();
9408
+ // The spawn's rule, against the drawn actor: a concrete event channel of any other principal is
9409
+ // refused, and a wildcard is an ordinary grant.
9410
+ const foreign = foreignEventChannels([...lists.allowSubscribe, ...lists.allowPublish], DEV_OWNER, identity.id);
9411
+ if (foreign.length)
9412
+ throw new EpEnvelopeError("bad-request", `chat-session-open asks for another agent's event channel: ${foreign.join(", ")}. A session may be granted its OWN event plane and no other, because that plane carries the session's tool inputs and outputs. Grant a reader out of band rather than through a chat session: ${this.readerRemedy(DEV_OWNER, foreign[0])}`, undefined, "not-executed");
9413
+ const lifecycleUid = mintLifecycleUid();
9414
+ const exp = Math.floor(Date.now() / 1000) + input.ttl;
9415
+ const pin = { owner: DEV_OWNER, actor: identity.id, lifecycleUid, alias };
9416
+ const session = { alias, actor: identity.id, lifecycleUid, channels: lists.allowSubscribe, expiresAt: exp * 1000 };
9417
+ // Registered before the first durable write, so the startup sweep never mistakes this opening's
9418
+ // provisioning row for a crashed spawn, and the stop knows it.
9419
+ try {
9420
+ this.chatExpiry.register(session);
9421
+ }
9422
+ catch (e) {
9423
+ throw new EpEnvelopeError("unavailable", rejectionText(e), undefined, "not-executed");
9424
+ }
9425
+ try {
9426
+ await this.withLifecycleExecutor(pin, (t) => activateStaticLifecycle(t, { owner: DEV_OWNER, alias, actor: identity.id, lifecycleUid, managerInstance: this.managerLifecycleUid, ownerInstanceId: this.managerInstanceId }));
9427
+ const creds = await this.withProvisioner((prov) => provisionAgent(prov, this.auth, identity, { subscribe: lists.subscribe, allowSubscribe: lists.allowSubscribe, allowPublish: lists.allowPublish, lifecycleUid, expiresAt: exp }));
9428
+ // The same credential id the issuance path derives, so a re-sent identical credential maps to
9429
+ // the same row.
9430
+ const credentialId = rawDigest(creds).replace("sha256:", "sha256-");
9431
+ await this.withLifecycleExecutor(pin, async (t) => {
9432
+ await recordSlotCredential(t, DEV_OWNER, alias, lifecycleUid, credentialId);
9433
+ await appendStaticCredentialRow(t, { lifecycleUid, credentialId, holderPrincipal: principalKey(DEV_OWNER, identity.id).key, exp });
9434
+ const slot = await readStaticSlot(t, DEV_OWNER, alias);
9435
+ if (slot === undefined || slot.row.lifecycleUid !== lifecycleUid || slot.row.phase !== "provisioning")
9436
+ throw new Error(`the static slot for "${alias}" is ${slot === undefined ? "absent" : `${slot.row.phase} at uid ${slot.row.lifecycleUid}`}, not this opening's provisioning intent`);
9437
+ await casStaticSlot(t, { ...slot.row, phase: "active" }, slot.revision);
9438
+ });
9439
+ const { sealer, sealed } = sealChatAdmission({
9440
+ space: this.space, managerInstanceId: this.managerInstanceId, alias, actor: identity.id, lifecycleUid, expiresAt: exp * 1000, sealTo: input.sealTo, creds,
9441
+ });
9442
+ this.chatExpiry.arm(lifecycleUid);
9443
+ console.error(`chat session ${alias} admitted as ${identity.id} (uid ${lifecycleUid}), expires ${new Date(exp * 1000).toISOString()}`);
9444
+ return { alias, actor: identity.id, lifecycleUid, expiresAt: exp * 1000, sealer, sealed };
9445
+ }
9446
+ catch (e) {
9447
+ // A confirmed rollback leaves a known state, a retired slot. An unconfirmed one does not, so
9448
+ // the session stays registered and its expiry, or the stop, retries the same terminal.
9449
+ let rollback;
9450
+ try {
9451
+ await this.chatTerminal(session);
9452
+ }
9453
+ catch (r) {
9454
+ rollback = rejectionText(r);
9455
+ try {
9456
+ this.chatExpiry.arm(lifecycleUid);
9457
+ }
9458
+ catch (t) {
9459
+ rollback += `; ${rejectionText(t)}`;
9460
+ }
9461
+ }
9462
+ throw new EpEnvelopeError(e instanceof EpEnvelopeError ? e.code : "unavailable", `chat-session-open did not complete for ${alias}: ${rejectionText(e)}; ${rollback === undefined ? "its rollback retired the slot" : `its rollback was not confirmed (${rollback}), so the slot stays held until a retire, its expiry here, this manager's stop, or the next start of this manager instance`}`, undefined, rollback === undefined ? "executed" : "unknown");
9463
+ }
9464
+ }
9465
+ /** `chat-session-retire`: end one static chat session through the native static terminal (ledger
9466
+ * row marked revoked, verified eviction of its principal's live connections, footprint cleanup,
9467
+ * gate and head retirement). It refuses before any effect unless {@link admitChatRetirement}
9468
+ * admits the request. A terminal that cannot complete answers `failed` with the reason, never
9469
+ * success; the slot stays held. The broker keeps accepting the leaf until its expiry either way. */
9470
+ async retireStaticChatSession(raw) {
9471
+ if (!this.auth || this.userMode)
9472
+ throw new EpEnvelopeError("unimplemented", "chat-session-retire retires static chat sessions only, and this manager is not a static-auth manager", undefined, "not-executed");
9473
+ const input = readChatRetireInput(raw);
9474
+ if ("error" in input)
9475
+ throw new EpEnvelopeError("bad-request", input.error, undefined, "not-executed");
9476
+ if (await this.admitChatRetirement(input) === "alreadyRetired")
9477
+ return { outcome: "alreadyRetired" };
9478
+ try {
9479
+ await this.chatTerminal({ ...input, channels: this.chatExpiry.channels(input.lifecycleUid) });
9480
+ }
9481
+ catch (e) {
9482
+ return { outcome: "failed", reason: `retirement pending: ${rejectionText(e)}` };
9483
+ }
9484
+ return { outcome: "retired" };
9485
+ }
9486
+ /** The expiry cleanup's retirement of one session this process admitted. It is lifecycle work, so
9487
+ * it is admitted like a control call and a stop drains it. A throw is a failure the tracker
9488
+ * retries; a slot that is no longer this instance's to retire ends the tracking instead. */
9489
+ async expireStaticChatSession(session) {
9490
+ const release = this.beginLifecycle();
9491
+ if (!release)
9492
+ throw new Error(this.maintenanceError());
9493
+ try {
9494
+ console.error(`chat session ${session.alias} (uid ${session.lifecycleUid}) reached its expiry; retiring it`);
9495
+ let admitted;
9496
+ try {
9497
+ admitted = await this.admitChatRetirement(session);
9498
+ }
9499
+ catch (e) {
9500
+ if (e instanceof EpEnvelopeError && (e.code === "not-found" || e.code === "failed-precondition")) {
9501
+ console.error(`! chat session ${session.alias} expired, but its slot is not this instance's to retire (${e.message}); expiry tracking ends`);
9502
+ return;
9503
+ }
9504
+ throw e;
9505
+ }
9506
+ if (admitted === "admitted")
9507
+ await this.chatTerminal(session);
9508
+ }
9509
+ finally {
9510
+ release();
9511
+ }
9512
+ }
9513
+ /** The ownership admission shared by `chat-session-retire`, the expiry and the stop. It throws a
9514
+ * not-executed refusal unless the alias is a chat alias whose slot this logical instance owns
9515
+ * at the requested UID and actor. An already retired slot ends its expiry tracking. */
9516
+ async admitChatRetirement(s) {
9517
+ if (!isChatAlias(s.alias))
9518
+ throw new EpEnvelopeError("permission-denied", `"${s.alias}" is not a chat session alias`, undefined, "not-executed");
9519
+ if (this.reconcilingAliases.has(s.alias))
9520
+ throw new EpEnvelopeError("unavailable", `the chat session "${s.alias}" is still reconciling at manager startup; that terminal owns it`, undefined, "not-executed");
9521
+ const recordsKv = this.goalWriter?.ctx.kv;
9522
+ if (!recordsKv)
9523
+ throw new EpEnvelopeError("unavailable", `the durable static slot store is not ready while retiring "${s.alias}"`, undefined, "not-executed");
9524
+ let slot;
9525
+ try {
9526
+ slot = await readStaticSlot(staticLifecycleTransport(recordsKv, recordsKv), DEV_OWNER, s.alias);
9527
+ }
9528
+ catch (e) {
9529
+ throw new EpEnvelopeError("unavailable", `the durable slot for "${s.alias}" could not be read: ${rejectionText(e)}`, undefined, "not-executed");
9530
+ }
9531
+ const refused = chatRetireRefusal(slot?.row, s, this.managerInstanceId);
9532
+ if (refused)
9533
+ throw new EpEnvelopeError(refused.code, refused.message, undefined, "not-executed");
9534
+ if (slot.row.phase !== "retired")
9535
+ return "admitted";
9536
+ this.chatExpiry.forget(s.lifecycleUid);
9537
+ return "alreadyRetired";
9538
+ }
9539
+ /** One static terminal per chat session UID, however many callers ask: the explicit retire, the
9540
+ * expiry cleanup and the open's rollback join it instead of racing its CAS sequence. A completed
9541
+ * terminal ends the expiry tracking. `channels` names the membership rows to delete by name;
9542
+ * without it the delivery inventory alone finds them. */
9543
+ chatTerminal(s) {
9544
+ let flight = this.chatRetirements.get(s.lifecycleUid);
9545
+ if (!flight) {
9546
+ flight = this.driveStaticRetirement({ id: s.actor, name: s.alias, lifecycleUid: s.lifecycleUid, secretPaths: {}, ...(s.channels ? { launch: { allowSubscribe: s.channels } } : {}) }, true)
9547
+ .then(() => this.chatExpiry.forget(s.lifecycleUid))
9548
+ .finally(() => this.chatRetirements.delete(s.lifecycleUid));
9549
+ this.chatRetirements.set(s.lifecycleUid, flight);
9550
+ }
9551
+ return flight;
9552
+ }
9553
+ /** Whether `row` is a chat slot this process registered, at its exact alias, actor and UID,
9554
+ * under this logical instance and this process's incarnation. */
9555
+ holdsLiveChatSlot(row) {
9556
+ return row.ownerInstanceId === this.managerInstanceId && row.managerInstance === this.managerLifecycleUid && this.chatExpiry.holdsExact(row);
9557
+ }
9558
+ /** The stop's retirement of every chat session this process still tracks, through the same
9559
+ * ownership admission and terminal as `chat-session-retire`, at most
9560
+ * {@link CHAT_EXPIRY_CONCURRENCY} at a time. It runs after the lifecycle fence closed and every
9561
+ * admitted operation drained, as the seat teardown does, and mints nothing the terminal does not
9562
+ * already mint. Every outcome is logged. It throws, naming each one, when any session was not
9563
+ * retired; that session stays held until the next start of this manager instance. */
9564
+ async retireChatSessionsOnStop() {
9565
+ const queue = this.chatExpiry.remaining();
9566
+ const failed = [];
9567
+ const retireOne = async (session) => {
9568
+ const at = `chat session ${session.alias} (uid ${session.lifecycleUid})`;
9569
+ try {
9570
+ if (await this.admitChatRetirement(session) === "alreadyRetired") {
9571
+ console.error(`${at} was already retired at stop`);
9572
+ return;
9573
+ }
9574
+ await this.chatTerminal(session);
9575
+ console.error(`${at} retired at stop`);
9576
+ }
9577
+ catch (e) {
9578
+ if (e instanceof EpEnvelopeError && (e.code === "not-found" || e.code === "failed-precondition")) {
9579
+ this.chatExpiry.forget(session.lifecycleUid);
9580
+ console.error(`! ${at} was not retired at stop: its slot is not this instance's to retire (${e.message})`);
9581
+ return;
9582
+ }
9583
+ failed.push(`${session.alias} (uid ${session.lifecycleUid}): ${rejectionText(e)}`);
9584
+ console.error(`! ${at} was not retired at stop (${rejectionText(e)}); it stays held until the next start of this manager instance`);
9585
+ }
9586
+ };
9587
+ await Promise.all(Array.from({ length: Math.min(CHAT_EXPIRY_CONCURRENCY, queue.length) }, async () => {
9588
+ for (let next = queue.shift(); next !== undefined; next = queue.shift())
9589
+ await retireOne(next);
9590
+ }));
9591
+ if (failed.length)
9592
+ throw new Error(`${failed.length} chat session(s) were not retired and stay held until the next start of this manager instance: ${failed.join("; ")}`);
9593
+ }
9594
+ /** Name, at a stop that does not retire them, each chat session this process still tracks. */
9595
+ reportChatSessionsLeftAtStop(why) {
9596
+ for (const session of this.chatExpiry.remaining())
9597
+ console.error(`! chat session ${session.alias} (uid ${session.lifecycleUid}) was not retired because ${why}; the next start of this manager instance retires it`);
9598
+ }
9144
9599
  /** F5(b) push renewal of ONE live managed-static credential (Unit B): re-mint the SAME nkey
9145
9600
  * identity with the SAME scope (recorded on the managed row at spawn) and a fresh bounded
9146
9601
  * exp, ledger the new credentialId (slot record first, then the row, then the file — a
@@ -9505,6 +9960,15 @@ export class Manager {
9505
9960
  preservedForeign++;
9506
9961
  continue;
9507
9962
  }
9963
+ // A chat session this process is opening or holds is live membership, as an adopted seat
9964
+ // is: its own open, expiry or stop runs its terminal, which this sweep must not race. The
9965
+ // match is the exact registered alias, actor and UID under this logical instance and this
9966
+ // process's incarnation, so a foreign, legacy or unrecorded row, or any UID this process
9967
+ // did not register, falls through to the ordinary plan below.
9968
+ if (this.holdsLiveChatSlot(row)) {
9969
+ preservedAdopted++;
9970
+ continue;
9971
+ }
9508
9972
  // ADOPTION is genuine membership: a slot backed by a live managed agent THIS process owns
9509
9973
  // at the SAME uid is never an orphan (empty at boot; exactly the adopted set at the
9510
9974
  // post-adoption sweep — the fix for the F3 resume hole).
@@ -9721,6 +10185,8 @@ export class Manager {
9721
10185
  * supervisor); regular agents are denied STREAM.PURGE under auth. Cleanup only: leaves live agents and
9722
10186
  * the TASK queue alone. */
9723
10187
  async opPurge(args, _caller) {
10188
+ if (this.execution === "none")
10189
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
9724
10190
  const includeDms = args.includeDms === true;
9725
10191
  try {
9726
10192
  const creds = this.auth ? await mintCreds(this.auth, newIdentity(), "purger") : undefined;
@@ -9748,6 +10214,8 @@ export class Manager {
9748
10214
  * only if `file.owner == caller`, else admin is required. Fail-closed: an ownerless file
9749
10215
  * (legacy / operator-written) is admin-only. */
9750
10216
  opDefinePersona(args, caller, admin) {
10217
+ if (this.execution === "none")
10218
+ throw new EpEnvelopeError("unimplemented", this.executionRefusal());
9751
10219
  const name = String(args.name ?? "").trim();
9752
10220
  if (!name)
9753
10221
  return { ok: false, error: "name required" };
@@ -9898,13 +10366,16 @@ export class Manager {
9898
10366
  * `enter` defaults to true: a harness command typed but not submitted has not been delivered.
9899
10367
  * Nothing is echoed back; the caller reads the resulting turns from the event plane. */
9900
10368
  async inputAuthorized(a, args) {
10369
+ // These three refuse before the first write, so nothing was typed (SPEC 13.3). A write the
10370
+ // runtime rejects below states no outcome: the runtime contract does not say a rejected write
10371
+ // delivered nothing, and an earlier slice may already have reached the seat.
9901
10372
  if (this.agents.get(a.name) !== a)
9902
- throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" was replaced during authorization - retry`);
10373
+ throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" was replaced during authorization - retry`, undefined, "not-executed");
9903
10374
  if (a.handle.status() !== "running")
9904
- throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" is not running (${a.handle.status()}); nothing to type into`);
10375
+ throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" is not running (${a.handle.status()}); nothing to type into`, undefined, "not-executed");
9905
10376
  const write = a.handle.write?.bind(a.handle);
9906
10377
  if (!write)
9907
- throw new EpEnvelopeError("unimplemented", `input is not supported by runtime ${a.handle.kind}`);
10378
+ throw new EpEnvelopeError("unimplemented", `input is not supported by runtime ${a.handle.kind}`, undefined, "not-executed");
9908
10379
  // The contract validated `text` (non-empty, <= 64KiB) and `enter` (boolean) before this ran, so
9909
10380
  // the only decision left is the carriage return. `!== false` and not `?? true`: an ABSENT enter
9910
10381
  // and an explicit `true` must behave identically, and only `false` may suppress the return.
@@ -9978,18 +10449,19 @@ export class Manager {
9978
10449
  * redeems it over the mesh with a per-session rails-only cred it mints itself. Only streamable
9979
10450
  * backends (pty/host) attach; an external runtime's attach() throws with per-runtime guidance. */
9980
10451
  async attachAuthorized(a, caller) {
10452
+ // Every refusal before establishAttach below has opened no session, so it states that (SPEC 13.3).
9981
10453
  if (!this.sessionPlane)
9982
- return { ok: false, error: "the manager session plane is not available (the manager is not fully started)" };
10454
+ throw new EpEnvelopeError("failed-precondition", "the manager session plane is not available (the manager is not fully started)", undefined, "not-executed");
9983
10455
  // A name is a reusable slot and the authorization above can await (user mode reads the ledger).
9984
10456
  // The ep target is incarnation-pinned, so `a` cannot BE a successor — but the slot it names can
9985
10457
  // have been stopped and refilled while we waited, and everything below must act on the
9986
10458
  // incarnation this caller was actually authorized for, never on whoever holds the name now.
9987
10459
  if (this.agents.get(a.name) !== a)
9988
- return { ok: false, error: `agent "${a.name}" was replaced during authorization - retry` };
10460
+ throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" was replaced during authorization - retry`, undefined, "not-executed");
9989
10461
  // Never establish a session over a dead agent — a doomed session (the caller would get an
9990
10462
  // immediate process-exit at best, a confusing empty terminal at worst). Refuse honestly.
9991
10463
  if (a.handle.status() !== "running")
9992
- return { ok: false, error: `agent "${a.name}" is not running (${a.handle.status()}); nothing to attach` };
10464
+ throw new EpEnvelopeError("failed-precondition", `agent "${a.name}" is not running (${a.handle.status()}); nothing to attach`, undefined, "not-executed");
9993
10465
  // CLAIM the session slot BEFORE attaching the target's PTY, matching the console door: this door
9994
10466
  // used to attach first and check capacity inside establishAttach, so a `resource-exhausted`
9995
10467
  // refusal landed AFTER the attach. The claim is carried INTO establishAttach, so one reservation
@@ -10007,7 +10479,8 @@ export class Manager {
10007
10479
  }
10008
10480
  catch (e) {
10009
10481
  slot.release();
10010
- return { ok: false, error: rejectionText(e) };
10482
+ // The external runtimes refuse attach() before acquiring anything (see the residual above).
10483
+ throw new EpEnvelopeError("failed-precondition", rejectionText(e), undefined, "not-executed");
10011
10484
  }
10012
10485
  // establishAttach releases the claim it was handed on every exit; nothing to unwind here.
10013
10486
  const { grant } = await this.sessionPlane.establishAttach(caller, { name: a.name, lifecycleUid: a.lifecycleUid }, session, slot);
@@ -10066,6 +10539,7 @@ export class Manager {
10066
10539
  * owner-domain bound); undefined = unbounded. {@link NO_OWNER_MATCHES} matches nothing. */
10067
10540
  list(ownerFilter) {
10068
10541
  const roster = new Map(this.ep.getRoster().map((p) => [p.card.name, p]));
10542
+ const observedAt = Date.now();
10069
10543
  // The roster is only evidence while THIS observer's presence watch is fresh. A stale view
10070
10544
  // (whole-bucket silence past TTL) or an unpopulated one (snapshot not yet replayed) cannot
10071
10545
  // support "offline" or "absent" for anyone: a live deployment on 2026-09-09 rendered every live seat as
@@ -10085,6 +10559,7 @@ export class Manager {
10085
10559
  // The connector-reported provider from presence (#785): the live roster card's `meta.provider`,
10086
10560
  // when it is a non-empty string. Absent when the connector reported none — never fabricated.
10087
10561
  const seen = roster.get(a.name);
10562
+ const presenceIdentityVerified = this.isOwnPresence(a, seen);
10088
10563
  const providerMeta = seen?.card.meta?.provider;
10089
10564
  const provider = typeof providerMeta === "string" && providerMeta ? providerMeta : undefined;
10090
10565
  // The harness-reported condition beside the mesh status (#618): a turn that died upstream reads
@@ -10129,6 +10604,8 @@ export class Manager {
10129
10604
  // `current` is the only state in which `mesh` is a verdict; the other two are the
10130
10605
  // observer's own condition and travel on the row (older CLIs ignore the field).
10131
10606
  meshView: view.state,
10607
+ presenceIdentityVerified,
10608
+ ...(view.state === "current" && presenceIdentityVerified ? { presenceObservedAt: observedAt } : {}),
10132
10609
  ...(offlineSince !== undefined ? { offlineSince } : {}),
10133
10610
  // The incarnation coordinate (SPEC 13.1) — with `id`, exactly what a v0.4 caller needs to
10134
10611
  // build a targeted (`despawn`/`attach`) request against THIS incarnation.