@crouton-kit/crouter 0.3.36 → 0.3.38

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 (177) hide show
  1. package/dist/build-root.js +3 -6
  2. package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
  3. package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
  5. package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
  6. package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
  7. package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
  8. package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
  9. package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
  10. package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
  11. package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
  12. package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
  13. package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
  14. package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
  15. package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
  16. package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
  17. package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
  18. package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
  19. package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
  20. package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
  21. package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
  22. package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
  23. package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
  24. package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
  25. package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
  26. package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
  27. package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
  28. package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
  29. package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
  30. package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
  31. package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
  32. package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
  33. package/dist/builtin-memory/internal/INDEX.md +1 -1
  34. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  35. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  36. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
  37. package/dist/builtin-views/canvas/core.mjs +3 -2
  38. package/dist/builtin-views/prompt-review/core.mjs +6 -134
  39. package/dist/builtin-views/settings/core.mjs +26 -94
  40. package/dist/builtin-views/settings/text.mjs +0 -5
  41. package/dist/builtin-views/settings/tui.mjs +1 -7
  42. package/dist/builtin-views/settings/web.jsx +3 -49
  43. package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
  44. package/dist/clients/attach/attach-cmd.js +429 -432
  45. package/dist/clients/web/web-cmd.js +7 -7
  46. package/dist/commands/__tests__/human.test.js +1 -15
  47. package/dist/commands/attention.js +9 -6
  48. package/dist/commands/canvas-history/search.js +2 -2
  49. package/dist/commands/canvas-history/show.js +3 -3
  50. package/dist/commands/canvas-history.js +1 -1
  51. package/dist/commands/canvas-issue.js +1 -0
  52. package/dist/commands/canvas-snapshot.js +1 -0
  53. package/dist/commands/canvas.js +10 -11
  54. package/dist/commands/chord.js +1 -0
  55. package/dist/commands/human/prompts.js +0 -1
  56. package/dist/commands/human/queue.js +1 -0
  57. package/dist/commands/memory/read.js +3 -3
  58. package/dist/commands/memory/shared.d.ts +13 -8
  59. package/dist/commands/memory/shared.js +22 -8
  60. package/dist/commands/memory/write.js +5 -3
  61. package/dist/commands/node-snapshot.js +1 -0
  62. package/dist/commands/node.js +52 -22
  63. package/dist/commands/profile/add-project.d.ts +1 -0
  64. package/dist/commands/profile/add-project.js +42 -0
  65. package/dist/commands/profile/delete.d.ts +1 -0
  66. package/dist/commands/profile/delete.js +39 -0
  67. package/dist/commands/profile/list.d.ts +1 -0
  68. package/dist/commands/profile/list.js +35 -0
  69. package/dist/commands/profile/new.d.ts +1 -0
  70. package/dist/commands/profile/new.js +50 -0
  71. package/dist/commands/profile/remove-project.d.ts +1 -0
  72. package/dist/commands/profile/remove-project.js +42 -0
  73. package/dist/commands/profile/rename.d.ts +1 -0
  74. package/dist/commands/profile/rename.js +42 -0
  75. package/dist/commands/profile/show.d.ts +1 -0
  76. package/dist/commands/profile/show.js +50 -0
  77. package/dist/commands/profile.d.ts +2 -0
  78. package/dist/commands/profile.js +33 -0
  79. package/dist/commands/push.d.ts +0 -1
  80. package/dist/commands/push.js +2 -228
  81. package/dist/commands/revive.js +2 -2
  82. package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
  83. package/dist/commands/surface.js +3 -2
  84. package/dist/commands/sys/config.js +6 -21
  85. package/dist/commands/sys/daemon.d.ts +2 -0
  86. package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
  87. package/dist/commands/sys/doctor.js +1 -0
  88. package/dist/commands/sys/prompt-review.js +90 -17
  89. package/dist/commands/sys.js +4 -3
  90. package/dist/core/__tests__/daemon-boot.test.js +1 -1
  91. package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
  92. package/dist/core/__tests__/daemon-wedge.test.js +87 -0
  93. package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
  94. package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
  95. package/dist/core/__tests__/history-inbox.test.js +105 -0
  96. package/dist/core/__tests__/kickoff.test.js +3 -3
  97. package/dist/core/__tests__/revive.test.js +39 -1
  98. package/dist/core/canvas/canvas.js +10 -9
  99. package/dist/core/canvas/db.js +11 -0
  100. package/dist/core/canvas/history.d.ts +1 -1
  101. package/dist/core/canvas/history.js +91 -3
  102. package/dist/core/canvas/paths.d.ts +6 -0
  103. package/dist/core/canvas/paths.js +9 -0
  104. package/dist/core/canvas/status-glyph.js +1 -0
  105. package/dist/core/canvas/types.d.ts +10 -0
  106. package/dist/core/config.d.ts +44 -1
  107. package/dist/core/config.js +155 -12
  108. package/dist/core/fault-classifier.d.ts +2 -2
  109. package/dist/core/feed/inbox.d.ts +7 -12
  110. package/dist/core/feed/inbox.js +45 -29
  111. package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
  112. package/dist/core/memory-resolver.d.ts +26 -15
  113. package/dist/core/memory-resolver.js +158 -40
  114. package/dist/core/preview-registry.js +14 -112
  115. package/dist/core/profiles/manifest.d.ts +33 -0
  116. package/dist/core/profiles/manifest.js +332 -0
  117. package/dist/core/profiles/select.d.ts +11 -0
  118. package/dist/core/profiles/select.js +115 -0
  119. package/dist/core/render.js +1 -1
  120. package/dist/core/runtime/bearings.d.ts +9 -0
  121. package/dist/core/runtime/bearings.js +31 -0
  122. package/dist/core/runtime/busy.d.ts +15 -0
  123. package/dist/core/runtime/busy.js +32 -1
  124. package/dist/core/runtime/close.d.ts +10 -0
  125. package/dist/core/runtime/close.js +18 -13
  126. package/dist/core/runtime/fault-recovery.js +4 -0
  127. package/dist/core/runtime/front-door.js +9 -2
  128. package/dist/core/runtime/kickoff.js +5 -7
  129. package/dist/core/runtime/launch.d.ts +25 -7
  130. package/dist/core/runtime/launch.js +42 -55
  131. package/dist/core/runtime/nodes.d.ts +7 -0
  132. package/dist/core/runtime/nodes.js +6 -0
  133. package/dist/core/runtime/persona.js +18 -32
  134. package/dist/core/runtime/placement.js +1 -1
  135. package/dist/core/runtime/revive.js +9 -1
  136. package/dist/core/runtime/spawn.d.ts +13 -0
  137. package/dist/core/runtime/spawn.js +25 -0
  138. package/dist/core/runtime/tmux.d.ts +2 -2
  139. package/dist/core/runtime/tmux.js +2 -2
  140. package/dist/core/scope.d.ts +14 -7
  141. package/dist/core/scope.js +76 -20
  142. package/dist/core/substrate/index.d.ts +3 -3
  143. package/dist/core/substrate/index.js +5 -3
  144. package/dist/core/substrate/on-read.js +3 -2
  145. package/dist/core/substrate/render.d.ts +16 -11
  146. package/dist/core/substrate/render.js +249 -62
  147. package/dist/core/substrate/schema.d.ts +11 -3
  148. package/dist/core/substrate/schema.js +25 -0
  149. package/dist/core/substrate/subject.d.ts +15 -2
  150. package/dist/core/substrate/subject.js +24 -2
  151. package/dist/daemon/crtrd-cli.js +1 -1
  152. package/dist/daemon/crtrd.d.ts +24 -0
  153. package/dist/daemon/crtrd.js +213 -5
  154. package/dist/daemon/manage.js +2 -2
  155. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  156. package/dist/pi-extensions/canvas-nav.js +2 -2
  157. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  158. package/dist/pi-extensions/canvas-stophook.js +59 -2
  159. package/dist/types.d.ts +57 -1
  160. package/dist/types.js +83 -1
  161. package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
  162. package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
  163. package/dist/web-client/index.html +2 -2
  164. package/package.json +2 -2
  165. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
  166. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
  167. package/dist/builtin-memory/crouter-development/personas.md +0 -112
  168. package/dist/builtin-personas/spine/no-manager.md +0 -2
  169. package/dist/commands/daemon.d.ts +0 -2
  170. package/dist/core/personas/index.d.ts +0 -13
  171. package/dist/core/personas/index.js +0 -11
  172. package/dist/core/personas/loader.d.ts +0 -159
  173. package/dist/core/personas/loader.js +0 -327
  174. package/dist/core/personas/resolve.d.ts +0 -59
  175. package/dist/core/personas/resolve.js +0 -376
  176. package/dist/web-client/assets/index-DUThOUzU.css +0 -2
  177. /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
@@ -2,6 +2,7 @@
2
2
  // Replaces old config.ts + doctor.ts + update.ts command files.
3
3
  import { defineBranch } from '../core/command.js';
4
4
  import { configBranch } from './sys/config.js';
5
+ import { daemonBranch } from './sys/daemon.js';
5
6
  import { sysDoctorLeaf } from './sys/doctor.js';
6
7
  import { sysFeedbackLeaf } from './sys/feedback.js';
7
8
  import { sysSettingsLeaf } from './sys/settings.js';
@@ -16,13 +17,13 @@ export function registerSys() {
16
17
  name: 'sys',
17
18
  rootEntry: {
18
19
  concept: 'crtr configuration, diagnostics, and self-management',
19
- desc: 'config, settings, setup, doctor, sysprompt, prompt-review, promptstudio, sync, update, version, feedback',
20
- useWhen: 'managing the crtr installation or opening built-in system views',
20
+ desc: 'config, settings, setup, doctor, sysprompt, prompt-review, promptstudio, sync, update, version, feedback, daemon',
21
+ useWhen: 'managing the crtr installation, the crtrd supervisor process, or opening built-in system views',
21
22
  },
22
23
  help: {
23
24
  name: 'sys',
24
25
  summary: 'crtr system configuration, settings, diagnostics, and self-management',
25
26
  },
26
- children: [configBranch, sysSettingsLeaf, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysPromptStudioLeaf, sysFeedbackLeaf, sysUpdateLeaf, sysVersionLeaf, sysSyncLeaf],
27
+ children: [configBranch, sysSettingsLeaf, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysPromptStudioLeaf, sysFeedbackLeaf, sysUpdateLeaf, sysVersionLeaf, sysSyncLeaf, daemonBranch],
27
28
  });
28
29
  }
@@ -262,7 +262,7 @@ test('the daemon honours an explicit CRTR_PIDFILE override', () => {
262
262
  // REGRESSION (bug #102 follow-up): with NO exit promise, the readiness loop must
263
263
  // still observe a real `connect`. A buggy `?? Promise.resolve('retry')` race lost
264
264
  // to the probe instantly, so live-broker callers (focus on an already-alive node,
265
- // canvas-tmux-spread) could never return true and stalled the full 30s deadline.
265
+ // surface tmux-spread) could never return true and stalled the full 30s deadline.
266
266
  test('waitForBrokerViewSocket returns true on a live socket when given no exit promise', async () => {
267
267
  // CRTR_SOCK_DIR override keeps the AF_UNIX path short — the temp CRTR_HOME path
268
268
  // would blow past macOS's 104-byte sun_path limit (listen EINVAL).
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/daemon-wedge.test.ts
2
+ //
3
+ // Issue #110 — pid-alive WEDGED node detection. The pure decision core:
4
+ // wedgeVerdict(busy, quietForMs, cpuPercent). No wall-clock sleeps, no ps
5
+ // shell-outs, no fake pids — every input is injected directly, mirroring the
6
+ // existing livenessVerdict/yieldStallVerdict suites in daemon-liveness.test.ts.
7
+ //
8
+ // LOCKS: (1) a routine long-running BUT ACTIVE turn (heartbeat recent, or CPU
9
+ // non-trivial) must never verdict 'wedged' — false positives are the failure
10
+ // mode this feature exists to avoid. (2) only busy+quiet+near-zero-CPU, all at
11
+ // once, past the generous grace, fires. (3) a null/failed CPU sample never
12
+ // fires on its own (fails toward 'pending', not 'wedged').
13
+ import { test } from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import { wedgeVerdict, WEDGE_QUIET_MS, WEDGE_CPU_MAX_PERCENT, sumTreeCpu } from '../../daemon/crtrd.js';
16
+ test('wedgeVerdict: a node whose turn already ended is never wedged, regardless of the other signals', () => {
17
+ assert.equal(wedgeVerdict({ busy: false, quietForMs: null, cpuPercent: null }), 'leave');
18
+ assert.equal(wedgeVerdict({ busy: false, quietForMs: 10 * 60 * 60_000, cpuPercent: 0 }), 'leave');
19
+ });
20
+ test('wedgeVerdict: a busy node with an unreadable heartbeat is left alone (not enough evidence)', () => {
21
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: null, cpuPercent: null }), 'leave');
22
+ });
23
+ test('wedgeVerdict: routine turns — even long ones — never trip before the grace elapses', () => {
24
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: 0, cpuPercent: 0 }), 'leave', 'just started');
25
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: 5 * 60_000, cpuPercent: 0 }), 'leave', 'a routine multi-minute tool call');
26
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS - 1, cpuPercent: 0 }), 'leave', 'just under the grace, even with 0% cpu');
27
+ });
28
+ test('wedgeVerdict: past the grace with NO cpu corroboration only pends — never fires on time alone', () => {
29
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS, cpuPercent: null }), 'pending', 'cpu sample failed/unavailable — never treated as confirmation');
30
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 10, cpuPercent: null }), 'pending', 'no amount of extra quiet time substitutes for the corroborating signal');
31
+ });
32
+ test('wedgeVerdict: a long-quiet turn that is STILL VISIBLY WORKING (real cpu) is exonerated, not wedged', () => {
33
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 3, cpuPercent: WEDGE_CPU_MAX_PERCENT + 0.1 }), 'pending', 'cpu just above the ceiling — a genuinely churning long build — must not fire');
34
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 3, cpuPercent: 40 }), 'pending', 'clearly-active cpu never fires regardless of how quiet the heartbeat looks');
35
+ });
36
+ test('wedgeVerdict: fires ONLY when busy + quiet-past-grace + near-zero cpu all corroborate', () => {
37
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS, cpuPercent: 0 }), 'wedged');
38
+ assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS + 60_000, cpuPercent: WEDGE_CPU_MAX_PERCENT }), 'wedged', 'at the cpu ceiling (inclusive)');
39
+ });
40
+ // ---------------------------------------------------------------------------
41
+ // sumTreeCpu (pure) — process-tree CPU sampling. Review fix for issue #110:
42
+ // sampling only the broker pid false-fired on a broker awaiting a busy CHILD
43
+ // subprocess. These lock the tree-sum behavior directly, independent of any
44
+ // `ps` shell-out.
45
+ // ---------------------------------------------------------------------------
46
+ test('sumTreeCpu: a busy CHILD subprocess exonerates an idle broker (the false-positive this fixes)', () => {
47
+ const ps = [
48
+ ' 100 1 0.0', // broker, idle
49
+ ' 200 100 85.0', // child of the broker, busy
50
+ ].join('\n');
51
+ assert.equal(sumTreeCpu(ps, 100), 85.0);
52
+ });
53
+ test('sumTreeCpu: broker + all descendants idle sums to ~0 (the real wedge is still detected)', () => {
54
+ const ps = [
55
+ ' 100 1 0.0',
56
+ ' 200 100 0.0',
57
+ ' 201 100 0.0',
58
+ ].join('\n');
59
+ assert.equal(sumTreeCpu(ps, 100), 0);
60
+ });
61
+ test('sumTreeCpu: sums across multiple generations (grandchild busy)', () => {
62
+ const ps = [
63
+ ' 100 1 0.0', // broker
64
+ ' 200 100 0.0', // child, idle
65
+ ' 300 200 40.0', // grandchild, busy
66
+ ].join('\n');
67
+ assert.equal(sumTreeCpu(ps, 100), 40.0);
68
+ });
69
+ test('sumTreeCpu: root pid absent from the ps output returns null (can\'t measure)', () => {
70
+ const ps = [
71
+ ' 200 1 0.0',
72
+ ' 300 200 10.0',
73
+ ].join('\n');
74
+ assert.equal(sumTreeCpu(ps, 100), null);
75
+ });
76
+ test('sumTreeCpu: malformed/blank lines are skipped; empty output returns null', () => {
77
+ const ps = [
78
+ '',
79
+ ' ',
80
+ 'garbage line not ps shaped',
81
+ ' 100 1 5.0',
82
+ ' bad 100 1.0',
83
+ ].join('\n');
84
+ assert.equal(sumTreeCpu(ps, 100), 5.0);
85
+ assert.equal(sumTreeCpu('', 100), null);
86
+ assert.equal(sumTreeCpu(' \n \n', 100), null);
87
+ });
@@ -91,7 +91,7 @@ test('every crtr invocation targets the baked binary by absolute path, never a b
91
91
  assert.match(script, /'\/usr\/local\/bin\/crtr' --json sys version/);
92
92
  assert.match(script, /'\/usr\/local\/bin\/crtr' surface web -h/);
93
93
  assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve -h/);
94
- assert.match(script, /'\/usr\/local\/bin\/crtr' canvas daemon start/);
94
+ assert.match(script, /'\/usr\/local\/bin\/crtr' sys daemon start/);
95
95
  assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve --host 0\.0\.0\.0/);
96
96
  // No bare `crtr <subcommand>` invocation anywhere — only the `command -v
97
97
  // crtr` shadow-detection probe and human-readable error-message text are
@@ -123,7 +123,7 @@ test('mkdirs the tmpfs CRTR_SOCK_DIR/CRTR_PIDFILE dirs strictly before the daemo
123
123
  assert.doesNotMatch(script, /CRTR_CANVAS_DB=/);
124
124
  assert.match(script, /unset CRTR_CANVAS_DB/);
125
125
  const mkdirIdx = indexOf(script, '/run/crtr/socks');
126
- const daemonStartIdx = indexOf(script, "'/usr/local/bin/crtr' canvas daemon start");
126
+ const daemonStartIdx = indexOf(script, "'/usr/local/bin/crtr' sys daemon start");
127
127
  assert.ok(mkdirIdx < daemonStartIdx, 'tmpfs mkdir must precede daemon start');
128
128
  const mkdirLine = script.split('\n').find((line) => line.startsWith('mkdir -p') && line.includes('/run/crtr/socks'));
129
129
  assert.ok(mkdirLine, 'expected a single mkdir line covering the tmpfs dirs');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ // Run with: node --import tsx/esm --test src/core/__tests__/history-inbox.test.ts
2
+ //
3
+ // Locks in the invariants of the `inbox` history source: it stays opt-in
4
+ // (never part of the default corpus), its refs resolve/round-trip as
5
+ // `<node>:inbox/NNNNNN`, direct-message bodies stay full/unclipped, and the
6
+ // sender (`from`) is visible in both the read body and the artifact title.
7
+ import { test, before, after, beforeEach } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { createNode } from '../canvas/canvas.js';
13
+ import { closeDb } from '../canvas/db.js';
14
+ import { appendInbox } from '../feed/inbox.js';
15
+ import { buildCorpus, resolveRef } from '../canvas/history.js';
16
+ let home;
17
+ function node(id, cwd) {
18
+ return {
19
+ node_id: id,
20
+ name: id,
21
+ created: new Date().toISOString(),
22
+ cwd,
23
+ kind: 'general',
24
+ mode: 'base',
25
+ lifecycle: 'terminal',
26
+ status: 'active',
27
+ };
28
+ }
29
+ before(() => {
30
+ home = mkdtempSync(join(tmpdir(), 'crtr-history-inbox-'));
31
+ process.env['CRTR_HOME'] = home;
32
+ });
33
+ beforeEach(() => {
34
+ closeDb();
35
+ rmSync(home, { recursive: true, force: true });
36
+ });
37
+ after(() => {
38
+ closeDb();
39
+ rmSync(home, { recursive: true, force: true });
40
+ delete process.env['CRTR_HOME'];
41
+ });
42
+ test('default history excludes inbox; --type inbox includes it, with full sender-attributed bodies', () => {
43
+ const cwd = '/tmp/hist-work';
44
+ createNode(node('sender-1', cwd));
45
+ const receiver = createNode(node('receiver-1', cwd));
46
+ const longBody = 'a'.repeat(4000); // well past any old label-clip length
47
+ appendInbox(receiver.node_id, {
48
+ from: 'sender-1',
49
+ tier: 'urgent',
50
+ kind: 'message',
51
+ label: 'first line of a long message',
52
+ data: { body: longBody },
53
+ });
54
+ // (a) default corpus (no explicit types) excludes inbox entirely.
55
+ const defaultCorpus = buildCorpus({ nodes: [receiver.node_id] }, {});
56
+ assert.equal(defaultCorpus.some((a) => a.source === 'inbox'), false, 'inbox is opt-in, not part of the default corpus');
57
+ // (b) explicit --type inbox includes it.
58
+ const inboxCorpus = buildCorpus({ nodes: [receiver.node_id] }, { types: ['inbox'] });
59
+ assert.equal(inboxCorpus.length, 1, 'exactly the one appended entry');
60
+ const artifact = inboxCorpus[0];
61
+ assert.equal(artifact.source, 'inbox');
62
+ // (c) ref resolves as <node>:inbox/000000 and round-trips through resolveRef.
63
+ const expectedRef = `${receiver.node_id}:inbox/000000`;
64
+ assert.equal(artifact.ref, expectedRef);
65
+ const resolved = resolveRef(expectedRef);
66
+ assert.ok(resolved !== null, 'ref resolves back to the entry');
67
+ assert.equal(resolved.source, 'inbox');
68
+ // (d) the direct-message body is full/unclipped in both the corpus artifact
69
+ // and the resolveRef read path.
70
+ assert.ok(artifact.loadBody().includes(longBody), 'corpus artifact body is unclipped');
71
+ assert.ok(resolved.body.includes(longBody), 'resolveRef body is unclipped');
72
+ // (e) the sender is visible — in the read body (not just resolvable via the
73
+ // node the inbox belongs to) and in the artifact title used by list rows.
74
+ assert.ok(resolved.body.includes('from: sender-1'), 'read body names the sender');
75
+ assert.ok(artifact.title.includes('sender-1'), 'artifact title names the sender for list/search rows');
76
+ });
77
+ test('a spilled message body is read RAW — a leading frontmatter-shaped block is not stripped', () => {
78
+ const cwd = '/tmp/hist-work-3';
79
+ createNode(node('sender-3', cwd));
80
+ const receiver = createNode(node('receiver-3', cwd));
81
+ // Long enough to force the spill-to-messages/ path (past BODY_MAX_CHARS),
82
+ // and starts with a block that LOOKS like crouter report frontmatter but
83
+ // isn't one — a spilled message has no frontmatter contract, so this must
84
+ // survive intact, unlike a report ref (which always has real frontmatter
85
+ // stripped by fileBodyLoader).
86
+ const body = '---\nfoo: bar\n---\n' + 'a'.repeat(4000);
87
+ appendInbox(receiver.node_id, {
88
+ from: 'sender-3',
89
+ tier: 'normal',
90
+ kind: 'message',
91
+ label: 'first line',
92
+ data: { body },
93
+ });
94
+ const resolved = resolveRef(`${receiver.node_id}:inbox/000000`);
95
+ assert.ok(resolved !== null);
96
+ assert.ok(resolved.body.includes('---\nfoo: bar\n---'), 'leading frontmatter-shaped block is preserved, not stripped');
97
+ });
98
+ test('a null-from (system) inbox entry reads as "system", not blank or thrown', () => {
99
+ const cwd = '/tmp/hist-work-2';
100
+ const receiver = createNode(node('receiver-2', cwd));
101
+ appendInbox(receiver.node_id, { from: null, tier: 'normal', kind: 'update', label: 'system note' });
102
+ const resolved = resolveRef(`${receiver.node_id}:inbox/000000`);
103
+ assert.ok(resolved !== null);
104
+ assert.ok(resolved.body.includes('from: system'), 'null sender renders as "system"');
105
+ });
@@ -58,7 +58,7 @@ test('drainBearings consumes the yield note + advances the cursor exactly once',
58
58
  assert.equal(b2.yieldMsg, null);
59
59
  assert.equal(b2.unreadDigest, null);
60
60
  });
61
- test('the feed block frames awaiting workers as alive + auto-waking, so a fresh revive has no reason to peek', () => {
61
+ test('the feed block frames awaiting workers as alive + auto-waking, so a fresh revive has no reason to poll', () => {
62
62
  const parent = createNode(node('p1'));
63
63
  const child = createNode(node('c1')); // status 'active' by default
64
64
  subscribe(parent.node_id, child.node_id); // parent awaits child
@@ -66,7 +66,7 @@ test('the feed block frames awaiting workers as alive + auto-waking, so a fresh
66
66
  // The roster names the live child...
67
67
  assert.ok(msg.includes(child.node_id), 'awaiting roster lists the live child');
68
68
  // ...and asserts aliveness + the automatic wake at the source, so the node
69
- // does not burn a turn confirming with `feed read`/`feed peek`.
69
+ // has nothing to check, poll, or verify separately.
70
70
  assert.ok(/alive and running/.test(msg), 'states the worker is alive and running');
71
71
  assert.ok(/wake you the moment/.test(msg), 'states the wake is automatic on push');
72
72
  assert.ok(/still working, not stalled/.test(msg), 'frames the empty feed as expected, not a problem');
@@ -101,7 +101,7 @@ test('a fresh revive is pointed at its subscriptions\' on-disk report history (c
101
101
  assert.ok(!msg.includes(newer), 'absolute report paths not rendered');
102
102
  assert.ok(/canvas history read <ref>/.test(msg), 'points at one-report expansion');
103
103
  assert.ok(/canvas history show <node-id> --type report/.test(msg), 'points at full per-node expansion');
104
- assert.ok(/feed read --all/.test(msg), 'points at the cursor-independent full-history replay');
104
+ assert.ok(/canvas history show <node-id> --type inbox/.test(msg), 'points at the cursor-independent full-history replay');
105
105
  });
106
106
  test('buildReviveKickoff is pure — building twice eats nothing', () => {
107
107
  const id = 'n2';
@@ -34,7 +34,8 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
34
34
  import { tmpdir } from 'node:os';
35
35
  import { join } from 'node:path';
36
36
  import { spawn, spawnSync } from 'node:child_process';
37
- import { createNode } from '../canvas/canvas.js';
37
+ import { createNode, subscribe } from '../canvas/canvas.js';
38
+ import { readInboxSince } from '../feed/inbox.js';
38
39
  import { closeDb } from '../canvas/db.js';
39
40
  import { buildPiArgv } from '../runtime/launch.js';
40
41
  import { headlessBrokerHost } from '../runtime/host.js';
@@ -298,3 +299,40 @@ test('reviveNode crashes and clears pid when launch/preflight throws', async ()
298
299
  await h.dispose();
299
300
  }
300
301
  });
302
+ // ---------------------------------------------------------------------------
303
+ // Crash-to-dead doctrine wake — a launch-refusal marks the node dead; a
304
+ // manager waiting dormant on it must be told, or it hangs forever (the
305
+ // daemon no longer supervises a dead node). Mirrors closeNode's step-4 fan-out.
306
+ // ---------------------------------------------------------------------------
307
+ test('reviveNode fans a doctrine wake to subscribers when launch/preflight throws (crash-to-dead)', async () => {
308
+ const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-crashwake' });
309
+ const originalLaunch = headlessBrokerHost.launch;
310
+ try {
311
+ const root = h.spawnRoot('crash-wake suite root');
312
+ const sessionFile = join(home, 'sessions', 'crtr-revive-crashwake.jsonl');
313
+ mkdirSync(join(home, 'sessions'), { recursive: true });
314
+ writeFileSync(sessionFile, '{}\n', 'utf8');
315
+ const M = h.fabricateBrokerNode({
316
+ parent: root,
317
+ status: 'active',
318
+ intent: null,
319
+ pi_pid: deadPid(),
320
+ pi_session_id: 'uuid-3',
321
+ pi_session_file: sessionFile,
322
+ });
323
+ // spawnChild-equivalent auto-subscription — done explicitly since this node
324
+ // was fabricated directly, not spawned through the CLI.
325
+ subscribe(root, M, true);
326
+ headlessBrokerHost.launch = (_nodeId, _inv, _opts) => { throw new Error('boom'); };
327
+ assert.throws(() => reviveNode(M, { resume: true }), /refusing to revive/i);
328
+ assert.equal(h.node(M).status, 'dead', 'failed revive marks the node dead');
329
+ const entries = readInboxSince(root, undefined);
330
+ const notice = entries.find((e) => e.data?.['reason'] === 'child-crashed' && e.data?.['child'] === M);
331
+ assert.ok(notice !== undefined, 'the surviving parent got a doctrine wake for the dead child');
332
+ assert.equal(notice.from, M, 'the notice is attributed to the crashed child');
333
+ }
334
+ finally {
335
+ headlessBrokerHost.launch = originalLaunch;
336
+ await h.dispose();
337
+ }
338
+ });
@@ -21,7 +21,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
21
21
  /** The identity keys meta.json persists. Listed explicitly so no runtime field
22
22
  * can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
23
23
  const IDENTITY_KEYS = [
24
- 'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'kind', 'mode',
24
+ 'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
25
25
  'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'passive_default',
26
26
  'pi_session_id', 'pi_session_file', 'model_override', 'launch',
27
27
  ];
@@ -101,13 +101,13 @@ function writeMeta(meta) {
101
101
  * state. A fresh insert takes the schema defaults for runtime. */
102
102
  function upsertRow(meta) {
103
103
  openDb()
104
- .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, parent, created)
105
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
104
+ .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created)
105
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
106
106
  ON CONFLICT(node_id) DO UPDATE SET
107
107
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
108
108
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
109
- parent=excluded.parent`)
110
- .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.parent ?? null, meta.created);
109
+ profile_id=excluded.profile_id, parent=excluded.parent`)
110
+ .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created);
111
111
  }
112
112
  /** Seed a node's row at BIRTH: identity columns + runtime columns taken from the
113
113
  * incoming meta (defaults: status='active', the rest null). The only writer
@@ -116,17 +116,17 @@ function upsertRow(meta) {
116
116
  function seedRow(meta) {
117
117
  openDb()
118
118
  .prepare(`INSERT INTO nodes
119
- (node_id, name, kind, mode, lifecycle, cwd, host_kind, parent, created,
119
+ (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created,
120
120
  status, intent, pi_pid, "window", tmux_session, pane)
121
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
121
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
122
122
  ON CONFLICT(node_id) DO UPDATE SET
123
123
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
124
124
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
125
- parent=excluded.parent,
125
+ profile_id=excluded.profile_id, parent=excluded.parent,
126
126
  status=excluded.status, intent=excluded.intent, pi_pid=excluded.pi_pid,
127
127
  "window"=excluded."window", tmux_session=excluded.tmux_session,
128
128
  pane=excluded.pane`)
129
- .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.parent ?? null, meta.created, meta.status ?? 'active', meta.intent ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
129
+ .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, meta.status ?? 'active', meta.intent ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
130
130
  }
131
131
  function rowFrom(r) {
132
132
  return {
@@ -138,6 +138,7 @@ function rowFrom(r) {
138
138
  status: r['status'],
139
139
  cwd: r['cwd'],
140
140
  host_kind: r['host_kind'] ?? null,
141
+ profile_id: r['profile_id'] ?? null,
141
142
  parent: r['parent'] ?? null,
142
143
  created: r['created'],
143
144
  intent: r['intent'] ?? null,
@@ -349,6 +349,16 @@ CREATE TABLE IF NOT EXISTS canvas_meta (
349
349
  );
350
350
  `);
351
351
  }
352
+ /** v11 — additive identity column `profile_id`: the profile-directory id
353
+ * (`<slug>-<id>`) a node runs under, chosen once at spawn (inherited from the
354
+ * spawner, or `--profile`). Durable IDENTITY mirrored from meta.json like
355
+ * `cwd`/`host_kind` (NOT runtime like `pane`), so it survives an index rebuild
356
+ * via the `IDENTITY_KEYS`/`upsertRow` path. Defaults NULL (root/no-profile);
357
+ * no data backfill. Additive, forward-only. */
358
+ function addProfileIdColumn(db) {
359
+ const columns = nodeColumns(db);
360
+ addNodeColumnIfMissing(db, columns, 'profile_id', `ALTER TABLE nodes ADD COLUMN profile_id TEXT;`);
361
+ }
352
362
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
353
363
  * `user_version` tracks how many have been applied. Append only. */
354
364
  export const MIGRATIONS = [
@@ -362,6 +372,7 @@ export const MIGRATIONS = [
362
372
  /* v8 */ addHostKindColumn,
363
373
  /* v9 */ migrateWakeupsToTriggers,
364
374
  /* v10 */ addCanvasMetaTable,
375
+ /* v11 */ addProfileIdColumn,
365
376
  ];
366
377
  /** Bring `db` up to the latest schema version. Reads `user_version`, runs each
367
378
  * pending migration in order, and bumps `user_version` after each so the work
@@ -1,5 +1,5 @@
1
1
  import type { NodeStatus } from './types.js';
2
- export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta';
2
+ export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta' | 'inbox';
3
3
  /** One searchable unit of the episodic record: a report, a context doc, a
4
4
  * node's roadmap, or a node's meta (name/description/kind). */
5
5
  export interface HistoryArtifact {
@@ -12,7 +12,7 @@
12
12
  import { statSync, readdirSync } from 'node:fs';
13
13
  import { join, relative, sep } from 'node:path';
14
14
  import { getNode, listNodes, view } from './canvas.js';
15
- import { contextDir, reportsDir, nodeDir } from './paths.js';
15
+ import { contextDir, reportsDir, nodeDir, inboxPath } from './paths.js';
16
16
  import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
17
17
  import { parseFrontmatterGeneric } from '../frontmatter.js';
18
18
  /** The cwd the caller means by default: the calling node's cwd (resolved from
@@ -126,6 +126,68 @@ function docArtifact(row, desc, abs) {
126
126
  loadBody: load,
127
127
  };
128
128
  }
129
+ /** Every entry ever appended to a node's inbox.jsonl, oldest first, cursor-
130
+ * independent (the cursor only gates the live wake path, not this read).
131
+ * Empty when the node has no inbox file yet. */
132
+ function allInboxEntries(nodeId) {
133
+ const raw = readTextIfExists(inboxPath(nodeId));
134
+ if (raw === null)
135
+ return [];
136
+ return raw
137
+ .split('\n')
138
+ .filter((l) => l.trim() !== '')
139
+ .map((l) => JSON.parse(l));
140
+ }
141
+ /** One inbox entry's full content: a direct message or system alert's
142
+ * un-clipped body — dereferenced from its spilled ref file when the inline
143
+ * preview clipped, else the inline body itself — or, for a push pointer
144
+ * (which carries no body of its own), its kind/label plus the sender-side
145
+ * report path it points to.
146
+ *
147
+ * A spilled ref (message/system notice too long to inline) is arbitrary
148
+ * text with no crouter frontmatter contract — read it RAW, unlike a report
149
+ * ref, which always carries the `---\n...\n---` header stripped by
150
+ * fileBodyLoader. The presence of an inline `data.body` preview is what
151
+ * distinguishes the two: only a spilled message/notice ever carries one
152
+ * (a push pointer's entry has no `data` field at all). */
153
+ function inboxEntryBody(e) {
154
+ const meta = [`from: ${e.from ?? 'system'}`, `tier: ${e.tier}`].join('\n');
155
+ const header = `[${e.kind}] ${e.label}`;
156
+ const inlinePreview = typeof e.data?.['body'] === 'string' ? e.data['body'].trim() : '';
157
+ const isSpilledMessage = typeof e.data?.['body'] === 'string';
158
+ const full = e.ref !== undefined
159
+ ? isSpilledMessage
160
+ ? (readTextIfExists(e.ref) ?? '')
161
+ : fileBodyLoader(e.ref).load()
162
+ : inlinePreview;
163
+ const hasBody = full !== '' && full !== e.label;
164
+ const parts = [meta, header];
165
+ if (e.ref !== undefined)
166
+ parts.push(`ref: ${e.ref}`);
167
+ if (hasBody)
168
+ parts.push(full);
169
+ return parts.join('\n\n');
170
+ }
171
+ function inboxArtifact(row, desc, entry, index) {
172
+ const relpath = `inbox/${String(index).padStart(6, '0')}`;
173
+ const body = inboxEntryBody(entry);
174
+ return {
175
+ ref: `${row.node_id}:${relpath}`,
176
+ nodeId: row.node_id,
177
+ relpath,
178
+ source: 'inbox',
179
+ ts: entry.ts,
180
+ tsMs: toMs(entry.ts),
181
+ title: `[${entry.kind}] from ${entry.from ?? 'system'}: ${entry.label}`,
182
+ nodeName: row.name,
183
+ nodeKind: row.kind,
184
+ nodeStatus: row.status,
185
+ nodeCwd: row.cwd,
186
+ nodeDesc: desc,
187
+ path: null,
188
+ loadBody: () => body,
189
+ };
190
+ }
129
191
  function metaArtifact(row, desc) {
130
192
  const body = `${row.name}\n${desc}\n${row.kind}`;
131
193
  return {
@@ -189,7 +251,9 @@ export function buildCorpus(scope, corpus) {
189
251
  return false;
190
252
  return true;
191
253
  });
192
- // --report-kind implies type=report.
254
+ // --report-kind implies type=report. `inbox` is opt-in only — never part of
255
+ // the default set — since a node's raw inbox history is unbounded diagnostic
256
+ // detail, not curated past work.
193
257
  const wantReportKind = corpus.reportKind;
194
258
  const types = corpus.types !== undefined && corpus.types.length > 0
195
259
  ? corpus.types
@@ -199,7 +263,7 @@ export function buildCorpus(scope, corpus) {
199
263
  const want = new Set(types);
200
264
  const out = [];
201
265
  for (const row of survivors) {
202
- const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap')
266
+ const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap') || want.has('inbox')
203
267
  ? nodeDescOf(row.node_id)
204
268
  : '';
205
269
  if (want.has('meta'))
@@ -224,6 +288,9 @@ export function buildCorpus(scope, corpus) {
224
288
  out.push(a);
225
289
  }
226
290
  }
291
+ if (want.has('inbox')) {
292
+ allInboxEntries(row.node_id).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
293
+ }
227
294
  }
228
295
  // Time window on artifact ts.
229
296
  return out.filter((a) => {
@@ -274,6 +341,22 @@ export function resolveRef(ref) {
274
341
  path: null,
275
342
  };
276
343
  }
344
+ if (relpath.startsWith('inbox/')) {
345
+ const index = Number(relpath.slice('inbox/'.length));
346
+ const entry = allInboxEntries(nodeId)[index];
347
+ if (entry === undefined)
348
+ return null;
349
+ return {
350
+ nodeId,
351
+ relpath,
352
+ source: 'inbox',
353
+ ts: entry.ts,
354
+ nodeName: node.name,
355
+ body: inboxEntryBody(entry),
356
+ raw: null,
357
+ path: null,
358
+ };
359
+ }
277
360
  if (relpath.includes('..') || relpath.startsWith('/')) {
278
361
  throw new Error(`illegal relpath in ref: ${relpath}`);
279
362
  }
@@ -321,6 +404,7 @@ export function nodeArtifacts(nodeId, types) {
321
404
  status: node.status,
322
405
  cwd: node.cwd,
323
406
  host_kind: node.host_kind ?? null,
407
+ profile_id: node.profile_id ?? null,
324
408
  parent: node.parent ?? null,
325
409
  created: node.created,
326
410
  intent: node.intent ?? null,
@@ -329,6 +413,7 @@ export function nodeArtifacts(nodeId, types) {
329
413
  tmux_session: node.tmux_session ?? null,
330
414
  pane: node.pane ?? null,
331
415
  };
416
+ // `inbox` is opt-in only, same as buildCorpus — must be requested explicitly.
332
417
  const want = new Set(types !== undefined && types.length > 0 ? types : ['report', 'doc', 'roadmap']);
333
418
  const desc = node.description ?? '';
334
419
  const out = [];
@@ -349,6 +434,9 @@ export function nodeArtifacts(nodeId, types) {
349
434
  out.push(a);
350
435
  }
351
436
  }
437
+ if (want.has('inbox')) {
438
+ allInboxEntries(nodeId).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
439
+ }
352
440
  return out;
353
441
  }
354
442
  /** Bounded per-cwd aggregate for the branch `-h` `<corpus>` block. Counts files
@@ -26,6 +26,12 @@ export declare function nodeDir(nodeId: string): string;
26
26
  export declare function contextDir(nodeId: string): string;
27
27
  export declare function jobDir(nodeId: string): string;
28
28
  export declare function reportsDir(nodeId: string): string;
29
+ /** Overflow store for inbox entries whose inline body exceeds the digest's
30
+ * bounded preview (a direct `node msg`, a daemon trigger/deadline notice, a
31
+ * close/cancel doctrine wake) — the full body spills here, mirroring how a
32
+ * push's body always lives in a file, never inline; the inbox entry then
33
+ * carries `ref` at this path, same as a push pointer. */
34
+ export declare function messagesDir(nodeId: string): string;
29
35
  export declare function nodeMetaPath(nodeId: string): string;
30
36
  export declare function inboxPath(nodeId: string): string;
31
37
  /** Passive-subscription accumulator. Pushes from publishers this node subscribes
@@ -7,6 +7,7 @@
7
7
  // context/ roadmap.md, initial-prompt.md, explore-*.md, artifacts
8
8
  // job/ log.jsonl, telemetry.json
9
9
  // reports/ append-only push history (<ts>-<kind>.md)
10
+ // messages/ overflow bodies for inbox entries too long to inline
10
11
  // inbox.jsonl messages + coalesced subscription feed
11
12
  // transcript.jsonl mirror/pointer of the pi session
12
13
  // session.ptr pi session id/path
@@ -73,6 +74,14 @@ export function jobDir(nodeId) {
73
74
  export function reportsDir(nodeId) {
74
75
  return join(nodeDir(nodeId), 'reports');
75
76
  }
77
+ /** Overflow store for inbox entries whose inline body exceeds the digest's
78
+ * bounded preview (a direct `node msg`, a daemon trigger/deadline notice, a
79
+ * close/cancel doctrine wake) — the full body spills here, mirroring how a
80
+ * push's body always lives in a file, never inline; the inbox entry then
81
+ * carries `ref` at this path, same as a push pointer. */
82
+ export function messagesDir(nodeId) {
83
+ return join(nodeDir(nodeId), 'messages');
84
+ }
76
85
  export function nodeMetaPath(nodeId) {
77
86
  return join(nodeDir(nodeId), 'meta.json');
78
87
  }
@@ -44,6 +44,7 @@ export function hangingLabel(kind) {
44
44
  case 'connection': return 'down';
45
45
  case 'auth': return 'auth';
46
46
  case 'protocol': return 'protocol';
47
+ case 'wedged': return 'wedged';
47
48
  default: return 'other';
48
49
  }
49
50
  }