@north-light/crouter 0.3.176 → 0.3.178

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 (152) hide show
  1. package/dist/api/client.d.ts +4 -4
  2. package/dist/api/client.js +2 -2
  3. package/dist/api/dto/lifecycle.d.ts +1 -1
  4. package/dist/api/dto/reports.d.ts +1 -1
  5. package/dist/clients/attach/__tests__/frame-selection.test.d.ts +1 -0
  6. package/dist/clients/attach/__tests__/frame-selection.test.js +84 -0
  7. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  8. package/dist/clients/attach/ansi-cells.d.ts +31 -0
  9. package/dist/clients/attach/ansi-cells.js +120 -0
  10. package/dist/clients/attach/config.js +5 -5
  11. package/dist/clients/attach/input/clipboard-image.js +2 -2
  12. package/dist/clients/attach/input/ref-autocomplete.js +1 -1
  13. package/dist/clients/attach/input/titled-editor.d.ts +12 -13
  14. package/dist/clients/attach/input/titled-editor.js +14 -15
  15. package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
  16. package/dist/clients/attach/overlays/dialogs.js +2 -2
  17. package/dist/clients/attach/overlays/help.js +2 -1
  18. package/dist/clients/attach/overlays/mcp.js +5 -12
  19. package/dist/clients/attach/render/chat-view.d.ts +1 -1
  20. package/dist/clients/attach/render/chat-view.js +2 -2
  21. package/dist/clients/attach/render/group-recap.js +7 -0
  22. package/dist/clients/attach/session/context.js +3 -3
  23. package/dist/clients/attach/session/editor-inventory.js +6 -6
  24. package/dist/clients/attach/session/file-links.js +15 -50
  25. package/dist/clients/attach/session/frame.d.ts +5 -1
  26. package/dist/clients/attach/session/frame.js +19 -4
  27. package/dist/clients/attach/session/input-wiring.js +2 -2
  28. package/dist/clients/attach/session/mouse.d.ts +4 -0
  29. package/dist/clients/attach/session/mouse.js +14 -3
  30. package/dist/clients/attach/session/selection.d.ts +28 -0
  31. package/dist/clients/attach/session/selection.js +108 -0
  32. package/dist/clients/attach/session/teardown.js +7 -9
  33. package/dist/clients/attach/slash/dispatch.d.ts +1 -2
  34. package/dist/clients/attach/slash/dispatch.js +5 -9
  35. package/dist/clients/attach/viewer.js +552 -550
  36. package/dist/clients/inbox/controller.js +3 -2
  37. package/dist/clients/inbox/deck-adapter.js +3 -2
  38. package/dist/clients/surfaces/host.d.ts +1 -1
  39. package/dist/commands/api-client.d.ts +12 -13
  40. package/dist/commands/api-client.js +15 -16
  41. package/dist/commands/chord.js +1 -2
  42. package/dist/commands/human/queue.js +6 -5
  43. package/dist/commands/memory/find.js +3 -3
  44. package/dist/commands/node/lifecycle.js +1 -1
  45. package/dist/commands/node-lifecycle-revive.d.ts +1 -1
  46. package/dist/commands/node-lifecycle-revive.js +3 -3
  47. package/dist/commands/push.js +1 -1
  48. package/dist/commands/surface/node/placement.d.ts +1 -1
  49. package/dist/commands/surface/node/placement.js +1 -1
  50. package/dist/commands/surface-edit.js +1 -4
  51. package/dist/commands/sys/panels/panel.d.ts +3 -3
  52. package/dist/commands/sys/panels/panel.js +3 -3
  53. package/dist/commands/sys/setup-core.js +1 -7
  54. package/dist/commands/sys/sync-deps.js +1 -3
  55. package/dist/commands/sys/sync-project-guidance.js +2 -11
  56. package/dist/commands/sys/sysprompt.js +1 -3
  57. package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
  58. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  59. package/dist/core/__tests__/helpers/harness.js +1 -1
  60. package/dist/core/canvas/boot.js +1 -1
  61. package/dist/core/canvas/browse/app.js +2 -2
  62. package/dist/core/canvas/canvas.d.ts +3 -3
  63. package/dist/core/canvas/canvas.js +3 -3
  64. package/dist/core/canvas/daemon-owner.js +1 -2
  65. package/dist/core/canvas/db.js +7 -8
  66. package/dist/core/canvas/index.js +2 -2
  67. package/dist/core/canvas/pid.d.ts +12 -14
  68. package/dist/core/canvas/pid.js +16 -20
  69. package/dist/core/canvas/remote-transport.js +2 -2
  70. package/dist/core/canvas/status-glyph.d.ts +1 -1
  71. package/dist/core/canvas/status-glyph.js +1 -1
  72. package/dist/core/canvas/types.d.ts +1 -1
  73. package/dist/core/command-manifests/manifest.js +4 -6
  74. package/dist/core/command-manifests/schema.js +10 -12
  75. package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
  76. package/dist/core/command-plugins/transport/http-invoke.js +4 -6
  77. package/dist/core/config.d.ts +1 -1
  78. package/dist/core/config.js +2 -2
  79. package/dist/core/events/read.js +1 -3
  80. package/dist/core/fault-classifier.js +1 -3
  81. package/dist/core/fs-utils.d.ts +7 -0
  82. package/dist/core/fs-utils.js +29 -2
  83. package/dist/core/host-exports/export.d.ts +4 -5
  84. package/dist/core/host-exports/export.js +4 -5
  85. package/dist/core/human/claim.js +3 -2
  86. package/dist/core/human/convention.d.ts +0 -1
  87. package/dist/core/human/convention.js +1 -9
  88. package/dist/core/human/scan.js +5 -4
  89. package/dist/core/profiles/manifest.d.ts +1 -1
  90. package/dist/core/profiles/manifest.js +1 -1
  91. package/dist/core/review/types.d.ts +1 -1
  92. package/dist/core/review/types.js +1 -1
  93. package/dist/core/runtime/broker/frame-dispatch.js +4 -4
  94. package/dist/core/runtime/broker-cli.js +1 -1
  95. package/dist/core/runtime/broker-protocol.d.ts +9 -10
  96. package/dist/core/runtime/broker-protocol.js +9 -10
  97. package/dist/core/runtime/broker-sdk.d.ts +1 -1
  98. package/dist/core/runtime/broker.js +6 -6
  99. package/dist/core/runtime/host.js +14 -14
  100. package/dist/core/runtime/launch.d.ts +1 -1
  101. package/dist/core/runtime/launch.js +1 -1
  102. package/dist/core/runtime/lifecycle.js +16 -21
  103. package/dist/core/runtime/node-read.js +2 -3
  104. package/dist/core/runtime/package-health.js +1 -8
  105. package/dist/core/runtime/persona.js +2 -2
  106. package/dist/core/runtime/pi-vendored.d.ts +4 -2
  107. package/dist/core/runtime/pi-vendored.js +5 -16
  108. package/dist/core/runtime/promote.js +1 -1
  109. package/dist/core/runtime/reopen.js +2 -2
  110. package/dist/core/runtime/revive-all.d.ts +3 -3
  111. package/dist/core/runtime/revive-all.js +6 -6
  112. package/dist/core/runtime/revive.js +1 -1
  113. package/dist/core/runtime/spawn.d.ts +1 -2
  114. package/dist/core/runtime/spawn.js +2 -3
  115. package/dist/core/runtime/structured-output.js +4 -6
  116. package/dist/core/runtime/tmux-driver.d.ts +2 -2
  117. package/dist/core/runtime/tmux-driver.js +3 -5
  118. package/dist/core/runtime/tool-group-summary.js +5 -4
  119. package/dist/core/runtime/warm-pool.js +1 -1
  120. package/dist/core/scope.js +2 -9
  121. package/dist/core/self-update.js +3 -3
  122. package/dist/core/spawn.d.ts +0 -1
  123. package/dist/core/spawn.js +1 -4
  124. package/dist/core/substrate/on-read.js +1 -9
  125. package/dist/core/wake.js +2 -2
  126. package/dist/core/worktree.d.ts +3 -3
  127. package/dist/core/worktree.js +5 -7
  128. package/dist/daemon/api/bridge.js +1 -1
  129. package/dist/daemon/api/handlers/broker-ops.js +1 -2
  130. package/dist/daemon/api/handlers/messages.js +2 -2
  131. package/dist/daemon/api/handlers/nodes.js +1 -1
  132. package/dist/daemon/api/handlers/reports.js +1 -1
  133. package/dist/daemon/api/handlers/validate.d.ts +2 -1
  134. package/dist/daemon/api/handlers/validate.js +2 -3
  135. package/dist/daemon/cron-run.js +7 -7
  136. package/dist/daemon/human/finish.js +3 -2
  137. package/dist/daemon/manage.d.ts +6 -6
  138. package/dist/daemon/manage.js +15 -16
  139. package/dist/pi-extensions/canvas-context-intro.js +1 -14
  140. package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
  141. package/dist/pi-extensions/canvas-review-boundary.js +1 -14
  142. package/dist/pi-extensions/truncate.d.ts +5 -0
  143. package/dist/pi-extensions/truncate.js +14 -0
  144. package/dist/shared/predicates.d.ts +2 -0
  145. package/dist/shared/predicates.js +4 -0
  146. package/dist/shared/shell-quote.d.ts +3 -0
  147. package/dist/shared/shell-quote.js +5 -0
  148. package/dist/shared/tool-groups.d.ts +4 -2
  149. package/dist/shared/tool-groups.js +3 -3
  150. package/dist/types.d.ts +4 -5
  151. package/package.json +1 -1
  152. package/runtime.lock.json +2 -2
@@ -1,7 +1,8 @@
1
1
  import { existsSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { claimPath, deckPath, readJson, responsePath, reviewPath } from './convention.js';
4
+ import { claimPath, deckPath, responsePath, reviewPath } from './convention.js';
5
+ import { readJsonOrNull } from '../fs-utils.js';
5
6
  import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
6
7
  const REMOTE_STALE_MS = 30_000;
7
8
  function parseClaim(raw) {
@@ -12,7 +13,7 @@ function parseClaim(raw) {
12
13
  return null;
13
14
  return { token: claim.token, host: claim.host, pid: claim.pid, claimedAt: claim.claimedAt, heartbeatAt: claim.heartbeatAt, ...(typeof claim.tmuxClient === 'string' ? { tmuxClient: claim.tmuxClient } : {}) };
14
15
  }
15
- export function readTicketClaim(dir) { return parseClaim(readJson(claimPath(dir))); }
16
+ export function readTicketClaim(dir) { return parseClaim(readJsonOrNull(claimPath(dir))); }
16
17
  function isLiveLocalPid(pid) { try {
17
18
  process.kill(pid, 0);
18
19
  return true;
@@ -14,7 +14,6 @@ export declare function stampCanvasNode(deck: Deck): void;
14
14
  export declare function atomicWriteJson(path: string, value: unknown): void;
15
15
  /** Publish one immutable JSON record without replacing an existing winner. */
16
16
  export declare function publishJsonExclusive(path: string, value: unknown): boolean;
17
- export declare function readJson<T>(path: string): T | null;
18
17
  export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string, deck?: Deck): string;
19
18
  export declare function writeProgress(dir: string, responses: InteractionResponse[]): void;
20
19
  export declare function clearProgress(dir: string): void;
@@ -1,4 +1,4 @@
1
- import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, linkSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { buildSummary } from './summary.js';
4
4
  export function deckPath(dir) { return `${dir}/deck.json`; }
@@ -53,14 +53,6 @@ export function publishJsonExclusive(path, value) {
53
53
  unlinkSync(tmp);
54
54
  }
55
55
  }
56
- export function readJson(path) {
57
- try {
58
- return JSON.parse(readFileSync(path, 'utf8'));
59
- }
60
- catch {
61
- return null;
62
- }
63
- }
64
56
  export function writeResponse(dir, responses, completedAt, deck) {
65
57
  const summary = deck === undefined ? '' : buildSummary(deck, responses);
66
58
  atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
@@ -1,16 +1,17 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import { basename, join } from 'node:path';
3
- import { claimPath, deckPath, isResolved, readJson, reviewPath } from './convention.js';
3
+ import { claimPath, deckPath, isResolved, reviewPath } from './convention.js';
4
+ import { readJsonOrNull } from '../fs-utils.js';
4
5
  import { validateDeck, validateReviewDescriptor } from './deck-schema.js';
5
6
  import { ticketsRoot } from './root.js';
6
7
  function claimSummary(dir) {
7
- const claim = readJson(claimPath(dir));
8
+ const claim = readJsonOrNull(claimPath(dir));
8
9
  if (claim === null || typeof claim.host !== 'string' || typeof claim.claimedAt !== 'string' || typeof claim.heartbeatAt !== 'string')
9
10
  return undefined;
10
11
  return { owner: claim.host, claimedAt: claim.claimedAt, heartbeatAt: claim.heartbeatAt };
11
12
  }
12
13
  function deckSummary(dir, id) {
13
- const raw = readJson(deckPath(dir));
14
+ const raw = readJsonOrNull(deckPath(dir));
14
15
  if (raw === null)
15
16
  return null;
16
17
  let deck;
@@ -35,7 +36,7 @@ function deckSummary(dir, id) {
35
36
  return { dir, id, kind: 'deck', title: deck.title, subtitle: first.subtitle, interactionKind: first.kind, interactionCount: deck.interactions.length, source: deck.source ?? {}, blockedSince, claim: claimSummary(dir) };
36
37
  }
37
38
  function reviewSummary(dir, id) {
38
- const raw = readJson(reviewPath(dir));
39
+ const raw = readJsonOrNull(reviewPath(dir));
39
40
  if (raw === null)
40
41
  return null;
41
42
  let review;
@@ -27,7 +27,7 @@ export declare function listProfiles(): ProfileEntry[];
27
27
  * actually-enumerated dirs, never a blind join), then a unique manifest
28
28
  * `name` match. Ambiguous names fail listing every matching id; no match
29
29
  * fails naming `profile list`/`profile new` as recovery. This is the ONLY
30
- * function every command leaf and future runtime consumer (Phase 5/6) should
30
+ * function every command leaf and runtime consumer should
31
31
  * call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
32
32
  * safe profile id. */
33
33
  export declare function loadProfileManifest(profileIdOrName: string): ProfileEntry;
@@ -147,7 +147,7 @@ export function listProfiles() {
147
147
  * actually-enumerated dirs, never a blind join), then a unique manifest
148
148
  * `name` match. Ambiguous names fail listing every matching id; no match
149
149
  * fails naming `profile list`/`profile new` as recovery. This is the ONLY
150
- * function every command leaf and future runtime consumer (Phase 5/6) should
150
+ * function every command leaf and runtime consumer should
151
151
  * call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
152
152
  * safe profile id. */
153
153
  export function loadProfileManifest(profileIdOrName) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Internal review/comment/error vocabulary and constants.
3
- * Phase 4 implementation: daemon-owned review lifecycle and comment authority.
3
+ * Backs the daemon-owned review lifecycle and comment authority.
4
4
  * This module declares no HTTP status, DTOs, or I/O.
5
5
  */
6
6
  import type { FeedbackComment } from '../human/types.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Internal review/comment/error vocabulary and constants.
3
- * Phase 4 implementation: daemon-owned review lifecycle and comment authority.
3
+ * Backs the daemon-owned review lifecycle and comment authority.
4
4
  * This module declares no HTTP status, DTOs, or I/O.
5
5
  */
6
6
  /** Limits */
@@ -620,8 +620,8 @@ export function createFrameDispatchContext(deps) {
620
620
  }
621
621
  };
622
622
  // -------------------------------------------------------------------------
623
- // Command-op helpers (T3, §2.3). The write guard is hoisted here (it was
624
- // inlined twice) and reused by all mutating ops; the ack/error replies
623
+ // Command-op helpers. The write guard is hoisted here and reused by all
624
+ // mutating ops; the ack/error replies
625
625
  // and a few resolvers keep the per-op cases one-liners.
626
626
  // -------------------------------------------------------------------------
627
627
  /** Reject a read-only client for a mutating op. The decision reads this
@@ -841,7 +841,7 @@ export function createFrameDispatchContext(deps) {
841
841
  };
842
842
  const handleEngineControlFrame = (client, frame) => {
843
843
  switch (frame.type) {
844
- // --- extended engine-command ops (T3, §1.2 floor set) ------------------
844
+ // --- extended engine-command ops -------------------------------------
845
845
  case 'set_model': {
846
846
  if (notWritable(client, 'set the model'))
847
847
  break;
@@ -1070,7 +1070,7 @@ export function createFrameDispatchContext(deps) {
1070
1070
  // engine, so it is not a controller-gated op. Terminal observer
1071
1071
  // connections use it to populate the command palette.
1072
1072
  // The merged command list rides in ack.detail as JSON (the foundation's
1073
- // AckFrame.detail field) — the viewer (T6) JSON.parses it when for ===
1073
+ // AckFrame.detail field) — the viewer JSON.parses it when for ===
1074
1074
  // 'get_commands'. Keeps every command op a uniform ack reply.
1075
1075
  try {
1076
1076
  ackTo(client, 'get_commands', true, JSON.stringify(buildCommandList(currentSession())));
@@ -1,6 +1,6 @@
1
1
  // broker-cli.ts — the headless broker's dedicated detached entry (plan T4 /
2
2
  // decision §1.13). Spawned directly via `spawn(execPath, [thisFile, nodeId], …)`
3
- // by HeadlessBrokerHost.launch (T6) — NEVER routed through src/cli.ts, which
3
+ // by HeadlessBrokerHost.launch — NEVER routed through src/cli.ts, which
4
4
  // would run the whole bootstrap chain (auto-update / scope-init / slash-template
5
5
  // rewrite) on every broker boot. Mirrors src/daemon/crtrd-cli.ts: parse the one
6
6
  // positional arg and hand off; keep this file a thin shim.
@@ -47,7 +47,7 @@ export interface BrokerSnapshot {
47
47
  title: string | undefined;
48
48
  };
49
49
  /** Broker-owned summaries keyed by the first tool-call id in each group.
50
- * Structured objects with bullets, nodesSpawned, and filesEdited.
50
+ * Structured objects with bullets, nodesSpawned, and filesDeleted.
51
51
  * Optional so snapshots from an older live runtime remain readable. */
52
52
  toolGroupSummaries?: Record<string, ToolGroupSummary>;
53
53
  /** The broker-selected normal activity label for the current turn. Optional so
@@ -68,7 +68,7 @@ export interface HelloFrame {
68
68
  };
69
69
  }
70
70
  /** Drive the engine — writable (`controller`) clients only. Map 1:1 to session.prompt/steer/followUp/abort.
71
- * `images` carries pasted/attached images to the engine (review M1): pi accepts
71
+ * `images` carries pasted/attached images to the engine: pi accepts
72
72
  * `prompt(text,{images})` / `steer(text,images)` / `followUp(text,images)` at
73
73
  * 0.78.1. The wire TYPE lives here; T3/T6 wire the runtime side. The BROKER read
74
74
  * cap (`BROKER_READ_CAPS`) is sized to hold a resizeImage-bounded PNG's base64. */
@@ -612,15 +612,14 @@ export declare class FrameOverflowError extends Error {
612
612
  }
613
613
  /** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
614
614
  * full message history and can be many MiB, so these are generous. Covers
615
- * realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known
616
- * limitation, review m3 — the long-lived broker makes a big welcome a realistic,
617
- * not pathological, trigger, but the fixed cap is accepted for Phase 4). */
615
+ * realistic long sessions; snapshot CHUNKING is deferred — the long-lived
616
+ * broker makes a big welcome a realistic, not pathological, trigger, but the
617
+ * fixed cap is accepted. */
618
618
  export declare const CLIENT_READ_CAPS: FrameDecoderCaps;
619
- /** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight
620
- * 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage-
621
- * bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are
622
- * RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/
623
- * `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
619
+ /** Caps the BROKER uses reading CLIENT frames. Sized so an image-paste frame
620
+ * fits: a resizeImage-bounded PNG's base64 is a few MiB and would clip a
621
+ * tighter 4 MiB line cap, so these hold an image-bearing `prompt`/`steer`/
622
+ * `follow_up` frame. Still bounded, so a malicious/buggy client→broker frame
624
623
  * is cap-and-dropped, never grow-to-OOM (C5). */
625
624
  export declare const BROKER_READ_CAPS: FrameDecoderCaps;
626
625
  /** Incremental newline-delimited JSON reader: feed raw socket chunks, get back
@@ -1,7 +1,7 @@
1
1
  // broker-protocol.ts — the client↔broker wire protocol for the headless node
2
2
  // Surface (design §5.2). Pure types + a newline-delimited JSON codec; no I/O, no
3
- // SDK construction. Consumed by the broker (src/core/runtime/broker.ts) and, in
4
- // Phase 4, by the `crtr surface attach` terminal client and crtrd's attach bridge.
3
+ // SDK construction. Consumed by the broker (src/core/runtime/broker.ts), the
4
+ // `crtr surface attach` terminal client, and crtrd's attach bridge.
5
5
  //
6
6
  // The transport is one unix socket per node (`nodeDir(id)/view.sock`) speaking
7
7
  // newline-delimited JSON frames. Live engine events are relayed VERBATIM — the
@@ -43,18 +43,17 @@ export class FrameOverflowError extends Error {
43
43
  }
44
44
  /** Caps the CLIENT uses reading BROKER frames. The `welcome.snapshot` carries the
45
45
  * full message history and can be many MiB, so these are generous. Covers
46
- * realistic long sessions; snapshot CHUNKING is deferred (plan §1.1 known
47
- * limitation, review m3 — the long-lived broker makes a big welcome a realistic,
48
- * not pathological, trigger, but the fixed cap is accepted for Phase 4). */
46
+ * realistic long sessions; snapshot CHUNKING is deferred — the long-lived
47
+ * broker makes a big welcome a realistic, not pathological, trigger, but the
48
+ * fixed cap is accepted. */
49
49
  export const CLIENT_READ_CAPS = {
50
50
  maxLineBytes: 256 * 1024 * 1024,
51
51
  maxTotalBytes: 256 * 1024 * 1024,
52
52
  };
53
- /** Caps the BROKER uses reading CLIENT frames. Plan §1.1 specified a tight
54
- * 4/16 MiB, but review M1 requires image-paste frames to fit: a resizeImage-
55
- * bounded PNG's base64 is a few MiB and would clip a 4 MiB line cap. So these are
56
- * RAISED above the plan's 4/16 to hold an image-bearing `prompt`/`steer`/
57
- * `follow_up` frame (M1). Still bounded, so a malicious/buggy client→broker frame
53
+ /** Caps the BROKER uses reading CLIENT frames. Sized so an image-paste frame
54
+ * fits: a resizeImage-bounded PNG's base64 is a few MiB and would clip a
55
+ * tighter 4 MiB line cap, so these hold an image-bearing `prompt`/`steer`/
56
+ * `follow_up` frame. Still bounded, so a malicious/buggy client→broker frame
58
57
  * is cap-and-dropped, never grow-to-OOM (C5). */
59
58
  export const BROKER_READ_CAPS = {
60
59
  maxLineBytes: 24 * 1024 * 1024,
@@ -15,7 +15,7 @@ export interface BrokerEngine {
15
15
  createAgentSessionServices: typeof createAgentSessionServices;
16
16
  createAgentSessionFromServices: typeof createAgentSessionFromServices;
17
17
  /**
18
- * The session-replacement runtime factory (T3 new_session/switch_session/fork).
18
+ * The session-replacement runtime factory (new_session/switch_session/fork).
19
19
  * OPTIONAL: the real SDK (0.78.1) exposes it, so production gets full session
20
20
  * replacement; the `fake-engine` test fixture does NOT provide it, so the
21
21
  * broker degrades those three ops to an `error{engine_error}` reply rather than
@@ -1,4 +1,4 @@
1
- // broker.ts — the headless node broker (design §4, §5; plan T4).
1
+ // broker.ts — the headless node broker (design §4, §5).
2
2
  //
3
3
  // One broker process per node — the SOLE host. It hosts ONE pi engine IN-PROCESS
4
4
  // via the SDK (createAgentSession), is the SOLE writer of the node's session
@@ -81,9 +81,9 @@ function formatExactModelSpec(model, thinkingLevel) {
81
81
  return base === null ? null : `${base}:${thinkingLevel === undefined || thinkingLevel === '' ? 'off' : thinkingLevel}`;
82
82
  }
83
83
  // ---------------------------------------------------------------------------
84
- // Tunables (T3 backpressure / T4 dialog anti-deadlock)
84
+ // Tunables (backpressure / dialog anti-deadlock)
85
85
  // ---------------------------------------------------------------------------
86
- /** Broker-side default dialog timeout (C2 anti-deadlock, T4). When an extension
86
+ /** Broker-side default dialog timeout (anti-deadlock). When an extension
87
87
  * dialog is forwarded to every writable client, the broker ALWAYS arms a timeout
88
88
  * (this default, or a shorter per-dialog `opts.timeout` if the extension passed
89
89
  * one) so unanswered dialogs — including ones whose writable viewers all detach
@@ -167,7 +167,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
167
167
  };
168
168
  // Defensive: any `crtr` child the engine/extensions spawn must inherit the
169
169
  // front-door recursion guard (see
170
- // src/core/runtime/.crouter/memory/INDEX.md). The host (T6) also sets this;
170
+ // src/core/runtime/.crouter/memory/INDEX.md). The host also sets this;
171
171
  // setting it here keeps the broker self-sufficient.
172
172
  process.env[FRONT_DOOR_ENV] = '1';
173
173
  // A successful in-turn crtr command stamps this process id into its stop
@@ -203,7 +203,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
203
203
  // 2–4. Build the engine session via the SERVICES path (C3) — see
204
204
  // buildBrokerSession below. Register it so the FATAL exit path (M3) can
205
205
  // dispose it and reap detached bash children.
206
- // `session` + `services` are MUTABLE holders (T3 session-rebind): when the
206
+ // `session` + `services` are MUTABLE holders (session-rebind): when the
207
207
  // controller drives new_session/switch_session/fork, the AgentSessionRuntime
208
208
  // tears down the old session and builds the next one, then runs our rebind
209
209
  // callback which reassigns these. Every closure below (buildSnapshot,
@@ -411,7 +411,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
411
411
  registry.forEachHelloed(sendWelcome);
412
412
  };
413
413
  // -------------------------------------------------------------------------
414
- // The single exit helper (plan §0): dispose the engine, close + unlink the
414
+ // The single exit helper: dispose the engine, close + unlink the
415
415
  // socket, exit 0. Idempotent — every trigger converges here.
416
416
  // -------------------------------------------------------------------------
417
417
  const completeExit = (status) => {
@@ -127,7 +127,7 @@ const BROKER_SIGKILL_GRACE_MS = 2_000;
127
127
  /** How often `pollUntilTreeDead` re-checks the process TREE. Short enough that
128
128
  * the common/happy path (tree already dead) pays only a couple of ticks; the
129
129
  * full grace ceilings above are reserved for the genuinely-wedged case,
130
- * where the added latency is the whole point (crouter#98 review finding 3). */
130
+ * where the added latency is the whole point. */
131
131
  const TEARDOWN_POLL_MS = 150;
132
132
  /** Poll `isAlive` every `TEARDOWN_POLL_MS` until it reports dead. If it is
133
133
  * STILL alive once `timeoutMs` elapses, fire `onTimeout` exactly once (the
@@ -136,7 +136,7 @@ const TEARDOWN_POLL_MS = 150;
136
136
  * up (an unkillable/D-state descendant is out of scope for this primitive).
137
137
  *
138
138
  * Deliberately a recursive, NEVER-`.unref()`'d `setTimeout` rather than a
139
- * fixed unref'd wait (crouter#98 review finding 3): most teardown callers
139
+ * fixed unref'd wait: most teardown callers
140
140
  * are short-lived CLI subprocesses (`node lifecycle close`, `canvas prune`)
141
141
  * that would otherwise exit before an unref'd SIGKILL timer ever fires,
142
142
  * silently skipping the last-resort rung. Polling (instead of a blind fixed
@@ -167,7 +167,7 @@ function pollUntil(isAlive, timeoutMs, onTimeout, hardCeilingMs = timeoutMs + 5_
167
167
  * `captureTeardownSnapshot` identity map over that same list) — both taken
168
168
  * ONCE by `teardown()` synchronously at its very top, before EITHER
169
169
  * escalation path (connect-error or post-shutdown-frame) can run, and
170
- * threaded through here unchanged (crouter#98 review finding 1). This
170
+ * threaded through here unchanged. This
171
171
  * function must NEVER re-walk the process tree itself: `launch()` spawns the
172
172
  * broker `detached: true`, so
173
173
  * signaling the broker's OWN group (the first thing `killProcessTreePids`
@@ -180,8 +180,8 @@ function pollUntil(isAlive, timeoutMs, onTimeout, hardCeilingMs = timeoutMs + 5_
180
180
  * "lose" a still-alive detached descendant — e.g. the pi bash tool's shell
181
181
  * child, itself `detached: true` and thus its own group leader, distinct
182
182
  * from the broker's group, so a plain `kill(-brokerPid)` never reaches it
183
- * either (crouton-labs/crouter#98). `identities` guards each signal against
184
- * PID REUSE across the multi-second escalation window (review finding 3) —
183
+ * either. `identities` guards each signal against
184
+ * PID REUSE across the multi-second escalation window —
185
185
  * see `killProcessTreePids`. `null` means the initial `captureTeardownSnapshot`
186
186
  * probe itself failed (no baseline to compare against) — `killProcessTreePids`
187
187
  * falls through to its normal unguarded signal path in that case, never
@@ -312,19 +312,19 @@ export const headlessBrokerHost = {
312
312
  // won't revive. On connect failure (broker dead/crashed/wedged) fall back
313
313
  // to escalateBrokerTeardown — a tree-wide SIGTERM now, a tree-wide SIGKILL
314
314
  // as the last resort — so no orphaned descendant of a dead/wedged broker
315
- // survives the node (crouton-labs/crouter#98) — + unlink the stale socket.
315
+ // survives the node — + unlink the stale socket.
316
316
  //
317
317
  // Capture the broker pid ONCE, synchronously, right here — never re-query
318
318
  // `getNode(nodeId)` inside the async callbacks/timers below. Callers such
319
319
  // as placement.ts's `reapIfEmpty()` call `teardown()` then immediately
320
320
  // `deleteNode()` in the same tick; by the time an async callback fires the
321
321
  // row can already be gone, so a re-query would silently resolve to
322
- // `undefined` and skip escalation entirely (crouter#98 review finding 2).
322
+ // `undefined` and skip escalation entirely.
323
323
  const node = getNode(nodeId);
324
324
  const pid = node?.pi_pid;
325
- // Snapshot the full descendant TREE (crouter#98 review finding 1) — the
325
+ // Snapshot the full descendant TREE — the
326
326
  // root pid plus every transitive descendant — and a stable identity
327
- // fingerprint for every pid in it (review finding 3), BOTH from ONE `ps`
327
+ // fingerprint for every pid in it, BOTH from ONE `ps`
328
328
  // table read (`captureTeardownSnapshot`, final review: closes the small
329
329
  // race between two separate probes), exactly ONCE, right here, while the
330
330
  // broker (if alive) still holds parentage over any detached descendant.
@@ -393,10 +393,10 @@ export const headlessBrokerHost = {
393
393
  // Bounded exit confirmation: a broker that connects fine but then HANGS
394
394
  // inside session.dispose() (disposeAndExit catches a throw, not a hang)
395
395
  // would leak the process holding the sole .jsonl writer — and any
396
- // bash→test→app descendants it spawned along the way (crouter#98). Poll
396
+ // bash→test→app descendants it spawned along the way. Poll
397
397
  // the whole process TREE (not just the broker's own pid) for up to
398
- // `BROKER_SHUTDOWN_GRACE_MS`, UNCONDITIONAL on the broker's own liveness
399
- // (crouter#98 review finding 1): a broker that exits cleanly can still
398
+ // `BROKER_SHUTDOWN_GRACE_MS`, UNCONDITIONAL on the broker's own liveness:
399
+ // a broker that exits cleanly can still
400
400
  // leave a live descendant behind (dispose() partially failed, or never
401
401
  // reached a detached SDK child), which is exactly the shape this
402
402
  // closes. `pollUntil` is ref'd — not `.unref()`'d — so a short-lived
@@ -405,8 +405,8 @@ export const headlessBrokerHost = {
405
405
  // couple of poll ticks.
406
406
  //
407
407
  // Checks `isAnyPidAlive(tree)` against the FIXED pre-signal snapshot,
408
- // NEVER a fresh `isProcessTreeAlive`/`descendantPids` re-walk (crouter#98
409
- // review finding 1, fixed): the broker can exit at any point during this
408
+ // NEVER a fresh `isProcessTreeAlive`/`descendantPids` re-walk: the broker
409
+ // can exit at any point during this
410
410
  // grace window while leaving a detached descendant alive, and the
411
411
  // instant it does, the kernel reparents that descendant away from `pid`
412
412
  // — a re-walk-by-ppid would then find nothing and silently skip
@@ -56,7 +56,7 @@ export declare function nextLadderModel(currentSpec: string, direction?: 1 | -1,
56
56
  * 3. Unset → pi default.
57
57
  * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
58
58
  * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
59
- * as-given — it is no longer persona-frontmatter-derived. The two canvas
59
+ * as-given, never derived from persona frontmatter. The two canvas
60
60
  * extensions are always first; kind-declared extensions follow. */
61
61
  export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
62
62
  lifecycle: Lifecycle;
@@ -249,7 +249,7 @@ export function nextLadderModel(currentSpec, direction = 1, providers) {
249
249
  * 3. Unset → pi default.
250
250
  * Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
251
251
  * a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
252
- * as-given — it is no longer persona-frontmatter-derived. The two canvas
252
+ * as-given, never derived from persona frontmatter. The two canvas
253
253
  * extensions are always first; kind-declared extensions follow. */
254
254
  export function buildLaunchSpec(kind, mode, opts) {
255
255
  const merged = readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null));
@@ -1,33 +1,28 @@
1
1
  // lifecycle.ts — the node status×intent state machine.
2
2
  //
3
3
  // ONE place defines which (status, intent) moves are legal and enacts them.
4
- // Before this, ~a dozen scattered setStatus()/setIntent() pairs across
5
- // reset/close/revive/feed/daemon/stophook/queue/promote re-derived the lifecycle
6
- // by hand, with no shared definition of "what move is legal." Here the legal
7
- // transition TABLE is the definition, and `transition(id, event)` is the single
8
- // writer of status+intent: it validates the from-status, then writes both fields
9
- // in ONE atomic statement (built on Phase 2's WAL'd row setters) so the two can
10
- // never disagree.
4
+ // The legal transition TABLE is the definition, and `transition(id, event)` is
5
+ // the single writer of status+intent for every caller (reset, close, revive,
6
+ // feed, daemon, stophook, queue, promote): it validates the from-status, then
7
+ // writes both fields in ONE atomic statement (on the WAL'd row setters) so the
8
+ // two can never disagree.
11
9
  //
12
10
  // This mirrors persona.ts: persona.ts is the single source of transition PROSE;
13
11
  // lifecycle.ts is the single source of which status/intent move is LEGAL. Two
14
12
  // parallel, legible state machines instead of scattered enactment.
15
13
  //
16
- // Crash-safety invariant (was a comment repeated in close/reapDescendants):
17
- // "flip status to a non-supervised value + clear intent BEFORE killing the
18
- // host" (tmux pane or headless broker) — the daemon only ever revives
19
- // active|idle nodes, so a teardown must
20
- // leave the node done/canceled first to close the revive race. That invariant is
21
- // now the DEFINITION of the `cancel` event: callers flip via transition()
22
- // and only THEN tear the host down.
14
+ // Crash-safety invariant: flip status to a non-supervised value + clear intent
15
+ // BEFORE killing the host (tmux pane or headless broker) — the daemon only ever
16
+ // revives active|idle nodes, so a teardown must leave the node done/canceled
17
+ // first to close the revive race. That invariant is the DEFINITION of the
18
+ // `cancel` event: callers flip via transition() and only THEN tear the host down.
23
19
  //
24
- // Unification (A5, human-confirmed 2026-06-06): an externally-reaped node — torn
25
- // down because the user moved on (close cascade) OR because a root relaunch
26
- // superseded its workers — ends `canceled`, NOT `done`. `done` is reserved for a
27
- // node that finished its OWN work (finish) — including a relaunched root, which
28
- // is parked `done` as history, not canceled. The old `reap` event (→ done) was
29
- // identical to `cancel` in every field and side effect once unified on status, so
30
- // it was COLLAPSED into `cancel`; reset.ts's reapDescendants routes through
20
+ // An externally-reaped node — torn down because the user moved on (close
21
+ // cascade) OR because a root relaunch superseded its workers — ends `canceled`,
22
+ // NOT `done`. `done` is reserved for a node that finished its OWN work (finish)
23
+ // — including a relaunched root, which is parked `done` as history, not
24
+ // canceled. There is no separate reap event: it would be identical to `cancel`
25
+ // in every field and side effect, so reset.ts's reapDescendants routes through
31
26
  // `cancel`.
32
27
  //
33
28
  // Layering note: lifecycle.ts is runtime, but it is the canvas write surface's
@@ -4,9 +4,8 @@
4
4
  // These are canvas/runtime reads (getNode + SessionManager + session-cycles):
5
5
  // they belong SERVER-SIDE. crtrd's node read handlers (daemon/api/handlers/
6
6
  // nodes.ts) call them behind `GET /v1/nodes/{id}/{snapshot,transcript}`. They
7
- // used to live in the CLI leaves (commands/node-snapshot.ts, node-transcript.ts)
8
- // and were imported into the handlers from there; Stage B-3 relocated them here
9
- // so the CLI leaves' import graph reaches no canvas state store (spec §1/§10).
7
+ // live here rather than in the CLI leaves so the leaves' import graph reaches
8
+ // no canvas state store (spec §1/§10).
10
9
  import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
11
10
  import { readJsonIfExists } from '../fs-utils.js';
12
11
  import { tmpdir } from 'node:os';
@@ -14,6 +14,7 @@ import { existsSync, readFileSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
15
  import { join, resolve } from 'node:path';
16
16
  import { isRendererReady } from '../termrender/termrender.js';
17
+ import { expandTilde } from '../fs-utils.js';
17
18
  /** A non-fatal, action-oriented diagnostic for an unprovisioned managed
18
19
  * renderer — the one remaining out-of-tree dependency. Deliberately calls
19
20
  * `isRendererReady()`, NOT `ensureRenderer()`: this runs on the front-door boot
@@ -25,14 +26,6 @@ export function rendererWarning() {
25
26
  return 'crtr: the pinned termrender renderer is not provisioned — decks, reviews, and inline diagrams render as plaintext.\n' +
26
27
  ' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
27
28
  }
28
- /** Expand a leading `~` against `homeDir` (pi's own convention). */
29
- function expandTilde(pathValue, homeDir) {
30
- if (pathValue === '~')
31
- return homeDir;
32
- if (pathValue.startsWith('~/'))
33
- return join(homeDir, pathValue.slice(2));
34
- return pathValue;
35
- }
36
29
  /**
37
30
  * The pi agent config dir (`~/.pi/agent`, or `$PI_CODING_AGENT_DIR`) — a local
38
31
  * reimplementation of pi's `getAgentDir()`. Reimplemented (not imported) ON
@@ -30,7 +30,7 @@ import { orchestratorContextNote } from './bearings.js';
30
30
  /** Load a builtin/user/project memory doc's body by its normalized name, or ''
31
31
  * if it can't be resolved. The single source for the lifecycle/spine/
32
32
  * orchestration-kernel fragments this injector re-delivers on a transition —
33
- * the SAME docs Phase 2 authored under `src/builtin-memory/`, so static
33
+ * the SAME docs under `src/builtin-memory/`, so static
34
34
  * (system-prompt splice) and transition prose can never drift. */
35
35
  function loadMemoryBody(name) {
36
36
  try {
@@ -48,7 +48,7 @@ function loadMemoryBody(name) {
48
48
  * resource is its own context window. Roadmap-shaping guidance is just another orchestrator-gated memory
49
49
  * doc the node already gets via the boot-render splice (kinds/<kind>/orchestrator
50
50
  * gated `{kind, mode: orchestrator}`). (Lifecycle is left to its own section —
51
- * promotion no longer forces resident, so this never asserts residency.) */
51
+ * promotion does not force resident, so this never asserts residency.) */
52
52
  function orchestrationGuidance(nodeId, kind) {
53
53
  const kernel = loadMemoryBody('orchestration-kernel');
54
54
  const roadmap = readRoadmap(nodeId) ?? '(no roadmap yet)';
@@ -4,7 +4,9 @@
4
4
  * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
5
5
  * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
6
6
  * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
7
- * the sessions dir — same vendoring rationale as the slash-command list below. */
7
+ * the sessions dir — same vendoring rationale as the slash-command list below.
8
+ * `PI_CODING_AGENT_DIR` goes through the same tilde expansion pi's
9
+ * `getAgentDir()` applies, or `~/custom-pi` yields a broken relative path. */
8
10
  export declare function piSessionsRoot(): string;
9
11
  /**
10
12
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
@@ -13,7 +15,7 @@ export declare function piSessionsRoot(): string;
13
15
  * Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
14
16
  * to the LLM as literal text), so the broker's `get_commands` op MERGES this list
15
17
  * with the engine's registered commands/templates/skills, and the viewer
16
- * autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
18
+ * autocomplete parses these locally. Both the broker and the viewer
17
19
  * import this single copy.
18
20
  *
19
21
  * Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
@@ -12,32 +12,21 @@
12
12
  // below to match and adjust the count note.
13
13
  import { homedir } from 'node:os';
14
14
  import { join } from 'node:path';
15
+ import { expandTilde } from '../fs-utils.js';
15
16
  /** pi's sessions root, VENDORED from pi `config.getSessionsDir()` (= `<agentDir>/
16
17
  * sessions`). pi's package `exports` map is `.`-only, so config.js can't be
17
18
  * deep-imported, and a ROOT import of `getAgentDir` would eager-load the entire
18
19
  * heavy pi SDK index on crtr's front-door hot path (the reason broker-sdk.ts
19
20
  * dynamic-imports the engine). Mirrors pi: `PI_CODING_AGENT_DIR` env, else
20
21
  * `~/.pi/agent` (APP_NAME defaults to 'pi'). Re-sync on a pi SDK bump that moves
21
- * the sessions dir — same vendoring rationale as the slash-command list below. */
22
+ * the sessions dir — same vendoring rationale as the slash-command list below.
23
+ * `PI_CODING_AGENT_DIR` goes through the same tilde expansion pi's
24
+ * `getAgentDir()` applies, or `~/custom-pi` yields a broken relative path. */
22
25
  export function piSessionsRoot() {
23
26
  const env = process.env['PI_CODING_AGENT_DIR'];
24
27
  const agentDir = env !== undefined && env !== '' ? expandTilde(env) : join(homedir(), '.pi', 'agent');
25
28
  return join(agentDir, 'sessions');
26
29
  }
27
- /** Mirror pi's `expandTildePath` (= `normalizePath` with the default expandTilde
28
- * on): a bare `~` is the home dir, and a leading `~/` (or `~\` on win32) joins
29
- * the remainder under home. pi's `getAgentDir()` runs `PI_CODING_AGENT_DIR`
30
- * through this, so the vendored root must too or `~/custom-pi` yields a broken
31
- * relative path. Other forms (absolute, relative, `~user`) pass through as pi
32
- * leaves them. */
33
- function expandTilde(p) {
34
- if (p === '~')
35
- return homedir();
36
- if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
37
- return join(homedir(), p.slice(2));
38
- }
39
- return p;
40
- }
41
30
  /**
42
31
  * pi's builtin slash commands — vendored verbatim from pi `core/slash-commands.js`
43
32
  * `BUILTIN_SLASH_COMMANDS` (review C1). **21 entries at 0.78.1** (review n1 — NOT 23).
@@ -45,7 +34,7 @@ function expandTilde(p) {
45
34
  * Builtins are NOT engine-interpreted (`session.prompt('/model')` ships `/model`
46
35
  * to the LLM as literal text), so the broker's `get_commands` op MERGES this list
47
36
  * with the engine's registered commands/templates/skills, and the viewer
48
- * autocomplete parses these locally. Both the broker (T3) and the viewer (T6)
37
+ * autocomplete parses these locally. Both the broker and the viewer
49
38
  * import this single copy.
50
39
  *
51
40
  * Upstream interpolates `${APP_NAME}` into the `quit` description; `APP_NAME`
@@ -7,7 +7,7 @@
7
7
  // orchestrator), and seeds a roadmap scaffold.
8
8
  // The transition guidance the node needs is injected CENTRALLY by the
9
9
  // persona injector (runtime/persona.ts) at the turn boundary — promote()
10
- // itself no longer returns or hand-emits guidance.
10
+ // itself returns no guidance and emits none by hand.
11
11
  // 2. Refresh → persona swap (permanent). On the next fresh revive the node
12
12
  // starts with the orchestrator system prompt baked in (because the launch
13
13
  // spec now says orchestrator). The injected guidance bridges until then.
@@ -1,4 +1,4 @@
1
- // The re-task/reopen gate (#339) — enforced at every doorway that can revive a
1
+ // The re-task/reopen gate — enforced at every doorway that can revive a
2
2
  // node: `node message send` immediate delivery, `node wait deadline`, and
3
3
  // `node lifecycle revive`.
4
4
  //
@@ -12,7 +12,7 @@
12
12
  // so the node's next real `push final` re-latches through the normal
13
13
  // `WHERE final_report IS NULL` guard.
14
14
  //
15
- // Concurrency (#340 review): every check here reads `final_report` FRESH from
15
+ // Concurrency: every check here reads `final_report` FRESH from
16
16
  // the DB — never a caller-cached snapshot from an earlier point in the
17
17
  // command — and the actual clear is a compare-and-swap (`UPDATE ... WHERE
18
18
  // final_report = <the exact value just observed>`), mirroring the CAS