@cotal-ai/manager 0.70.1 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manager.js CHANGED
@@ -4,7 +4,7 @@ import { credsAuthenticator } from "@nats-io/transport-node";
4
4
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
5
5
  import { isAbsolute, join, dirname, resolve } from "node:path";
6
6
  import Headless from "@xterm/headless";
7
- import { CotalEndpoint, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
7
+ import { CotalEndpoint, canonicalJson, MANAGED_HANDOFF_KIND, accountFromCreds, credsClaims, DEFAULT_SERVER, DEV_OWNER, MANAGER_LEASE_RENEW_MS, STANDING_RENEWABLE_TTL_SEC, assertLifecycleToken, discardLaunchArtifacts, divergentSecretStoreNotice, parseDaemonStoreAnswer, parseSecretStoreIdentity, sameSecretStoreIdentity, agentFilePath, clearSpaceHistory, dialerFor, connectorServers, spawnEnvAllow, deprovisionAgent, DeprovisionError, isConcreteChannel, deprovisionTargetPrincipal, firstFreeName, spawnNameError, idFromCreds, inspectCredHealth, credsRenewalDelayMs, composeWirePersona, loadAgentFile, listPersonaCatalog, personaCatalogDescription, personaCatalogReadable, loadCotalConfig, modelPolicyRefusal, mintCreds, mintGeneration, mintAcceptedToken, openIssuedStore, acceptedBucket, issuedBucket, mintLifecycleUid, managedRetirementOpId, mkSecretDir, newIdentity, actionContext, parsePrincipalKey, principalKey, probeConnect, provisionAgent, provisionAgentDurables, registry, resolveAuthProvider, resolveReadAcl, runAgentBearer, saveAgentFile, subjectMatches, AUTH_ENDPOINT, EP_CMD_RETIRE_LIFECYCLE, controlServiceSubject, eventChannelPrincipal, resolveService, invokeCommand, } from "@cotal-ai/core";
8
8
  import { agentAuthState, agentCredsDir, agentLifecycleSecretFilePaths, agentSecretFilePaths, agentSecretKeyForFile, authDir, claimManagerSiblingIdentities, connectorInstallHint, DELIVERY_CREDS_KIND, extensionConnectors, findCotalRoot, getSpaceAuth, hasUserAuthState, loadExtensionsManifest, loadManagerInstanceIdentity, loadMeshes, localTrustOfSpace, manifestExtensionNames, materializeFromManifest, materializeSecretToFile, MEMBERSHIP_RW_CREDS_KIND, mergeLaunchOptions, remintDaemonCreds, resolveAgentType, resolveOnPath, createManagerInstanceIdentity, spaceMaterialKey, SYSTEM_CREDS_FILES, userAuthStateDir, workspaceSecretStore, writeRenewalRecord } from "@cotal-ai/workspace";
9
9
  import { createRuntime, isCustodialRuntime, requireRuntimeAdopt, requireRuntimeReap, RuntimeReapUnproven, } from "./runtime/index.js";
10
10
  import { AttachEndpoint } from "./attach-endpoint.js";
@@ -22,7 +22,7 @@ import { TranscriptReceiver } from "./transcript-receive.js";
22
22
  // manager-side adapter (transport + slot orchestration + the F1 terminal) — see static-lifecycle.ts.
23
23
  import { jetstream, jetstreamManager } from "@nats-io/jetstream";
24
24
  import { Kvm } from "@nats-io/kv";
25
- import { recordsBucket, epAuthBucket, ensureAuthorityStores, ensureContractStore, createEndpointStreams, contractStoreContext, publishContractArtifact, contractArtifactCanonicalBytes, standaloneConnectOpts, STATIC_SLOT_PREFIX, rawDigest, STANDING_RENEWABLE_TTL_SEC as MANAGED_STATIC_TTL_SEC, newArtifactSigner, RotatingSigner, generationAnchor, sessionsBucket, SESSION_GRANT_MAX_TTL_MS, REMOTE_MANAGER_IDENTITY_NAMES, } from "@cotal-ai/core";
25
+ import { recordsBucket, epAuthBucket, ensureAuthorityStores, ensureContractStore, createEndpointStreams, contractStoreContext, publishContractArtifact, contractArtifactCanonicalBytes, standaloneConnectOpts, rawDigest, STANDING_RENEWABLE_TTL_SEC as MANAGED_STATIC_TTL_SEC, newArtifactSigner, RotatingSigner, generationAnchor, sessionsBucket, SESSION_GRANT_MAX_TTL_MS, REMOTE_MANAGER_IDENTITY_NAMES, } from "@cotal-ai/core";
26
26
  // P2 item 6: the manager's ONE §13.6 session plane — offer mint + one-use redeem + PTY-bridge
27
27
  // standup for `attach`, over a dedicated standing session-LEDGER connection (the byte rails ride
28
28
  // per-session credentials on their own short-lived connections).
@@ -32,7 +32,7 @@ import { ManagerSessionPlane, openSessionLedgerKv } from "./session/index.js";
32
32
  // the register/authorize/serve seams, all driven over a scoped one-shot executor connection.
33
33
  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
34
  import { MANAGER_ENDPOINT, managerClusterArtifacts, managerCommandDefs, managerContractArtifactValues } from "./manager-service-contract.js";
35
- import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
35
+ import { staticLifecycleTransport, activateStaticLifecycle, runStaticTerminal, readStaticSlot, casStaticSlot, recordSlotCredential, appendStaticCredentialRow, planStaticSlotResume, ownedBySibling, observeStaticSlot, listStaticSlotObservations, walkStaticSlots, renderStaticSlotObservation, StaticSlotReadError, STATIC_SLOT_READ_FAILED_DETAIL, } from "./static-lifecycle.js";
36
36
  /** Minimum slot lifetime for rate-flooring (P4c). A slot freed (by despawn OR natural exit/reap)
37
37
  * before living this long leaves a cooling stamp that still counts toward the ceiling until it
38
38
  * expires — so churn (spawn↔despawn or spawn↔fast-exit) can't outrun the concurrency bound. */
@@ -126,9 +126,11 @@ function retireOpId(lifecycleUid) {
126
126
  * dash) — what {@link Manager.psOwnerFilter} returns for an unparseable caller so a malformed
127
127
  * principal fail-closes to an empty `ps` instead of an unbounded one. */
128
128
  const NO_OWNER_MATCHES = "-no-owner-";
129
- /** A caught value's text for a refusal. A host-supplied store or callback may reject with any value,
130
- * including one whose `message` is a Symbol or whose `message` getter or `toString` throws, and the
131
- * refusal must still be returned, so the coercion to text runs inside the guard. */
129
+ /** A caught value's text for a refusal or a log line. A host-supplied store, runtime or callback may
130
+ * reject with any value, including `null` or one whose `message` is a Symbol or whose `message`
131
+ * getter or `toString` throws. A handler that throws while building its text skips the work after
132
+ * it, and on a detached chain ends the manager with an unhandled rejection, so the coercion to text
133
+ * runs inside the guard. */
132
134
  function rejectionText(e) {
133
135
  try {
134
136
  return String(e?.message ?? e);
@@ -137,6 +139,34 @@ function rejectionText(e) {
137
139
  return "an unreadable rejection";
138
140
  }
139
141
  }
142
+ /** A caught value as a lifecycle envelope, or undefined when it is not one. A thrown Proxy can throw
143
+ * from `instanceof` itself, so the test runs inside the guard. */
144
+ function caughtEnvelope(e) {
145
+ try {
146
+ return e instanceof EpEnvelopeError ? e : undefined;
147
+ }
148
+ catch {
149
+ return undefined;
150
+ }
151
+ }
152
+ /** The failed terminal a post-accept fallback commits for `error` and `source`'s optional details.
153
+ * `commitGoalResult` refuses a payload that is not interchangeable JSON, and this terminal must commit
154
+ * whatever failed, so a lone surrogate in the text becomes U+FFFD and details it cannot read or carry
155
+ * (a Proxy's throwing getter, an accessor, a cycle) are dropped. A Proxy can answer the check and the
156
+ * serializer differently, so the detached copy is checked again before it commits. */
157
+ function failedTerminal(error, source) {
158
+ const text = error.replace(/\p{Cs}/gu, "\uFFFD");
159
+ try {
160
+ const details = source?.details;
161
+ if (details) {
162
+ const copy = JSON.parse(canonicalJson(details));
163
+ canonicalJson(copy);
164
+ return { error: text, details: copy };
165
+ }
166
+ }
167
+ catch { /* unreadable or not interchangeable */ }
168
+ return { error: text };
169
+ }
140
170
  /** Run one cleanup step, recording its failure in `failed` as `<what>: <reason>`. A step that threw
141
171
  * would otherwise skip every step after it, and a swallowed one would leave nothing to report. */
142
172
  async function attemptCleanup(failed, what, step) {
@@ -159,6 +189,18 @@ function withTimeout(p, ms, msg) {
159
189
  function sameStrings(a, b) {
160
190
  return JSON.stringify([...(a ?? [])].sort()) === JSON.stringify([...(b ?? [])].sort());
161
191
  }
192
+ /** The caller keeps the arrays it passed, so a launch form is admitted with its own copies: a later
193
+ * write to the caller's array cannot change what the manager grants, shares or preserves. */
194
+ function ownLists(form) {
195
+ return {
196
+ ...form,
197
+ ...(form.subscribe && { subscribe: [...form.subscribe] }),
198
+ ...(form.allowSubscribe && { allowSubscribe: [...form.allowSubscribe] }),
199
+ ...(form.allowPublish && { allowPublish: [...form.allowPublish] }),
200
+ ...(form.capabilities && { capabilities: [...form.capabilities] }),
201
+ ...(form.shareTools && { shareTools: [...form.shareTools] }),
202
+ };
203
+ }
162
204
  /** Operator-facing phrasing per cause. Kept beside the union so adding a member without a sentence
163
205
  * is a type error rather than a blank in the log. The stop family's sentences carry the principal
164
206
  * from the cause, so they render through {@link STOP_CAUSE_TEXT} under the same exhaustive-Record
@@ -899,7 +941,7 @@ export class Manager {
899
941
  await this.attach.stop();
900
942
  throw new Error(held
901
943
  ? `manager instance ${this.managerInstanceId} already serves space "${this.space}" from ${held.root === this.leaseInfo.root ? "this" : "another"} workspace root (${held.runtime}, pid ${held.pid}, root ${held.root}) - stop it first before restarting the same instance`
902
- : `could not acquire the manager lease for space "${this.space}": ${e.message}`);
944
+ : `could not acquire the manager lease for space "${this.space}": ${rejectionText(e)}`);
903
945
  }
904
946
  this.leaseTimer = setInterval(() => { void this.renewLease(); }, MANAGER_LEASE_RENEW_MS);
905
947
  this.leaseTimer.unref?.();
@@ -936,7 +978,7 @@ export class Manager {
936
978
  // attempt returns.
937
979
  const startupReconcile = this.auth && !this.userMode ? this.reconcileStaticLifecycles() : undefined;
938
980
  if (startupReconcile)
939
- void startupReconcile.catch((e) => console.error(`! ${STARTUP_RECONCILING}: ${e.message} - no per-alias retry could be planned; a later manager start re-reads unfinished durable terminals`));
981
+ void startupReconcile.catch((e) => console.error(`! ${STARTUP_RECONCILING}: ${rejectionText(e)} - no per-alias retry could be planned; a later manager start re-reads unfinished durable terminals`));
940
982
  // P2 item 1 (1d): the manager serves NO ctl tiers - its whole control surface is the v0.4
941
983
  // service endpoint registered below. The old three-tier rail (self/manager/admin) is deleted;
942
984
  // `ctl.delivery`/`ctl.delivery-admin` (the delivery daemon) and `ctl.auth-admin` (the auth
@@ -987,13 +1029,13 @@ export class Manager {
987
1029
  await this.renewRemoteExecutor();
988
1030
  }
989
1031
  catch (e) {
990
- console.error(`! remote manager renewal pass failed: ${e.message}`);
1032
+ console.error(`! remote manager renewal pass failed: ${rejectionText(e)}`);
991
1033
  }
992
1034
  try {
993
1035
  await this.runHosting?.renew();
994
1036
  }
995
1037
  catch (e) {
996
- console.error(`! remote run-hosting renewal pass failed: ${e.message}`);
1038
+ console.error(`! remote run-hosting renewal pass failed: ${rejectionText(e)}`);
997
1039
  }
998
1040
  }, intervalMs);
999
1041
  this.credRenewTimer.unref?.();
@@ -1104,7 +1146,7 @@ export class Manager {
1104
1146
  reply = await this.ep.requestDeliveryAdmin("reloadStoreIdentity", {}, timeoutMs);
1105
1147
  }
1106
1148
  catch (e) {
1107
- const msg = e.message;
1149
+ const msg = rejectionText(e);
1108
1150
  if (isAbsentDeliveryAdmin(msg))
1109
1151
  return this.absentByLeaseRow(isUnansweredDeliveryAdmin(e));
1110
1152
  const failure = `could not challenge the delivery daemon's SecretStore: ${msg}`;
@@ -1117,7 +1159,7 @@ export class Manager {
1117
1159
  answer = parseDaemonStoreAnswer(reply.data);
1118
1160
  }
1119
1161
  catch (e) {
1120
- throw new Error(`delivery daemon named an unreadable SecretStore: ${e.message}. A daemon older ` +
1162
+ throw new Error(`delivery daemon named an unreadable SecretStore: ${rejectionText(e)}. A daemon older ` +
1121
1163
  `than #1694 replies with a bare store identity and no answerer binding; this manager ` +
1122
1164
  `cannot establish which process answered, so nothing reminted - upgrade the delivery daemon`);
1123
1165
  }
@@ -1173,7 +1215,7 @@ export class Manager {
1173
1215
  }
1174
1216
  catch (e) {
1175
1217
  throw new Error(`could not read the delivery lease row to settle whether the delivery daemon is absent ` +
1176
- `(${e.message}) - absence is undetermined, nothing reminted`);
1218
+ `(${rejectionText(e)}) - absence is undetermined, nothing reminted`);
1177
1219
  }
1178
1220
  const holder = entry?.info.holder;
1179
1221
  if (holder !== undefined && holder !== "") {
@@ -1198,7 +1240,7 @@ export class Manager {
1198
1240
  this.daemonRenewalOwner = held;
1199
1241
  }
1200
1242
  catch (e) {
1201
- console.error(`! could not refresh the daemon renewal lease: ${e.message} - ownership is re-decided on the next pass`);
1243
+ console.error(`! could not refresh the daemon renewal lease: ${rejectionText(e)} - ownership is re-decided on the next pass`);
1202
1244
  }
1203
1245
  }
1204
1246
  /** Decide whether THIS manager owns the daemon's credential renewal (#1634). Both conditions are
@@ -1284,7 +1326,7 @@ export class Manager {
1284
1326
  return true;
1285
1327
  }
1286
1328
  catch (e) {
1287
- console.error(`! manager ${label} re-dial attempt failed: ${e.message}`);
1329
+ console.error(`! manager ${label} re-dial attempt failed: ${rejectionText(e)}`);
1288
1330
  }
1289
1331
  }
1290
1332
  return false;
@@ -1337,7 +1379,7 @@ export class Manager {
1337
1379
  : { ok: false, error: reply.error, detail: reply.data };
1338
1380
  }
1339
1381
  catch (e) {
1340
- adoption = { ok: false, error: `no delivery-admin responder (${e.message}) - the daemon's 75% re-read backstop adopts the re-signed file` };
1382
+ adoption = { ok: false, error: `no delivery-admin responder (${rejectionText(e)}) - the daemon's 75% re-read backstop adopts the re-signed file` };
1341
1383
  }
1342
1384
  }
1343
1385
  for (const r of results.filter((x) => !x.ok && !x.skipped))
@@ -1380,7 +1422,7 @@ export class Manager {
1380
1422
  await this.renewManagedStaticCred(a);
1381
1423
  }
1382
1424
  catch (e) {
1383
- console.error(`! managed cred renewal ${a.name}: ${e.message} - the agent dies loud at this cred's expiry unless it is reminted`);
1425
+ console.error(`! managed cred renewal ${a.name}: ${rejectionText(e)} - the agent dies loud at this cred's expiry unless it is reminted`);
1384
1426
  }
1385
1427
  }
1386
1428
  }
@@ -1408,7 +1450,7 @@ export class Manager {
1408
1450
  }
1409
1451
  }
1410
1452
  catch (e) {
1411
- console.error(`! endpoint-serve renewal: ${e.message} - the manager's service endpoint dies loud at this cred's expiry unless it is re-registered`);
1453
+ console.error(`! endpoint-serve renewal: ${rejectionText(e)} - the manager's service endpoint dies loud at this cred's expiry unless it is re-registered`);
1412
1454
  }
1413
1455
  }
1414
1456
  // The manager is also the goal-writer's renewal owner — re-mint the SAME
@@ -1427,7 +1469,7 @@ export class Manager {
1427
1469
  }
1428
1470
  }
1429
1471
  catch (e) {
1430
- console.error(`! goal-writer renewal: ${e.message} - spawn-as-action stops accepting at this cred's expiry unless the manager restarts`);
1472
+ console.error(`! goal-writer renewal: ${rejectionText(e)} - spawn-as-action stops accepting at this cred's expiry unless the manager restarts`);
1431
1473
  }
1432
1474
  }
1433
1475
  // P2 item 6: the manager is also the session-ledger's renewal owner — re-mint the SAME nkey
@@ -1449,12 +1491,12 @@ export class Manager {
1449
1491
  }
1450
1492
  }
1451
1493
  catch (e) {
1452
- console.error(`! session-ledger renewal: ${e.message} - attach stops establishing sessions at this cred's expiry unless the manager restarts`);
1494
+ console.error(`! session-ledger renewal: ${rejectionText(e)} - attach stops establishing sessions at this cred's expiry unless the manager restarts`);
1453
1495
  }
1454
1496
  }
1455
1497
  }
1456
1498
  catch (e) {
1457
- console.error(`! credential renewal pass failed: ${e.message}`);
1499
+ console.error(`! credential renewal pass failed: ${rejectionText(e)}`);
1458
1500
  }
1459
1501
  finally {
1460
1502
  release();
@@ -1472,7 +1514,7 @@ export class Manager {
1472
1514
  this.remoteExecutorCreds = await this.remoteAuthority.renewExecutor();
1473
1515
  }
1474
1516
  catch (e) {
1475
- console.error(`! remote manager executor renewal: ${e.message} - registration maintenance and clean deregistration remain unavailable until renewal succeeds`);
1517
+ console.error(`! remote manager executor renewal: ${rejectionText(e)} - registration maintenance and clean deregistration remain unavailable until renewal succeeds`);
1476
1518
  if (force)
1477
1519
  throw e;
1478
1520
  }
@@ -1543,7 +1585,7 @@ export class Manager {
1543
1585
  // endpoint's own reconnect loop retries with the adopted credential, not unadopted debt.
1544
1586
  this.remoteRenewalDebt = undefined;
1545
1587
  await Promise.all([
1546
- this.ep.reconnect().catch((e) => console.error(`! remote manager supervisor reconnect after renewal: ${e.message}`)),
1588
+ this.ep.reconnect().catch((e) => console.error(`! remote manager supervisor reconnect after renewal: ${rejectionText(e)}`)),
1547
1589
  this.pushRenewedCreds(this.serviceServe.nc),
1548
1590
  ...(this.goalWriter ? [this.pushRenewedCreds(this.goalWriter.nc)] : []),
1549
1591
  ...(this.sessionLedgerConn ? [this.pushRenewedCreds(this.sessionLedgerConn.nc)] : []),
@@ -1558,8 +1600,8 @@ export class Manager {
1558
1600
  return [];
1559
1601
  } })
1560
1602
  : [];
1561
- this.remoteRenewalDebt = { at: Date.now(), processEpoch, reason: e.message, unadoptedSubjects };
1562
- console.error(`! remote manager all-duty renewal: ${e.message} - no unsigned or local replacement is available`);
1603
+ this.remoteRenewalDebt = { at: Date.now(), processEpoch, reason: rejectionText(e), unadoptedSubjects };
1604
+ console.error(`! remote manager all-duty renewal: ${rejectionText(e)} - no unsigned or local replacement is available`);
1563
1605
  if (force)
1564
1606
  throw e;
1565
1607
  }
@@ -1631,7 +1673,7 @@ export class Manager {
1631
1673
  trackDeprovision(a, context = "") {
1632
1674
  this.lifecycleInFlight++;
1633
1675
  void this.deprovision(a)
1634
- .catch((e) => console.error(`deprovision${context ? ` ${context}` : ""} ${a.name} (${a.id}): ${e.message}`))
1676
+ .catch((e) => console.error(`deprovision${context ? ` ${context}` : ""} ${a.name} (${a.id}): ${rejectionText(e)}`))
1635
1677
  .finally(() => this.releaseLifecycle());
1636
1678
  }
1637
1679
  async awaitLifecycleDrain() {
@@ -1736,7 +1778,7 @@ export class Manager {
1736
1778
  parseResumeControlArgs({ attemptId, inventory });
1737
1779
  }
1738
1780
  catch (e) {
1739
- failures.push({ name: "<inventory>", id: attemptId, error: `prepared inventory would be rejected at resume: ${e.message}` });
1781
+ failures.push({ name: "<inventory>", id: attemptId, error: `prepared inventory would be rejected at resume: ${rejectionText(e)}` });
1740
1782
  }
1741
1783
  this.preservationInventory = inventory;
1742
1784
  this.preservationFailures = failures;
@@ -1821,7 +1863,7 @@ export class Manager {
1821
1863
  a.handle.stop({ graceful: false });
1822
1864
  }
1823
1865
  catch (e) {
1824
- failures.push({ name: a.name, id: a.id, error: `stop failed: ${e.message}` });
1866
+ failures.push({ name: a.name, id: a.id, error: `stop failed: ${rejectionText(e)}` });
1825
1867
  return;
1826
1868
  }
1827
1869
  try {
@@ -1830,7 +1872,7 @@ export class Manager {
1830
1872
  this.agents.delete(a.name);
1831
1873
  }
1832
1874
  catch (e) {
1833
- failures.push({ name: a.name, id: a.id, error: e.message });
1875
+ failures.push({ name: a.name, id: a.id, error: rejectionText(e) });
1834
1876
  }
1835
1877
  }));
1836
1878
  if (failures.length === 0)
@@ -1875,7 +1917,7 @@ export class Manager {
1875
1917
  return `retained reference is not a regular non-symlink file: ${path}`;
1876
1918
  }
1877
1919
  catch (e) {
1878
- return `retained reference unavailable: ${path} (${e.message})`;
1920
+ return `retained reference unavailable: ${path} (${rejectionText(e)})`;
1879
1921
  }
1880
1922
  }
1881
1923
  if (process.platform !== "win32") {
@@ -1904,7 +1946,7 @@ export class Manager {
1904
1946
  }
1905
1947
  }
1906
1948
  catch (e) {
1907
- return `retained reference cannot be hashed: ${e.message}`;
1949
+ return `retained reference cannot be hashed: ${rejectionText(e)}`;
1908
1950
  }
1909
1951
  return undefined;
1910
1952
  }
@@ -2019,11 +2061,11 @@ export class Manager {
2019
2061
  await this.awaitHandleExit(a.handle);
2020
2062
  }
2021
2063
  catch (e) {
2022
- failures.push(`${a.name}: ${e.message}`);
2064
+ failures.push(`${a.name}: ${rejectionText(e)}`);
2023
2065
  }
2024
2066
  }));
2025
2067
  // Deprovision EVERY snapshot entry regardless of whether its stop failed (allSettled + a loud log).
2026
- await Promise.allSettled(managed.filter((a) => !a.suppressCleanup).map((a) => this.deprovision({ ...a, ...(a.handedOff ? { delegatedHandle: a.handle } : {}) }).catch((e) => console.error(`deprovision ${a.name} (${a.id}) on shutdown: ${e.message}`))));
2068
+ await Promise.allSettled(managed.filter((a) => !a.suppressCleanup).map((a) => this.deprovision({ ...a, ...(a.handedOff ? { delegatedHandle: a.handle } : {}) }).catch((e) => console.error(`deprovision ${a.name} (${a.id}) on shutdown: ${rejectionText(e)}`))));
2027
2069
  if (failures.length)
2028
2070
  throw new Error(`manager shutdown could not prove every seat exited: ${failures.join("; ")}`);
2029
2071
  }
@@ -2040,7 +2082,7 @@ export class Manager {
2040
2082
  this.agents.delete(a.name);
2041
2083
  }
2042
2084
  catch (e) {
2043
- failures.push(`${a.name}: ${e.message}`);
2085
+ failures.push(`${a.name}: ${rejectionText(e)}`);
2044
2086
  }
2045
2087
  }
2046
2088
  return failures;
@@ -2059,7 +2101,7 @@ export class Manager {
2059
2101
  a.handle.stop({ graceful: false });
2060
2102
  }
2061
2103
  catch (e) {
2062
- failures.push(`${a.name}: stop failed: ${e.message}`);
2104
+ failures.push(`${a.name}: stop failed: ${rejectionText(e)}`);
2063
2105
  }
2064
2106
  try {
2065
2107
  await this.awaitHandleExit(a.handle);
@@ -2067,7 +2109,7 @@ export class Manager {
2067
2109
  this.agents.delete(a.name);
2068
2110
  }
2069
2111
  catch (e) {
2070
- failures.push(`${a.name}: ${e.message}`);
2112
+ failures.push(`${a.name}: ${rejectionText(e)}`);
2071
2113
  }
2072
2114
  }));
2073
2115
  if (failures.length)
@@ -2132,7 +2174,7 @@ export class Manager {
2132
2174
  }
2133
2175
  }
2134
2176
  catch (e) {
2135
- seatFailure = e;
2177
+ seatFailure = rejectionText(e);
2136
2178
  }
2137
2179
  // Every step below runs even when an earlier one fails, so a failed stop still closes this
2138
2180
  // process's connections and listener (#2290). Failures are collected and thrown at the end.
@@ -2142,12 +2184,12 @@ export class Manager {
2142
2184
  await run();
2143
2185
  }
2144
2186
  catch (e) {
2145
- failures.push(`${name}: ${e.message}`);
2187
+ failures.push(`${name}: ${rejectionText(e)}`);
2146
2188
  }
2147
2189
  };
2148
2190
  // A seat that may still be alive keeps this instance's leases and registration (see
2149
2191
  // teardownManagedAgents): they lapse on their TTL instead of being handed to a successor.
2150
- if (!seatFailure) {
2192
+ if (seatFailure === undefined) {
2151
2193
  await step("release manager lease", () => this.releaseOwnManagerLease());
2152
2194
  // #1634: hand the renewal lease back on a clean stop so a sibling picks it up on its next pass
2153
2195
  // rather than waiting out the bucket TTL. A non-owner holds no revision and this is a no-op.
@@ -2163,15 +2205,15 @@ export class Manager {
2163
2205
  // released run's status write is the last thing this incarnation says about it.
2164
2206
  await step("stop run hosting", () => this.runHosting?.stop());
2165
2207
  this.runHosting = undefined;
2166
- if (registrationRevision !== undefined && !seatFailure)
2208
+ if (registrationRevision !== undefined && seatFailure === undefined)
2167
2209
  await step("deregister service", () => this.deregisterServiceOnStop(registrationRevision));
2168
2210
  await step("stop goal writer", () => this.stopGoalWriter());
2169
2211
  await step("stop session plane", () => this.stopSessionPlane());
2170
2212
  await step("stop endpoint", () => this.ep.stop());
2171
2213
  await step("stop attach endpoint", () => this.attach.stop());
2172
2214
  const also = failures.length ? `; teardown also failed: ${failures.join("; ")}` : "";
2173
- if (seatFailure)
2174
- throw new Error(`${seatFailure.message}${also}`);
2215
+ if (seatFailure !== undefined)
2216
+ throw new Error(`${seatFailure}${also}`);
2175
2217
  if (releaseFailures.length)
2176
2218
  throw new Error(`manager shutdown could not release every detachable seat: ${releaseFailures.join("; ")}${also}`);
2177
2219
  if (failures.length)
@@ -2246,7 +2288,7 @@ export class Manager {
2246
2288
  // `absent` is silent: there was nothing registered to remove, which is not news at shutdown.
2247
2289
  }
2248
2290
  catch (e) {
2249
- console.error(`! could not deregister manager instance ${iid}: ${e.message}\n` +
2291
+ console.error(`! could not deregister manager instance ${iid}: ${rejectionText(e)}\n` +
2250
2292
  ` Its registration now outlives this process and will be frozen into every class scatter in space "${this.space}", each paying the full deadline.\n` +
2251
2293
  ` NEXT: remove it with \`cotal deregister-instance --instance ${iid}\` once this process is gone.`);
2252
2294
  }
@@ -2321,7 +2363,7 @@ export class Manager {
2321
2363
  return;
2322
2364
  }
2323
2365
  catch (renewError) {
2324
- const why = renewError.message;
2366
+ const why = rejectionText(renewError);
2325
2367
  const verdict = await this.reconcileLease();
2326
2368
  switch (verdict.kind) {
2327
2369
  case "held":
@@ -2346,7 +2388,7 @@ export class Manager {
2346
2388
  }
2347
2389
  catch (e) {
2348
2390
  this.leaseState = before;
2349
- this.noteLease("gone", `found its liveness lease key gone (renew: ${why}) and could not re-acquire it yet (${e.message}); serving, retrying`);
2391
+ this.noteLease("gone", `found its liveness lease key gone (renew: ${why}) and could not re-acquire it yet (${rejectionText(e)}); serving, retrying`);
2350
2392
  }
2351
2393
  return;
2352
2394
  }
@@ -2385,7 +2427,7 @@ export class Manager {
2385
2427
  current = await this.ep.readOwnManagerLease(this.managerInstanceId);
2386
2428
  }
2387
2429
  catch (e) {
2388
- return { kind: "unknown", why: e.message };
2430
+ return { kind: "unknown", why: rejectionText(e) };
2389
2431
  }
2390
2432
  if (current === undefined)
2391
2433
  return { kind: "gone" };
@@ -2402,7 +2444,7 @@ export class Manager {
2402
2444
  args = parseResumeFinalizeArgs(rawArgs);
2403
2445
  }
2404
2446
  catch (e) {
2405
- return { ok: false, error: e.message };
2447
+ return { ok: false, error: rejectionText(e) };
2406
2448
  }
2407
2449
  if (!this.resumeAttemptId || this.resumeAttemptId !== args.attemptId)
2408
2450
  return { ok: false, error: `manager expects resume attempt ${this.resumeAttemptId ?? "<none>"}, not ${args.attemptId}` };
@@ -2420,7 +2462,7 @@ export class Manager {
2420
2462
  inactive = this.resumeLivenessErrors(inventory, this.ep.getRoster());
2421
2463
  }
2422
2464
  catch (e) {
2423
- return { ok: false, error: `resume attempt ${args.attemptId} cannot verify live principals at finalize: ${e.message}` };
2465
+ return { ok: false, error: `resume attempt ${args.attemptId} cannot verify live principals at finalize: ${rejectionText(e)}` };
2424
2466
  }
2425
2467
  if (inactive.length)
2426
2468
  return { ok: false, error: `resume attempt ${args.attemptId} is not live at finalize: ${inactive.join("; ")}` };
@@ -2439,7 +2481,7 @@ export class Manager {
2439
2481
  // retiredPrincipals, so a copied JWT is refused). Best-effort + loud: a sweep failure must
2440
2482
  // not fail the finalize (the next non-resume boot re-drives it), but it is never swallowed.
2441
2483
  if (this.auth && !this.userMode)
2442
- await this.reconcileStaticLifecycles(true).catch((e) => console.error(`! post-resume static reconcile: ${e.message} - no per-alias retry could be planned; a later manager start re-reads unfinished durable terminals`));
2484
+ await this.reconcileStaticLifecycles(true).catch((e) => console.error(`! post-resume static reconcile: ${rejectionText(e)} - no per-alias retry could be planned; a later manager start re-reads unfinished durable terminals`));
2443
2485
  this.resumeFinalized = true;
2444
2486
  this.resumeRequired = false;
2445
2487
  return { ok: true, data: { attemptId: args.attemptId, state: "active" } };
@@ -2452,7 +2494,7 @@ export class Manager {
2452
2494
  attemptId = parseResumeCommitArgs(rawArgs).attemptId;
2453
2495
  }
2454
2496
  catch (e) {
2455
- return { ok: false, error: e.message };
2497
+ return { ok: false, error: rejectionText(e) };
2456
2498
  }
2457
2499
  if (!this.resumeAttemptId || this.resumeAttemptId !== attemptId)
2458
2500
  return { ok: false, error: `manager expects resume attempt ${this.resumeAttemptId ?? "<none>"}, not ${attemptId}` };
@@ -2509,7 +2551,7 @@ export class Manager {
2509
2551
  : { ok: false, data, error: result.error ?? "retained-agent resume failed" };
2510
2552
  }
2511
2553
  catch (e) {
2512
- return { ok: false, error: e.message };
2554
+ return { ok: false, error: rejectionText(e) };
2513
2555
  }
2514
2556
  }
2515
2557
  }
@@ -2536,7 +2578,7 @@ export class Manager {
2536
2578
  };
2537
2579
  }
2538
2580
  catch (e) {
2539
- return { ok: false, error: e.message };
2581
+ return { ok: false, error: rejectionText(e) };
2540
2582
  }
2541
2583
  }
2542
2584
  /** The ONE shared control-admission chokepoint (P2 item 1, checklist 3/8) BOTH dispatch doors
@@ -2767,7 +2809,7 @@ export class Manager {
2767
2809
  observed = await observeStaticSlot(recordsKv, DEV_OWNER, name, this.managerInstanceId);
2768
2810
  }
2769
2811
  catch (error) {
2770
- throw new EpEnvelopeError("unavailable", `the durable static slot state for "${name}" could not be read: ${error.message}`, [
2812
+ throw new EpEnvelopeError("unavailable", `the durable static slot state for "${name}" could not be read: ${rejectionText(error)}`, [
2771
2813
  { kind: STATIC_SLOT_READ_FAILED_DETAIL, name, record: error instanceof StaticSlotReadError ? error.record : "slot-or-head", operation: "read" },
2772
2814
  ]);
2773
2815
  }
@@ -2804,7 +2846,7 @@ export class Manager {
2804
2846
  observed = await listStaticSlotObservations(recordsKv, DEV_OWNER, this.managerInstanceId);
2805
2847
  }
2806
2848
  catch (error) {
2807
- throw new EpEnvelopeError("unavailable", `the durable static slot state could not be scanned: ${error.message}`, [
2849
+ throw new EpEnvelopeError("unavailable", `the durable static slot state could not be scanned: ${rejectionText(error)}`, [
2808
2850
  { kind: STATIC_SLOT_READ_FAILED_DETAIL, name: "*", record: error instanceof StaticSlotReadError ? error.record : "slot-or-head", operation: "scan" },
2809
2851
  ]);
2810
2852
  }
@@ -2942,7 +2984,7 @@ export class Manager {
2942
2984
  return undefined;
2943
2985
  }
2944
2986
  catch (e) {
2945
- return `${entry.name}: ${e.message}`;
2987
+ return `${entry.name}: ${rejectionText(e)}`;
2946
2988
  }
2947
2989
  }));
2948
2990
  const drift = authority.filter((error) => error !== undefined);
@@ -2953,7 +2995,7 @@ export class Manager {
2953
2995
  inactive = this.resumeLivenessErrors(inventory, this.ep.getRoster());
2954
2996
  }
2955
2997
  catch (e) {
2956
- return { ok: false, error: `resume attempt ${attemptId} cannot verify live principals: ${e.message}` };
2998
+ return { ok: false, error: `resume attempt ${attemptId} cannot verify live principals: ${rejectionText(e)}` };
2957
2999
  }
2958
3000
  if (inactive.length)
2959
3001
  return { ok: false, error: `resume attempt ${attemptId} is not live at commit: ${inactive.join("; ")}` };
@@ -3046,7 +3088,7 @@ export class Manager {
3046
3088
  }
3047
3089
  }
3048
3090
  catch (e) {
3049
- inactive.push(`${entry.name} runtime status failed: ${e.message}`);
3091
+ inactive.push(`${entry.name} runtime status failed: ${rejectionText(e)}`);
3050
3092
  continue;
3051
3093
  }
3052
3094
  if (!roster.some((presence) => presence.card.id === expectedPrincipal && presence.card.name === entry.name && presence.status !== "offline" &&
@@ -3102,7 +3144,7 @@ export class Manager {
3102
3144
  a.handle.stop({ graceful });
3103
3145
  }
3104
3146
  catch (e) {
3105
- console.error(`stop ${a.name} (${a.id}): ${e.message}`);
3147
+ console.error(`stop ${a.name} (${a.id}): ${rejectionText(e)}`);
3106
3148
  }
3107
3149
  }
3108
3150
  /** Keep an accepted stop inside the lifecycle drain until the runtime proves the child is gone,
@@ -3145,7 +3187,7 @@ export class Manager {
3145
3187
  id: a.id,
3146
3188
  handle: a.handle,
3147
3189
  authoritative: true,
3148
- error: `accepted stop could not prove exit: ${e.message}`,
3190
+ error: `accepted stop could not prove exit: ${rejectionText(e)}`,
3149
3191
  });
3150
3192
  })
3151
3193
  .finally(() => this.releaseLifecycle());
@@ -3185,7 +3227,7 @@ export class Manager {
3185
3227
  provider = resolveAuthProvider();
3186
3228
  }
3187
3229
  catch (e) {
3188
- return { error: e.message };
3230
+ return { error: rejectionText(e) };
3189
3231
  }
3190
3232
  const dir = userAuthStateDir(this.workspaceRoot, this.space);
3191
3233
  // The agent's capability scope rides its ledger row (act.scope in every bearer) — same
@@ -3293,7 +3335,7 @@ export class Manager {
3293
3335
  provider = resolveAuthProvider();
3294
3336
  }
3295
3337
  catch (e) {
3296
- return { error: e.message };
3338
+ return { error: rejectionText(e) };
3297
3339
  }
3298
3340
  const secrets = this.secrets;
3299
3341
  // The token the agent will present to `POST /exchange` for the rest of its life. Generated
@@ -3451,7 +3493,7 @@ export class Manager {
3451
3493
  catch (e) {
3452
3494
  // A runtime that throws while being asked has told us something real; it must not take the
3453
3495
  // log line (or the free path it sits on) down with it.
3454
- detail = `exit detail unreadable from runtime "${a.handle.kind}": ${e.message}`;
3496
+ detail = `exit detail unreadable from runtime "${a.handle.kind}": ${rejectionText(e)}`;
3455
3497
  }
3456
3498
  console.error(`seat reaped: ${a.name} (${a.id}, uid ${a.lifecycleUid}${a.handle.pid !== undefined ? `, pid ${a.handle.pid}` : ""}) - ${causeText}; ${detail}`);
3457
3499
  }
@@ -3671,8 +3713,8 @@ export class Manager {
3671
3713
  catch (e) {
3672
3714
  const h = this.retiring.get(a.name);
3673
3715
  if (h && h.lifecycleUid === a.lifecycleUid)
3674
- h.lastError = `the agent's standing mint authority could not be revoked (${e.message}); the name stays held so a copied actor token cannot mint fresh credentials. NEXT: a same-name spawn re-drives the full teardown (including the revoke), or recover the auth state.`;
3675
- console.error(`revoke agent grant ${a.name}: ${e.message}`);
3716
+ h.lastError = `the agent's standing mint authority could not be revoked (${rejectionText(e)}); the name stays held so a copied actor token cannot mint fresh credentials. NEXT: a same-name spawn re-drives the full teardown (including the revoke), or recover the auth state.`;
3717
+ console.error(`revoke agent grant ${a.name}: ${rejectionText(e)}`);
3676
3718
  }
3677
3719
  }
3678
3720
  const shredFailure = unshredded.length ? `could not shred "${a.name}": ${unshredded.join("; ")}` : undefined;
@@ -3685,7 +3727,7 @@ export class Manager {
3685
3727
  catch (e) {
3686
3728
  const h = this.retiring.get(a.name);
3687
3729
  if (h && h.lifecycleUid === a.lifecycleUid) {
3688
- h.lastError = e.message;
3730
+ h.lastError = rejectionText(e);
3689
3731
  if (e instanceof DeprovisionError)
3690
3732
  h.lastResources = e.accounting;
3691
3733
  }
@@ -3831,7 +3873,7 @@ export class Manager {
3831
3873
  }
3832
3874
  }
3833
3875
  catch (e) {
3834
- const copy = uncertain(e.message);
3876
+ const copy = uncertain(rejectionText(e));
3835
3877
  if (held)
3836
3878
  held.lastError = copy;
3837
3879
  console.error(`despawn ${a.name}: ${copy}`);
@@ -3925,7 +3967,7 @@ export class Manager {
3925
3967
  return { complete: true, channels: data.channels };
3926
3968
  }
3927
3969
  catch (e) {
3928
- return { complete: false, channels: [], reason: e.message };
3970
+ return { complete: false, channels: [], reason: rejectionText(e) };
3929
3971
  }
3930
3972
  }
3931
3973
  /** Reap a parent's children on its exit (P4b). Every descendant remains managed until the runtime's
@@ -3947,7 +3989,7 @@ export class Manager {
3947
3989
  parsed = JSON.parse(readFileSync(path, "utf8"));
3948
3990
  }
3949
3991
  catch (error) {
3950
- throw new Error(`cannot read connector session state ${path}: ${error.message}`);
3992
+ throw new Error(`cannot read connector session state ${path}: ${rejectionText(error)}`);
3951
3993
  }
3952
3994
  const state = parsed;
3953
3995
  if (state.version !== 1 || typeof state.sessionId !== "string" || !state.sessionId.trim() || state.sessionId.length > 4096 ||
@@ -3974,7 +4016,7 @@ export class Manager {
3974
4016
  }
3975
4017
  catch (error) {
3976
4018
  throw new Error(`retained ${connector.name} agent ${entry.name} has no sessionId in its older inventory and no usable upgrade state at ${path} ` +
3977
- `(${error.message}). Reload the live Pi seat before the preservation cut; refusing to resume fresh and lose its context.`);
4019
+ `(${rejectionText(error)}). Reload the live Pi seat before the preservation cut; refusing to resume fresh and lose its context.`);
3978
4020
  }
3979
4021
  }
3980
4022
  /** A resumed seat's fork provenance (#1500). The seat forks after it launches, so this reads the
@@ -4059,7 +4101,7 @@ export class Manager {
4059
4101
  this.recordContinuity(a, this.readManagedSession(a));
4060
4102
  }
4061
4103
  catch (error) {
4062
- console.error(`! ${a.name}: continuity: exact assignment not updated at ${boundary}: ${error.message}`);
4104
+ console.error(`! ${a.name}: continuity: exact assignment not updated at ${boundary}: ${rejectionText(error)}`);
4063
4105
  }
4064
4106
  }
4065
4107
  async awaitManagedSessionState(a) {
@@ -4072,7 +4114,7 @@ export class Manager {
4072
4114
  return this.readManagedSessionState(a);
4073
4115
  }
4074
4116
  catch (error) {
4075
- last = error.message;
4117
+ last = rejectionText(error);
4076
4118
  }
4077
4119
  await new Promise((resolve) => setTimeout(resolve, 100));
4078
4120
  }
@@ -4105,7 +4147,7 @@ export class Manager {
4105
4147
  return;
4106
4148
  }
4107
4149
  catch (error) {
4108
- last = error.message;
4150
+ last = rejectionText(error);
4109
4151
  }
4110
4152
  }
4111
4153
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -4210,7 +4252,7 @@ export class Manager {
4210
4252
  tail = await this.tail((replacement ?? a.handle).attach());
4211
4253
  }
4212
4254
  catch { /* runtime has no readable tail */ }
4213
- console.error(`! ${a.name}: ${supervised ? "supervised restart" : "Pi session recovery"} failed: ${error.message}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
4255
+ console.error(`! ${a.name}: ${supervised ? "supervised restart" : "Pi session recovery"} failed: ${rejectionText(error)}${tail ? ` - last output: ${tail}` : ""} - retiring the managed seat`);
4214
4256
  // The replacement may be alive but unable to prove readiness. Stop it BEFORE
4215
4257
  // retiring credentials/durables; otherwise an untracked process survives under torn auth.
4216
4258
  try {
@@ -4250,7 +4292,7 @@ export class Manager {
4250
4292
  }
4251
4293
  }
4252
4294
  catch (error) {
4253
- console.error(`! ${a.name}: cannot classify Pi process exit for recovery: ${error.message} - retiring the seat`);
4295
+ console.error(`! ${a.name}: cannot classify Pi process exit for recovery: ${rejectionText(error)} - retiring the seat`);
4254
4296
  }
4255
4297
  }
4256
4298
  this.freeSlot(a, true, "process-exit");
@@ -4335,6 +4377,8 @@ export class Manager {
4335
4377
  // but a raw control message could otherwise slip an empty value through and silently start fresh.
4336
4378
  if (args.resume !== undefined && !String(args.resume).trim())
4337
4379
  return Promise.resolve({ ok: false, error: "resume: session id must not be empty" });
4380
+ if (args.model !== undefined && !String(args.model).trim())
4381
+ return Promise.resolve({ ok: false, error: "model: must not be empty" });
4338
4382
  if (args.variant !== undefined && !String(args.variant).trim())
4339
4383
  return Promise.resolve({ ok: false, error: "variant: must not be empty" });
4340
4384
  if (args.defaultAgent !== undefined && !String(args.defaultAgent).trim())
@@ -4375,7 +4419,7 @@ export class Manager {
4375
4419
  shareTools = strList(args.shareTools, "shareTools");
4376
4420
  }
4377
4421
  catch (e) {
4378
- return Promise.resolve({ ok: false, error: e.message });
4422
+ return Promise.resolve({ ok: false, error: rejectionText(e) });
4379
4423
  }
4380
4424
  // #373: the event-plane admission gate. `startAgent` resolves `eventsRequired || opts.events
4381
4425
  // !== false`, so omission ARMS the plane — a gate on the explicit bit alone is bypassed by
@@ -4436,14 +4480,14 @@ export class Manager {
4436
4480
  cwd = realpathSync(resolved);
4437
4481
  }
4438
4482
  catch (error) {
4439
- return { refusal: `path cannot be resolved: ${error.message}` };
4483
+ return { refusal: `path cannot be resolved: ${rejectionText(error)}` };
4440
4484
  }
4441
4485
  try {
4442
4486
  if (!statSync(cwd).isDirectory())
4443
4487
  return { refusal: "path is not a directory" };
4444
4488
  }
4445
4489
  catch (error) {
4446
- return { refusal: error.message };
4490
+ return { refusal: rejectionText(error) };
4447
4491
  }
4448
4492
  return { cwd, host: hostname() };
4449
4493
  }
@@ -4491,7 +4535,7 @@ export class Manager {
4491
4535
  declared = loadExtensionsManifest().extensions.flatMap((ext) => extensionConnectors(ext));
4492
4536
  }
4493
4537
  catch (error) {
4494
- const reason = error.message;
4538
+ const reason = rejectionText(error);
4495
4539
  console.error(`! manager boot: connector inventory unavailable - ${reason}`);
4496
4540
  this.connectorStatuses = [{ agent: "(inventory)", state: "unavailable", binaries: {}, reason }];
4497
4541
  console.error("! manager boot: no connector available - spawn and launch stay on this instance rail only so a sibling that can launch them can take the class queue");
@@ -4575,7 +4619,7 @@ export class Manager {
4575
4619
  return { agent: connector.name, supported: true, ...catalog };
4576
4620
  }
4577
4621
  catch (e) {
4578
- return { agent: connector.name, supported: true, models: [], error: e.message };
4622
+ return { agent: connector.name, supported: true, models: [], error: rejectionText(e) };
4579
4623
  }
4580
4624
  };
4581
4625
  if (requested) {
@@ -4584,7 +4628,7 @@ export class Manager {
4584
4628
  connector = await this.resolveConnector(requested);
4585
4629
  }
4586
4630
  catch (e) {
4587
- return { ok: false, error: e.message };
4631
+ return { ok: false, error: rejectionText(e) };
4588
4632
  }
4589
4633
  const result = await one(connector);
4590
4634
  return result.error ? { ok: false, error: result.error } : { ok: true, data: result };
@@ -4598,7 +4642,7 @@ export class Manager {
4598
4642
  connector = await this.resolveConnector(name);
4599
4643
  }
4600
4644
  catch (e) {
4601
- return { agent: name, supported: false, models: [], error: e.message };
4645
+ return { agent: name, supported: false, models: [], error: rejectionText(e) };
4602
4646
  }
4603
4647
  return one(connector);
4604
4648
  }));
@@ -4660,7 +4704,7 @@ export class Manager {
4660
4704
  persistLaunchSpec(this.workspaceRoot, spec);
4661
4705
  }
4662
4706
  catch (e) {
4663
- return { ok: false, error: e.message };
4707
+ return { ok: false, error: rejectionText(e) };
4664
4708
  }
4665
4709
  }
4666
4710
  else {
@@ -4668,7 +4712,7 @@ export class Manager {
4668
4712
  spec = launchSpecForRun(this.workspaceRoot, runId);
4669
4713
  }
4670
4714
  catch (e) {
4671
- return { ok: false, error: e.message };
4715
+ return { ok: false, error: rejectionText(e) };
4672
4716
  }
4673
4717
  }
4674
4718
  const la = spec.agents.find((a) => a.name === name);
@@ -4691,7 +4735,7 @@ export class Manager {
4691
4735
  configPath = materializePersona(this.workspaceRoot, runId, la);
4692
4736
  }
4693
4737
  catch (e) {
4694
- return { ok: false, error: e.message };
4738
+ return { ok: false, error: rejectionText(e) };
4695
4739
  }
4696
4740
  const reply = await this.startAgent({ ...launchAgentToStartOpts(la, configPath, spec.owner, runId), route }, caller, hooks);
4697
4741
  if (reply.ok)
@@ -4718,9 +4762,10 @@ export class Manager {
4718
4762
  }
4719
4763
  }
4720
4764
  async startAgentActive(opts, spawner, hooks) {
4721
- // The caller keeps its array, so the selection is copied before the first await: a later write
4722
- // to it cannot change what this launch shares or what preservation retains.
4723
- const shareTools = opts.shareTools && [...opts.shareTools];
4765
+ // Before the first await, so the grant, the launch and the retained form all read the lists as admitted.
4766
+ opts = ownLists(opts);
4767
+ if (opts.resolved)
4768
+ opts.resolved = ownLists(opts.resolved);
4724
4769
  if (opts.delegatedIntent) {
4725
4770
  if (!this.remoteAuthority?.executeDelegatedUserIntent)
4726
4771
  return { ok: false, error: `"${opts.name}" names a delegated user intent, and this manager has no host execution for one` };
@@ -4774,7 +4819,7 @@ export class Manager {
4774
4819
  def = loadAgentFile(configPath);
4775
4820
  }
4776
4821
  catch (e) {
4777
- return { ok: false, error: e.message };
4822
+ return { ok: false, error: rejectionText(e) };
4778
4823
  }
4779
4824
  }
4780
4825
  const agent = resolveAgentType({ flag: opts.agent, pin: def?.agent, callerDefault: opts.defaultAgent });
@@ -4788,7 +4833,7 @@ export class Manager {
4788
4833
  connector = await this.resolveConnector(agent);
4789
4834
  }
4790
4835
  catch (e) {
4791
- return { ok: false, error: e.message };
4836
+ return { ok: false, error: rejectionText(e) };
4792
4837
  }
4793
4838
  const readinessTimeoutMs = connector.readinessTimeoutMs ?? this.readinessTimeoutMs;
4794
4839
  if (!Number.isSafeInteger(readinessTimeoutMs) || readinessTimeoutMs <= 0)
@@ -4827,7 +4872,7 @@ export class Manager {
4827
4872
  carried = this.transcripts.peek(opts.resumeClaim, opts.resume);
4828
4873
  }
4829
4874
  catch (e) {
4830
- return { ok: false, error: e.message };
4875
+ return { ok: false, error: rejectionText(e) };
4831
4876
  }
4832
4877
  }
4833
4878
  // A restart policy this host cannot honour is refused at accept, never accepted and ignored.
@@ -4850,7 +4895,7 @@ export class Manager {
4850
4895
  this.delegatedMeshRecord();
4851
4896
  }
4852
4897
  catch (e) {
4853
- return { ok: false, error: e.message };
4898
+ return { ok: false, error: rejectionText(e) };
4854
4899
  }
4855
4900
  if (opts.resume)
4856
4901
  return { ok: false, error: `${delegatedBy}, so it cannot resume a session held on this host (resume)` };
@@ -4858,7 +4903,7 @@ export class Manager {
4858
4903
  return { ok: false, error: `${delegatedBy}, so it cannot reopen a session held on this host (continuity: exact)` };
4859
4904
  if (typeof opts.cwd === "string" && opts.cwd !== "")
4860
4905
  return { ok: false, error: `${delegatedBy}, so it cannot run in a directory on this host's filesystem (cwd); the seat runs in its runtime resource's own directory` };
4861
- const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent, shareTools));
4906
+ const shared = Object.keys(connectorServers(loadCotalConfig(this.workspaceRoot), agent, opts.shareTools));
4862
4907
  if (shared.length)
4863
4908
  return { ok: false, error: `${delegatedBy}, so it cannot share MCP servers that run on this host (${shared.join(", ")}); pass --share-tools none` };
4864
4909
  }
@@ -4894,7 +4939,7 @@ export class Manager {
4894
4939
  if (opts.resolved) {
4895
4940
  // A manifest launch is the access + identity authority: imperative overrides arriving
4896
4941
  // alongside `resolved` are a caller contract error, not something to merge (no fallbacks).
4897
- if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt || shareTools || opts.identity)
4942
+ if (opts.subscribe || opts.allowSubscribe || opts.allowPublish || opts.prompt || opts.shareTools || opts.identity)
4898
4943
  return { ok: false, error: "a manifest launch (resolved) rejects imperative overrides (identity/subscribe/allow*/prompt/shareTools)" };
4899
4944
  const r = opts.resolved;
4900
4945
  identityName = r.name;
@@ -4939,14 +4984,14 @@ export class Manager {
4939
4984
  allowSubscribe = resolveReadAcl(subscribe ?? [], allowSubscribe);
4940
4985
  }
4941
4986
  catch (e) {
4942
- return { ok: false, error: opts.resolved ? `launch agent: ${e.message}` : `persona ${configPath}: ${e.message}` };
4943
- }
4944
- // #651: an empty or whitespace-only model string is not a pin. Coerce it to undefined here, at
4945
- // the single point every path (persona, manifest, imperative) has resolved `model`, so it
4946
- // serializes ABSENT rather than present-but-empty (`"model": ""`), which a key-presence consumer
4947
- // would misread as "a pin was recorded".
4948
- if (model !== undefined && model.trim() === "")
4949
- model = undefined;
4987
+ return { ok: false, error: opts.resolved ? `launch agent: ${rejectionText(e)}` : `persona ${configPath}: ${rejectionText(e)}` };
4988
+ }
4989
+ // Checked after every path has resolved its selectors: a direct `startAgent` call skips the
4990
+ // `start` op's checks, and no connector may receive a blank one (#2862).
4991
+ if (model !== undefined && !model.trim())
4992
+ return { ok: false, error: "model: must not be empty" };
4993
+ if (variant !== undefined && !variant.trim())
4994
+ return { ok: false, error: "variant: must not be empty" };
4950
4995
  const idErr = this.nameError(identityName);
4951
4996
  if (idErr)
4952
4997
  return { ok: false, error: opts.resolved ? `launch agent: ${idErr}` : `persona ${configPath}: ${idErr}` };
@@ -4990,7 +5035,7 @@ export class Manager {
4990
5035
  });
4991
5036
  }
4992
5037
  catch (e) {
4993
- return { ok: false, error: e.message };
5038
+ return { ok: false, error: rejectionText(e) };
4994
5039
  }
4995
5040
  if (policyRefusal)
4996
5041
  return { ok: false, error: policyRefusal };
@@ -5021,7 +5066,7 @@ export class Manager {
5021
5066
  reopenSession = readContinuityAssignment(this.workspaceRoot, { space: this.space, name: identityName, connector: agent, cwd: resolvedCwd ?? this.workspaceRoot });
5022
5067
  }
5023
5068
  catch (e) {
5024
- return { ok: false, error: e.message };
5069
+ return { ok: false, error: rejectionText(e) };
5025
5070
  }
5026
5071
  }
5027
5072
  // The child takes launch options as `--opt k=v`, so only string values can cross to it.
@@ -5283,7 +5328,7 @@ export class Manager {
5283
5328
  // (cotal config; default none → isolated, the memory-safe default this guards), narrowed by
5284
5329
  // an optional --share-tools selection (absent → all declared, the pre-merge behavior).
5285
5330
  const cotalConfig = loadCotalConfig(this.workspaceRoot);
5286
- const mcpServers = connectorServers(cotalConfig, agent, shareTools);
5331
+ const mcpServers = connectorServers(cotalConfig, agent, opts.shareTools);
5287
5332
  // The operator's spawn-env policy travels the same route: absent means no extras (the OS
5288
5333
  // allow-list + operator knobs + connector-declared inputs), present means those names too.
5289
5334
  // A connector never reads the config itself.
@@ -5410,7 +5455,7 @@ export class Manager {
5410
5455
  allowPublish,
5411
5456
  capabilities,
5412
5457
  events,
5413
- shareTools,
5458
+ shareTools: opts.shareTools,
5414
5459
  forkSource: opts.resume,
5415
5460
  ...(opts.resume !== undefined && spec?.resumeRecordPath ? { resumeRecordPath: spec.resumeRecordPath } : {}),
5416
5461
  ...(carried ? { carried: { transcriptSha256: carried.sha256, host: carried.sourceHost, transferredAt: carried.stagedAt } } : {}),
@@ -5514,7 +5559,7 @@ export class Manager {
5514
5559
  }
5515
5560
  }
5516
5561
  catch (error) {
5517
- const detail = `${managed.name} joined, but its exact host session could not be bound for supervised recovery: ${error.message}`;
5562
+ const detail = `${managed.name} joined, but its exact host session could not be bound for supervised recovery: ${rejectionText(error)}`;
5518
5563
  this.stopHandle(managed, false);
5519
5564
  this.freeSlot(managed, true, "session-bind-failed");
5520
5565
  await hooks?.onOutcome?.({ kind: "failed", data: { error: detail } });
@@ -5587,9 +5632,9 @@ export class Manager {
5587
5632
  return { ok: false, agents: [], error: `unsupported manager resume inventory version ${String(inventory.version)}` };
5588
5633
  if (inventory.space !== this.space)
5589
5634
  return { ok: false, agents: [], error: `resume inventory belongs to space "${inventory.space}", not "${this.space}"` };
5590
- // The caller keeps its inventory, so each selection is copied before the first await: a later
5591
- // write to it cannot change what a resumed seat shares or what the next preservation retains.
5592
- const entries = inventory.agents.map((entry) => ({ ...entry, launch: { ...entry.launch, shareTools: entry.launch.shareTools && [...entry.launch.shareTools] } }));
5635
+ // The caller keeps its inventory, so it is copied before the first await: a later write to it
5636
+ // cannot change what a resumed seat launches with or what the next preservation retains.
5637
+ const entries = structuredClone(inventory.agents);
5593
5638
  const seen = new Set();
5594
5639
  const principals = new Set();
5595
5640
  const orphans = [];
@@ -5631,7 +5676,7 @@ export class Manager {
5631
5676
  : principalKey(DEV_OWNER, entry.identity.id).key;
5632
5677
  }
5633
5678
  catch (e) {
5634
- return { ok: false, agents: [], error: `invalid retained principal for ${entry.name}: ${e.message}` };
5679
+ return { ok: false, agents: [], error: `invalid retained principal for ${entry.name}: ${rejectionText(e)}` };
5635
5680
  }
5636
5681
  if (principals.has(principal))
5637
5682
  return { ok: false, agents: [], error: `resume inventory contains duplicate principal "${principal}"` };
@@ -5707,7 +5752,7 @@ export class Manager {
5707
5752
  discardLaunchArtifacts(spec.artifacts);
5708
5753
  }
5709
5754
  catch (e) {
5710
- console.error(`! resume: ${e.message}`);
5755
+ console.error(`! resume: ${rejectionText(e)}`);
5711
5756
  }
5712
5757
  }
5713
5758
  release();
@@ -5786,7 +5831,7 @@ export class Manager {
5786
5831
  throw new Error(`retained credential identity ${actual} does not match inventory principal ${entry.identity.id}`);
5787
5832
  }
5788
5833
  catch (e) {
5789
- throw new Error(`retained credential for ${entry.name} is unusable: ${e.message}`);
5834
+ throw new Error(`retained credential for ${entry.name} is unusable: ${rejectionText(e)}`);
5790
5835
  }
5791
5836
  const accepted = await this.probeStaticCredential(credentialText);
5792
5837
  if (!accepted.ok)
@@ -5874,7 +5919,7 @@ export class Manager {
5874
5919
  };
5875
5920
  }
5876
5921
  catch (e) {
5877
- throw new Error(`retained user principal ${entry.identity.owner}.${entry.identity.actor} could not be reused: ${e.message}`);
5922
+ throw new Error(`retained user principal ${entry.identity.owner}.${entry.identity.actor} could not be reused: ${rejectionText(e)}`);
5878
5923
  }
5879
5924
  }
5880
5925
  /** The seat reference behind a live retained principal, when that seat is provably one a lost
@@ -5907,7 +5952,7 @@ export class Manager {
5907
5952
  console.error(`resume: reaped orphaned seat ${reference.kind}:${reference.id} of retained principal "${principal}" (${evidence.detail})`);
5908
5953
  }
5909
5954
  catch (e) {
5910
- return `retained principal "${principal}" is already live and its recorded seat ${reference.kind}:${reference.id} could not be reaped: ${e.message}`;
5955
+ return `retained principal "${principal}" is already live and its recorded seat ${reference.kind}:${reference.id} could not be reaped: ${rejectionText(e)}`;
5911
5956
  }
5912
5957
  const deadline = Date.now() + ORPHAN_PRESENCE_CLEAR_MS;
5913
5958
  while (this.ep.getRoster().some((p) => p.card.id === principal && p.status !== "offline")) {
@@ -5943,7 +5988,7 @@ export class Manager {
5943
5988
  await this.validateRetainedAuthority(entry);
5944
5989
  }
5945
5990
  catch (e) {
5946
- return { ok: false, error: e.message };
5991
+ return { ok: false, error: rejectionText(e) };
5947
5992
  }
5948
5993
  return this.launchPreparedResume(entry, cached, batchReserved);
5949
5994
  }
@@ -5953,7 +5998,7 @@ export class Manager {
5953
5998
  return { ok: false, error: `retained cwd is not a real directory: ${entry.launch.cwd}` };
5954
5999
  }
5955
6000
  catch (e) {
5956
- return { ok: false, error: `retained cwd unavailable: ${entry.launch.cwd} (${e.message})` };
6001
+ return { ok: false, error: `retained cwd unavailable: ${entry.launch.cwd} (${rejectionText(e)})` };
5957
6002
  }
5958
6003
  let connector;
5959
6004
  try {
@@ -5967,7 +6012,7 @@ export class Manager {
5967
6012
  connector = await this.resolveConnector(entry.launch.connector);
5968
6013
  }
5969
6014
  catch (e) {
5970
- return { ok: false, error: e.message };
6015
+ return { ok: false, error: rejectionText(e) };
5971
6016
  }
5972
6017
  const harness = this.launchHarness(connector);
5973
6018
  if ("refusal" in harness)
@@ -5977,7 +6022,7 @@ export class Manager {
5977
6022
  retainedSession = this.retainedSessionId(entry, connector);
5978
6023
  }
5979
6024
  catch (error) {
5980
- return { ok: false, error: error.message };
6025
+ return { ok: false, error: rejectionText(error) };
5981
6026
  }
5982
6027
  if (entry.launch.forkSource && !retainedSession && !connector.supportsResume)
5983
6028
  return { ok: false, error: `${connector.name} connector does not support session fork (resume)` };
@@ -5997,7 +6042,7 @@ export class Manager {
5997
6042
  spec = source.agents.find((a) => a.name === launchSource.requested);
5998
6043
  }
5999
6044
  catch (e) {
6000
- return { ok: false, error: e.message };
6045
+ return { ok: false, error: rejectionText(e) };
6001
6046
  }
6002
6047
  if (!spec || spec.hash !== launchSource.hash)
6003
6048
  return { ok: false, error: `retained manifest agent ${launchSource.requested} is missing or its hash changed; refusing same-principal resume` };
@@ -6009,7 +6054,7 @@ export class Manager {
6009
6054
  launchOptions = loadAgentFile(entry.launch.source.configPath).launchOptions;
6010
6055
  }
6011
6056
  catch (e) {
6012
- return { ok: false, error: e.message };
6057
+ return { ok: false, error: rejectionText(e) };
6013
6058
  }
6014
6059
  }
6015
6060
  // Asked once `exact` is known: an exact seat reopens its retained session or fails, like its
@@ -6022,7 +6067,7 @@ export class Manager {
6022
6067
  authority = await this.validateRetainedAuthority(entry);
6023
6068
  }
6024
6069
  catch (e) {
6025
- return { ok: false, error: e.message };
6070
+ return { ok: false, error: rejectionText(e) };
6026
6071
  }
6027
6072
  try {
6028
6073
  const resumeConfig = loadCotalConfig(this.workspaceRoot);
@@ -6071,7 +6116,7 @@ export class Manager {
6071
6116
  return this.launchPreparedResume(entry, value, batchReserved);
6072
6117
  }
6073
6118
  catch (e) {
6074
- return { ok: false, error: e.message };
6119
+ return { ok: false, error: rejectionText(e) };
6075
6120
  }
6076
6121
  }
6077
6122
  finally {
@@ -6154,7 +6199,7 @@ export class Manager {
6154
6199
  await this.recordSlotRuntime(managed);
6155
6200
  }
6156
6201
  catch (error) {
6157
- return await this.stopFailedResume(managed, "resume-custody-unrecorded", `${managed.name} resumed, but ${error.message}`);
6202
+ return await this.stopFailedResume(managed, "resume-custody-unrecorded", `${managed.name} resumed, but ${rejectionText(error)}`);
6158
6203
  }
6159
6204
  const readiness = await this.awaitReadiness(managed, readinessTimeoutMs);
6160
6205
  if (!readiness.ok && !readiness.uncertain)
@@ -6181,7 +6226,7 @@ export class Manager {
6181
6226
  }
6182
6227
  }
6183
6228
  catch (error) {
6184
- return await this.stopFailedResume(managed, "resume-session-rebind-failed", `${managed.name} resumed, but its exact host session could not be rebound: ${error.message}`);
6229
+ return await this.stopFailedResume(managed, "resume-session-rebind-failed", `${managed.name} resumed, but its exact host session could not be rebound: ${rejectionText(error)}`);
6185
6230
  }
6186
6231
  }
6187
6232
  if (!this.resumeAttemptId)
@@ -6195,7 +6240,7 @@ export class Manager {
6195
6240
  };
6196
6241
  }
6197
6242
  catch (e) {
6198
- return { ok: false, error: e.message };
6243
+ return { ok: false, error: rejectionText(e) };
6199
6244
  }
6200
6245
  finally {
6201
6246
  if (!batchReserved) {
@@ -6214,7 +6259,7 @@ export class Manager {
6214
6259
  }
6215
6260
  catch (exit) {
6216
6261
  this.watchExit(managed);
6217
- return { ok: false, error: `${detail}; it stays managed because its stop is unproven: ${exit.message}` };
6262
+ return { ok: false, error: `${detail}; it stays managed because its stop is unproven: ${rejectionText(exit)}` };
6218
6263
  }
6219
6264
  this.freeSlot(managed, true, cause, true);
6220
6265
  return { ok: false, error: detail };
@@ -6966,7 +7011,7 @@ export class Manager {
6966
7011
  // One registration operation for this boot. Fresh executors may replace a dead connection, but
6967
7012
  // they must resume THIS freeze rather than minting a new op that discards Phase-2 progress.
6968
7013
  const registrationOpId = mintLifecycleUid();
6969
- const executorExpired = (e) => /closed connection/i.test(e?.message ?? String(e));
7014
+ const executorExpired = (e) => /closed connection/i.test(rejectionText(e));
6970
7015
  const retryExpiredExecutor = async (label, fn) => {
6971
7016
  for (;;) {
6972
7017
  try {
@@ -7573,14 +7618,14 @@ export class Manager {
7573
7618
  await this.reconcileOneGoal(entry.ref, entry.iid);
7574
7619
  }
7575
7620
  catch (e) {
7576
- console.error(`! goal reconcile for ${entry.ref.goalId}: ${e.message}`);
7621
+ console.error(`! goal reconcile for ${entry.ref.goalId}: ${rejectionText(e)}`);
7577
7622
  }
7578
7623
  }
7579
7624
  if (entries.length)
7580
7625
  console.error(`goal-index boot reconcile: swept ${entries.length} inherited goal(s)`);
7581
7626
  }
7582
7627
  catch (e) {
7583
- console.error(`! goal-index boot reconcile failed: ${e.message} - accepted goals from a predecessor may stay unsettled until the next restart`);
7628
+ console.error(`! goal-index boot reconcile failed: ${rejectionText(e)} - accepted goals from a predecessor may stay unsettled until the next restart`);
7584
7629
  }
7585
7630
  finally {
7586
7631
  this.goalReconcileDone = true;
@@ -7630,7 +7675,7 @@ export class Manager {
7630
7675
  return;
7631
7676
  }
7632
7677
  console.error(`goal reconcile ${ref.goalId}: within the readiness window (${remaining}ms) - arming a bounded settle`);
7633
- const t = setTimeout(() => { settle().catch((e) => console.error(`! goal reconcile settle ${ref.goalId}: ${e.message}`)); }, remaining + 100);
7678
+ const t = setTimeout(() => { settle().catch((e) => console.error(`! goal reconcile settle ${ref.goalId}: ${rejectionText(e)}`)); }, remaining + 100);
7634
7679
  t.unref?.();
7635
7680
  }
7636
7681
  /** P2 item 2: publish a goal PROGRESS event on the caller-scoped epe subtree, over the SERVE
@@ -7646,7 +7691,7 @@ export class Manager {
7646
7691
  nc.publish(epeSubject(this.space, MANAGER_ENDPOINT, this.managerInstanceId, epoch, goalProgressTopic(ref)), new TextEncoder().encode(JSON.stringify({ v: 1, goalId: ref.goalId, ...event })));
7647
7692
  }
7648
7693
  catch (e) {
7649
- console.error(`! goal progress emit for ${ref.goalId} failed: ${e.message}`);
7694
+ console.error(`! goal progress emit for ${ref.goalId} failed: ${rejectionText(e)}`);
7650
7695
  }
7651
7696
  }
7652
7697
  /** The own-gate currency belt: before the goal-writer commits a terminal fact, the manager reads
@@ -7785,7 +7830,7 @@ export class Manager {
7785
7830
  // ENTRY. This leg is infrastructure-class and converges through the reconcile index at the
7786
7831
  // next boot, which is why the index is NOT cleared above on this path. Do not "close" it
7787
7832
  // here with a retry loop.
7788
- console.error(`! goal terminal commit for ${goalId} failed: ${e.message}`);
7833
+ console.error(`! goal terminal commit for ${goalId} failed: ${rejectionText(e)}`);
7789
7834
  }
7790
7835
  };
7791
7836
  const bg = run({
@@ -7870,7 +7915,7 @@ export class Manager {
7870
7915
  // the caller follows epe to a terminal that never comes, and the reconcile index that would
7871
7916
  // settle it is only swept at BOOT, so a manager that stays up never converges it.
7872
7917
  if (reply.ok === false && !terminalEntered)
7873
- return onOutcome({ kind: "failed", data: { error: reply.error ?? "spawn failed after accept", ...(reply.details ? { details: reply.details } : {}) } });
7918
+ return onOutcome({ kind: "failed", data: failedTerminal(reply.error ?? "spawn failed after accept", reply) });
7874
7919
  }).catch((e) => {
7875
7920
  if (acceptance === undefined) {
7876
7921
  rejectAccept(e);
@@ -7878,9 +7923,9 @@ export class Manager {
7878
7923
  }
7879
7924
  // Same obligation for a genuine rejection (one that escaped `run`'s own catch).
7880
7925
  if (!terminalEntered)
7881
- return onOutcome({ kind: "failed", data: { error: e?.message ?? String(e), ...(e instanceof EpEnvelopeError && e.details ? { details: e.details } : {}) } });
7882
- console.error(`! spawn-as-action async body for ${goalId}: ${e?.message ?? String(e)}`);
7883
- }).catch((e) => console.error(`! goal terminal fallback for ${goalId}: ${e?.message ?? String(e)}`));
7926
+ return onOutcome({ kind: "failed", data: failedTerminal(rejectionText(e), caughtEnvelope(e)) });
7927
+ console.error(`! spawn-as-action async body for ${goalId}: ${rejectionText(e)}`);
7928
+ }).catch((e) => console.error(`! goal terminal fallback for ${goalId}: ${rejectionText(e)}`));
7884
7929
  return acceptP;
7885
7930
  }
7886
7931
  /** H2: an acceptance served from a WINNER'S durable acceptance floor (the goal-index entry it
@@ -8023,7 +8068,7 @@ export class Manager {
8023
8068
  // The accept is inline (no launch closure to fail later), so a post-bind throw unwinds HERE:
8024
8069
  // commit the failed terminal this attempt owns, clear the index, and refuse the accept — an
8025
8070
  // accepted-but-unanswered goal must never be left for the boot sweep to find (H1's rule).
8026
- const msg = e?.message ?? String(e);
8071
+ const msg = rejectionText(e);
8027
8072
  try {
8028
8073
  await this.assertGoalWriterEpochCurrent(executor.epoch);
8029
8074
  // The goal is TARGET-PINNED, so its completion proves the seat's currency exactly as the
@@ -8039,9 +8084,9 @@ export class Manager {
8039
8084
  await clearGoalIndex(gw.ctx, ref);
8040
8085
  }
8041
8086
  catch (e2) {
8042
- console.error(`! turn accept unwind for ${goalId}: ${e2.message}`);
8087
+ console.error(`! turn accept unwind for ${goalId}: ${rejectionText(e2)}`);
8043
8088
  }
8044
- throw e instanceof EpEnvelopeError ? e : new EpEnvelopeError("internal", `turn accept for ${goalId} failed: ${msg}`);
8089
+ throw caughtEnvelope(e) ?? new EpEnvelopeError("internal", `turn accept for ${goalId} failed: ${msg}`);
8045
8090
  }
8046
8091
  const pending = {
8047
8092
  ref, goalId,
@@ -8241,7 +8286,7 @@ export class Manager {
8241
8286
  await clearGoalIndex(gw.ctx, p.ref);
8242
8287
  }
8243
8288
  catch (e) {
8244
- console.error(`! turn deadline terminal for ${p.goalId}: ${e.message}`);
8289
+ console.error(`! turn deadline terminal for ${p.goalId}: ${rejectionText(e)}`);
8245
8290
  }
8246
8291
  this.maybeStopTurnSweep();
8247
8292
  }
@@ -8273,7 +8318,7 @@ export class Manager {
8273
8318
  ensureTurnSweep() {
8274
8319
  if (this.turnSweepTimer !== undefined)
8275
8320
  return;
8276
- const t = setInterval(() => { void this.sweepTurnDeadlines().catch((e) => console.error(`! turn deadline sweep: ${e.message}`)); }, TURN_SWEEP_MS);
8321
+ const t = setInterval(() => { void this.sweepTurnDeadlines().catch((e) => console.error(`! turn deadline sweep: ${rejectionText(e)}`)); }, TURN_SWEEP_MS);
8277
8322
  t.unref?.();
8278
8323
  this.turnSweepTimer = t;
8279
8324
  }
@@ -8342,7 +8387,7 @@ export class Manager {
8342
8387
  await this.commitTurnDeadline(p);
8343
8388
  }
8344
8389
  catch (e) {
8345
- console.error(`! turn deadline sweep for ${p.goalId}: ${e.message}`);
8390
+ console.error(`! turn deadline sweep for ${p.goalId}: ${rejectionText(e)}`);
8346
8391
  }
8347
8392
  }
8348
8393
  }
@@ -8473,7 +8518,7 @@ export class Manager {
8473
8518
  });
8474
8519
  }
8475
8520
  catch (e) {
8476
- throw new Error(`could not record the custody reference ${runtime.kind}:${runtime.id} on the static slot of "${a.name}": ${e.message}`);
8521
+ throw new Error(`could not record the custody reference ${runtime.kind}:${runtime.id} on the static slot of "${a.name}": ${rejectionText(e)}`);
8477
8522
  }
8478
8523
  }
8479
8524
  /** Mint the custody reference for a seat this manager is about to spawn, BEFORE the processes
@@ -8519,7 +8564,7 @@ export class Manager {
8519
8564
  disposal =
8520
8565
  e instanceof RuntimeReapUnproven
8521
8566
  ? `the spawned seat was NOT proved gone and may still be running as ${e.reference.kind}:${e.reference.id} (${e.message})`
8522
- : `the spawned seat could NOT be reaped: ${e.message}`;
8567
+ : `the spawned seat could NOT be reaped: ${rejectionText(e)}`;
8523
8568
  }
8524
8569
  }
8525
8570
  throw new Error(`runtime "${this.runtime.kind}" reserved custody ${reserved.kind}:${reserved.id} for "${name}" but spawned ` +
@@ -8620,8 +8665,8 @@ export class Manager {
8620
8665
  catch (e) {
8621
8666
  const h = this.retiring.get(a.name);
8622
8667
  if (h && h.lifecycleUid === a.lifecycleUid)
8623
- h.lastError = `the static retirement did not complete (${e.message}); the name stays held - a same-name spawn retries the same terminal (op ${opId})`;
8624
- console.error(`static retirement ${a.name} (${a.id}): ${e.message}`);
8668
+ h.lastError = `the static retirement did not complete (${rejectionText(e)}); the name stays held - a same-name spawn retries the same terminal (op ${opId})`;
8669
+ console.error(`static retirement ${a.name} (${a.id}): ${rejectionText(e)}`);
8625
8670
  if (surfaceFailure)
8626
8671
  throw e;
8627
8672
  }
@@ -8724,7 +8769,7 @@ export class Manager {
8724
8769
  await this.retryStaticReconcile(key);
8725
8770
  };
8726
8771
  void run().catch((error) => {
8727
- item.lastError = `retry driver failed before the exact terminal: ${error.message}`;
8772
+ item.lastError = `retry driver failed before the exact terminal: ${rejectionText(error)}`;
8728
8773
  item.disposition = "retry-exhausted";
8729
8774
  item.remedy = "restart this manager for a fresh per-process retry budget";
8730
8775
  console.error(`! static reconcile retry-exhausted alias=${item.alias} phase=${item.phase} uid=${item.lifecycleUid}: ${item.lastError}; NEXT: restart this manager for a fresh per-process retry budget`);
@@ -8765,7 +8810,7 @@ export class Manager {
8765
8810
  row = await this.readStaticReconcileSlot(item.alias);
8766
8811
  }
8767
8812
  catch (error) {
8768
- item.lastError = `durable slot re-read failed: ${error.message}`;
8813
+ item.lastError = `durable slot re-read failed: ${rejectionText(error)}`;
8769
8814
  if (item.attempts >= item.maxAttempts) {
8770
8815
  item.disposition = "retry-exhausted";
8771
8816
  item.remedy = "restart this manager for a fresh per-process retry budget";
@@ -8849,7 +8894,7 @@ export class Manager {
8849
8894
  return true;
8850
8895
  }
8851
8896
  catch (error) {
8852
- item.lastError = error.message;
8897
+ item.lastError = rejectionText(error);
8853
8898
  try {
8854
8899
  const fresh = await this.readStaticReconcileSlot(item.alias);
8855
8900
  if (fresh?.lifecycleUid === item.lifecycleUid && fresh.actor === item.actor)
@@ -8948,16 +8993,8 @@ export class Manager {
8948
8993
  const kvm = new Kvm(nc);
8949
8994
  await ensureAuthorityStores(jsm, kvm, this.space);
8950
8995
  const recordsKv = await kvm.open(recordsBucket(this.space));
8951
- const t = staticLifecycleTransport(recordsKv, recordsKv /* auth reads unused in the sweep */);
8952
- const keys = await recordsKv.keys(`${STATIC_SLOT_PREFIX}.${DEV_OWNER}.>`);
8953
- const aliases = [];
8954
- for await (const k of keys)
8955
- aliases.push(k.split(".").slice(2).join("."));
8956
- for (const alias of aliases) {
8957
- const slot = await readStaticSlot(t, DEV_OWNER, alias);
8958
- if (slot !== undefined)
8959
- slotRows.push(slot.row);
8960
- }
8996
+ for (const slot of await walkStaticSlots(recordsKv, DEV_OWNER))
8997
+ slotRows.push(slot.row);
8961
8998
  }
8962
8999
  catch (error) {
8963
9000
  sweep.completedAt = new Date().toISOString();
@@ -9206,7 +9243,7 @@ export class Manager {
9206
9243
  return { ok: true, data: result };
9207
9244
  }
9208
9245
  catch (e) {
9209
- return { ok: false, error: e.message };
9246
+ return { ok: false, error: rejectionText(e) };
9210
9247
  }
9211
9248
  }
9212
9249
  /** Persist a peer-defined persona as config. After this, `start name` auto-discovers
@@ -9238,7 +9275,7 @@ export class Manager {
9238
9275
  existing = loadAgentFile(path);
9239
9276
  }
9240
9277
  catch (e) {
9241
- return { ok: false, error: e.message };
9278
+ return { ok: false, error: rejectionText(e) };
9242
9279
  }
9243
9280
  if (!admin && existing.owner !== caller) {
9244
9281
  const owner = existing.owner ? `owned by ${existing.owner}` : "operator-owned (legacy file - no agent owner)";
@@ -9265,13 +9302,13 @@ export class Manager {
9265
9302
  }, existing);
9266
9303
  }
9267
9304
  catch (e) {
9268
- return { ok: false, error: e.message };
9305
+ return { ok: false, error: rejectionText(e) };
9269
9306
  }
9270
9307
  try {
9271
9308
  saveAgentFile(path, def);
9272
9309
  }
9273
9310
  catch (e) {
9274
- return { ok: false, error: e.message };
9311
+ return { ok: false, error: rejectionText(e) };
9275
9312
  }
9276
9313
  return { ok: true, data: { name, path } };
9277
9314
  }
@@ -9429,7 +9466,7 @@ export class Manager {
9429
9466
  accepted = await write(part.data);
9430
9467
  }
9431
9468
  catch (error) {
9432
- throw new EpEnvelopeError("unavailable", `input for seat "${a.name}" failed: ${error.message}`);
9469
+ throw new EpEnvelopeError("unavailable", `input for seat "${a.name}" failed: ${rejectionText(error)}`);
9433
9470
  }
9434
9471
  if (!Number.isSafeInteger(accepted)) {
9435
9472
  throw new EpEnvelopeError("unavailable", `input for seat "${a.name}" failed: runtime accepted ${String(accepted)} of ${intendedBytes} bytes`);
@@ -9480,7 +9517,7 @@ export class Manager {
9480
9517
  }
9481
9518
  catch (e) {
9482
9519
  slot.release();
9483
- return { ok: false, error: e.message };
9520
+ return { ok: false, error: rejectionText(e) };
9484
9521
  }
9485
9522
  // establishAttach releases the claim it was handed on every exit; nothing to unwind here.
9486
9523
  const { grant } = await this.sessionPlane.establishAttach(caller, { name: a.name, lifecycleUid: a.lifecycleUid }, session, slot);