@north-light/crouter 0.3.207 → 0.3.209

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 (172) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/api/dto/broker-ops.d.ts +2 -12
  4. package/dist/api/dto/nodes.d.ts +16 -0
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +3 -2
  8. package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
  9. package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
  10. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
  11. package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
  12. package/dist/builtin-memory/04-base-worker.md +3 -2
  13. package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
  14. package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
  15. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
  16. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
  17. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
  18. package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
  19. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
  20. package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
  21. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
  22. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
  23. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
  31. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
  32. package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
  33. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
  34. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
  35. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
  36. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
  37. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
  38. package/dist/builtin-memory/advisor/council.md +0 -2
  39. package/dist/builtin-memory/design.md +3 -2
  40. package/dist/builtin-memory/development.md +3 -2
  41. package/dist/builtin-memory/insights/capture.md +1 -3
  42. package/dist/builtin-memory/insights/init.md +1 -3
  43. package/dist/builtin-memory/insights/listen.md +3 -2
  44. package/dist/builtin-memory/internal/INDEX.md +5 -4
  45. package/dist/builtin-memory/internal/agent-shaping.md +5 -4
  46. package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
  47. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
  48. package/dist/builtin-memory/internal/marketplaces.md +3 -2
  49. package/dist/builtin-memory/internal/memory-loading.md +31 -20
  50. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
  51. package/dist/builtin-memory/internal/plugins.md +7 -6
  52. package/dist/builtin-memory/internal/storage-tiers.md +3 -2
  53. package/dist/builtin-memory/plan/roadmap.md +3 -2
  54. package/dist/builtin-memory/spec/guide.md +0 -2
  55. package/dist/builtin-memory/spec/requirements.md +0 -2
  56. package/dist/builtin-memory/spec/roadmap.md +3 -2
  57. package/dist/builtin-memory/testing.md +1 -3
  58. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
  62. package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
  63. package/dist/clients/attach/render/card-presentation.d.ts +31 -0
  64. package/dist/clients/attach/render/card-presentation.js +163 -0
  65. package/dist/clients/attach/render/chat-view.d.ts +0 -2
  66. package/dist/clients/attach/render/chat-view.js +12 -23
  67. package/dist/clients/attach/render/context-message.d.ts +5 -9
  68. package/dist/clients/attach/render/context-message.js +20 -43
  69. package/dist/clients/attach/render/group-activity.d.ts +28 -4
  70. package/dist/clients/attach/render/group-activity.js +65 -11
  71. package/dist/clients/attach/render/group-recap.d.ts +3 -0
  72. package/dist/clients/attach/render/group-recap.js +17 -0
  73. package/dist/clients/attach/viewer.js +516 -516
  74. package/dist/commands/memory/delete.js +3 -18
  75. package/dist/commands/memory/edit.js +23 -30
  76. package/dist/commands/memory/history.js +1 -1
  77. package/dist/commands/memory/lint.d.ts +7 -8
  78. package/dist/commands/memory/lint.js +178 -132
  79. package/dist/commands/memory/list.d.ts +0 -1
  80. package/dist/commands/memory/list.js +2 -12
  81. package/dist/commands/memory/move.d.ts +1 -0
  82. package/dist/commands/memory/move.js +195 -0
  83. package/dist/commands/memory/read.js +135 -141
  84. package/dist/commands/memory/shared.d.ts +18 -17
  85. package/dist/commands/memory/shared.js +93 -39
  86. package/dist/commands/memory/write.js +23 -33
  87. package/dist/commands/memory.js +5 -4
  88. package/dist/commands/pkg/browse/catalog.js +2 -4
  89. package/dist/commands/pkg/browse/doc-view.js +17 -11
  90. package/dist/commands/pkg/browse/model.d.ts +7 -9
  91. package/dist/commands/sys/migrate.d.ts +1 -0
  92. package/dist/commands/sys/migrate.js +106 -0
  93. package/dist/commands/sys/sync-deps.js +5 -10
  94. package/dist/commands/sys/sync-project-guidance.js +36 -16
  95. package/dist/commands/sys/sync-skills.js +8 -4
  96. package/dist/commands/sys.js +3 -2
  97. package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
  98. package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
  99. package/dist/core/__tests__/helpers/broker-clients.js +7 -2
  100. package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
  101. package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
  102. package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
  103. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
  104. package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
  105. package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
  106. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
  107. package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
  108. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
  109. package/dist/core/canvas/db.d.ts +3 -1
  110. package/dist/core/canvas/db.js +12 -2
  111. package/dist/core/memory/history.d.ts +4 -1
  112. package/dist/core/memory/history.js +1 -0
  113. package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
  114. package/dist/core/memory/inline-ref-inventory.js +23 -19
  115. package/dist/core/memory-resolver.d.ts +4 -4
  116. package/dist/core/memory-resolver.js +16 -43
  117. package/dist/core/runtime/bearings.d.ts +4 -3
  118. package/dist/core/runtime/bearings.js +4 -4
  119. package/dist/core/runtime/broker/fault-retry.js +2 -7
  120. package/dist/core/runtime/broker/frame-dispatch.js +1 -1
  121. package/dist/core/runtime/broker/tool-groups.js +7 -6
  122. package/dist/core/runtime/broker-extension-render.d.ts +3 -2
  123. package/dist/core/runtime/broker-extension-render.js +3 -3
  124. package/dist/core/runtime/broker.js +4 -4
  125. package/dist/core/runtime/kickoff.js +4 -10
  126. package/dist/core/runtime/memory.js +2 -3
  127. package/dist/core/runtime/node-read.js +7 -1
  128. package/dist/core/runtime/stop-guard.js +3 -3
  129. package/dist/core/substrate/index.d.ts +7 -4
  130. package/dist/core/substrate/index.js +6 -4
  131. package/dist/core/substrate/injected-store.d.ts +24 -12
  132. package/dist/core/substrate/injected-store.js +80 -33
  133. package/dist/core/substrate/listings.d.ts +21 -0
  134. package/dist/core/substrate/listings.js +88 -0
  135. package/dist/core/substrate/on-read-node.d.ts +5 -5
  136. package/dist/core/substrate/on-read-node.js +4 -5
  137. package/dist/core/substrate/on-read.d.ts +25 -4
  138. package/dist/core/substrate/on-read.js +81 -102
  139. package/dist/core/substrate/render-node.d.ts +5 -2
  140. package/dist/core/substrate/render-node.js +5 -3
  141. package/dist/core/substrate/render.d.ts +9 -8
  142. package/dist/core/substrate/render.js +104 -96
  143. package/dist/core/substrate/schema.d.ts +34 -18
  144. package/dist/core/substrate/schema.js +75 -32
  145. package/dist/core/substrate/surface-match.d.ts +32 -0
  146. package/dist/core/substrate/surface-match.js +179 -0
  147. package/dist/daemon/api/handlers/nodes.js +9 -0
  148. package/dist/daemon/review/comment-notify.js +1 -8
  149. package/dist/daemon/review/deliver.js +1 -1
  150. package/dist/daemon/review/finish.js +1 -1
  151. package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
  152. package/dist/migrations/001-surfaces-frontmatter.js +276 -0
  153. package/dist/migrations/convergent.d.ts +31 -0
  154. package/dist/migrations/convergent.js +71 -0
  155. package/dist/migrations/registry.d.ts +2 -0
  156. package/dist/migrations/registry.js +19 -0
  157. package/dist/migrations/types.d.ts +40 -0
  158. package/dist/migrations/types.js +11 -0
  159. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
  160. package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
  161. package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
  162. package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
  163. package/dist/pi-extensions/canvas-review-boundary.js +1 -1
  164. package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
  165. package/dist/shared/generated-context.d.ts +0 -18
  166. package/dist/shared/generated-context.js +16 -120
  167. package/dist/shared/tool-groups.d.ts +1 -0
  168. package/dist/shared/tool-groups.js +7 -0
  169. package/package.json +1 -1
  170. package/runtime.lock.json +2 -2
  171. package/dist/core/substrate/ceiling.d.ts +0 -17
  172. package/dist/core/substrate/ceiling.js +0 -67
@@ -1,8 +1,8 @@
1
1
  // `crtr sys sync project-guidance` — one-way migration of a project's own
2
2
  // CLAUDE.md/AGENTS.md/.claude/rules into that SAME directory's OWN
3
- // `.crouter/memory/`. Each project guide becomes the root INDEX front door;
4
- // rules retain their explicit path routes, or use the `.` route (workspace
5
- // mount + any file read beneath the store's owning dir) when they are pathless.
3
+ // `.crouter/memory/`. Each project guide becomes the workspace front door
4
+ // (workspace-open + ./** read entries); rules keep their explicit path routes
5
+ // as read entries, or take the front-door pair when they are pathless.
6
6
  import { existsSync, readdirSync, statSync } from 'node:fs';
7
7
  import { basename, dirname, join, relative, resolve, sep } from 'node:path';
8
8
  import { defineLeaf } from '../../core/command.js';
@@ -218,9 +218,30 @@ function projectDocLabel(candidate) {
218
218
  .replace(/^\.$/, '');
219
219
  return relDir === '' ? rootLabel : normalizedName(`${rootLabel}/${relDir}`);
220
220
  }
221
+ /** The surfaces pair every workspace front door carries: whole-doc delivery
222
+ * when cwd/profile mounts the store, plus on any file read beneath the
223
+ * store's owning dir. */
224
+ function frontDoorSurfaces() {
225
+ return [
226
+ { on: 'workspace-open', at: 'content' },
227
+ { on: 'read', match: './**', at: 'content' },
228
+ ];
229
+ }
230
+ /** `./`-anchor a rule glob that could only match repo-relatively: any glob
231
+ * containing `/` that starts with neither `/`, `**`, nor `./`. Mirrors the
232
+ * surfaces migration's anchoring rule — this leaf MINTS new read globs, so it
233
+ * must anchor them the same way or the routes it writes silently never fire. */
234
+ function anchorReadGlob(g) {
235
+ const t = g.trim();
236
+ if (!t.includes('/'))
237
+ return t;
238
+ if (t.startsWith('/') || t.startsWith('**') || t.startsWith('./'))
239
+ return t;
240
+ return `./${t}`;
241
+ }
221
242
  /** AGENTS.md/CLAUDE.md → `<sourceDir>/.crouter/memory/INDEX.md`, the
222
243
  * workspace front door. Its explicit project identity keeps sibling roots
223
- * distinct; system none/file content plus `applies-to: "."` puts the wholly
244
+ * distinct; the workspace-open + ./** read entry pair puts the wholly
224
245
  * important guide in first-message context when cwd/profile mounts the store,
225
246
  * and surfaces it on any file read beneath the store's owning dir. */
226
247
  function prepareAgentsCandidate(candidate) {
@@ -231,9 +252,7 @@ function prepareAgentsCandidate(candidate) {
231
252
  name: docLabel,
232
253
  'when-and-why-to-read': `When working in ${docLabel}, this knowledge should be read because it is the project's operating guide.`,
233
254
  'short-form': synthesizeShortForm(body),
234
- 'system-prompt-visibility': 'none',
235
- 'file-read-visibility': 'content',
236
- 'applies-to': ['.'],
255
+ surfaces: frontDoorSurfaces(),
237
256
  };
238
257
  return {
239
258
  candidate,
@@ -244,9 +263,10 @@ function prepareAgentsCandidate(candidate) {
244
263
  };
245
264
  }
246
265
  /** `.claude/rules/<name>.md` → `<sourceDir>/.crouter/memory/<name>.md`. A
247
- * `paths:` rule keeps its `applies-to` file globs at none/content; a pathless
248
- * rule receives `applies-to: "."` and surfaces when the workspace opens or a
249
- * file beneath the store's owning dir is read. Like the INDEX docs, each rule
266
+ * `paths:` rule keeps its file globs as a `content` read entry (`./`-anchored
267
+ * when repo-relative); a pathless rule takes the front-door pair and surfaces
268
+ * when the workspace opens or a file beneath the store's owning dir is read.
269
+ * Like the INDEX docs, each rule
250
270
  * carries an explicit namespaced substrate name (`<root>/<subdir>/<leaf>`):
251
271
  * without it the resolver falls back to the store-relative leaf, and two
252
272
  * nested stores sharing a rule filename (e.g. apps/core and apps/gateway both
@@ -266,11 +286,11 @@ function prepareRuleCandidate(candidate) {
266
286
  ? routeFromDescription(description, 'knowledge')
267
287
  : `When working under ${dirLabel}, this knowledge should be read because it is migrated project rule guidance.`,
268
288
  'short-form': description !== '' ? description.replace(/\s+/g, ' ').trim() : synthesizeShortForm(parsed.body),
269
- 'system-prompt-visibility': 'none',
270
- 'file-read-visibility': 'content',
271
289
  };
272
- const globs = normalizeRulePaths(fm['paths']);
273
- frontmatter['applies-to'] = globs.length > 0 ? globs : ['.'];
290
+ const globs = normalizeRulePaths(fm['paths']).map(anchorReadGlob);
291
+ frontmatter['surfaces'] = globs.length > 0
292
+ ? [{ on: 'read', match: globs.length === 1 ? globs[0] : globs, at: 'content' }]
293
+ : frontDoorSurfaces();
274
294
  return {
275
295
  candidate,
276
296
  name,
@@ -321,7 +341,7 @@ function renderSummary(results) {
321
341
  export const sysSyncProjectGuidanceLeaf = defineLeaf({
322
342
  name: 'project-guidance',
323
343
  description: "migrate CLAUDE.md/AGENTS.md/.claude/rules into that directory's own crouter memory docs",
324
- whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its root INDEX front door; path-scoped rules keep their globs and pathless rules open with the workspace. Never exports memory docs back to these source files.",
344
+ whenToUse: "migrating per-project CLAUDE.md/AGENTS.md/.claude/rules into the crouter memory-doc model: one-way copy into that same directory's OWN .crouter/memory/. The project guide becomes its workspace front door; path-scoped rules keep their globs as read entries and pathless rules open with the workspace. Never exports memory docs back to these source files.",
325
345
  help: {
326
346
  name: 'sys sync project-guidance',
327
347
  summary: "one-way migration: CLAUDE.md/AGENTS.md/.claude/rules → that dir's own crouter memory docs",
@@ -338,7 +358,7 @@ export const sysSyncProjectGuidanceLeaf = defineLeaf({
338
358
  ],
339
359
  outputKind: 'object',
340
360
  effects: [
341
- "Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name at system-prompt-visibility none, file-read-visibility content, and applies-to `.`. Copies each .claude/rules/*.md into <dir>/.crouter/memory/<name>.md at none/content, using the source paths as applies-to globs or `.` (workspace mount + reads beneath the owning dir) when no paths are declared. Originals are left untouched.",
361
+ "Copies each discovered CLAUDE.md/AGENTS.md into <dir>/.crouter/memory/INDEX.md with an explicit project name and the workspace front-door surfaces pair ({on: workspace-open, at: content} + {on: read, match: './**', at: content}). Copies each .claude/rules/*.md into <dir>/.crouter/memory/<name>.md with a content read entry over the source paths (‘./’-anchored when repo-relative), or the same front-door pair when no paths are declared. Originals are left untouched.",
342
362
  'Skips existing memory docs unless --overwrite is present.',
343
363
  'With --dry-run: read-only; writes nothing.',
344
364
  ],
@@ -167,6 +167,12 @@ function convertPrepared(prepared, opts) {
167
167
  delete frontmatter.description;
168
168
  delete frontmatter.type;
169
169
  delete frontmatter.keywords;
170
+ // Retired visibility-axis fields a stale source may still carry — they must
171
+ // never pass through into a substrate doc.
172
+ delete frontmatter['system-prompt-visibility'];
173
+ delete frontmatter['file-read-visibility'];
174
+ delete frontmatter['applies-to'];
175
+ delete frontmatter['read-when'];
170
176
  frontmatter.kind = prepared.kind;
171
177
  if (typeof frontmatter['when-and-why-to-read'] !== 'string' || frontmatter['when-and-why-to-read'].trim() === '') {
172
178
  frontmatter['when-and-why-to-read'] = routeFromDescription(description, prepared.kind);
@@ -174,10 +180,8 @@ function convertPrepared(prepared, opts) {
174
180
  if (typeof frontmatter['short-form'] !== 'string' || frontmatter['short-form'].trim() === '') {
175
181
  frontmatter['short-form'] = description.replace(/\s+/g, ' ').trim();
176
182
  }
177
- if (typeof frontmatter['system-prompt-visibility'] !== 'string')
178
- frontmatter['system-prompt-visibility'] = 'preview';
179
- if (typeof frontmatter['file-read-visibility'] !== 'string')
180
- frontmatter['file-read-visibility'] = 'none';
183
+ if (!Array.isArray(frontmatter['surfaces']))
184
+ frontmatter['surfaces'] = [{ on: 'boot', at: 'preview' }];
181
185
  if (!opts.dryRun)
182
186
  writeText(prepared.target, serializeMemoryDoc(frontmatter, prepared.parsed.body));
183
187
  return {
@@ -11,19 +11,20 @@ import { supportBranch } from './sys/support.js';
11
11
  import { sysSyspromptLeaf } from './sys/sysprompt.js';
12
12
  import { promptReviewLeaf } from './sys/prompt-review.js';
13
13
  import { sysSyncBranch } from './sys/sync.js';
14
+ import { sysMigrateLeaf } from './sys/migrate.js';
14
15
  import { sysUpdateLeaf, sysVersionLeaf } from './sys/update.js';
15
16
  export function registerSys() {
16
17
  return defineBranch({
17
18
  name: 'sys',
18
19
  rootEntry: {
19
20
  concept: 'crtr configuration, diagnostics, and self-management',
20
- desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, update, version, feedback, logs, support, daemon',
21
+ desc: 'settings, config, setup, doctor, sysprompt, prompt-review, sync, migrate, update, version, feedback, logs, support, daemon',
21
22
  useWhen: 'managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process',
22
23
  },
23
24
  help: {
24
25
  name: 'sys',
25
26
  summary: 'crtr system configuration, settings, diagnostics, and self-management',
26
27
  },
27
- children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysSyncBranch, daemonBranch],
28
+ children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
28
29
  });
29
30
  }
@@ -106,7 +106,7 @@ test('coalesce renders one inbox card whose entries inline short report bodies a
106
106
  const card = parseCard({ role: 'user', content: digest });
107
107
  assert.notEqual(card, null);
108
108
  assert.equal(card.kind, 'inbox');
109
- assert.deepEqual({ ...card.facts }, { senders: '1', updates: '1' });
109
+ assert.deepEqual({ ...card.facts }, {});
110
110
  assert.equal(card.senders.length, 1);
111
111
  assert.equal(card.senders[0].id, 'child');
112
112
  // The label is NOT reprinted above the body it duplicates.
@@ -25,7 +25,9 @@ export interface RawClient {
25
25
  }
26
26
  export interface AttachKit {
27
27
  /** Connect a BrokerClient to a node's running broker, hello, await welcome. */
28
- attach(id: string, role: ClientRole, clientId: string): Promise<Attached>;
28
+ attach(id: string, role: ClientRole, clientId: string, opts?: {
29
+ snapshotTail?: number;
30
+ }): Promise<Attached>;
29
31
  /** Attach (as `role`) and retry until the welcome satisfies `pred`. */
30
32
  attachUntil(id: string, role: ClientRole, clientId: string, pred: (a: Attached) => boolean, label: string): Promise<Attached>;
31
33
  /** A raw node:net peer. read:false leaves the socket PAUSED (G8 stalled viewer). */
@@ -37,7 +37,7 @@ export function createAttachKit(getH) {
37
37
  // awaitBoot returns once the boot proof is written — which is BEFORE the broker's
38
38
  // server.listen() binds view.sock — so a fresh attach can momentarily race the
39
39
  // bind; retry the connect on BrokerUnavailable until it is listening.
40
- async function attach(id, role, clientId) {
40
+ async function attach(id, role, clientId, opts = {}) {
41
41
  const h = getH();
42
42
  await h.waitFor(() => existsSync(h.brokerSock(id)), { label: `view.sock for ${id}`, timeoutMs: 20_000 });
43
43
  const frames = [];
@@ -65,7 +65,12 @@ export function createAttachKit(getH) {
65
65
  // round-trip — so a welcome timeout cannot leak a connected socket past the test.
66
66
  liveClients.push({ close: () => client.close() });
67
67
  const waitFrame = (pred, label, timeoutMs = 15_000) => h.waitFor(() => frames.find(pred) ?? null, { label, timeoutMs });
68
- client.send({ type: 'hello', role, client_id: clientId });
68
+ client.send({
69
+ type: 'hello',
70
+ role,
71
+ client_id: clientId,
72
+ ...(opts.snapshotTail === undefined ? {} : { snapshot_tail: opts.snapshotTail }),
73
+ });
69
74
  let welcome;
70
75
  try {
71
76
  welcome = (await waitFrame((f) => f.type === 'welcome', `welcome for ${clientId}`));
@@ -3,7 +3,7 @@
3
3
  // Load-bearing corpus regression for `buildRefInventory` — the authoritative
4
4
  // inventory the broker and terminal attach viewer both resolve inline memory
5
5
  // references against. Covers scope precedence dedup, referenceability
6
- // (both kinds, gated and slash docs), INDEX folding, malformed-doc omission,
6
+ // (both kinds, gated and slash docs), directory browse names, malformed-doc omission,
7
7
  // metadata-only rows, and stable ordering, all over real on-disk fixtures.
8
8
  import { test, describe, before, beforeEach, after } from 'node:test';
9
9
  import assert from 'node:assert/strict';
@@ -17,7 +17,7 @@ import { buildRefInventory } from '../memory/inline-ref-inventory.js';
17
17
  // ---------------------------------------------------------------------------
18
18
  // Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
19
19
  // backed (node/project/user scope docs on disk), since precedence dedup,
20
- // referenceability-ignores-gate/slash, and INDEX folding are all corpus-shape
20
+ // referenceability-ignores-gate/slash, and directory browse names are all corpus-shape
21
21
  // facts that a pure in-memory test can't exercise. Fixture names are prefixed
22
22
  // to avoid any collision with the real shipped builtin/user corpus that
23
23
  // listAllMemoryDocs() also picks up in this environment.
@@ -128,12 +128,15 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
128
128
  assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-knowledge')?.kind, 'knowledge');
129
129
  assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-preference')?.kind, 'preference');
130
130
  });
131
- test('an INDEX doc folds to its bare directory name, not "<dir>/INDEX"', () => {
131
+ test('an INDEX doc keeps its canonical name; the bare dir name is a browse link, not a ref row', () => {
132
132
  writeDoc(join(projectMemoryDir(), 'crtr-inline-ref-fixture-taste'), 'INDEX.md', "kind: knowledge\nshort-form: 'taste index'");
133
133
  const { refs, names } = buildRefInventory();
134
+ assert.ok(names.has('crtr-inline-ref-fixture-taste/INDEX'));
135
+ // The directory name resolves as a listing link — in `names` for linkify,
136
+ // never a RefMeta row (no doc answers for a directory).
134
137
  assert.ok(names.has('crtr-inline-ref-fixture-taste'));
135
- assert.ok(!names.has('crtr-inline-ref-fixture-taste/INDEX'));
136
- const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste');
138
+ assert.ok(!refs.some((r) => r.name === 'crtr-inline-ref-fixture-taste'));
139
+ const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste/INDEX');
137
140
  assert.equal(winner?.shortForm, 'taste index');
138
141
  });
139
142
  test('a root INDEX doc is omitted, never exposed as an empty ref name', () => {
@@ -128,13 +128,14 @@ test('a plain path-named doc with no explicit name resolves to the nearest copy'
128
128
  process.chdir(prevCwd);
129
129
  }
130
130
  });
131
- test('a bare-directory query resolves to that dir\'s INDEX.md, nearest copy first', () => {
131
+ test('a bare-directory query does not resolve to a doc — directories answer with listings, not INDEX.md', () => {
132
132
  process.chdir(child);
133
133
  resetScopeCache();
134
134
  try {
135
- const doc = resolveMemoryDoc('taste');
136
- assert.equal(doc.path, join(child, '.crouter', 'memory', 'taste', 'INDEX.md'));
137
- assert.match(doc.body, /CHILD TASTE INDEX/);
135
+ assert.throws(() => resolveMemoryDoc('taste'), (err) => {
136
+ assert.equal(err.code, 'not_found');
137
+ return true;
138
+ });
138
139
  }
139
140
  finally {
140
141
  process.chdir(prevCwd);
@@ -76,9 +76,7 @@ test('resolveMemoryDocForTarget resolves a nested doc only with includeDescendan
76
76
  mkdirSync(nestedMemory, { recursive: true });
77
77
  writeFileSync(join(nestedMemory, 'guide.md'), '---\nkind: knowledge\n' +
78
78
  'name: nested-guide\n' +
79
- 'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n' +
80
- 'system-prompt-visibility: none\n' +
81
- 'file-read-visibility: none\n---\n' +
79
+ 'when-and-why-to-read: When working in sub, this knowledge should be read because it carries the subsystem constraints.\n---\n' +
82
80
  'NESTED GUIDE BODY\n');
83
81
  const target = { cwd: proj, profileId: null, nodeId: null };
84
82
  assert.throws(() => resolveMemoryDocForTarget('nested-guide', target, {}), (e) => e instanceof CrtrError && e.code === 'not_found', 'without the opt-in the flat ancestor stack does not see the nested store');
@@ -50,15 +50,15 @@ test('a store nested in the crouter home does not leak into a canvas-artifact re
50
50
  mkdirSync(leakedStore, { recursive: true });
51
51
  writeFileSync(join(leakedStore, 'private-thing.md'), '---\nkind: knowledge\n' +
52
52
  "when-and-why-to-read: When X, read this because Y.\n" +
53
- 'file-read-visibility: content\n' +
54
- 'applies-to: "**/*.md"\n---\n' +
53
+ 'surfaces:\n' +
54
+ ' - {on: read, match: "**/*.md", at: content}\n---\n' +
55
55
  'PRIVATE ASSISTANT MEMORY MUST NOT LEAK\n');
56
56
  // A canvas runtime artifact under the crouter home — the shape of every node
57
57
  // report/context file the harness auto-reads.
58
58
  const reportFile = join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports', 'r.md');
59
59
  mkdirSync(join(fakeHome, '.crouter', 'canvas', 'nodes', 'somenode', 'reports'), { recursive: true });
60
60
  writeFileSync(reportFile, '# a node report\n');
61
- const rendered = renderOnReadDocs(node, reportFile, new Set());
61
+ const rendered = renderOnReadDocs(node, reportFile, new Map());
62
62
  assert.ok(!rendered.includes('PRIVATE ASSISTANT MEMORY MUST NOT LEAK'), `crouter-home-nested store must not surface on a canvas read; got: ${rendered}`);
63
63
  assert.ok(!rendered.includes('private-thing'), `crouter-home-nested store name must not surface on a canvas read; got: ${rendered}`);
64
64
  });
@@ -45,7 +45,7 @@ after(() => {
45
45
  else
46
46
  process.env['HOME'] = prevHomeEnv;
47
47
  });
48
- test('project and user memory docs without applies-to do not fire on file reads', () => {
48
+ test('project and user memory docs without read surfaces do not fire on file reads', () => {
49
49
  const fakeHome = mkdtempSync(join(tmpdir(), 'crtr-onread-user-home-'));
50
50
  process.env['HOME'] = fakeHome;
51
51
  resetScopeCache();
@@ -53,18 +53,16 @@ test('project and user memory docs without applies-to do not fire on file reads'
53
53
  const userMemDir = join(fakeHome, '.crouter', 'memory');
54
54
  mkdirSync(userMemDir, { recursive: true });
55
55
  writeFileSync(join(userMemDir, 'noisy-global.md'), '---\nkind: preference\n' +
56
- 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n' +
57
- 'file-read-visibility: content\n---\n' +
56
+ 'when-and-why-to-read: When reading any file, this preference should be read because this fixture must not fire without an explicit route\n---\n' +
58
57
  'NOISY USER BODY\n');
59
58
  const projectMemDir = join(fakeHome, 'work', '.crouter', 'memory');
60
59
  mkdirSync(projectMemDir, { recursive: true });
61
60
  writeFileSync(join(projectMemDir, 'local.md'), '---\nkind: knowledge\n' +
62
- 'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n' +
63
- 'file-read-visibility: content\n---\n' +
61
+ 'when-and-why-to-read: When reading project files, this reference should be read because this fixture proves explicit file routing\n---\n' +
64
62
  'LOCAL PROJECT BODY\n');
65
63
  const readFile = join(fakeHome, 'work', 'ideas.md');
66
64
  writeFileSync(readFile, 'turn off xml closer\n');
67
- const rendered = renderOnReadDocs(node, readFile, new Set());
65
+ const rendered = renderOnReadDocs(node, readFile, new Map());
68
66
  assert.ok(!rendered.includes('NOISY USER BODY'), 'user-global doc has no explicit file route');
69
67
  assert.ok(!rendered.includes('LOCAL PROJECT BODY'), 'project doc also has no implicit file route');
70
68
  assert.equal(rendered, '', 'no file context renders without an explicit trigger');
@@ -72,13 +70,15 @@ test('project and user memory docs without applies-to do not fire on file reads'
72
70
  });
73
71
  test('on-read doc surfaces once, stays deduped across a revive(resume), re-surfaces after a fresh launch', () => {
74
72
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
75
- // A file-routed substrate doc in a `.crouter/memory/` ancestor.
73
+ // A file-routed substrate doc in a `.crouter/memory/` ancestor. The
74
+ // `./`-anchored glob pins the repo-relative anchor: `src/file.ts` matches
75
+ // only relative to the doc's owning repo dir, never absolutely.
76
76
  const memDir = join(work, '.crouter', 'memory');
77
77
  mkdirSync(memDir, { recursive: true });
78
78
  writeFileSync(join(memDir, 'onread-fixture.md'), '---\nkind: knowledge\n' +
79
79
  'when-and-why-to-read: When reading work files, this reference should be read because it is the on-read regression fixture\n' +
80
- 'file-read-visibility: content\n' +
81
- 'applies-to: "src/**"\n---\n' +
80
+ 'surfaces:\n' +
81
+ ' - {on: read, match: "./src/**", at: content}\n---\n' +
82
82
  `${FIXTURE_BODY}\n`);
83
83
  const readFile = join(work, 'src', 'file.ts');
84
84
  mkdirSync(join(work, 'src'), { recursive: true });
@@ -106,13 +106,13 @@ test('an explicitly routed doc injects on-read under its frontmatter name, not t
106
106
  writeFileSync(join(memDir, 'physical-guide.md'), '---\nkind: knowledge\n' +
107
107
  'name: acme-project\n' +
108
108
  'when-and-why-to-read: When reading acme source, this knowledge should be read because it carries the project-specific implementation constraints.\n' +
109
- 'file-read-visibility: content\n' +
110
- 'applies-to: "src/**"\n---\n' +
109
+ 'surfaces:\n' +
110
+ ' - {on: read, match: "./src/**", at: content}\n---\n' +
111
111
  'ACME PROJECT OPERATING GUIDE\n');
112
112
  const readFile = join(work, 'src', 'file.ts');
113
113
  mkdirSync(join(work, 'src'), { recursive: true });
114
114
  writeFileSync(readFile, 'export const x = 1;\n');
115
- const rendered = renderOnReadDocs(node, readFile, new Set());
115
+ const rendered = renderOnReadDocs(node, readFile, new Map());
116
116
  assert.ok(rendered.includes('ACME PROJECT OPERATING GUIDE'), 'the matching explicit route fires');
117
117
  assert.ok(rendered.includes('<memory kind="knowledge" name="acme-project"'), `expected explicit frontmatter name "acme-project" in the injected envelope, got: ${rendered}`);
118
118
  assert.ok(!rendered.includes('name="physical-guide"'), 'must not fall back to the path-derived name when frontmatter sets one');
@@ -1,9 +1,9 @@
1
1
  // A `.crouter/memory` store nested BELOW a mounted project root is delivered
2
- // by the read path, not by boot: its `.`-routed docs surface on the first read
3
- // beneath the store's owning dir, deduplicated against the workspace-open
4
- // render through the shared transcript dedup set. The `.` route stays fenced
5
- // to project-store docs — a hand-authored `.` in a user-scope doc must never
6
- // match a read.
2
+ // by the read path, not by boot: its `./**`-routed docs surface on the first
3
+ // read beneath the store's owning dir, deduplicated against the
4
+ // workspace-open render through the shared transcript dedup set. The `./`
5
+ // anchor stays fenced to project-store docs — a hand-authored `./` glob in a
6
+ // user-scope doc must never match a read.
7
7
  //
8
8
  // Run: node --import tsx/esm --test src/core/__tests__/on-read-nested-store.test.ts
9
9
  import { test, before, beforeEach, after } from 'node:test';
@@ -45,22 +45,25 @@ after(() => {
45
45
  process.env['HOME'] = prevHomeEnv;
46
46
  });
47
47
  /** A work tree with a root front door and a nested store front door, both
48
- * routed `.`. Returns the work dir. */
48
+ * carrying the explicit front-door pair (workspace-open + `./**` read).
49
+ * Returns the work dir. */
49
50
  function makeWorkTree() {
50
51
  const work = join(fakeHome, 'work');
51
52
  const rootStore = join(work, '.crouter', 'memory');
52
53
  mkdirSync(rootStore, { recursive: true });
53
54
  writeFileSync(join(rootStore, 'INDEX.md'), '---\nkind: knowledge\n' +
54
55
  'when-and-why-to-read: When working in this workspace, this knowledge should be read because it is the operating guide.\n' +
55
- 'file-read-visibility: content\n' +
56
- 'applies-to: "."\n---\n' +
56
+ 'surfaces:\n' +
57
+ ' - {on: workspace-open, at: content}\n' +
58
+ ' - {on: read, match: "./**", at: content}\n---\n' +
57
59
  `${ROOT_BODY}\n`);
58
60
  const nestedStore = join(work, 'pkg', '.crouter', 'memory');
59
61
  mkdirSync(nestedStore, { recursive: true });
60
62
  writeFileSync(join(nestedStore, 'INDEX.md'), '---\nkind: knowledge\n' +
61
63
  'when-and-why-to-read: When working in pkg, this knowledge should be read because it carries the package constraints.\n' +
62
- 'file-read-visibility: content\n' +
63
- 'applies-to: "."\n---\n' +
64
+ 'surfaces:\n' +
65
+ ' - {on: workspace-open, at: content}\n' +
66
+ ' - {on: read, match: "./**", at: content}\n---\n' +
64
67
  `${NESTED_BODY}\n`);
65
68
  return work;
66
69
  }
@@ -70,18 +73,18 @@ test('a nested store front door surfaces on a read beneath its owning dir, not o
70
73
  const inside = join(work, 'pkg', 'lib', 'file.ts');
71
74
  mkdirSync(join(work, 'pkg', 'lib'), { recursive: true });
72
75
  writeFileSync(inside, 'export const x = 1;\n');
73
- const rendered = renderOnReadDocs(node, inside, new Set());
76
+ const rendered = renderOnReadDocs(node, inside, new Map());
74
77
  assert.ok(rendered.includes(NESTED_BODY), `read beneath pkg surfaces the nested front door; got: ${rendered}`);
75
78
  const outside = join(work, 'docs', 'notes.ts');
76
79
  mkdirSync(join(work, 'docs'), { recursive: true });
77
80
  writeFileSync(outside, 'export const y = 2;\n');
78
- const renderedOutside = renderOnReadDocs(node, outside, new Set());
81
+ const renderedOutside = renderOnReadDocs(node, outside, new Map());
79
82
  assert.ok(!renderedOutside.includes(NESTED_BODY), `read outside pkg must not surface the nested front door; got: ${renderedOutside}`);
80
83
  });
81
84
  test('workspace-open and on-read share one dedup set: root front door once, nested store on first read beneath it', () => {
82
85
  const work = makeWorkTree();
83
86
  const node = spawnNode({ kind: 'general', cwd: work, parent: null }).node_id;
84
- const seen = new Set();
87
+ const seen = new Map();
85
88
  const opened = renderWorkspaceOpenDocs(node, seen);
86
89
  assert.ok(opened.includes(ROOT_BODY), 'workspace open delivers the mounted root front door');
87
90
  assert.ok(!opened.includes(NESTED_BODY), 'boot stays flat: the nested store is not in the mounted stack');
@@ -92,18 +95,18 @@ test('workspace-open and on-read share one dedup set: root front door once, nest
92
95
  assert.ok(onRead.includes(NESTED_BODY), 'the nested front door delivers on the first read beneath it');
93
96
  assert.ok(!onRead.includes(ROOT_BODY), 'the root front door already delivered at workspace open — never twice');
94
97
  });
95
- test('a hand-authored `.` route in a user-scope doc never matches a read', () => {
98
+ test('a hand-authored `./`-anchored read glob in a user-scope doc never matches a read', () => {
96
99
  const userStore = join(fakeHome, '.crouter', 'memory');
97
100
  mkdirSync(userStore, { recursive: true });
98
101
  writeFileSync(join(userStore, 'control.md'), '---\nkind: knowledge\n' +
99
102
  'when-and-why-to-read: When reading markdown, this knowledge should be read because it is the corpus-loaded control fixture.\n' +
100
- 'file-read-visibility: content\n' +
101
- 'applies-to: "**/*.md"\n---\n' +
103
+ 'surfaces:\n' +
104
+ ' - {on: read, match: "**/*.md", at: content}\n---\n' +
102
105
  'USER CONTROL BODY\n');
103
106
  writeFileSync(join(userStore, 'dot-routed.md'), '---\nkind: knowledge\n' +
104
107
  'when-and-why-to-read: When anything, this knowledge should be read because it must never fire.\n' +
105
- 'file-read-visibility: content\n' +
106
- 'applies-to: "."\n---\n' +
108
+ 'surfaces:\n' +
109
+ ' - {on: read, match: "./**", at: content}\n---\n' +
107
110
  'USER DOT BODY MUST NOT FIRE\n');
108
111
  const work = join(fakeHome, 'work');
109
112
  mkdirSync(work, { recursive: true });
@@ -111,7 +114,7 @@ test('a hand-authored `.` route in a user-scope doc never matches a read', () =>
111
114
  const readFile = join(fakeHome, 'notes', 'thing.md');
112
115
  mkdirSync(join(fakeHome, 'notes'), { recursive: true });
113
116
  writeFileSync(readFile, '# a note\n');
114
- const rendered = renderOnReadDocs(node, readFile, new Set());
117
+ const rendered = renderOnReadDocs(node, readFile, new Map());
115
118
  assert.ok(rendered.includes('USER CONTROL BODY'), 'the glob-routed user doc proves the user store is in the corpus');
116
- assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`.\` route is project-store only — a user doc's owning root is ~; got: ${rendered}`);
119
+ assert.ok(!rendered.includes('USER DOT BODY MUST NOT FIRE'), `the \`./\` anchor is project-store only — a user doc's owning root is ~; got: ${rendered}`);
117
120
  });
@@ -253,3 +253,12 @@ test('G10 — retained extension display state rides IN welcome.snapshot.display
253
253
  assert.equal(live.method, 'setStatus');
254
254
  assert.equal(live.statusText, 'painted while attached');
255
255
  });
256
+ test('snapshot_tail is ignored when the session has no stable message ids', async () => {
257
+ const token = tok('snapshot-tail-no-ids');
258
+ h.fakeCmd(id, { cmd: 'stream', text: token });
259
+ const full = await attachUntil(id, 'observer', 'snapshot-tail-full', (a) => JSON.stringify(a.welcome.snapshot.messages).includes(token), 'full snapshot after a tail-less attach');
260
+ assert.equal(full.welcome.snapshot.messageIds, undefined, 'the fake session has no stable ids');
261
+ const tailed = await attach(id, 'observer', 'snapshot-tail-request', { snapshotTail: 1 });
262
+ assert.equal(tailed.welcome.snapshot.messageIds, undefined, 'the tail request cannot invent stable ids');
263
+ assert.deepEqual(tailed.welcome.snapshot.messages, full.welcome.snapshot.messages, 'a welcome without stable ids keeps the full history instead of stranding an unpageable tail');
264
+ });
@@ -34,7 +34,7 @@ import { CANVAS_EXTENSIONS } from '../../runtime/launch.js';
34
34
  import { buildBrokerSession, snapshotMessages } from '../../runtime/broker.js';
35
35
  import { createNode } from '../../canvas/canvas.js';
36
36
  import { closeDb } from '../../canvas/db.js';
37
- import { transcriptMarkdown } from '../../runtime/node-read.js';
37
+ import { readNodeMessagesPage, transcriptMarkdown } from '../../runtime/node-read.js';
38
38
  // M-9 (broker.ts fault-and-die model gate): a boot with no cfg.model now calls
39
39
  // the REAL SDK's modelRegistry.getAvailable() and faults if it's empty, which it
40
40
  // always is on a host/CI runner with no authenticated provider. This test only
@@ -128,6 +128,57 @@ test('broker welcome snapshot serves the persisted history, not pi\u2019s sliced
128
128
  rmSync(cwd, { recursive: true, force: true });
129
129
  }
130
130
  });
131
+ test('readNodeMessagesPage keeps a tool call with all following tool results across cursor pages', async () => {
132
+ const home = mkdtempSync(join(tmpdir(), 'crtr-page-tools-home-'));
133
+ const sessionCwd = mkdtempSync(join(tmpdir(), 'crtr-page-tools-session-'));
134
+ const prevHome = process.env['CRTR_HOME'];
135
+ process.env['CRTR_HOME'] = home;
136
+ closeDb();
137
+ try {
138
+ const seed = SessionManager.create(sessionCwd);
139
+ seed.appendMessage(userMsg('before the tool run'));
140
+ seed.appendMessage({
141
+ ...assistantMsg('calling tools'),
142
+ content: [
143
+ { type: 'toolCall', id: 'tool-1', name: 'read', arguments: { path: 'one.txt' } },
144
+ { type: 'toolCall', id: 'tool-2', name: 'read', arguments: { path: 'two.txt' } },
145
+ ],
146
+ });
147
+ seed.appendMessage({ role: 'toolResult', toolCallId: 'tool-1', toolName: 'read', content: [{ type: 'text', text: 'one' }], isError: false, timestamp: Date.now() });
148
+ seed.appendMessage({ role: 'toolResult', toolCallId: 'tool-2', toolName: 'read', content: [{ type: 'text', text: 'two' }], isError: false, timestamp: Date.now() });
149
+ seed.appendMessage(userMsg('after the tool run'));
150
+ seed.appendMessage(assistantMsg('finished'));
151
+ const sessionFile = seed.getSessionFile();
152
+ const nodeId = 'tool-page-boundary';
153
+ createNode({
154
+ node_id: nodeId,
155
+ name: nodeId,
156
+ created: new Date().toISOString(),
157
+ cwd: sessionCwd,
158
+ kind: 'general',
159
+ mode: 'base',
160
+ lifecycle: 'terminal',
161
+ status: 'active',
162
+ pi_session_file: sessionFile,
163
+ });
164
+ const newest = await readNodeMessagesPage(nodeId, { limit: 3 });
165
+ assert.equal(newest.messages[0].role, 'assistant', 'the page walks back from the tool-result cut to its call');
166
+ assert.deepEqual(newest.messages.slice(1, 3).map((message) => message.role), ['toolResult', 'toolResult'], 'all tool results share the page with their call');
167
+ assert.ok(newest.nextCursor, 'the widened page still has an older cursor');
168
+ const older = await readNodeMessagesPage(nodeId, { cursor: newest.nextCursor, limit: 3 });
169
+ const persisted = SessionManager.open(sessionFile).buildSessionContext().messages;
170
+ assert.deepEqual([...older.messages, ...newest.messages], persisted, 'cursor pages remain gap-free and non-overlapping after widening');
171
+ assert.deepEqual([...(older.messageIds ?? []), ...(newest.messageIds ?? [])], SessionManager.open(sessionFile).getEntries().map(({ id }) => id), 'stable ids remain aligned across the widened page boundary');
172
+ }
173
+ finally {
174
+ closeDb();
175
+ process.env['CRTR_HOME'] = prevHome;
176
+ if (prevHome === undefined)
177
+ delete process.env['CRTR_HOME'];
178
+ rmSync(home, { recursive: true, force: true });
179
+ rmSync(sessionCwd, { recursive: true, force: true });
180
+ }
181
+ });
131
182
  // C3 (inline memory references, Wave 1A): transcriptMarkdown is the human-
132
183
  // facing render of a dormant node's persisted session. It must generically
133
184
  // skip any custom message whose `display === false` (the same hidden-guidance
@@ -229,7 +229,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
229
229
  const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
230
230
  assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
231
231
  assert.equal(stop.reason, 'stalled', 'classified stalled');
232
- assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', { reason: 'stalled' }, STALL_REPROMPT), 'the stall reprompt nudge, delivered as a whole-message stop-guard envelope');
232
+ assert.equal(stop.action === 'reprompt' ? stop.message : '', formatCard('stop-guard', {}, STALL_REPROMPT), 'the stall reprompt nudge, delivered as a whole-message stop-guard envelope');
233
233
  }
234
234
  // ===================================================================
235
235
  // S10 — B YIELDS. The real plain `node yield` verb sets intent=refresh
@@ -1,6 +1,8 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
3
- * `user_version` tracks how many have been applied. Append only. */
3
+ * `user_version` tracks how many have been applied. Append only. Journaled
4
+ * entries from `src/migrations/registry.ts` follow the local steps, so a new
5
+ * registry migration lands as the next version. */
4
6
  export declare const MIGRATIONS: ReadonlyArray<(db: DatabaseSync) => void>;
5
7
  /** Bring `db` up to the latest schema version. Reads `user_version`, runs each
6
8
  * pending migration in order, and bumps `user_version` after each so the work
@@ -7,7 +7,8 @@
7
7
  import { DatabaseSync } from 'node:sqlite';
8
8
  import { existsSync, readFileSync, readdirSync, appendFileSync } from 'node:fs';
9
9
  import { basename, join, resolve } from 'node:path';
10
- import { canvasDbPath, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
10
+ import { canvasDbPath, crtrHome, ensureHome, isSafeNodeId, nodesRoot, nodeMetaPath } from './paths.js';
11
+ import { STATE_MIGRATIONS } from '../../migrations/registry.js';
11
12
  import { customExtensionPaths } from './extensions.js';
12
13
  import { updateJsonFileDurably } from './meta-file.js';
13
14
  // --- Schema as a forward-only migration list ------------------------------
@@ -1152,8 +1153,16 @@ ALTER TABLE crons ADD COLUMN held INTEGER NOT NULL DEFAULT 0;
1152
1153
  ALTER TABLE crons ADD COLUMN last_poke_at TEXT;
1153
1154
  `);
1154
1155
  }
1156
+ /** The journaled lane of the state-migration registry, adapted to this chain's
1157
+ * step shape. Registry order is preserved, so these versions stay stable as
1158
+ * long as the registry is append-only (same contract as the local list). */
1159
+ const JOURNALED_STATE_MIGRATIONS = STATE_MIGRATIONS
1160
+ .filter((m) => m.lane === 'journaled')
1161
+ .map((m) => (db) => m.apply(db, crtrHome()));
1155
1162
  /** The ordered migration list. Index `i` is migration version `i + 1`; the db's
1156
- * `user_version` tracks how many have been applied. Append only. */
1163
+ * `user_version` tracks how many have been applied. Append only. Journaled
1164
+ * entries from `src/migrations/registry.ts` follow the local steps, so a new
1165
+ * registry migration lands as the next version. */
1157
1166
  export const MIGRATIONS = [
1158
1167
  /* v1 */ baselineSchema,
1159
1168
  /* v2 */ addRuntimeColumns,
@@ -1189,6 +1198,7 @@ export const MIGRATIONS = [
1189
1198
  /* v30 */ dropConsultOutbox,
1190
1199
  /* v31 */ addReviewSubmitRequested,
1191
1200
  /* v32 */ addCronHeldColumns,
1201
+ ...JOURNALED_STATE_MIGRATIONS,
1192
1202
  ];
1193
1203
  /** Migration indexes that manage their OWN transaction and therefore must not
1194
1204
  * be wrapped by `migrate()` — `node:sqlite` rejects a nested BEGIN. Index 3