@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.
- package/dist/chat-session.d.ts +165 -0
- package/dist/chat-session.d.ts.map +1 -0
- package/dist/chat-session.js +281 -0
- package/dist/chat-session.js.map +1 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +384 -163
- package/dist/commands.js.map +1 -1
- package/dist/endpoint-evict.d.ts +2 -0
- package/dist/endpoint-evict.d.ts.map +1 -1
- package/dist/endpoint-evict.js +1 -0
- package/dist/endpoint-evict.js.map +1 -1
- package/dist/holder-liveness.d.ts +1 -0
- package/dist/holder-liveness.d.ts.map +1 -1
- package/dist/holder-liveness.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manager-service-contract.d.ts +61 -1
- package/dist/manager-service-contract.d.ts.map +1 -1
- package/dist/manager-service-contract.js +225 -4
- package/dist/manager-service-contract.js.map +1 -1
- package/dist/manager.d.ts +106 -3
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +526 -49
- package/dist/manager.js.map +1 -1
- package/dist/operator-deregister.d.ts +4 -0
- package/dist/operator-deregister.d.ts.map +1 -0
- package/dist/operator-deregister.js +75 -0
- package/dist/operator-deregister.js.map +1 -0
- package/dist/remote-authority.d.ts +4 -2
- package/dist/remote-authority.d.ts.map +1 -1
- package/dist/remote-authority.js +39 -0
- package/dist/remote-authority.js.map +1 -1
- package/dist/run-hosting.d.ts +34 -1
- package/dist/run-hosting.d.ts.map +1 -1
- package/dist/run-hosting.js +369 -15
- package/dist/run-hosting.js.map +1 -1
- package/dist/service-adapter.d.ts +12 -0
- package/dist/service-adapter.d.ts.map +1 -0
- package/dist/service-adapter.js +174 -0
- package/dist/service-adapter.js.map +1 -0
- package/dist/service-child.d.ts +37 -0
- package/dist/service-child.d.ts.map +1 -0
- package/dist/service-child.js +195 -0
- package/dist/service-child.js.map +1 -0
- package/dist/service-observation.d.ts +19 -0
- package/dist/service-observation.d.ts.map +1 -0
- package/dist/service-observation.js +97 -0
- package/dist/service-observation.js.map +1 -0
- package/dist/static-lifecycle.d.ts +8 -6
- package/dist/static-lifecycle.d.ts.map +1 -1
- package/dist/static-lifecycle.js +19 -8
- package/dist/static-lifecycle.js.map +1 -1
- package/dist/vitals.d.ts +37 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +117 -0
- package/dist/vitals.js.map +1 -0
- 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" && (
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|