@cotal-ai/cli 0.51.0 → 0.52.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 (113) hide show
  1. package/dist/command.d.ts.map +1 -1
  2. package/dist/command.js +8 -0
  3. package/dist/command.js.map +1 -1
  4. package/dist/commands/agents.d.ts +41 -2
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +67 -18
  7. package/dist/commands/agents.js.map +1 -1
  8. package/dist/commands/clean.d.ts.map +1 -1
  9. package/dist/commands/clean.js +5 -1
  10. package/dist/commands/clean.js.map +1 -1
  11. package/dist/commands/console.d.ts.map +1 -1
  12. package/dist/commands/console.js +9 -1
  13. package/dist/commands/console.js.map +1 -1
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +13 -8
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/down.d.ts.map +1 -1
  18. package/dist/commands/down.js +38 -3
  19. package/dist/commands/down.js.map +1 -1
  20. package/dist/commands/setup.d.ts.map +1 -1
  21. package/dist/commands/setup.js +9 -8
  22. package/dist/commands/setup.js.map +1 -1
  23. package/dist/commands/spawn.d.ts.map +1 -1
  24. package/dist/commands/spawn.js +18 -2
  25. package/dist/commands/spawn.js.map +1 -1
  26. package/dist/commands/status.d.ts.map +1 -1
  27. package/dist/commands/status.js +122 -35
  28. package/dist/commands/status.js.map +1 -1
  29. package/dist/commands/up.d.ts +7 -0
  30. package/dist/commands/up.d.ts.map +1 -1
  31. package/dist/commands/up.js +104 -11
  32. package/dist/commands/up.js.map +1 -1
  33. package/dist/commands/update.d.ts +12 -0
  34. package/dist/commands/update.d.ts.map +1 -1
  35. package/dist/commands/update.js +58 -5
  36. package/dist/commands/update.js.map +1 -1
  37. package/dist/console/app.d.ts +14 -6
  38. package/dist/console/app.d.ts.map +1 -1
  39. package/dist/console/app.js +308 -40
  40. package/dist/console/app.js.map +1 -1
  41. package/dist/console/commands.d.ts +29 -1
  42. package/dist/console/commands.d.ts.map +1 -1
  43. package/dist/console/commands.js +109 -13
  44. package/dist/console/commands.js.map +1 -1
  45. package/dist/console/control.d.ts +141 -0
  46. package/dist/console/control.d.ts.map +1 -0
  47. package/dist/console/control.js +189 -0
  48. package/dist/console/control.js.map +1 -0
  49. package/dist/console/mesh.d.ts +1 -1
  50. package/dist/console/mesh.d.ts.map +1 -1
  51. package/dist/console/root.d.ts +13 -1
  52. package/dist/console/root.d.ts.map +1 -1
  53. package/dist/console/root.js +25 -5
  54. package/dist/console/root.js.map +1 -1
  55. package/dist/console/ui/CommandPalette.d.ts +2 -1
  56. package/dist/console/ui/CommandPalette.d.ts.map +1 -1
  57. package/dist/console/ui/CommandPalette.js +3 -2
  58. package/dist/console/ui/CommandPalette.js.map +1 -1
  59. package/dist/console/ui/Confirm.d.ts +13 -4
  60. package/dist/console/ui/Confirm.d.ts.map +1 -1
  61. package/dist/console/ui/Confirm.js +13 -5
  62. package/dist/console/ui/Confirm.js.map +1 -1
  63. package/dist/console/ui/Detail.d.ts +8 -1
  64. package/dist/console/ui/Detail.d.ts.map +1 -1
  65. package/dist/console/ui/Detail.js +11 -4
  66. package/dist/console/ui/Detail.js.map +1 -1
  67. package/dist/console/ui/Help.d.ts.map +1 -1
  68. package/dist/console/ui/Help.js +3 -2
  69. package/dist/console/ui/Help.js.map +1 -1
  70. package/dist/console/ui/Roster.d.ts +9 -1
  71. package/dist/console/ui/Roster.d.ts.map +1 -1
  72. package/dist/console/ui/Roster.js +16 -5
  73. package/dist/console/ui/Roster.js.map +1 -1
  74. package/dist/console/ui/Sparkline.d.ts +13 -0
  75. package/dist/console/ui/Sparkline.d.ts.map +1 -0
  76. package/dist/console/ui/Sparkline.js +21 -0
  77. package/dist/console/ui/Sparkline.js.map +1 -0
  78. package/dist/console/ui/StatusBar.d.ts +4 -1
  79. package/dist/console/ui/StatusBar.d.ts.map +1 -1
  80. package/dist/console/ui/StatusBar.js +5 -3
  81. package/dist/console/ui/StatusBar.js.map +1 -1
  82. package/dist/console/ui/Tabs.d.ts +4 -2
  83. package/dist/console/ui/Tabs.d.ts.map +1 -1
  84. package/dist/console/ui/Tabs.js +5 -3
  85. package/dist/console/ui/Tabs.js.map +1 -1
  86. package/dist/console/ui/topo/Map.d.ts.map +1 -1
  87. package/dist/console/ui/topo/Map.js +34 -2
  88. package/dist/console/ui/topo/Map.js.map +1 -1
  89. package/dist/console/ui/topo/Matrix.d.ts.map +1 -1
  90. package/dist/console/ui/topo/Matrix.js +12 -3
  91. package/dist/console/ui/topo/Matrix.js.map +1 -1
  92. package/dist/console/ui/topo/Sequence.js +1 -1
  93. package/dist/console/ui/topo/Sequence.js.map +1 -1
  94. package/dist/console/ui/topo/Topo.d.ts +10 -2
  95. package/dist/console/ui/topo/Topo.d.ts.map +1 -1
  96. package/dist/console/ui/topo/Topo.js +6 -4
  97. package/dist/console/ui/topo/Topo.js.map +1 -1
  98. package/dist/console/ui/topo/model.d.ts +50 -2
  99. package/dist/console/ui/topo/model.d.ts.map +1 -1
  100. package/dist/console/ui/topo/model.js +69 -6
  101. package/dist/console/ui/topo/model.js.map +1 -1
  102. package/dist/lib/attach-client.d.ts.map +1 -1
  103. package/dist/lib/attach-client.js +7 -1
  104. package/dist/lib/attach-client.js.map +1 -1
  105. package/dist/lib/delivery-proc.d.ts +10 -2
  106. package/dist/lib/delivery-proc.d.ts.map +1 -1
  107. package/dist/lib/delivery-proc.js +7 -1
  108. package/dist/lib/delivery-proc.js.map +1 -1
  109. package/dist/view/mesh-view.d.ts +41 -1
  110. package/dist/view/mesh-view.d.ts.map +1 -1
  111. package/dist/view/mesh-view.js +107 -8
  112. package/dist/view/mesh-view.js.map +1 -1
  113. package/package.json +13 -6
@@ -35,6 +35,7 @@ export const upFlags = [
35
35
  { name: "host", type: "string", value: "<host>", description: "bind host override (an IP/hostname, not a URL; the URL is --server)" },
36
36
  { name: "space", type: "string", value: "<s>", description: "space name (default: the folder's)" },
37
37
  { name: "store-dir", type: "string", value: "<dir>", description: "JetStream store directory" },
38
+ { name: "max-file-store", type: "string", value: "<bytes>", description: "JetStream file storage cap in bytes (default: nats-server's dynamic cap). Fixed at broker start; recorded and kept across `down --preserve-state`" },
38
39
  { name: "channels", type: "string", value: "<path>", description: "channel-registry seed file (JSON; default .cotal/channels.json)" },
39
40
  { name: "tls-cert", type: "string", value: "<file>", description: "serve the broker over TLS with this certificate (requires --tls-key)" },
40
41
  { name: "tls-key", type: "string", value: "<file>", description: "the private key for --tls-cert" },
@@ -54,6 +55,7 @@ export const upFlags = [
54
55
  { name: "detach", type: "boolean", description: "run in the background (stop with `cotal down`)" },
55
56
  { name: "runtime", type: "string", value: "<name>", description: "agent runtime for the mesh manager (default pty; extension runtimes are explicit-only, see `cotal runtimes`); with -f overrides the manifest's runtime" },
56
57
  { name: "max-sessions", type: "string", value: "<n>", description: "live-session ceiling for the mesh manager (default 64; one session per console pane, so size for agents × panes). Recorded and reused by later manager launches" },
58
+ { name: "no-manager", type: "boolean", description: "broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager (refuses on a mesh whose manager is live)" },
57
59
  { name: "file", type: "string", short: "f", value: "<cotal.yaml>", description: "launch a whole mesh from a manifest" },
58
60
  { name: "dry-run", type: "boolean", description: "with -f: print the plan, mutate nothing" },
59
61
  ];
@@ -145,6 +147,11 @@ async function runUp(args, inheritedLock, onAdopt) {
145
147
  // read. A rotation is a stopped, fresh boot; a half-finished maintenance attempt is neither.
146
148
  if (values["rotate-sys"] && (values.__restoreAttempt || values.__ordinaryResumeAttempt))
147
149
  throw new Error("--rotate-sys cannot run during a restore/resume re-entry - finish or roll back the maintenance attempt, then `cotal down` and `cotal up --rotate-sys`");
150
+ // Same seam, same reason: a restore/resume re-entry needs the manager to commit the preserved
151
+ // inventory (completeResumeActivation asks it for resumePreserved/commitResume/finalizeResume), so
152
+ // a broker-only ask there is a contradiction to refuse, never a flag to drop mid-recovery.
153
+ if (values["no-manager"] && (values.__restoreAttempt || values.__ordinaryResumeAttempt))
154
+ throw new Error("--no-manager cannot run during a restore/resume re-entry - the resume needs a manager to commit the preserved inventory; finish or roll back the maintenance attempt first");
148
155
  if (!values.__restoreAttempt && !values.__ordinaryResumeAttempt && !values.file) {
149
156
  const root = cotalRoot();
150
157
  // Same refusal for the AUTO-recovered journal, raised before the recovery is prepared and
@@ -152,6 +159,11 @@ async function runUp(args, inheritedLock, onAdopt) {
152
159
  // `up` that has already begun adopting a maintenance attempt.
153
160
  if (values["rotate-sys"] && readMaintenanceJournal(root))
154
161
  throw new Error("--rotate-sys is refused while this root has a maintenance attempt to recover - finish or roll back that attempt (`cotal up` alone recovers it), then `cotal down` and `cotal up --rotate-sys`");
162
+ // Raised here for the stronger reason the journal itself carries: a pending maintenance attempt
163
+ // means the store is mid-recovery, and a broker-only boot over it is wrong with or without the
164
+ // manager half. The manager half is why the FLAG specifically cannot apply.
165
+ if (values["no-manager"] && readMaintenanceJournal(root))
166
+ throw new Error("--no-manager is refused while this root has a maintenance attempt to recover - a resume needs a manager to commit the preserved inventory; recover it (`cotal up` alone) or roll it back first");
155
167
  const lock = acquireMaintenanceLock(root);
156
168
  let pending;
157
169
  let recoveredRestore;
@@ -205,6 +217,12 @@ async function runUp(args, inheritedLock, onAdopt) {
205
217
  // Journal the CANONICAL identity-checked path, never the caller's spelling: a relative or
206
218
  // symlinked spelling re-resolved later (other cwd, retargeted link) could open another store.
207
219
  const resumeStore = journal.source.path;
220
+ // The cap is part of the preserved launch, like the store: a resume re-renders it, and a
221
+ // different --max-file-store is refused rather than silently resizing the preserved broker.
222
+ const resumeMaxFileStore = journaledMaxFileStore(launch.maxFileStore);
223
+ const requestedMaxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
224
+ if (requestedMaxFileStore !== undefined && requestedMaxFileStore !== resumeMaxFileStore)
225
+ throw new Error(`--max-file-store ${requestedMaxFileStore} is not the preserved launch cap (${resumeMaxFileStore ?? "unset"}); ordinary up resumes exactly the preserved cap - omit --max-file-store`);
208
226
  if (values.runtime && runtimes[0] && values.runtime !== runtimes[0])
209
227
  throw new Error(`--runtime ${values.runtime} contradicts the preserved agent runtime ${runtimes[0]}; omit it to resume the same principals`);
210
228
  const resumeRuntime = values.runtime ?? runtimes[0] ?? "pty";
@@ -219,6 +237,7 @@ async function runUp(args, inheritedLock, onAdopt) {
219
237
  launch: {
220
238
  server: resumeServer,
221
239
  storeDir: resumeStore,
240
+ ...(resumeMaxFileStore !== undefined ? { maxFileStore: resumeMaxFileStore } : {}),
222
241
  runtime: resumeRuntime,
223
242
  detached: Boolean(values.detach),
224
243
  serverName,
@@ -238,6 +257,7 @@ async function runUp(args, inheritedLock, onAdopt) {
238
257
  mode: journal.mode,
239
258
  server: resumeServer,
240
259
  storeDir: resumeStore,
260
+ ...(resumeMaxFileStore !== undefined ? { maxFileStore: resumeMaxFileStore } : {}),
241
261
  runtime: resumeRuntime,
242
262
  detached: Boolean(values.detach),
243
263
  inventory: resume.inventory,
@@ -251,6 +271,10 @@ async function runUp(args, inheritedLock, onAdopt) {
251
271
  const resume = readMaintenanceResumeDocument(root, journal.resume);
252
272
  const launch = journal.ordinaryResume.launch;
253
273
  const attemptId = journal.ordinaryResume.attemptId;
274
+ const recoveredMaxFileStore = journaledMaxFileStore(launch.maxFileStore);
275
+ const requestedMaxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
276
+ if (requestedMaxFileStore !== undefined && requestedMaxFileStore !== recoveredMaxFileStore)
277
+ throw new Error(`--max-file-store ${requestedMaxFileStore} is not the preserved launch cap (${recoveredMaxFileStore ?? "unset"}); resume attempt ${attemptId} re-renders exactly the preserved cap - omit --max-file-store`);
254
278
  // Recovery re-asserts the source identity rather than trusting the journaled spelling.
255
279
  assertStoreIdentity(journal.source);
256
280
  // The bound listener decides re-entry: a live exact listener is ADOPTED, a provably dead
@@ -295,6 +319,7 @@ async function runUp(args, inheritedLock, onAdopt) {
295
319
  mode: journal.mode,
296
320
  server: typeof launch.server === "string" ? launch.server : DEFAULT_SERVER,
297
321
  storeDir: journal.source.path,
322
+ ...(recoveredMaxFileStore !== undefined ? { maxFileStore: recoveredMaxFileStore } : {}),
298
323
  runtime: typeof launch.runtime === "string" ? launch.runtime : "pty",
299
324
  detached: launch.detached === true,
300
325
  inventory: resume.inventory,
@@ -353,6 +378,7 @@ async function runUp(args, inheritedLock, onAdopt) {
353
378
  space: pending.space,
354
379
  server: pending.server,
355
380
  "store-dir": pending.storeDir,
381
+ "max-file-store": pending.maxFileStore === undefined ? undefined : String(pending.maxFileStore),
356
382
  runtime: pending.runtime,
357
383
  detach: pending.detached,
358
384
  open: pending.mode === "open",
@@ -382,8 +408,22 @@ async function runUp(args, inheritedLock, onAdopt) {
382
408
  if (values.idp && !wantUser && !values.file) {
383
409
  throw new Error('--idp is for user-auth spaces; pair it with --user-auth, or set broker.auth: "user" in a manifest');
384
410
  }
411
+ // BROKER-ONLY MODE (#1417). `--no-manager` names what it omits, and the two manager-shaping flags
412
+ // are refused beside it rather than silently dropped: a runtime selected or a ceiling recorded for
413
+ // a manager that will never start is a misunderstanding, and dropping it quietly is the exact
414
+ // silent-no-op shape this repo refuses everywhere else.
415
+ const noManager = Boolean(values["no-manager"]);
416
+ if (noManager && values.runtime)
417
+ throw new Error("--no-manager cannot be combined with --runtime - no manager starts, so there is no runtime to select; drop --runtime, or drop --no-manager");
418
+ if (noManager && values["max-sessions"])
419
+ throw new Error("--no-manager cannot be combined with --max-sessions - no manager starts, so there is no session ceiling to set; drop --max-sessions, or drop --no-manager");
385
420
  const publicExchange = publicExchangeArgs(values, wantUser);
386
421
  const maxSessions = parsePositiveIntegerFlag("--max-sessions", values["max-sessions"]);
422
+ const maxFileStore = parsePositiveIntegerFlag("--max-file-store", values["max-file-store"]);
423
+ // The manifest path renders its broker from the manifest, which has no store fields, so the flag
424
+ // would be accepted and never rendered. Refuse it rather than drop it.
425
+ if (maxFileStore !== undefined && values.file)
426
+ throw new Error("--max-file-store cannot be combined with --file/-f - a manifest launch does not render a store cap; drop --max-file-store, or start the mesh without -f");
387
427
  // An open mesh has no operator, no system account, and no $SYS creds, so there is nothing to
388
428
  // rotate; the request is a misunderstanding to name, never a silent no-op that reports success.
389
429
  if (values["rotate-sys"] && values.open) {
@@ -473,6 +513,7 @@ async function runUp(args, inheritedLock, onAdopt) {
473
513
  idp: values.idp,
474
514
  rotateSys: values["rotate-sys"],
475
515
  maxSessions,
516
+ noManager,
476
517
  });
477
518
  return;
478
519
  }
@@ -491,6 +532,7 @@ async function runUp(args, inheritedLock, onAdopt) {
491
532
  idp: values.idp,
492
533
  rotateSys: values["rotate-sys"],
493
534
  maxSessions,
535
+ noManager,
494
536
  });
495
537
  }
496
538
  finally {
@@ -628,6 +670,13 @@ async function runUp(args, inheritedLock, onAdopt) {
628
670
  console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change its transport; \`cotal down\` it first, then \`cotal up --tls-cert <cert> --tls-key <key>\``));
629
671
  process.exit(1);
630
672
  }
673
+ // The store cap is fixed when nats-server reads its config, and nats-server refuses a reload
674
+ // from its dynamic cap to an explicit one. This branch starts nothing, so a different cap can
675
+ // only be refused; the same cap is a no-op.
676
+ if (maxFileStore !== undefined && maxFileStore !== held.maxFileStore) {
677
+ 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}\``));
678
+ process.exit(1);
679
+ }
631
680
  // `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
632
681
  // (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
633
682
  // That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
@@ -640,6 +689,18 @@ async function runUp(args, inheritedLock, onAdopt) {
640
689
  process.exit(1);
641
690
  }
642
691
  }
692
+ // `--no-manager` on a refresh whose manager is LIVE is the same class of refusal: this branch
693
+ // starts nothing, so the flag cannot apply, and silently keeping the manager would answer a
694
+ // broker-only ask with a full stack while silently stopping it would kill agents this command
695
+ // was never asked to touch. Same shape as `--runtime`/`--max-sessions` above: refuse, name the
696
+ // exact remedy. A refresh of a mesh whose manager is NOT live simply proceeds broker-only.
697
+ if (noManager) {
698
+ const live = managerRecordState(undefined, undefined, held.space);
699
+ if (live.state === "alive") {
700
+ console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} with a live manager - \`cotal up --no-manager\` will not keep or stop one; \`cotal down manager\` first, then \`cotal up --no-manager\``));
701
+ process.exit(1);
702
+ }
703
+ }
643
704
  // Live INFO must agree with the recorded/requested transport. A bare refresh that greets
644
705
  // "already running" over a plaintext listener while policy claims TLS is S5's second half —
645
706
  // the refuse left a durable lie and this path used to reprint TLS success over cleartext.
@@ -751,6 +812,7 @@ async function runUp(args, inheritedLock, onAdopt) {
751
812
  runtime: values.runtime,
752
813
  attachHost: attachHostFor(held.space, values.host),
753
814
  maxSessions: maxSessionsFor(held.space, maxSessions),
815
+ noManager,
754
816
  });
755
817
  if (!controlPlane)
756
818
  process.exitCode = 1;
@@ -763,7 +825,7 @@ async function runUp(args, inheritedLock, onAdopt) {
763
825
  // no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
764
826
  // the field here would erase the requirement on every bare refresh, exactly the way dropping
765
827
  // `attachHost` would silently demote the mesh to loopback.
766
- recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ts: new Date().toISOString() }, "refresh");
828
+ 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");
767
829
  return;
768
830
  }
769
831
  const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
@@ -795,6 +857,7 @@ async function runUp(args, inheritedLock, onAdopt) {
795
857
  transport,
796
858
  server,
797
859
  storeDir: values["store-dir"],
860
+ maxFileStore,
798
861
  space: values.space,
799
862
  open: values.open,
800
863
  userAuth: wantUser ? { idpUrl: values.idp } : undefined,
@@ -807,6 +870,7 @@ async function runUp(args, inheritedLock, onAdopt) {
807
870
  host: values.host,
808
871
  runtime: values.runtime,
809
872
  maxSessions,
873
+ noManager,
810
874
  resumeAttempt,
811
875
  resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
812
876
  ...(restored ? {
@@ -862,14 +926,14 @@ async function runUp(args, inheritedLock, onAdopt) {
862
926
  // Decided above the branch; the dial host is only settled here (a port collision may have moved
863
927
  // the server, and the hostname is what the certificate has to match).
864
928
  assertServesDialHost(transport, new URL(server).hostname);
865
- const setup = useAuth ? await authSetup(storeDir, server, space, host, wantUser ? { idpUrl: values.idp } : undefined, transport, values["rotate-sys"]) : undefined;
929
+ const setup = useAuth ? await authSetup(storeDir, server, space, host, wantUser ? { idpUrl: values.idp } : undefined, transport, values["rotate-sys"], maxFileStore) : undefined;
866
930
  const port = Number(new URL(server).port) || 4222;
867
931
  const restored = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
868
932
  // Both modes go through a RENDERER, never bare CLI flags. Open mode used to start from
869
933
  // `-js -sd … -p … -a …`, which never called a renderer at all — so the required transport union
870
934
  // protected the auth path and was silent on the open one, and a cert/key pair passed to an
871
935
  // open-mode `up` would have been accepted while the listener came up in cleartext.
872
- const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport });
936
+ const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport, maxFileStore });
873
937
  const natsArgs = [
874
938
  "-c", confPath,
875
939
  ...(restored ? ["--name", restored.serverName]
@@ -1001,6 +1065,7 @@ async function runUp(args, inheritedLock, onAdopt) {
1001
1065
  // though the operator had chosen it.
1002
1066
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
1003
1067
  ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
1068
+ ...(maxFileStore !== undefined ? { maxFileStore } : {}),
1004
1069
  ts: new Date().toISOString(),
1005
1070
  }, "started");
1006
1071
  // Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
@@ -1014,6 +1079,7 @@ async function runUp(args, inheritedLock, onAdopt) {
1014
1079
  // is never a side effect of anything but the operator binding the mesh somewhere reachable.
1015
1080
  attachHost: effectiveAttachHost,
1016
1081
  maxSessions: effectiveMaxSessions,
1082
+ noManager,
1017
1083
  resumeAttempt,
1018
1084
  resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
1019
1085
  wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
@@ -1365,6 +1431,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
1365
1431
  ...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
1366
1432
  ...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
1367
1433
  ...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
1434
+ ...(pending.maxFileStore !== undefined ? { maxFileStore: pending.maxFileStore } : {}),
1368
1435
  ts: new Date().toISOString(),
1369
1436
  }, "started");
1370
1437
  const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
@@ -1687,6 +1754,12 @@ async function upManifest(file, opts) {
1687
1754
  const open = m.broker?.auth === false; // default is auth
1688
1755
  const userAuth = m.broker?.auth === "user" ? { idpUrl: opts.idp ?? m.broker?.idp } : undefined; // flag > manifest
1689
1756
  const runtime = m.runtime ?? "pty";
1757
+ // A manifest's agents are booted BY the manager `up -f` starts with the mesh, so `--no-manager`
1758
+ // against a manifest declaring agents asks for a boot that cannot deliver its own plan. Refused
1759
+ // before the dry-run print and before anything boots, with the two workable spellings named.
1760
+ if (opts.noManager && eff.agents.length)
1761
+ throw new Error(`--no-manager cannot launch this manifest: it declares ${eff.agents.length} agent(s), and a manifest's agents are booted by its manager (which --no-manager omits). Deploy it from a manager host with \`cotal spawn -f\`, or drop --no-manager.`);
1762
+ const effectiveNoManager = Boolean(opts.noManager);
1690
1763
  // The open-mesh refusal must be RE-STATED here, not only against the CLI `--open` flag: on this
1691
1764
  // path openness comes from the MANIFEST (`broker.auth: false`), which the flag-level guard cannot
1692
1765
  // see. Without it, `cotal up -f open.yaml --rotate-sys` boots an open broker, exits 0 and rotates
@@ -1703,6 +1776,10 @@ async function upManifest(file, opts) {
1703
1776
  if (opts.transport.kind === "tls-required")
1704
1777
  announceTransport(opts.transport);
1705
1778
  console.log(renderUpPlan(eff, server));
1779
+ // The omission is part of the plan (#1417): a dry run that lists a manager the real boot
1780
+ // will not start is the exact "green-lit a launch the applying command refuses" failure.
1781
+ if (effectiveNoManager)
1782
+ console.log(c.dim(" no manager will start on this host (--no-manager): broker, channels and, in auth mode, the delivery daemon come up, and no agents launch from here"));
1706
1783
  return;
1707
1784
  }
1708
1785
  // Preflight an extension runtime before starting the broker. The detached manager re-execs this
@@ -1756,7 +1833,7 @@ async function upManifest(file, opts) {
1756
1833
  try {
1757
1834
  // `m.broker?.host`, not the defaulted `host`: same reason as the flag path — only a host the
1758
1835
  // manifest actually declared is an exposure decision worth persisting.
1759
- ({ pid, controlPlane, authService } = await startMeshDetached({ transport: opts.transport, server, space: m.space, open, userAuth, rotateSys: opts.rotateSys, host: m.broker?.host, seed: manifestToChannels(eff), runtime, launch: specPath, maxSessions: opts.maxSessions }));
1836
+ ({ pid, controlPlane, authService } = await startMeshDetached({ transport: opts.transport, server, space: m.space, open, userAuth, rotateSys: opts.rotateSys, host: m.broker?.host, seed: manifestToChannels(eff), runtime, launch: specPath, maxSessions: opts.maxSessions, noManager: effectiveNoManager }));
1760
1837
  }
1761
1838
  catch (e) {
1762
1839
  console.error(c.red(`✗ ${e.message}`));
@@ -1767,7 +1844,12 @@ async function upManifest(file, opts) {
1767
1844
  console.log(c.dim(` seeded ${m.channels.length} channel(s): ${m.channels.map((ch) => "#" + ch.name).join(", ")}`));
1768
1845
  // Never claim a launch the control plane can't deliver: the manager carries the launch spec, so a
1769
1846
  // degraded control plane (announced above) means the agents are NOT coming up.
1770
- if (controlPlane) {
1847
+ if (effectiveNoManager) {
1848
+ // Truthful summary for the broker-only boot (#1417): nothing was omitted by accident, and the
1849
+ // agents this manifest declares (none — a declaring manifest is refused above) launch elsewhere.
1850
+ console.log(c.dim(` broker-only (--no-manager): no manager started on this host - run \`cotal supervise --space ${m.space} --server ${server}\` from a manager host to manage agents`));
1851
+ }
1852
+ else if (controlPlane) {
1771
1853
  // U6: on a user mesh the agents run under the OPERATOR's identity — say whose they are.
1772
1854
  console.log(c.green(`✓ launching ${eff.agents.length} agent(s)`) + c.dim(` via manager (${runtime})${owner ? ` as you (owner ${owner})` : ""} - see ${managerLogDisplayPath(m.space)}`));
1773
1855
  }
@@ -1780,7 +1862,7 @@ async function upManifest(file, opts) {
1780
1862
  console.log("\n" + inherited);
1781
1863
  if (eff.warnings.length)
1782
1864
  console.log("\n" + renderWarnings(eff.warnings));
1783
- console.log(c.dim(`\nWatch: \`cotal console --space ${m.space}\` or \`cotal web\` · Tear down: \`cotal down\``));
1865
+ console.log(c.dim(`\nWatch: \`cotal web\` (browser dashboard) or \`cotal console --space ${m.space}\` (terminal) · Tear down: \`cotal down\``));
1784
1866
  // A declared-user-auth manifest whose auth service never became ready: the mesh is up + recorded
1785
1867
  // (re-`cotal up` heals), but this launch did not deliver a usable identity plane — exit non-zero
1786
1868
  // so CI/wrappers don't read success (the red consequence line printed at the failure above).
@@ -1885,11 +1967,11 @@ opts) {
1885
1967
  // effective server is finally known — a manifest may name a different host than the flags did, and
1886
1968
  // a certificate that is valid for one is not thereby valid for the other.
1887
1969
  const transport = assertServesDialHost(opts.transport, new URL(server).hostname);
1888
- const setup = useAuth ? await authSetup(storeDir, server, space, host, opts.userAuth, transport, opts.rotateSys) : undefined;
1970
+ const setup = useAuth ? await authSetup(storeDir, server, space, host, opts.userAuth, transport, opts.rotateSys, opts.maxFileStore) : undefined;
1889
1971
  const port = Number(new URL(server).port) || 4222;
1890
1972
  // Same rule as the foreground path: every route to a listener names its transport (see
1891
1973
  // `writeOpenBrokerConf`). Detach must not be the mode where the fence quietly does not apply.
1892
- const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport });
1974
+ const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport, maxFileStore: opts.maxFileStore });
1893
1975
  const args = [
1894
1976
  "-c", confPath,
1895
1977
  ...(opts.boundListener ? ["--name", opts.boundListener.serverName] : []),
@@ -1985,6 +2067,7 @@ opts) {
1985
2067
  // Persist only a real decision — declared now, or carried forward — never the loopback default.
1986
2068
  ...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
1987
2069
  ...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
2070
+ ...(opts.maxFileStore !== undefined ? { maxFileStore: opts.maxFileStore } : {}),
1988
2071
  ts: new Date().toISOString(),
1989
2072
  }, "started");
1990
2073
  // Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
@@ -1997,6 +2080,7 @@ opts) {
1997
2080
  // See the foreground path: the broker's bind address is what makes attach reachable off-box.
1998
2081
  attachHost: effectiveAttachHost,
1999
2082
  maxSessions: effectiveMaxSessions,
2083
+ noManager: opts.noManager,
2000
2084
  resumeAttempt: opts.resumeAttempt,
2001
2085
  resumeCommitToken: opts.resumeCommitToken,
2002
2086
  wsPort: setup?.wsPort, // P2 item 6: the console session client's broker ws port
@@ -2121,6 +2205,15 @@ export function attachHostFor(space, explicit) {
2121
2205
  export function maxSessionsFor(space, explicit) {
2122
2206
  return explicit ?? findMesh(space)?.maxSessions;
2123
2207
  }
2208
+ /** The store cap a preserved launch recorded: absent, or a positive integer byte count. Anything
2209
+ * else is a damaged journal, refused rather than rendered as the dynamic default. */
2210
+ function journaledMaxFileStore(value) {
2211
+ if (value === undefined)
2212
+ return undefined;
2213
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
2214
+ throw new Error(`the preserved launch records an invalid maxFileStore (${JSON.stringify(value)}); refusing to guess the broker's store cap`);
2215
+ return value;
2216
+ }
2124
2217
  /** {@link recordOurMesh} under a name that says it is a test seam: the origin rule it enforces is
2125
2218
  * asserted directly by the registry smoke, since reaching each branch through a full `up` would
2126
2219
  * need a live broker per case. */
@@ -2509,7 +2602,7 @@ function validateRecorded(transport, dialHost, opts = {}) {
2509
2602
  */
2510
2603
  function writeOpenBrokerConf(storeDir, opts) {
2511
2604
  const confPath = resolve(storeDir, "..", "server-open.conf");
2512
- writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, transport: opts.transport }));
2605
+ writeFileSync(confPath, openServerConfig({ port: opts.port, host: opts.host, storeDir, maxFileStore: opts.maxFileStore, transport: opts.transport }));
2513
2606
  return confPath;
2514
2607
  }
2515
2608
  async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
@@ -2518,7 +2611,7 @@ async function authSetup(storeDir, server, space, host = "127.0.0.1", user,
2518
2611
  // operator's `--tls-cert` sat validated and unused. A default here cannot be right, because the
2519
2612
  // safe value and the common value are different values — every caller knows its transport, and
2520
2613
  // the one that does not is the one that must not compile.
2521
- transport, rotateSys = false) {
2614
+ transport, rotateSys = false, maxFileStore) {
2522
2615
  const dir = authDir(cotalRoot()); // the broker config (server.conf) still lands under the FS auth dir
2523
2616
  const store = workspaceSecretStore(cotalRoot());
2524
2617
  let auth = await getSpaceAuth(store, space);
@@ -2627,7 +2720,7 @@ transport, rotateSys = false) {
2627
2720
  : await resolveAuthProvider().preloadAccounts({ store, space: enabledSpace });
2628
2721
  extraAccounts.push(...accounts);
2629
2722
  }
2630
- writeFileSync(confPath, serverConfig(auth, spaces, { transport, port, storeDir, host, wsPort, wsHost: host, extraAccounts }));
2723
+ writeFileSync(confPath, serverConfig(auth, spaces, { transport, port, storeDir, maxFileStore, host, wsPort, wsHost: host, extraAccounts }));
2631
2724
  // Ephemeral setup cred: used only to probe reachability, pre-create the space streams/buckets
2632
2725
  // (setupSpaceStreams) and seed the channel registry (seedChannelRegistry) — all within the
2633
2726
  // enumerated `provisioner` scope. No broad `manager` residual for the up path.