@north-light/crouter 0.3.226 → 0.3.228

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 (288) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +37 -9
  212. package/dist/daemon/api/bridge.d.ts +35 -4
  213. package/dist/daemon/api/bridge.js +50 -8
  214. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  215. package/dist/daemon/api/handlers/memory.js +2 -0
  216. package/dist/daemon/crtrd.js +2 -0
  217. package/dist/daemon/human/finish.js +8 -1
  218. package/dist/daemon/manage.d.ts +0 -1
  219. package/dist/daemon/manage.js +7 -16
  220. package/dist/daemon/startup-policy.d.ts +1 -0
  221. package/dist/daemon/startup-policy.js +1 -0
  222. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  223. package/dist/migrations/002-profile-project-memory.js +1 -0
  224. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  225. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  226. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  227. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  228. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  229. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  230. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  231. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  232. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  234. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/activation.test.js +149 -0
  236. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  237. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  238. package/dist/migrations/activation.d.ts +16 -0
  239. package/dist/migrations/activation.js +78 -0
  240. package/dist/migrations/convergent.d.ts +14 -3
  241. package/dist/migrations/convergent.js +21 -10
  242. package/dist/migrations/corpus.d.ts +78 -0
  243. package/dist/migrations/corpus.js +497 -0
  244. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  245. package/dist/migrations/frontmatter-splice.js +176 -0
  246. package/dist/migrations/registry.d.ts +6 -1
  247. package/dist/migrations/registry.js +7 -2
  248. package/dist/migrations/runner.d.ts +41 -0
  249. package/dist/migrations/runner.js +81 -0
  250. package/dist/migrations/types.d.ts +148 -9
  251. package/dist/migrations/types.js +9 -2
  252. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  253. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  254. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  255. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  256. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  257. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  258. package/dist/pi-extensions/canvas-stophook.js +6 -13
  259. package/dist/shared/generated-context.d.ts +0 -3
  260. package/dist/shared/generated-context.js +0 -57
  261. package/dist/shared/tool-groups.js +2 -3
  262. package/package.json +5 -4
  263. package/runtime.lock.json +2 -2
  264. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  265. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  266. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  267. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  268. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  269. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  272. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  273. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  275. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  276. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  285. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  286. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  287. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  288. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -0,0 +1,219 @@
1
+ // Repository-root project-memory identity: real Git and non-Git fixtures prove
2
+ // that declarations live at a repository root while every nested store derives
3
+ // its effective prefix from its physical owner path.
4
+ //
5
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/repository-root-identity.test.ts
6
+ import { after, before, beforeEach, test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { gitSync } from '../git.js';
12
+ import { createProfile } from '../profiles/manifest.js';
13
+ import { loadMemoryStoreView, memoryStoresInPrecedence, openProjectMemoryStore, resolveMemoryDocForTarget, } from '../memory-resolver.js';
14
+ import { resetScopeCache } from '../scope.js';
15
+ const cleanups = [];
16
+ let previousHome;
17
+ before(() => {
18
+ previousHome = process.env.HOME;
19
+ });
20
+ beforeEach(() => {
21
+ process.env.HOME = fixtureDir('crtr-repository-root-identity-home-');
22
+ resetScopeCache();
23
+ });
24
+ after(() => {
25
+ resetScopeCache();
26
+ for (const path of cleanups)
27
+ rmSync(path, { recursive: true, force: true });
28
+ if (previousHome === undefined)
29
+ delete process.env.HOME;
30
+ else
31
+ process.env.HOME = previousHome;
32
+ });
33
+ function fixtureDir(prefix) {
34
+ const path = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
35
+ cleanups.push(path);
36
+ return path;
37
+ }
38
+ function writeDoc(path, body, frontmatter) {
39
+ mkdirSync(join(path, '..'), { recursive: true });
40
+ const yaml = frontmatter === undefined ? '' : `---\n${frontmatter}\n---\n`;
41
+ writeFileSync(path, `${yaml}${body}\n`, 'utf8');
42
+ }
43
+ function writeStore(owner, namespace) {
44
+ const frontmatter = namespace === undefined ? undefined : `namespace: ${namespace}`;
45
+ writeDoc(join(owner, '.crouter', 'memory', 'INDEX.md'), 'STORE INDEX', frontmatter);
46
+ }
47
+ function initGit(repository) {
48
+ mkdirSync(repository, { recursive: true });
49
+ assert.equal(gitSync(['-C', repository, 'init', '-q'], undefined).status, 0, 'git init succeeds');
50
+ }
51
+ function commitInitial(repository) {
52
+ writeFileSync(join(repository, 'README.md'), 'fixture\n', 'utf8');
53
+ assert.equal(gitSync(['-C', repository, 'add', 'README.md'], undefined).status, 0);
54
+ assert.equal(gitSync(['-C', repository, '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', 'commit', '-qm', 'initial'], undefined).status, 0);
55
+ }
56
+ function target(cwd, profileId = null) {
57
+ return { cwd, profileId, nodeId: null };
58
+ }
59
+ test('a Git root declaration gives a metadata-free nested store its repository-relative prefix', () => {
60
+ const repository = join(process.env.HOME, 'repository');
61
+ const nested = join(repository, 'packages', 'agent');
62
+ initGit(repository);
63
+ writeStore(repository, 'acme');
64
+ mkdirSync(join(nested, '.crouter', 'memory'), { recursive: true });
65
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
66
+ const doc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested), { scope: 'project' });
67
+ assert.equal(doc.path, join(nested, '.crouter', 'memory', 'guide.md'));
68
+ assert.equal(doc.name, 'acme/packages/agent/guide');
69
+ const store = openProjectMemoryStore(nested);
70
+ assert.equal(store.mountStatus, 'ready');
71
+ assert.equal(store.repositoryRoot, repository);
72
+ assert.equal(store.ownerRelativePath, 'packages/agent');
73
+ assert.equal(store.namespacePath, join(repository, '.crouter', 'memory', 'INDEX.md'));
74
+ });
75
+ test('repository-relative ordering prefixes are stripped before nested identity composition', () => {
76
+ const repository = join(process.env.HOME, 'repository');
77
+ const nested = join(repository, '01-api');
78
+ initGit(repository);
79
+ writeStore(repository, 'acme');
80
+ writeStore(nested);
81
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'ORDERED GUIDE');
82
+ const store = openProjectMemoryStore(nested);
83
+ assert.equal(store.mountStatus, 'ready');
84
+ assert.equal(store.ownerRelativePath, 'api');
85
+ assert.equal(store.namespace, 'acme/api');
86
+ const doc = resolveMemoryDocForTarget('acme/api/guide', target(nested), { scope: 'project' });
87
+ assert.equal(doc.path, join(nested, '.crouter', 'memory', 'guide.md'));
88
+ assert.equal(doc.name, 'acme/api/guide');
89
+ });
90
+ test('a metadata-free nested INDEX answers exactly at the nested effective prefix', () => {
91
+ const repository = join(process.env.HOME, 'repository');
92
+ const nested = join(repository, 'packages', 'agent');
93
+ initGit(repository);
94
+ writeStore(repository, 'acme');
95
+ writeStore(nested);
96
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
97
+ const store = openProjectMemoryStore(nested);
98
+ assert.equal(store.mountStatus, 'ready');
99
+ assert.equal(store.namespace, 'acme/packages/agent');
100
+ const view = loadMemoryStoreView(store, true);
101
+ assert.deepEqual(view.docs.map((doc) => doc.name), ['acme/packages/agent', 'acme/packages/agent/guide']);
102
+ assert.equal(resolveMemoryDocForTarget('acme/packages/agent', target(nested), { scope: 'project' }).path, join(nested, '.crouter', 'memory', 'INDEX.md'));
103
+ });
104
+ test('linked worktree owners have identical canonical names from different physical paths', () => {
105
+ const main = join(process.env.HOME, 'main');
106
+ const linked = join(process.env.HOME, 'linked');
107
+ initGit(main);
108
+ commitInitial(main);
109
+ assert.equal(gitSync(['-C', main, 'worktree', 'add', '-q', '-b', 'linked', linked], undefined).status, 0);
110
+ const mainOwner = join(main, 'packages', 'agent');
111
+ const linkedOwner = join(linked, 'packages', 'agent');
112
+ writeStore(main, 'acme');
113
+ writeStore(linked, 'acme');
114
+ writeStore(mainOwner);
115
+ writeStore(linkedOwner);
116
+ writeDoc(join(mainOwner, '.crouter', 'memory', 'guide.md'), 'MAIN GUIDE');
117
+ writeDoc(join(linkedOwner, '.crouter', 'memory', 'guide.md'), 'LINKED GUIDE');
118
+ const mainDoc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(mainOwner), { scope: 'project' });
119
+ const linkedDoc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(linkedOwner), { scope: 'project' });
120
+ assert.equal(mainDoc.name, linkedDoc.name);
121
+ assert.equal(mainDoc.path, join(mainOwner, '.crouter', 'memory', 'guide.md'));
122
+ assert.equal(linkedDoc.path, join(linkedOwner, '.crouter', 'memory', 'guide.md'));
123
+ const mainStore = openProjectMemoryStore(mainOwner);
124
+ const linkedStore = openProjectMemoryStore(linkedOwner);
125
+ assert.equal(mainStore.repositoryKey, linkedStore.repositoryKey);
126
+ assert.notEqual(mainStore.repositoryRoot, linkedStore.repositoryRoot);
127
+ });
128
+ test('an embedded Git repository uses its own declaration from cwd and explicit store opening', () => {
129
+ const outer = join(process.env.HOME, 'outer');
130
+ const embedded = join(outer, 'vendor');
131
+ const nested = join(embedded, 'pkg');
132
+ initGit(outer);
133
+ writeStore(outer, 'outer');
134
+ initGit(embedded);
135
+ writeStore(embedded, 'inner');
136
+ writeStore(nested);
137
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'EMBEDDED GUIDE');
138
+ const fromCwd = resolveMemoryDocForTarget('inner/pkg/guide', target(nested), { scope: 'project' });
139
+ assert.equal(fromCwd.path, join(nested, '.crouter', 'memory', 'guide.md'));
140
+ const explicit = openProjectMemoryStore(nested);
141
+ assert.equal(explicit.repositoryRoot, embedded);
142
+ assert.equal(explicit.namespace, 'inner/pkg');
143
+ assert.equal(loadMemoryStoreView(explicit, true).resolve('inner/pkg/guide').path, fromCwd.path);
144
+ });
145
+ test('a non-Git nested store derives from the outermost ancestor project store', () => {
146
+ const root = join(process.env.HOME, 'non-git');
147
+ const nested = join(root, 'packages', 'agent');
148
+ writeStore(root, 'workspace');
149
+ writeStore(nested);
150
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NON-GIT GUIDE');
151
+ const store = openProjectMemoryStore(nested);
152
+ assert.equal(store.mountStatus, 'ready');
153
+ assert.equal(store.repositoryRoot, root);
154
+ assert.equal(store.repositoryKey, `path:${root}`);
155
+ assert.equal(store.ownerRelativePath, 'packages/agent');
156
+ assert.equal(store.namespace, 'workspace/packages/agent');
157
+ assert.equal(resolveMemoryDocForTarget('workspace/packages/agent/guide', target(nested), { scope: 'project' }).path, join(nested, '.crouter', 'memory', 'guide.md'));
158
+ });
159
+ test('a Git descendant without a root store is undeclared until the root declaration is created', () => {
160
+ const repository = join(process.env.HOME, 'repository');
161
+ const nested = join(repository, 'packages', 'agent');
162
+ initGit(repository);
163
+ writeStore(nested);
164
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
165
+ const before = openProjectMemoryStore(nested);
166
+ assert.equal(before.mountStatus, 'undeclared');
167
+ assert.equal(before.mountFailure, 'undeclared-root');
168
+ assert.match(before.diagnostic ?? '', new RegExp(repository.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
169
+ writeStore(repository, 'acme');
170
+ const after = openProjectMemoryStore(nested);
171
+ assert.equal(after.mountStatus, 'ready');
172
+ assert.equal(after.namespace, 'acme/packages/agent');
173
+ });
174
+ test('sibling owner paths that normalize to one prefix, including ordering prefixes, leave both stores invalid', () => {
175
+ const repository = join(process.env.HOME, 'repository');
176
+ const first = join(repository, '01-My Notes');
177
+ const second = join(repository, 'my-notes');
178
+ initGit(repository);
179
+ writeStore(repository, 'acme');
180
+ writeStore(first);
181
+ writeStore(second);
182
+ const profileId = createProfile('collision', [{ path: second, memory: 'content' }]).profileId;
183
+ const stores = memoryStoresInPrecedence(target(first, profileId), 'project');
184
+ const colliding = stores.filter((store) => store.ownerDir === first || store.ownerDir === second);
185
+ assert.equal(colliding.length, 2);
186
+ for (const store of colliding) {
187
+ assert.equal(store.mountStatus, 'invalid');
188
+ assert.equal(store.mountFailure, 'collision');
189
+ assert.equal(store.namespace, 'acme/my-notes');
190
+ assert.match(store.diagnostic ?? '', /My Notes/);
191
+ assert.match(store.diagnostic ?? '', /my-notes/);
192
+ }
193
+ });
194
+ test('profile presence and order do not change a nested store canonical name', () => {
195
+ const repository = join(process.env.HOME, 'repository');
196
+ const nested = join(repository, 'packages', 'agent');
197
+ const unrelated = join(process.env.HOME, 'unrelated');
198
+ initGit(repository);
199
+ writeStore(repository, 'acme');
200
+ writeStore(nested);
201
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
202
+ mkdirSync(unrelated, { recursive: true });
203
+ const absent = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested), { scope: 'project' });
204
+ const rootSecond = createProfile('root-second', [
205
+ { path: unrelated, memory: 'content' },
206
+ { path: repository, memory: 'content' },
207
+ ]).profileId;
208
+ const rootFirst = createProfile('root-first', [
209
+ { path: repository, memory: 'content' },
210
+ { path: unrelated, memory: 'content' },
211
+ ]).profileId;
212
+ const withRootSecond = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested, rootSecond), { scope: 'project' });
213
+ const withRootFirst = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested, rootFirst), { scope: 'project' });
214
+ assert.equal(absent.name, 'acme/packages/agent/guide');
215
+ assert.equal(withRootSecond.name, absent.name);
216
+ assert.equal(withRootFirst.name, absent.name);
217
+ assert.equal(withRootSecond.path, absent.path);
218
+ assert.equal(withRootFirst.path, absent.path);
219
+ });
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -9,7 +9,9 @@ import { loadedCommandPreview } from '../../../clients/attach/slash/prompt-previ
9
9
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
10
10
  const EXTENSION = join(ROOT, 'dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts');
11
11
  const LIVE_PROBE = join(ROOT, 'dist/core/__tests__/fixtures/memory-slash-live-probe.js');
12
- const COMMAND_NAMES = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
12
+ const COMMAND_LOCALS = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
13
+ const PROJECT_NAMESPACE = 'workspace';
14
+ const COMMAND_NAMES = COMMAND_LOCALS.map((name) => `${PROJECT_NAMESPACE}:workspace:${name}`);
13
15
  function writeSlashDoc(memoryDir, name, description, body) {
14
16
  writeFileSync(join(memoryDir, `${name}.md`), `---\nkind: knowledge\nwhen-and-why-to-read: Acceptance fixture.\nshort-form: ${description}\nslash: true\n---\n${body}`);
15
17
  }
@@ -60,15 +62,19 @@ function fixtureRows(rows) {
60
62
  return row;
61
63
  });
62
64
  }
63
- test('node-relative memory slash commands omit preview metadata without changing live sends', async () => {
64
- const cwd = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
65
- const home = join(cwd, 'canvas');
65
+ test('node-relative memory slash commands omit preview metadata and register invoked bodies at content', async () => {
66
+ const repositoryRoot = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
67
+ const cwd = join(repositoryRoot, 'workspace');
68
+ const home = join(repositoryRoot, 'canvas');
66
69
  const memoryDir = join(cwd, '.crouter', 'memory');
70
+ const repositoryMemory = join(repositoryRoot, '.crouter', 'memory');
67
71
  const nodeId = 'acceptance-live-node';
68
72
  mkdirSync(memoryDir, { recursive: true });
69
- writeSlashDoc(memoryDir, COMMAND_NAMES[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
70
- writeSlashDoc(memoryDir, COMMAND_NAMES[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
71
- writeSlashDoc(memoryDir, COMMAND_NAMES[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
73
+ mkdirSync(repositoryMemory, { recursive: true });
74
+ writeFileSync(join(repositoryMemory, 'INDEX.md'), `---\nnamespace: ${PROJECT_NAMESPACE}\n---\nWorkspace front door.\n`);
75
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
76
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
77
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
72
78
  try {
73
79
  const live = await liveProbe({
74
80
  cwd,
@@ -83,6 +89,9 @@ test('node-relative memory slash commands omit preview metadata without changing
83
89
  const prospectiveRows = fixtureRows(live.prospective.commands);
84
90
  const liveRows = fixtureRows(live.commands);
85
91
  assert.deepEqual(liveRows, prospectiveRows);
92
+ for (const local of COMMAND_LOCALS) {
93
+ assert.equal(live.commands.some((row) => row.name === local), false, `/${local} is not retained as an old command`);
94
+ }
86
95
  assert.deepEqual(prospectiveRows, [
87
96
  { name: COMMAND_NAMES[0], description: 'Context-relative fixture.', source: 'command' },
88
97
  { name: COMMAND_NAMES[1], description: 'Node-relative fixture.', source: 'command' },
@@ -101,8 +110,9 @@ test('node-relative memory slash commands omit preview metadata without changing
101
110
  assert.ok(insightsInit, 'Gateway completion includes /insights:init');
102
111
  assert.equal(insightsInit.expansion, undefined);
103
112
  assert.equal(loadedCommandPreview('/insights:init agent-facing CLI', live.commands), undefined);
104
- assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands)?.content, 'Independent payload: first evidence.\n');
105
- assert.equal(loadedCommandPreview('/acceptance-independent changed evidence', live.commands)?.content, 'Independent payload: changed evidence.\n');
113
+ assert.equal(loadedCommandPreview(`/${COMMAND_NAMES[2]} first evidence`, live.commands)?.content, 'Independent payload: first evidence.\n');
114
+ assert.equal(loadedCommandPreview(`/${COMMAND_NAMES[2]} changed evidence`, live.commands)?.content, 'Independent payload: changed evidence.\n');
115
+ assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands), undefined);
106
116
  assert.deepEqual(live.notifications, []);
107
117
  const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
108
118
  assert.deepEqual(sentContent, {
@@ -120,8 +130,14 @@ test('node-relative memory slash commands omit preview metadata without changing
120
130
  }
121
131
  assert.doesNotMatch(sentContent[COMMAND_NAMES[0]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
122
132
  assert.doesNotMatch(sentContent[COMMAND_NAMES[1]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
133
+ const persisted = JSON.parse(readFileSync(join(home, 'nodes', nodeId, 'injected-docs.json'), 'utf8'));
134
+ assert.equal(persisted.v, 3);
135
+ for (const name of COMMAND_LOCALS) {
136
+ const path = realpathSync(join(memoryDir, `${name}.md`));
137
+ assert.deepEqual(persisted.exposures[path], { transcript: 3 }, `/${name} registers the invoked document body as transcript content`);
138
+ }
123
139
  }
124
140
  finally {
125
- rmSync(cwd, { recursive: true, force: true });
141
+ rmSync(repositoryRoot, { recursive: true, force: true });
126
142
  }
127
143
  });
@@ -0,0 +1,40 @@
1
+ // The warm-pool claim must leave a FROZEN preference snapshot behind.
2
+ //
3
+ // A spare boots with no claimant, so canvas-context-intro skips it and
4
+ // canvas-doc-substrate's before_agent_start is the only other freezer — and Pi
5
+ // emits that event on the typed-prompt path only. A claimed spare whose first
6
+ // turn is a slash command (`sendMessage({ triggerTurn: true })`) therefore
7
+ // reached the persona gate with no snapshot and died with "context preference
8
+ // snapshot is missing". Claim time is where the snapshot is frozen.
9
+ //
10
+ // Run: node --import tsx/esm --test src/core/__tests__/warm-claim-preference-snapshot.test.ts
11
+ import { test, before, beforeEach, after } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { closeDb } from '../canvas/db.js';
17
+ import { spawnNode } from '../runtime/nodes.js';
18
+ import { deliverBearingsLive } from '../runtime/deliver-live.js';
19
+ import { loadContextExposureState } from '../substrate/injected-store.js';
20
+ let home;
21
+ before(() => {
22
+ home = mkdtempSync(join(tmpdir(), 'crtr-warm-snapshot-'));
23
+ process.env['CRTR_HOME'] = home;
24
+ });
25
+ beforeEach(() => {
26
+ closeDb();
27
+ rmSync(home, { recursive: true, force: true });
28
+ mkdirSync(home, { recursive: true });
29
+ });
30
+ after(() => {
31
+ closeDb();
32
+ rmSync(home, { recursive: true, force: true });
33
+ delete process.env['CRTR_HOME'];
34
+ });
35
+ test('the claim path persists a frozen preference snapshot', async () => {
36
+ const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
37
+ assert.equal(loadContextExposureState(meta.node_id).preferenceSnapshot, null, 'a spare mints with nothing frozen');
38
+ await deliverBearingsLive(meta.node_id);
39
+ assert.notEqual(loadContextExposureState(meta.node_id).preferenceSnapshot, null, 'claim freezes the snapshot the persona gate requires');
40
+ });
@@ -5,6 +5,9 @@ export declare function createNode(meta: NodeMeta): NodeMeta;
5
5
  /** The canonical node record: durable identity (meta.json) ∪ authoritative
6
6
  * runtime (the row). Null if unknown. */
7
7
  export declare function getNode(nodeId: string): NodeMeta | null;
8
+ export declare function canonicalSessionFile(sessionFile: string): string;
9
+ /** Resolve the crouter node that owns one exact Pi session file. */
10
+ export declare function findNodeBySessionFile(sessionFile: string): NodeMeta | null;
8
11
  /** The indexed row (from the db) — cheap for queries that don't need full meta. */
9
12
  export declare function getRow(nodeId: string): NodeRow | null;
10
13
  /** The node row whose durable LOCATION pane is `pane`, or null. Lets placement
@@ -4,8 +4,8 @@
4
4
  // Source-of-truth split: a node's meta.json is canonical for its own fields;
5
5
  // the db row is a queryable index re-derivable from it. The subscribes_to edges
6
6
  // are db-authoritative (mutable, many-writers — what WAL is for).
7
- import { existsSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
8
- import { join } from 'node:path';
7
+ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
9
  import { openDb, withCanvasWrite } from './db.js';
10
10
  import { withMetaLock, readMetaObject, writeMetaObjectLocked, writeJsonFileDurably } from './meta-file.js';
11
11
  import { isPidAlive, recordedPidLiveness, capturePidIdentities, identitiesMatch, isLegacyTicksIdentity, isBootScopedIdentity, } from './pid.js';
@@ -22,7 +22,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
22
22
  * can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
23
23
  const IDENTITY_KEYS = [
24
24
  'node_id', 'name', 'description', 'icon', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
25
- 'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'review_binding', 'passive_default', 'pi_session_id',
25
+ 'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'fork_source_file', 'review_binding', 'passive_default', 'pi_session_id',
26
26
  'pi_session_file', 'cycle_pending', 'model_override',
27
27
  'launch', 'managed_worktree',
28
28
  ];
@@ -33,6 +33,10 @@ function toIdentity(m) {
33
33
  if (m[k] !== undefined)
34
34
  out[k] = m[k];
35
35
  }
36
+ for (const key of ['pi_session_file', 'fork_source_file']) {
37
+ if (typeof out[key] === 'string')
38
+ out[key] = canonicalSessionFile(out[key]);
39
+ }
36
40
  return out;
37
41
  }
38
42
  function readMeta(nodeId) {
@@ -83,13 +87,14 @@ function writeMeta(meta) {
83
87
  * state. A fresh insert takes the schema defaults for runtime. */
84
88
  function upsertRow(meta) {
85
89
  openDb()
86
- .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created)
87
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
90
+ .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file)
91
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
92
  ON CONFLICT(node_id) DO UPDATE SET
89
93
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
90
94
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
91
- profile_id=excluded.profile_id, parent=excluded.parent`)
92
- .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);
95
+ profile_id=excluded.profile_id, parent=excluded.parent,
96
+ pi_session_file=excluded.pi_session_file`)
97
+ .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, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null);
93
98
  }
94
99
  /** Seed a node's row at BIRTH: identity columns + runtime columns taken from the
95
100
  * incoming meta (defaults: status='active', the rest null). The only writer
@@ -98,18 +103,19 @@ function upsertRow(meta) {
98
103
  function seedRow(meta) {
99
104
  openDb()
100
105
  .prepare(`INSERT INTO nodes
101
- (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created,
106
+ (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file,
102
107
  status, intent, waiting_for, final_report, finalized_at, pi_pid, "window", tmux_session, pane)
103
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
104
109
  ON CONFLICT(node_id) DO UPDATE SET
105
110
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
106
111
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
107
112
  profile_id=excluded.profile_id, parent=excluded.parent,
113
+ pi_session_file=excluded.pi_session_file,
108
114
  status=excluded.status, intent=excluded.intent, waiting_for=excluded.waiting_for,
109
115
  final_report=excluded.final_report, finalized_at=excluded.finalized_at,
110
116
  pi_pid=excluded.pi_pid, "window"=excluded."window", tmux_session=excluded.tmux_session,
111
117
  pane=excluded.pane`)
112
- .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.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
118
+ .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, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
113
119
  }
114
120
  function rowFrom(r) {
115
121
  return {
@@ -179,6 +185,20 @@ export function getNode(nodeId) {
179
185
  // exists yet (rare — pre-rebuild), keep whatever the meta carried.
180
186
  return { ...ident, ...(rt ?? {}) };
181
187
  }
188
+ export function canonicalSessionFile(sessionFile) {
189
+ const absolute = resolve(sessionFile);
190
+ return existsSync(absolute) ? realpathSync(absolute) : absolute;
191
+ }
192
+ /** Resolve the crouter node that owns one exact Pi session file. */
193
+ export function findNodeBySessionFile(sessionFile) {
194
+ const canonical = canonicalSessionFile(sessionFile);
195
+ const matches = openDb()
196
+ .prepare('SELECT node_id FROM nodes WHERE pi_session_file = ? ORDER BY created')
197
+ .all(canonical);
198
+ if (matches.length > 1)
199
+ throw new Error(`multiple nodes own pi session file: ${canonical}`);
200
+ return matches[0] === undefined ? null : getNode(matches[0].node_id);
201
+ }
182
202
  /** The indexed row (from the db) — cheap for queries that don't need full meta. */
183
203
  export function getRow(nodeId) {
184
204
  const r = openDb()
@@ -1175,6 +1175,27 @@ CREATE TABLE human_action_deliveries (
1175
1175
  CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
1176
1176
  `);
1177
1177
  }
1178
+ /** v34 — index the exact Pi session file coordinate used to recover fork
1179
+ * exposure provenance without hydrating every node meta. Meta remains authoritative;
1180
+ * normal identity upserts keep this projection current after the one-time backfill. */
1181
+ function addNodeSessionFileIndex(db) {
1182
+ const columns = nodeColumns(db);
1183
+ addNodeColumnIfMissing(db, columns, 'pi_session_file', 'ALTER TABLE nodes ADD COLUMN pi_session_file TEXT;');
1184
+ for (const entry of readdirSync(nodesRoot(), { withFileTypes: true })) {
1185
+ if (!entry.isDirectory())
1186
+ continue;
1187
+ try {
1188
+ const meta = JSON.parse(readFileSync(nodeMetaPath(entry.name), 'utf8'));
1189
+ if (typeof meta.pi_session_file === 'string') {
1190
+ db.prepare('UPDATE nodes SET pi_session_file = ? WHERE node_id = ?').run(meta.pi_session_file, entry.name);
1191
+ }
1192
+ }
1193
+ catch {
1194
+ // Corrupt node metadata is ignored by the ordinary canvas rebuild too.
1195
+ }
1196
+ }
1197
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);');
1198
+ }
1178
1199
  /** The journaled lane of the state-migration registry, adapted to this chain's
1179
1200
  * step shape. Registry order is preserved, so these versions stay stable as
1180
1201
  * long as the registry is append-only (same contract as the local list). */
@@ -1221,6 +1242,7 @@ export const MIGRATIONS = [
1221
1242
  /* v31 */ addReviewSubmitRequested,
1222
1243
  /* v32 */ addCronHeldColumns,
1223
1244
  /* v33 */ addHumanActionDeliveries,
1245
+ /* v34 */ addNodeSessionFileIndex,
1224
1246
  ...JOURNALED_STATE_MIGRATIONS,
1225
1247
  ];
1226
1248
  /** Migration indexes that manage their OWN transaction and therefore must not
@@ -1348,11 +1370,16 @@ export function openDb() {
1348
1370
  ensureHome();
1349
1371
  recordOpenLedger();
1350
1372
  const db = new DatabaseSync(path);
1373
+ // FIRST, before any pragma that can contend. `journal_mode = WAL` takes a
1374
+ // brief exclusive lock, so on the default zero busy_timeout a cold open that
1375
+ // races a concurrent writer throws `database is locked` outright — crtrd, a
1376
+ // CLI child and a broker all open this same file, and every one of them opens
1377
+ // it cold. Setting the wait window first makes that open wait its turn.
1378
+ db.exec('PRAGMA busy_timeout = 5000;');
1351
1379
  db.exec('PRAGMA journal_mode = WAL;');
1352
1380
  // Load-bearing as of migration v4: the edges→nodes FK (ON DELETE CASCADE)
1353
1381
  // needs this ON at the deleting connection so a node delete reaps its edges.
1354
1382
  db.exec('PRAGMA foreign_keys = ON;');
1355
- db.exec('PRAGMA busy_timeout = 5000;');
1356
1383
  migrate(db);
1357
1384
  handles.set(path, db);
1358
1385
  return db;
@@ -10,6 +10,8 @@ export declare function crtrHome(): string;
10
10
  * local disk — so the db shares the node data's location and survives a
11
11
  * sandbox recreate together with `nodes/`. */
12
12
  export declare function canvasDbPath(): string;
13
+ export declare function onDiskMigrationLockPath(): string;
14
+ export declare function onDiskMigrationMarkerPath(): string;
13
15
  /** Absolute path to a node's broker `view.sock` — always `view.sock` under the
14
16
  * node dir. Stale socket files (a host reboot, a sandbox recreate on a
15
17
  * durable volume) are harmless: the broker unlinks any existing socket before
@@ -73,12 +75,8 @@ export declare function situationalContextPath(nodeId: string): string;
73
75
  export declare function passivePath(nodeId: string): string;
74
76
  export declare function transcriptPath(nodeId: string): string;
75
77
  export declare function sessionPtrPath(nodeId: string): string;
76
- /** On-read injection dedup set — the doc realpaths already surfaced on-read in
77
- * this node's CURRENT conversation transcript. Persisted so the once-per-
78
- * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
79
- * the same .jsonl transcript in a NEW pi process, so the in-memory set would
80
- * otherwise start empty and re-inject docs already present in the transcript.
81
- * Deleted by the launch paths that start a FRESH transcript. */
78
+ /** Frozen preference snapshot and rank-aware exposure state for the node's
79
+ * currently loaded LLM context. The filename remains stable across schema versions. */
82
80
  export declare function injectedDocsPath(nodeId: string): string;
83
81
  /** Create the full directory skeleton for a node. Idempotent. */
84
82
  export declare function ensureNodeDirs(nodeId: string): void;
@@ -36,6 +36,12 @@ export function crtrHome() {
36
36
  export function canvasDbPath() {
37
37
  return join(crtrHome(), 'canvas.db');
38
38
  }
39
+ export function onDiskMigrationLockPath() {
40
+ return join(crtrHome(), 'on-disk-migration.lock');
41
+ }
42
+ export function onDiskMigrationMarkerPath() {
43
+ return join(crtrHome(), 'on-disk-migrations.json');
44
+ }
39
45
  /** Absolute path to a node's broker `view.sock` — always `view.sock` under the
40
46
  * node dir. Stale socket files (a host reboot, a sandbox recreate on a
41
47
  * durable volume) are harmless: the broker unlinks any existing socket before
@@ -159,12 +165,8 @@ export function transcriptPath(nodeId) {
159
165
  export function sessionPtrPath(nodeId) {
160
166
  return join(nodeDir(nodeId), 'session.ptr');
161
167
  }
162
- /** On-read injection dedup set — the doc realpaths already surfaced on-read in
163
- * this node's CURRENT conversation transcript. Persisted so the once-per-
164
- * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
165
- * the same .jsonl transcript in a NEW pi process, so the in-memory set would
166
- * otherwise start empty and re-inject docs already present in the transcript.
167
- * Deleted by the launch paths that start a FRESH transcript. */
168
+ /** Frozen preference snapshot and rank-aware exposure state for the node's
169
+ * currently loaded LLM context. The filename remains stable across schema versions. */
168
170
  export function injectedDocsPath(nodeId) {
169
171
  return join(nodeDir(nodeId), 'injected-docs.json');
170
172
  }
@@ -20,7 +20,7 @@ import { jobDir, contextDir } from './paths.js';
20
20
  import { isPidAlive } from './pid.js';
21
21
  import { resolveNodeVisual, faultSummary } from './status-glyph.js';
22
22
  import { readFault } from '../runtime/fault.js';
23
- import { isLegacyRuntimeText, parseCard } from '../../shared/generated-context.js';
23
+ import { parseCard } from '../../shared/generated-context.js';
24
24
  // ---------------------------------------------------------------------------
25
25
  // Active-fault overlay: stale daemon-owned provider faults are hidden once the
26
26
  // broker pid is gone; manual/client faults stay visible until cleared.
@@ -192,7 +192,7 @@ export function readSessionParts(sessionFile) {
192
192
  const text = extractUserText(rec.message.content);
193
193
  if (text === '')
194
194
  continue;
195
- if (parseCard({ role: 'user', content: text }) !== null || isLegacyRuntimeText(text))
195
+ if (parseCard({ role: 'user', content: text }) !== null)
196
196
  continue;
197
197
  const capped = text.length > CONVO_MSG_CAP ? text.slice(0, CONVO_MSG_CAP) : text;
198
198
  parts.push(capped);
@@ -113,13 +113,13 @@ export interface NodeIdentity {
113
113
  kind: string;
114
114
  mode: Mode;
115
115
  lifecycle: Lifecycle;
116
- /** The last persona state {mode,lifecycle} the node was GIVEN transition
117
- * guidance for. Meta-only (not a db column). Born equal to the node's initial
118
- * {mode,lifecycle} at spawn so a fresh node never gets spurious guidance. The
119
- * persona injector (runtime/persona.ts) compares the live {mode,lifecycle}
120
- * against this and, on drift, injects guidance for the new state then commits
121
- * it here. */
116
+ /** The last {kind,mode,lifecycle} whose context delivery was acknowledged.
117
+ * Meta-only (not a db column). Born equal to the initial persona; later
118
+ * changes advance only after a baseline or provider-gated transition is
119
+ * durably registered. Historical rows may omit `kind`; reads resolve it to
120
+ * the current kind so they do not fabricate a transition. */
122
121
  persona_ack?: {
122
+ kind?: string;
123
123
  mode: Mode;
124
124
  lifecycle: Lifecycle;
125
125
  };
@@ -130,14 +130,10 @@ export interface NodeIdentity {
130
130
  * `parent` so an INDEPENDENT root (parent=null) still records its lineage.
131
131
  * Audit only; null for a user-opened root. Defaults to `parent` for a child. */
132
132
  spawned_by?: string | null;
133
- /** Fork provenance — the `--fork-from` source this node was forked from at
134
- * spawn (the node id / session path / pi session uuid the caller passed). pi
135
- * COPIED that conversation into this node's first session, so the node boots
136
- * carrying the SOURCE's first-person history. Persisted here so the boot
137
- * intro can assert this node's OWN identity over that inherited narrative
138
- * (without it a fork impersonates its source). Undefined for a fresh node;
139
- * identity/audit only. */
133
+ /** Raw `--fork-from` operand retained for identity framing and audit. */
140
134
  fork_from?: string | null;
135
+ /** Resolved absolute source session file used for every pre-bind fork retry. */
136
+ fork_source_file?: string | null;
141
137
  /** Immutable review-companion binding. Undefined for ordinary nodes. */
142
138
  review_binding?: ReviewBinding | null;
143
139
  /** New subscriptions this node opens default to passive when true. */
@@ -12,3 +12,5 @@ export interface ExclusiveLockOptions {
12
12
  * however long its operation runs.
13
13
  */
14
14
  export declare function withExclusiveDirectoryLock<T>(path: string, operation: () => T, options?: ExclusiveLockOptions): T;
15
+ /** Run an awaited operation under the same token-checked directory lock. */
16
+ export declare function withExclusiveDirectoryLockAsync<T>(path: string, operation: () => Promise<T>, options?: ExclusiveLockOptions): Promise<T>;