@cotal-ai/cli 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/commands/agents.d.ts +29 -26
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +103 -38
  4. package/dist/commands/agents.js.map +1 -1
  5. package/dist/commands/backup.d.ts.map +1 -1
  6. package/dist/commands/backup.js +2 -1
  7. package/dist/commands/backup.js.map +1 -1
  8. package/dist/commands/down-manifest.js +0 -4
  9. package/dist/commands/down-manifest.js.map +1 -1
  10. package/dist/commands/down.d.ts +3 -2
  11. package/dist/commands/down.d.ts.map +1 -1
  12. package/dist/commands/down.js +8 -7
  13. package/dist/commands/down.js.map +1 -1
  14. package/dist/commands/endpoints.js +2 -2
  15. package/dist/commands/endpoints.js.map +1 -1
  16. package/dist/commands/meshes-add.d.ts +0 -3
  17. package/dist/commands/meshes-add.d.ts.map +1 -1
  18. package/dist/commands/meshes-add.js +0 -3
  19. package/dist/commands/meshes-add.js.map +1 -1
  20. package/dist/commands/mint.d.ts +0 -9
  21. package/dist/commands/mint.d.ts.map +1 -1
  22. package/dist/commands/mint.js +9 -9
  23. package/dist/commands/mint.js.map +1 -1
  24. package/dist/commands/service.d.ts.map +1 -1
  25. package/dist/commands/service.js +91 -29
  26. package/dist/commands/service.js.map +1 -1
  27. package/dist/commands/setup.d.ts +2 -1
  28. package/dist/commands/setup.d.ts.map +1 -1
  29. package/dist/commands/setup.js +29 -6
  30. package/dist/commands/setup.js.map +1 -1
  31. package/dist/commands/spawn.d.ts +18 -0
  32. package/dist/commands/spawn.d.ts.map +1 -1
  33. package/dist/commands/spawn.js +30 -20
  34. package/dist/commands/spawn.js.map +1 -1
  35. package/dist/commands/status.d.ts.map +1 -1
  36. package/dist/commands/status.js +28 -28
  37. package/dist/commands/status.js.map +1 -1
  38. package/dist/commands/up.d.ts +0 -4
  39. package/dist/commands/up.d.ts.map +1 -1
  40. package/dist/commands/up.js +260 -73
  41. package/dist/commands/up.js.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/lib/assist.d.ts +10 -7
  45. package/dist/lib/assist.d.ts.map +1 -1
  46. package/dist/lib/assist.js +20 -31
  47. package/dist/lib/assist.js.map +1 -1
  48. package/dist/lib/auth-proc.d.ts.map +1 -1
  49. package/dist/lib/auth-proc.js +22 -19
  50. package/dist/lib/auth-proc.js.map +1 -1
  51. package/dist/lib/control.d.ts +3 -2
  52. package/dist/lib/control.d.ts.map +1 -1
  53. package/dist/lib/control.js.map +1 -1
  54. package/dist/lib/delivery-proc.d.ts.map +1 -1
  55. package/dist/lib/delivery-proc.js +13 -13
  56. package/dist/lib/delivery-proc.js.map +1 -1
  57. package/dist/lib/delivery-responder.d.ts +12 -2
  58. package/dist/lib/delivery-responder.d.ts.map +1 -1
  59. package/dist/lib/delivery-responder.js +3 -13
  60. package/dist/lib/delivery-responder.js.map +1 -1
  61. package/dist/lib/ext-mutation.d.ts.map +1 -1
  62. package/dist/lib/ext-mutation.js.map +1 -1
  63. package/dist/lib/manager-proc.d.ts +4 -4
  64. package/dist/lib/manager-proc.d.ts.map +1 -1
  65. package/dist/lib/manager-proc.js +22 -20
  66. package/dist/lib/manager-proc.js.map +1 -1
  67. package/dist/lib/restore.d.ts.map +1 -1
  68. package/dist/lib/restore.js +2 -2
  69. package/dist/lib/restore.js.map +1 -1
  70. package/dist/lib/self-exec.d.ts.map +1 -1
  71. package/dist/lib/self-exec.js +38 -13
  72. package/dist/lib/self-exec.js.map +1 -1
  73. package/dist/lib/status.d.ts +19 -9
  74. package/dist/lib/status.d.ts.map +1 -1
  75. package/dist/lib/status.js +41 -54
  76. package/dist/lib/status.js.map +1 -1
  77. package/dist/lib/steps.d.ts +7 -3
  78. package/dist/lib/steps.d.ts.map +1 -1
  79. package/dist/lib/steps.js +22 -10
  80. package/dist/lib/steps.js.map +1 -1
  81. package/dist/lib/theme.d.ts.map +1 -1
  82. package/dist/lib/theme.js +3 -2
  83. package/dist/lib/theme.js.map +1 -1
  84. package/dist/seed/reconcile.d.ts.map +1 -1
  85. package/dist/seed/reconcile.js +13 -9
  86. package/dist/seed/reconcile.js.map +1 -1
  87. package/dist/ui.d.ts +13 -0
  88. package/dist/ui.d.ts.map +1 -1
  89. package/dist/ui.js +21 -0
  90. package/dist/ui.js.map +1 -1
  91. package/package.json +7 -7
@@ -2,11 +2,13 @@ import { spawn } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { createConnection, createServer } from "node:net";
4
4
  import { hostname } from "node:os";
5
- import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, } from "node:fs";
5
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, readdirSync, } from "node:fs";
6
6
  import { dirname, join, resolve } from "node:path";
7
- import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, standaloneConnectOpts, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
7
+ import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, ttlBuckets, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, presenceBucket, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
8
8
  import { connect } from "@nats-io/transport-node";
9
- import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removeIdentityPin, writeIdentityPin, localProcessPath, MANAGER_PIDFILE, assertManagerCanSpare, verifyIdentityPin, isLoopbackHost, } from "@cotal-ai/workspace";
9
+ import { jetstreamManager, StorageType } from "@nats-io/jetstream";
10
+ import { Kvm } from "@nats-io/kv";
11
+ import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removePidPair, writePidPair, localProcessPath, MANAGER_PIDFILE, assertManagerCanSpare, verifyIdentityPin, isLoopbackHost, connectUserControlOrThrow, userViewAuth, } from "@cotal-ai/workspace";
10
12
  import { ensureAuthService, resolveAuthProvider, stopAuthService } from "../lib/auth-proc.js";
11
13
  import { resolveSpace } from "../lib/status.js";
12
14
  import { c } from "../ui.js";
@@ -584,8 +586,6 @@ async function runUp(args, inheritedLock, onAdopt) {
584
586
  // Gated on the ATTEMPT, not on holding the lock. The lock is now always held, and this refuses
585
587
  // whenever a journal exists — which is precisely the state a resume re-entry is in, so gating
586
588
  // it on the lock would refuse the very resume the lock is here to protect.
587
- if (!resumeAttempt)
588
- assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
589
589
  let server = values.server ?? DEFAULT_SERVER;
590
590
  const host = values.host ?? "127.0.0.1";
591
591
  // `--host` is the BIND address; `server` is the URL the readiness probe, the mesh registry, and
@@ -596,6 +596,46 @@ async function runUp(args, inheritedLock, onAdopt) {
596
596
  // pair rather than starting a broker nothing can reach.
597
597
  if (values.host)
598
598
  server = reconcileHostAndServer(values.host, values.server);
599
+ // A same-root repair reopens the store the mesh record names, ONCE, before the ordinary-up
600
+ // assertion below reads --store-dir (#2218): an ordinary `up` on a space whose broker died must
601
+ // not silently fall back to the root's default store while the record still names a custom one.
602
+ // Never runs during a resume attempt, which keeps its own store checks. The reachability probe
603
+ // here is reused as `listenerReachable` further down instead of probing twice.
604
+ let repairProbe;
605
+ if (!resumeAttempt) {
606
+ const repairSpace = values.space ?? resolveSpace(process.cwd());
607
+ const repairRoot = cotalRoot();
608
+ const heldForRepair = loadMeshes().find((m) => m.origin !== "manual" && m.origin !== "catalog" && m.server === server && m.root === repairRoot && m.space === repairSpace);
609
+ if (heldForRepair) {
610
+ repairProbe = await isReachable(server);
611
+ if (!repairProbe) {
612
+ if (values["store-dir"] !== undefined) {
613
+ const requestedStoreDir = resolve(values["store-dir"]);
614
+ if (heldForRepair.storeDir !== undefined && requestedStoreDir !== heldForRepair.storeDir) {
615
+ console.error(c.red(`✗ mesh "${repairSpace}" is recorded at ${server} with store ${heldForRepair.storeDir} - a stopped broker can't change --store-dir (it is fixed at start); repair it with \`cotal up --store-dir ${heldForRepair.storeDir}\`, or \`cotal down\` it first`));
616
+ process.exit(1);
617
+ }
618
+ }
619
+ else if (heldForRepair.storeDir !== undefined) {
620
+ values["store-dir"] = heldForRepair.storeDir;
621
+ console.log(c.dim(`reopening the recorded store ${heldForRepair.storeDir}`));
622
+ }
623
+ else {
624
+ const defaultStore = cotalPath("nats");
625
+ let hasEntries = false;
626
+ try {
627
+ hasEntries = readdirSync(join(defaultStore, "jetstream")).length > 0;
628
+ }
629
+ catch { /* absent: no jetstream dir yet */ }
630
+ if (!hasEntries) {
631
+ console.error(c.yellow(`! mesh "${repairSpace}" was recorded before the store directory was kept, so this repair opens ${defaultStore}; if the previous broker used --store-dir, stop this one with \`cotal down\` and repair with that flag (its Store Directory: line is in ${repairRoot}/.cotal/nats.log)`));
632
+ }
633
+ }
634
+ }
635
+ }
636
+ }
637
+ if (!resumeAttempt)
638
+ assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
599
639
  const restoredAttempt = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
600
640
  if (restoredAttempt?.reentry) {
601
641
  if (!restoredAttempt.listenerProof)
@@ -622,7 +662,7 @@ async function runUp(args, inheritedLock, onAdopt) {
622
662
  }
623
663
  if (ordinaryAttempt?.journalState === "resume-committed")
624
664
  throw new Error(`resume attempt ${resumeAttempt} is manager-committed but has no adoptable bound listener; preserving the commit token and retained suppression`);
625
- const listenerReachable = await isReachable(server);
665
+ const listenerReachable = repairProbe !== undefined ? repairProbe : await isReachable(server);
626
666
  if (resumeAttempt && listenerReachable)
627
667
  throw new Error(`resume attempt ${resumeAttempt} refuses the unproven occupied listener at ${server}`);
628
668
  if (listenerReachable) {
@@ -678,6 +718,13 @@ async function runUp(args, inheritedLock, onAdopt) {
678
718
  console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --max-file-store (it is fixed at start); \`cotal down\` it first, then \`cotal up --max-file-store ${maxFileStore}\``));
679
719
  process.exit(1);
680
720
  }
721
+ // The store directory is fixed the same way: an explicit --store-dir on a live refresh can
722
+ // only be refused when it disagrees with what the running broker actually opened. Absent
723
+ // `held.storeDir` (a pre-field record) has nothing to compare against, so it is not checked.
724
+ if (values["store-dir"] !== undefined && held.storeDir !== undefined && resolve(values["store-dir"]) !== held.storeDir) {
725
+ console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --store-dir (it is fixed at start); \`cotal down\` it first, then \`cotal up --store-dir ${resolve(values["store-dir"])}\``));
726
+ process.exit(1);
727
+ }
681
728
  // `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
682
729
  // (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
683
730
  // That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
@@ -746,6 +793,9 @@ async function runUp(args, inheritedLock, onAdopt) {
746
793
  // a same-root caller holds the space's signing material either way.
747
794
  reconcileCreds = await mintCreds(spaceAuth, newIdentity(), "provisioner");
748
795
  }
796
+ const v = await brokerVersion(server, held.space, reconcileCreds);
797
+ if (belowPresenceSafeBroker(v.version) && v.presenceFileBacked)
798
+ console.error(c.yellow(`! nats-server ${v.version} is below 2.14.5 and the presence bucket of "${held.space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
749
799
  for (const r of await reconcileSpaceTtls({ servers: server, space: held.space, creds: reconcileCreds }))
750
800
  console.log(c.dim(` reconciled ${r.stream} TTL ${r.fromMs === 0 ? "none" : `${r.fromMs}ms`} -> ${r.toMs}ms`));
751
801
  }
@@ -826,7 +876,7 @@ async function runUp(args, inheritedLock, onAdopt) {
826
876
  // no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
827
877
  // the field here would erase the requirement on every bare refresh, exactly the way dropping
828
878
  // `attachHost` would silently demote the mesh to loopback.
829
- recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ts: new Date().toISOString() }, "refresh");
879
+ recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ...(held.storeDir !== undefined ? { storeDir: held.storeDir } : {}), ts: new Date().toISOString() }, "refresh");
830
880
  return;
831
881
  }
832
882
  const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
@@ -947,9 +997,9 @@ async function runUp(args, inheritedLock, onAdopt) {
947
997
  const listenerStartedAt = new Date().toISOString();
948
998
  const child = spawn(bin, natsArgs, { stdio: "inherit" });
949
999
  let activationFinished = !resumeAttempt;
950
- if (child.pid)
951
- writeFileSync(cotalPath("nats.pid"), String(child.pid));
952
- writeIdentityPin(cotalPath("nats.pid"), child.pid ?? 0); // #969: pin pid to process start
1000
+ if (!child.pid)
1001
+ throw new Error("nats-server spawned with no pid");
1002
+ writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
953
1003
  if (restored && process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
954
1004
  process.exit(87);
955
1005
  if (restored)
@@ -1041,28 +1091,46 @@ async function runUp(args, inheritedLock, onAdopt) {
1041
1091
  process.on("SIGTERM", stop);
1042
1092
  // The broker is gone — drop it from the registry (and the `current` pointer if it was the default)
1043
1093
  // so a later `cotal spawn` doesn't try to join a dead mesh.
1044
- child.on("exit", async (code) => {
1045
- removeIdentityPin(cotalPath("nats.pid"));
1046
- rmSync(cotalPath("nats.pid"), { force: true });
1094
+ child.on("exit", async (code, signal) => {
1095
+ removePidPair(cotalPath("nats.pid"), String(child.pid));
1047
1096
  // Logged, never silently swallowed; the daemon kill runs in stopDelivery's finally regardless.
1048
1097
  await stopDelivery(undefined, undefined, space).catch((e) => console.error(`! delivery teardown: ${e.message}`));
1049
1098
  await stopManager(undefined, undefined, undefined, space).catch((e) => console.error(`! manager teardown: ${e.message}`));
1050
1099
  await stopAuthService(space).catch((e) => console.error(`! auth teardown: ${e.message}`));
1051
- // Only unrecord if the registry still points at THIS broker. A newer broker for the same space
1052
- // (a concurrent `up`, or a different-port re-up that recorded after us) may have replaced our
1053
- // record — removing by name would clobber the live winner and hide it from the registry.
1054
- // …and only if it is still OUR kind of record. A concurrent `cotal meshes add --force` can
1055
- // replace it with a hand-registered one carrying the same server + root; that record outlives
1056
- // this broker by design (it is the operator's, and only they remove it), so unrecording it on
1057
- // our exit would delete a registration this process never owned.
1058
- const mine = findMesh(space);
1059
- if (mine && mine.origin !== "manual" && mine.server === server && mine.root === cotalRoot()) {
1060
- removeMesh(space);
1061
- if (getCurrent() === space)
1062
- clearCurrent();
1100
+ // Unrecording (and the exit code below) both depend on WHY the broker is gone. `stopping` is
1101
+ // true only for the intended shutdown `stop()` drives (Ctrl-C, SIGTERM) — everything else here
1102
+ // (a crash, an OOM kill, a signal from outside `up`) leaves the record and its store in place.
1103
+ if (stopping) {
1104
+ // Only unrecord if the registry still points at THIS broker. A newer broker for the same space
1105
+ // (a concurrent `up`, or a different-port re-up that recorded after us) may have replaced our
1106
+ // record — removing by name would clobber the live winner and hide it from the registry.
1107
+ // …and only if it is still OUR kind of record. A concurrent `cotal meshes add --force` can
1108
+ // replace it with a hand-registered one carrying the same server + root; that record outlives
1109
+ // this broker by design (it is the operator's, and only they remove it), so unrecording it on
1110
+ // our exit would delete a registration this process never owned.
1111
+ const mine = findMesh(space);
1112
+ if (mine && mine.origin !== "manual" && mine.server === server && mine.root === cotalRoot()) {
1113
+ removeMesh(space);
1114
+ if (getCurrent() === space)
1115
+ clearCurrent();
1116
+ }
1117
+ if (activationFinished)
1118
+ process.exit(code ?? 0);
1119
+ }
1120
+ else {
1121
+ // The record is kept ON PURPOSE: it still names the store this broker opened, so a repair
1122
+ // `up` reopens that SAME store instead of falling back to the root's default, and a
1123
+ // supervising unit's restart takes the refresh path against the recorded store rather than
1124
+ // racing a second broker into existence. Foreground stdio is inherited (its own log lines
1125
+ // are already on this terminal, above), so there is no `.cotal/nats.log` to name here.
1126
+ const detail = signal ? `signal ${signal}` : `code ${code}`;
1127
+ console.error(c.red(`✗ nats-server exited unexpectedly (${detail}); the mesh "${space}" stays recorded (\`cotal meshes\` lists it offline); log: see the terminal output above; repair: \`cotal up --server ${server} --space ${space}\``));
1128
+ // A foreground broker under `Restart=on-failure` must exit non-zero on a crash so the unit
1129
+ // restarts it; exiting 0 here (as an unconditional `code ?? 0` used to) looked like a clean
1130
+ // stop and left the unit sitting dead.
1131
+ if (activationFinished)
1132
+ process.exit(code ?? 1);
1063
1133
  }
1064
- if (activationFinished)
1065
- process.exit(code ?? 0);
1066
1134
  });
1067
1135
  const ready = await waitReady(server, setup?.creds);
1068
1136
  if (!ready) {
@@ -1073,6 +1141,9 @@ async function runUp(args, inheritedLock, onAdopt) {
1073
1141
  markPendingResumeDegraded(resumeAttempt ?? "", reason, startupLock);
1074
1142
  throw new Error(reason);
1075
1143
  }
1144
+ const v = await brokerVersion(server, space, setup?.creds);
1145
+ if (belowPresenceSafeBroker(v.version) && v.presenceFileBacked)
1146
+ console.error(c.yellow(`! nats-server ${v.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
1076
1147
  if (restored)
1077
1148
  await provePreparedRestoreListener(restored);
1078
1149
  // Listener ready — commit the transport decision (S5: not before start).
@@ -1080,6 +1151,8 @@ async function runUp(args, inheritedLock, onAdopt) {
1080
1151
  {
1081
1152
  if (!resumeAttempt)
1082
1153
  await postStart(server, space, setup, seedFile);
1154
+ else
1155
+ await recreateMemoryBuckets(server, space, setup);
1083
1156
  // USER MODE: the auth service comes up FIRST among the daemons — until its callout answers,
1084
1157
  // every user-mode connect to this broker is denied, so `up` must not report a usable user mesh
1085
1158
  // (nor let agents race it) on a half-started auth plane. (Foreground `up` doesn't exit here, so
@@ -1110,6 +1183,7 @@ async function runUp(args, inheritedLock, onAdopt) {
1110
1183
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
1111
1184
  ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
1112
1185
  ...(maxFileStore !== undefined ? { maxFileStore } : {}),
1186
+ storeDir,
1113
1187
  ts: new Date().toISOString(),
1114
1188
  }, "started");
1115
1189
  // Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
@@ -1204,6 +1278,28 @@ async function resumeControlAuth(root, mode) {
1204
1278
  epCaller: { owner: DEV_OWNER, actor: identity.id, uid },
1205
1279
  };
1206
1280
  }
1281
+ /** The caller for the resume's admin-gated manager asks on a user mesh. The gate reads the CURRENT
1282
+ * ledger, so they ride the logged-in operator's manager view, the principal the preserve cut used:
1283
+ * a static instrument carries a caller the ledger has no row for. The view selects a registered
1284
+ * manager instance, so it is taken after the readiness wait. Each ask is its own connection and a
1285
+ * bearer lives no longer than the IdP proof behind it, so every call after the first takes a fresh
1286
+ * bearer from the view's source, pinned to the same principal and manager instance. */
1287
+ async function userManagerAuth(space) {
1288
+ const conn = await connectUserControlOrThrow({ space });
1289
+ const view = await userViewAuth(conn, "manager-caller");
1290
+ let first = view.bearer;
1291
+ return async () => {
1292
+ const bearer = first ?? await view.source();
1293
+ first = undefined;
1294
+ return {
1295
+ bearer,
1296
+ sentinelCreds: view.sentinelCreds,
1297
+ tls: conn.tls,
1298
+ epCaller: { owner: view.owner, actor: view.actor, uid: view.lifecycleUid },
1299
+ managerInstanceId: view.managerInstanceId,
1300
+ };
1301
+ };
1302
+ }
1207
1303
  function restoreListenerOwner(pid, nonce, startedAt) {
1208
1304
  if (!Number.isInteger(pid) || pid <= 0)
1209
1305
  throw new Error("restore listener spawn returned no pid");
@@ -1476,6 +1572,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
1476
1572
  ...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
1477
1573
  ...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
1478
1574
  ...(pending.maxFileStore !== undefined ? { maxFileStore: pending.maxFileStore } : {}),
1575
+ storeDir: resolve(pending.storeDir),
1479
1576
  ts: new Date().toISOString(),
1480
1577
  }, "started");
1481
1578
  const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
@@ -1521,6 +1618,7 @@ async function resumeProvenRestoreListener(prepared, heldLock) {
1521
1618
  ...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
1522
1619
  ...(restoreAttachHost ? { attachHost: restoreAttachHost } : {}),
1523
1620
  ...(restoreMaxSessions !== undefined ? { maxSessions: restoreMaxSessions } : {}),
1621
+ storeDir: resolve(prepared.targetPath),
1524
1622
  ts: new Date().toISOString(),
1525
1623
  }, "started");
1526
1624
  const controlPlane = await startDeliveryWithBroker(prepared.space, prepared.server, adoptedTlsRequired(prepared.root), {
@@ -1599,12 +1697,28 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
1599
1697
  }
1600
1698
  await new Promise((resolveWait) => setTimeout(resolveWait, 150));
1601
1699
  }
1700
+ let userAuth;
1701
+ // The auth for one admin-gated ask. A failed user-mode renewal degrades the journal only where a
1702
+ // refusal of that ask would.
1703
+ const askAuth = async (degrade) => {
1704
+ if (pending.mode !== "user")
1705
+ return auth;
1706
+ try {
1707
+ userAuth ??= await userManagerAuth(pending.space);
1708
+ return await userAuth();
1709
+ }
1710
+ catch (e) {
1711
+ if (degrade)
1712
+ markPendingResumeDegraded(attemptId, e instanceof Error ? e.message : String(e), heldLock);
1713
+ throw e;
1714
+ }
1715
+ };
1602
1716
  const resumed = await askManager(pending.space, server, "resumePreserved", {
1603
1717
  attemptId,
1604
1718
  inventory: restored?.selection === "registry"
1605
1719
  ? { ...pending.inventory, agents: [] }
1606
1720
  : pending.inventory,
1607
- }, auth, "any", 10 * 60 * 1000);
1721
+ }, await askAuth(true), "any", 10 * 60 * 1000);
1608
1722
  if (!resumed.ok) {
1609
1723
  const detail = resumed.data ? ` (${JSON.stringify(resumed.data)})` : "";
1610
1724
  const message = `${resumed.error ?? "retained-agent resume failed"}${detail}`;
@@ -1633,7 +1747,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
1633
1747
  }
1634
1748
  }
1635
1749
  if (!managerCommit) {
1636
- const committed = await askManager(pending.space, server, "commitResume", { attemptId }, auth, "any", 40_000);
1750
+ const committed = await askManager(pending.space, server, "commitResume", { attemptId }, await askAuth(true), "any", 40_000);
1637
1751
  if (!committed.ok) {
1638
1752
  const message = committed.error ?? "manager resume commit failed";
1639
1753
  markPendingResumeDegraded(attemptId, message, heldLock);
@@ -1664,7 +1778,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
1664
1778
  process.exit(89);
1665
1779
  }
1666
1780
  else {
1667
- const recommitted = await askManager(pending.space, server, "commitResume", { attemptId }, auth, "any", 40_000);
1781
+ const recommitted = await askManager(pending.space, server, "commitResume", { attemptId }, await askAuth(false), "any", 40_000);
1668
1782
  // A surviving manager that already finalized legitimately answers {state:"active"} with the
1669
1783
  // exact durable token: accept both committed shapes idempotently, then reissue the token-bound
1670
1784
  // finalize (itself idempotent) below.
@@ -1676,7 +1790,7 @@ async function completeResumeActivation(attemptId, healthy, reason, server, held
1676
1790
  if (!exactToken)
1677
1791
  throw new Error(`replacement manager did not recover the durable commit token for attempt ${attemptId}`);
1678
1792
  }
1679
- const finalized = await askManager(pending.space, server, "finalizeResume", { attemptId, durableCommitToken: managerCommit.durableCommitToken }, auth, "any", 40_000);
1793
+ const finalized = await askManager(pending.space, server, "finalizeResume", { attemptId, durableCommitToken: managerCommit.durableCommitToken }, await askAuth(false), "any", 40_000);
1680
1794
  if (!finalized.ok)
1681
1795
  throw new Error(finalized.error ?? `manager resume finalize failed for attempt ${attemptId}`);
1682
1796
  if (!isManagerFinalizeResult(finalized.data, attemptId))
@@ -2041,8 +2155,9 @@ opts) {
2041
2155
  const child = spawn(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
2042
2156
  closeSync(fd);
2043
2157
  if (opts.boundListener) {
2044
- writeFileSync(cotalPath("nats.pid"), String(child.pid));
2045
- writeIdentityPin(cotalPath("nats.pid"), child.pid ?? 0); // #969: pin pid to process start
2158
+ if (!child.pid)
2159
+ throw new Error("nats-server spawned with no pid");
2160
+ writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
2046
2161
  if (process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
2047
2162
  process.exit(87);
2048
2163
  try {
@@ -2062,14 +2177,25 @@ opts) {
2062
2177
  tailing = false;
2063
2178
  if (!ready) {
2064
2179
  child.kill("SIGTERM");
2065
- if (opts.boundListener)
2066
- removeIdentityPin(cotalPath("nats.pid"));
2067
- rmSync(cotalPath("nats.pid"), { force: true });
2180
+ removePidPair(cotalPath("nats.pid"), String(child.pid));
2068
2181
  throw new Error(`nats-server did not become reachable at ${server} - see ${logPath}`);
2069
2182
  }
2070
- if (!opts.boundListener)
2071
- writeFileSync(cotalPath("nats.pid"), String(child.pid));
2072
- writeIdentityPin(cotalPath("nats.pid"), child.pid ?? 0); // #969: pin pid to process start
2183
+ let brokerVer;
2184
+ try {
2185
+ brokerVer = await brokerVersion(server, space, setup?.creds);
2186
+ }
2187
+ catch (e) {
2188
+ child.kill("SIGTERM");
2189
+ removePidPair(cotalPath("nats.pid"), String(child.pid));
2190
+ throw e;
2191
+ }
2192
+ if (belowPresenceSafeBroker(brokerVer.version) && brokerVer.presenceFileBacked)
2193
+ console.error(c.yellow(`! nats-server ${brokerVer.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
2194
+ if (!opts.boundListener) {
2195
+ if (!child.pid)
2196
+ throw new Error("nats-server spawned with no pid");
2197
+ writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
2198
+ }
2073
2199
  if (opts.boundListener)
2074
2200
  await opts.boundListener.verify();
2075
2201
  // POST-START MUST NOT LEAVE AN ORPHAN LISTENER.
@@ -2097,13 +2223,15 @@ opts) {
2097
2223
  }
2098
2224
  catch { /* already gone */ }
2099
2225
  try {
2100
- removeIdentityPin(cotalPath("nats.pid"));
2101
- rmSync(cotalPath("nats.pid"), { force: true });
2226
+ removePidPair(cotalPath("nats.pid"), String(child.pid));
2102
2227
  }
2103
2228
  catch { /* best effort */ }
2104
2229
  throw e;
2105
2230
  }
2106
2231
  }
2232
+ else {
2233
+ await recreateMemoryBuckets(server, space, setup);
2234
+ }
2107
2235
  // USER MODE: the auth service comes up FIRST among the daemons (see the foreground path).
2108
2236
  const svc = await startUserAuthService(space, server, setup, opts.publicExchange);
2109
2237
  // Record BEFORE the control plane: the manager's fail-closed mode detection needs the
@@ -2125,6 +2253,7 @@ opts) {
2125
2253
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
2126
2254
  ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
2127
2255
  ...(opts.maxFileStore !== undefined ? { maxFileStore: opts.maxFileStore } : {}),
2256
+ storeDir,
2128
2257
  ts: new Date().toISOString(),
2129
2258
  }, "started");
2130
2259
  // Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
@@ -2303,6 +2432,10 @@ function journaledMaxFileStore(value) {
2303
2432
  * asserted directly by the registry smoke, since reaching each branch through a full `up` would
2304
2433
  * need a live broker per case. */
2305
2434
  export const recordOurMeshForTest = (m, provenance) => recordOurMesh(m, provenance);
2435
+ /** Record this mesh in the registry, and set it as the `current` default when there's no usable one
2436
+ * — i.e. the first mesh, OR when `current` dangles at a space that's no longer in the registry (a
2437
+ * ghost pointer is not a default). Never silently redirect a `current` that still resolves to a live
2438
+ * mesh; just say another is the default and how to switch. */
2306
2439
  function recordOurMesh(m, provenance) {
2307
2440
  const cur = getCurrent();
2308
2441
  const usableCurrent = cur && findMesh(cur) ? cur : undefined; // compute before recording m
@@ -2356,6 +2489,43 @@ async function waitReady(server, creds) {
2356
2489
  }
2357
2490
  return false;
2358
2491
  }
2492
+ /** The broker's version, and whether `space`'s presence stream exists file-backed. Cotal creates
2493
+ * it in memory (#1356), so only a bucket an older cotal created is file-backed; an absent one
2494
+ * will be created in memory by `setupSpaceStreams`. */
2495
+ async function brokerVersion(server, space, creds) {
2496
+ const nc = await connect({ servers: server, ...standaloneConnectOpts({ creds, tls: false }) });
2497
+ try {
2498
+ requireBrokerFloor(nc);
2499
+ const version = nc.info?.version ?? "";
2500
+ let presenceFileBacked = false;
2501
+ try {
2502
+ const info = await (await jetstreamManager(nc)).streams.info(`KV_${presenceBucket(space)}`);
2503
+ presenceFileBacked = info.config.storage === StorageType.File;
2504
+ }
2505
+ catch (e) {
2506
+ const err = e;
2507
+ if (err.api_error?.err_code !== 10059 && !/stream not found/i.test(err.message ?? ""))
2508
+ throw e;
2509
+ }
2510
+ return { version, presenceFileBacked };
2511
+ }
2512
+ finally {
2513
+ await nc.drain();
2514
+ }
2515
+ }
2516
+ // #1356: the presence bucket's file-backed latch is fixed in nats-server 2.14.5. Below that,
2517
+ // `cotal up` warns when the space's presence bucket is file-backed (never refuses: 2.14.0 meets the
2518
+ // SPEC §13.12 floor and is the bundled broker). A memory-backed bucket has no file store to latch.
2519
+ function belowPresenceSafeBroker(version) {
2520
+ const p = parseServerVersion(version);
2521
+ if (!p)
2522
+ return false;
2523
+ if (p.major !== 2)
2524
+ return p.major < 2;
2525
+ if (p.minor !== 14)
2526
+ return p.minor < 14;
2527
+ return p.patch < 5;
2528
+ }
2359
2529
  /** Wildcard binds mean "every interface", so they are NOT an address a client can be told to dial:
2360
2530
  * the probe/registry URL stays whatever it already is (loopback by default), which the wildcard
2361
2531
  * bind necessarily covers. */
@@ -2463,6 +2633,23 @@ async function postStart(server, space, setup, seedFile) {
2463
2633
  deliveryClass: setup ? "durable" : "live",
2464
2634
  });
2465
2635
  }
2636
+ /** A resume or restore skips `postStart` because the preserved store already holds every canonical
2637
+ * stream. A memory-backed bucket is the exception: the broker stop that preserved the store emptied
2638
+ * it, stream and all (#1356 made presence memory-backed). Recreate those, and only those, from the
2639
+ * same `ttlBuckets` list `setupSpaceStreams` creates them from, before any daemon opens them. */
2640
+ async function recreateMemoryBuckets(server, space, setup) {
2641
+ const nc = await connect({ servers: server, ...standaloneConnectOpts({ creds: setup?.creds, tls: false }) });
2642
+ try {
2643
+ requireBrokerFloor(nc);
2644
+ const kvm = new Kvm(nc);
2645
+ for (const [bucket, ttl, storage] of ttlBuckets(space))
2646
+ if (storage === StorageType.Memory)
2647
+ await kvm.create(bucket, { ttl, storage });
2648
+ }
2649
+ finally {
2650
+ await nc.drain();
2651
+ }
2652
+ }
2466
2653
  /** Load the declarative channels-config file to seed the registry. An explicit `--channels`
2467
2654
  * path that's missing is a hard error; the default `.cotal/channels.json` is optional (absent
2468
2655
  * ⇒ nothing to seed). */
@@ -2476,21 +2663,6 @@ function loadChannelsFile(explicit) {
2476
2663
  }
2477
2664
  return JSON.parse(readFileSync(path, "utf8"));
2478
2665
  }
2479
- /** Ensure the space's trust material exists, render a server config, and mint a privileged
2480
- * setup creds (used to pre-create streams once the server is up). The account signing key
2481
- * in `.cotal/auth` is what `cotal mint` and the manager later use to issue per-agent creds.
2482
- *
2483
- * USER MODE (`user` set): additionally run the registered auth provider's `prepareServer` over the
2484
- * SPACE-SCOPED state dir (`.cotal/auth/<space>/` — the multi-space-ready layout; nothing user-auth
2485
- * lives flat) and preload its extra account(s) into the broker config. The inverse is fail-closed:
2486
- * a space whose user-auth state exists MUST keep being started with --user-auth — regenerating the
2487
- * config without the callout account would silently break every sentinel connect.
2488
- *
2489
- * `rotateSys` (`cotal up --rotate-sys`) is the class-3 renewal for an EXISTING space: rotate the
2490
- * system account, re-mint `membership-observer.creds` + `connection-evictor.creds` against the
2491
- * successor, and render the config from the ROTATED record so the broker this `up` starts is the one
2492
- * that trusts them. It belongs here because this is the single site that both owns the trust record
2493
- * and renders `server.conf`; anywhere else would publish creds the live broker cannot honor. */
2494
2666
  /**
2495
2667
  * Turn the `--tls-cert`/`--tls-key` pair into a validated {@link BrokerTransport}, or `plaintext`
2496
2668
  * when neither was given.
@@ -2690,6 +2862,21 @@ function writeOpenBrokerConf(storeDir, opts) {
2690
2862
  writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, maxFileStore: opts.maxFileStore, transport: opts.transport }));
2691
2863
  return confPath;
2692
2864
  }
2865
+ /** Ensure the space's trust material exists, render a server config, and mint a privileged
2866
+ * setup creds (used to pre-create streams once the server is up). The account signing key
2867
+ * in `.cotal/auth` is what `cotal mint` and the manager later use to issue per-agent creds.
2868
+ *
2869
+ * USER MODE (`user` set): additionally run the registered auth provider's `prepareServer` over the
2870
+ * SPACE-SCOPED state dir (`.cotal/auth/<space>/` — the multi-space-ready layout; nothing user-auth
2871
+ * lives flat) and preload its extra account(s) into the broker config. The inverse is fail-closed:
2872
+ * a space whose user-auth state exists MUST keep being started with --user-auth — regenerating the
2873
+ * config without the callout account would silently break every sentinel connect.
2874
+ *
2875
+ * `rotateSys` (`cotal up --rotate-sys`) is the class-3 renewal for an EXISTING space: rotate the
2876
+ * system account, re-mint `membership-observer.creds` + `connection-evictor.creds` against the
2877
+ * successor, and render the config from the ROTATED record so the broker this `up` starts is the one
2878
+ * that trusts them. It belongs here because this is the single site that both owns the trust record
2879
+ * and renders `server.conf`; anywhere else would publish creds the live broker cannot honor. */
2693
2880
  async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
2694
2881
  // NON-OPTIONAL, and it used to default to `{ kind: "plaintext" }`. That default was the hole: the
2695
2882
  // manifest path called this with five arguments and silently got a cleartext listener while the
@@ -2868,23 +3055,6 @@ async function assertRootBrokerStopped(root) {
2868
3055
  throw new Error(`--rotate-sys: mesh "${m.space}" for this root is still reachable at ${m.server} - stop it first (\`cotal down\`), then rotate`);
2869
3056
  }
2870
3057
  }
2871
- /** Mint the two scoped creds the delivery daemon's membership feed loads (broker-sourced graph
2872
- * membership), at the FRESH `cotal up` while the in-memory `$SYS` signing seed still exists:
2873
- * - `membership-observer.creds` — SYSTEM-account CONNZ reader (the only window it can be minted: the
2874
- * `$SYS` seed is never persisted).
2875
- * - `membership-rw.creds` — DATA-account members-read + feed-write.
2876
- * - `membership.json` — the DATA account id (the CONNZ/event subjects pin it; non-secret, but kept
2877
- * 0600 alongside the creds).
2878
- * All 0600. Best-effort: a failure logs and leaves the feed disabled (the graph degrades to traffic-
2879
- * only, delivery is untouched). Runs only on a FRESH space (the `if (!auth)` branch); a normal down/up
2880
- * keeps `.cotal/auth` + these creds and reuses them. A space provisioned before this feature has no
2881
- * in-memory `$SYS` seed, so it gains membership only when its auth is regenerated (a fresh `.cotal/auth`)
2882
- * — a documented migration property, not a silent no-op.
2883
- * Coupling: `cotal clean all` deletes this identity-derived set (removeLocalState in clean.ts) —
2884
- * a cred added here must be added to that removal list too. `membership-rw.creds` is a MIGRATED kind:
2885
- * its write/read/delete all go through the {@link SecretStore} seam (here, the feed reader, and clean),
2886
- * so the renewal owner can re-sign it into a hosted store. The observer / evictor / config stay on the
2887
- * raw FS (static $SYS creds + non-secret config, not renewable kinds). */
2888
3058
  /** Provision the DATA-account half of the membership bundle, on EVERY `up` rather than only a fresh
2889
3059
  * space. Idempotent: it writes only what is absent and is silent when both are present.
2890
3060
  *
@@ -2934,6 +3104,23 @@ async function healMembershipDataCreds(auth, root, space) {
2934
3104
  console.error(c.dim(`• broker-sourced membership not repaired: ${e.message}`));
2935
3105
  }
2936
3106
  }
3107
+ /** Mint the two scoped creds the delivery daemon's membership feed loads (broker-sourced graph
3108
+ * membership), at the FRESH `cotal up` while the in-memory `$SYS` signing seed still exists:
3109
+ * - `membership-observer.creds` — SYSTEM-account CONNZ reader (the only window it can be minted: the
3110
+ * `$SYS` seed is never persisted).
3111
+ * - `membership-rw.creds` — DATA-account members-read + feed-write.
3112
+ * - `membership.json` — the DATA account id (the CONNZ/event subjects pin it; non-secret, but kept
3113
+ * 0600 alongside the creds).
3114
+ * All 0600. Best-effort: a failure logs and leaves the feed disabled (the graph degrades to traffic-
3115
+ * only, delivery is untouched). Runs only on a FRESH space (the `if (!auth)` branch); a normal down/up
3116
+ * keeps `.cotal/auth` + these creds and reuses them. A space provisioned before this feature has no
3117
+ * in-memory `$SYS` seed, so it gains membership only when its auth is regenerated (a fresh `.cotal/auth`)
3118
+ * — a documented migration property, not a silent no-op.
3119
+ * Coupling: `cotal clean all` deletes this identity-derived set (removeLocalState in clean.ts) —
3120
+ * a cred added here must be added to that removal list too. `membership-rw.creds` is a MIGRATED kind:
3121
+ * its write/read/delete all go through the {@link SecretStore} seam (here, the feed reader, and clean),
3122
+ * so the renewal owner can re-sign it into a hosted store. The observer / evictor / config stay on the
3123
+ * raw FS (static $SYS creds + non-secret config, not renewable kinds). */
2937
3124
  async function provisionMembershipCreds(auth, root, space) {
2938
3125
  try {
2939
3126
  const observer = await mintMembershipObserverCreds(auth, newIdentity());