@vellumai/assistant 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +29 -28
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/knip.json +1 -0
  6. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  7. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  8. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  9. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  11. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  12. package/openapi.yaml +22 -4
  13. package/package.json +3 -1
  14. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  15. package/src/__tests__/approval-cascade.test.ts +8 -16
  16. package/src/__tests__/approval-routes-http.test.ts +6 -0
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  18. package/src/__tests__/call-constants.test.ts +10 -1
  19. package/src/__tests__/call-controller.test.ts +127 -0
  20. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  21. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  22. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  23. package/src/__tests__/context-search-pkb-source.test.ts +12 -6
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  27. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  28. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  29. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -6
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  32. package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
  33. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  34. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  37. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  38. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  39. package/src/__tests__/filing-service.test.ts +2 -19
  40. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  41. package/src/__tests__/injector-chain.test.ts +24 -16
  42. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  43. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  44. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  45. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  46. package/src/__tests__/oauth-cli.test.ts +121 -0
  47. package/src/__tests__/relay-server.test.ts +46 -2
  48. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  49. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  50. package/src/__tests__/secret-response-routing.test.ts +7 -5
  51. package/src/__tests__/server-history-render.test.ts +82 -0
  52. package/src/__tests__/skill-include-graph.test.ts +31 -0
  53. package/src/__tests__/skill-load-tool.test.ts +44 -16
  54. package/src/__tests__/skills.test.ts +39 -0
  55. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  56. package/src/__tests__/tool-executor.test.ts +155 -0
  57. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  58. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  59. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  60. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  61. package/src/agent/loop.ts +11 -0
  62. package/src/approvals/guardian-decision-primitive.ts +0 -13
  63. package/src/approvals/guardian-request-resolvers.ts +4 -32
  64. package/src/calls/call-constants.ts +5 -8
  65. package/src/calls/call-controller.ts +130 -67
  66. package/src/calls/relay-server.ts +7 -1
  67. package/src/calls/voice-session-bridge.ts +1 -1
  68. package/src/cli/commands/memory-v2.ts +7 -7
  69. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
  70. package/src/cli/commands/oauth/connect.ts +10 -52
  71. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  72. package/src/config/feature-flag-registry.json +1 -17
  73. package/src/config/loader.ts +72 -19
  74. package/src/config/schemas/memory-v2.ts +1 -1
  75. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  76. package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
  77. package/src/daemon/conversation-agent-loop.ts +13 -10
  78. package/src/daemon/conversation-lifecycle.ts +22 -8
  79. package/src/daemon/conversation-surfaces.ts +16 -14
  80. package/src/daemon/conversation-tool-setup.ts +9 -5
  81. package/src/daemon/conversation.ts +1 -1
  82. package/src/daemon/handlers/shared.ts +26 -0
  83. package/src/daemon/host-bash-proxy.ts +1 -1
  84. package/src/daemon/host-browser-proxy.ts +1 -1
  85. package/src/daemon/host-cu-proxy.ts +1 -1
  86. package/src/daemon/host-file-proxy.ts +1 -1
  87. package/src/daemon/host-transfer-proxy.ts +2 -2
  88. package/src/daemon/lifecycle.ts +88 -73
  89. package/src/daemon/memory-v2-startup.ts +55 -14
  90. package/src/daemon/message-types/messages.ts +19 -1
  91. package/src/documents/document-store.ts +35 -1
  92. package/src/filing/filing-service.ts +2 -3
  93. package/src/heartbeat/heartbeat-service.ts +1 -1
  94. package/src/ipc/assistant-server.ts +93 -36
  95. package/src/ipc/skill-server.ts +99 -42
  96. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  97. package/src/memory/context-search/sources/memory-v2.ts +1 -17
  98. package/src/memory/context-search/sources/memory.ts +2 -2
  99. package/src/memory/context-search/sources/pkb.ts +2 -3
  100. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  101. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  102. package/src/memory/graph/conversation-graph-memory.ts +32 -9
  103. package/src/memory/graph/graph-search.test.ts +6 -5
  104. package/src/memory/graph/graph-search.ts +3 -4
  105. package/src/memory/graph/retriever.test.ts +12 -7
  106. package/src/memory/graph/retriever.ts +4 -5
  107. package/src/memory/graph/tool-handlers.ts +3 -4
  108. package/src/memory/graph/tools.ts +4 -4
  109. package/src/memory/indexer.ts +1 -2
  110. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  111. package/src/memory/jobs/embed-concept-page.ts +223 -87
  112. package/src/memory/jobs-worker.ts +8 -4
  113. package/src/memory/pkb/pkb-search.test.ts +6 -5
  114. package/src/memory/pkb/pkb-search.ts +4 -5
  115. package/src/memory/qdrant-client.ts +3 -0
  116. package/src/memory/search/semantic.ts +4 -5
  117. package/src/memory/v2/__tests__/activation.test.ts +35 -5
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
  119. package/src/memory/v2/__tests__/injection.test.ts +140 -23
  120. package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
  121. package/src/memory/v2/__tests__/sim.test.ts +118 -7
  122. package/src/memory/v2/__tests__/static-context.test.ts +1 -13
  123. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  124. package/src/memory/v2/consolidation-job.ts +7 -8
  125. package/src/memory/v2/injection.ts +32 -12
  126. package/src/memory/v2/page-store.ts +39 -0
  127. package/src/memory/v2/prompts/consolidation.ts +5 -0
  128. package/src/memory/v2/qdrant.ts +209 -48
  129. package/src/memory/v2/sim.ts +67 -26
  130. package/src/memory/v2/static-context.ts +4 -8
  131. package/src/memory/v2/sweep-job.ts +5 -6
  132. package/src/memory/v2/types.ts +7 -0
  133. package/src/notifications/copy-composer.ts +46 -12
  134. package/src/notifications/decision-engine.ts +46 -0
  135. package/src/permissions/gateway-threshold-reader.ts +116 -8
  136. package/src/permissions/prompter.ts +86 -96
  137. package/src/permissions/secret-prompter.ts +31 -31
  138. package/src/plugins/defaults/injectors.ts +1 -2
  139. package/src/proactive-artifact/job.test.ts +51 -4
  140. package/src/proactive-artifact/job.ts +16 -2
  141. package/src/proactive-artifact/message-copy.ts +18 -1
  142. package/src/prompts/templates/SOUL.md +13 -28
  143. package/src/runtime/auth/route-policy.ts +1 -0
  144. package/src/runtime/channel-approvals.ts +3 -2
  145. package/src/runtime/guardian-reply-router.ts +0 -10
  146. package/src/runtime/pending-interactions.ts +19 -15
  147. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  148. package/src/runtime/routes/approval-routes.ts +7 -3
  149. package/src/runtime/routes/consolidation-routes.ts +8 -9
  150. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  151. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  152. package/src/runtime/routes/filing-routes.ts +2 -3
  153. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
  154. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  155. package/src/runtime/routes/memory-item-routes.ts +5 -6
  156. package/src/runtime/routes/memory-v2-routes.ts +103 -17
  157. package/src/skills/include-graph.ts +35 -13
  158. package/src/tools/document/document-tool.ts +20 -0
  159. package/src/tools/executor.ts +18 -2
  160. package/src/tools/memory/register.test.ts +7 -5
  161. package/src/tools/permission-checker.ts +15 -0
  162. package/src/tools/skills/load.ts +24 -20
  163. package/src/tools/tool-name-aliases.ts +19 -0
  164. package/src/tools/types.ts +19 -1
  165. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  166. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  167. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  168. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  169. package/src/workspace/migrations/registry.ts +6 -0
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Tests for `assistant/src/memory/v2/consolidation-job.ts`.
3
3
  *
4
- * Coverage matrix (from PR 20 acceptance criteria):
5
- * - Flag off → no provider/wake calls; returns flag_off.
6
- * - Flag on, empty buffer → no wake call; returns empty_buffer.
7
- * - Flag on, non-empty buffer → bootstrap conversation, wake invoked with
4
+ * Coverage matrix:
5
+ * - v2 disabled in config → no provider/wake calls; returns disabled.
6
+ * - v2 on, empty buffer → no wake call; returns empty_buffer.
7
+ * - v2 on, non-empty buffer → bootstrap conversation, wake invoked with
8
8
  * the cutoff-templated prompt, follow-up jobs enqueued.
9
9
  * - Lock file already present → second call returns locked; first call's
10
10
  * in-flight semantics preserved by leaving the lock in place.
@@ -27,7 +27,6 @@ import { tmpdir } from "node:os";
27
27
  import { join } from "node:path";
28
28
  import {
29
29
  afterAll,
30
- afterEach,
31
30
  beforeAll,
32
31
  beforeEach,
33
32
  describe,
@@ -129,19 +128,18 @@ afterAll(() => {
129
128
  rmSync(tmpWorkspace, { recursive: true, force: true });
130
129
  });
131
130
 
132
- const { _setOverridesForTesting } =
133
- await import("../../../config/assistant-feature-flags.js");
134
131
  const { memoryV2ConsolidateJob } = await import("../consolidation-job.js");
135
132
  const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
136
133
  await import("../prompts/consolidation.js");
137
134
 
138
- // `isAssistantFeatureFlagEnabled` ignores the `config` argument it receives
139
- // (resolution is purely from the overrides + registry caches), and the
140
- // resolver only reads `config.memory.v2.consolidation_prompt_path` so a
141
- // minimal stand-in covers both call sites without materializing the full
142
- // default config.
135
+ // The resolver only reads `config.memory.v2.enabled` and
136
+ // `config.memory.v2.consolidation_prompt_path`, so a minimal stand-in
137
+ // covers both call sites without materializing the full default config.
143
138
  const CONFIG = {
144
- memory: { v2: { consolidation_prompt_path: null } },
139
+ memory: { v2: { enabled: true, consolidation_prompt_path: null } },
140
+ } as Parameters<typeof memoryV2ConsolidateJob>[1];
141
+ const CONFIG_DISABLED = {
142
+ memory: { v2: { enabled: false, consolidation_prompt_path: null } },
145
143
  } as Parameters<typeof memoryV2ConsolidateJob>[1];
146
144
 
147
145
  function makeJob(): Parameters<typeof memoryV2ConsolidateJob>[0] {
@@ -187,34 +185,25 @@ beforeEach(() => {
187
185
  wakeReason = undefined;
188
186
  });
189
187
 
190
- afterEach(() => {
191
- _setOverridesForTesting({});
192
- });
193
-
194
188
  // ---------------------------------------------------------------------------
195
189
 
196
- describe("memoryV2ConsolidateJob — flag off", () => {
197
- test("returns flag_off without invoking the wake when flag is off", async () => {
198
- _setOverridesForTesting({ "memory-v2-enabled": false });
190
+ describe("memoryV2ConsolidateJob — v2 disabled", () => {
191
+ test("returns disabled without invoking the wake when memory.v2.enabled is false", async () => {
199
192
  writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
200
193
 
201
- const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
194
+ const result = await memoryV2ConsolidateJob(makeJob(), CONFIG_DISABLED);
202
195
 
203
- expect(result).toEqual({ kind: "flag_off" });
196
+ expect(result).toEqual({ kind: "disabled" });
204
197
  expect(bootstrapCalls).toBe(0);
205
198
  expect(wakeCalls).toBe(0);
206
199
  expect(enqueuedJobs).toHaveLength(0);
207
- // Lock must NOT linger on the flag-off path — the handler bailed before
200
+ // Lock must NOT linger on the disabled path — the handler bailed before
208
201
  // the lock was acquired.
209
202
  expect(existsSync(lockPath())).toBe(false);
210
203
  });
211
204
  });
212
205
 
213
- describe("memoryV2ConsolidateJob — flag on, empty buffer", () => {
214
- beforeEach(() => {
215
- _setOverridesForTesting({ "memory-v2-enabled": true });
216
- });
217
-
206
+ describe("memoryV2ConsolidateJob — empty buffer", () => {
218
207
  test("returns empty_buffer when buffer.md is missing", async () => {
219
208
  expect(existsSync(bufferPath())).toBe(false);
220
209
 
@@ -244,9 +233,8 @@ describe("memoryV2ConsolidateJob — flag on, empty buffer", () => {
244
233
  });
245
234
  });
246
235
 
247
- describe("memoryV2ConsolidateJob — flag on, non-empty buffer", () => {
236
+ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
248
237
  beforeEach(() => {
249
- _setOverridesForTesting({ "memory-v2-enabled": true });
250
238
  writeFileSync(
251
239
  bufferPath(),
252
240
  "- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
@@ -288,7 +276,9 @@ describe("memoryV2ConsolidateJob — flag on, non-empty buffer", () => {
288
276
  "CUSTOM CONSOLIDATION at {{CUTOFF}}\n",
289
277
  );
290
278
  const overrideConfig = {
291
- memory: { v2: { consolidation_prompt_path: "custom-prompt.md" } },
279
+ memory: {
280
+ v2: { enabled: true, consolidation_prompt_path: "custom-prompt.md" },
281
+ },
292
282
  } as Parameters<typeof memoryV2ConsolidateJob>[1];
293
283
 
294
284
  const result = await memoryV2ConsolidateJob(makeJob(), overrideConfig);
@@ -368,7 +358,6 @@ describe("memoryV2ConsolidateJob — flag on, non-empty buffer", () => {
368
358
 
369
359
  describe("memoryV2ConsolidateJob — concurrent invocations", () => {
370
360
  beforeEach(() => {
371
- _setOverridesForTesting({ "memory-v2-enabled": true });
372
361
  writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Alice prefers VS Code.\n");
373
362
  });
374
363
 
@@ -114,8 +114,11 @@ class MockQdrantClient {
114
114
  _name: string,
115
115
  params: { using: string; limit: number; filter?: unknown },
116
116
  ) {
117
- const queue = state.queryResponses[params.using as "dense" | "sparse"];
118
- return queue.shift() ?? { points: [] };
117
+ // The four-channel hybrid query fires body-dense, body-sparse,
118
+ // summary-dense, summary-sparse in order; both dense channels share
119
+ // the dense queue and both sparse channels share the sparse queue.
120
+ const channel = params.using.endsWith("sparse") ? "sparse" : "dense";
121
+ return state.queryResponses[channel].shift() ?? { points: [] };
119
122
  }
120
123
  }
121
124
 
@@ -229,6 +232,18 @@ ref_files:
229
232
  ---
230
233
  Demo body content.`,
231
234
  );
235
+ // A page WITH a `summary` in its frontmatter — exercises the summary-only
236
+ // injection path. Body is intentionally longer than the summary so tests
237
+ // can assert that the body is *not* injected when the summary is present.
238
+ writeFileSync(
239
+ join(tmpWorkspace, "memory", "concepts", "summarized-page.md"),
240
+ `---
241
+ edges: []
242
+ ref_files: []
243
+ summary: A short prose description of the summarized page that retrieval injects in place of the full body.
244
+ ---
245
+ Long-form body content that should NOT appear in the injection block when the page has a summary in frontmatter — the agent reads the file on demand instead.`,
246
+ );
232
247
  });
233
248
 
234
249
  afterAll(() => {
@@ -308,14 +323,26 @@ function makeConfig(
308
323
  /**
309
324
  * Stage one set of dense/sparse hits, used uniformly by every `simBatch`
310
325
  * channel call (user/assistant/now) AND by the un-restricted ANN candidate
311
- * query. The candidate query runs first, then three simBatch calls, so we
312
- * push 4 dense + 4 sparse responses per turn.
326
+ * query. The candidate query runs first, then three simBatch calls that's
327
+ * `channels` (= 4) logical hybrid queries. Each logical hybrid query now
328
+ * fires a four-channel fan-out (body dense, body sparse, summary dense,
329
+ * summary sparse), so we push 2 dense + 2 sparse responses per logical
330
+ * call to match the post-summary-vector wire pattern.
313
331
  *
314
332
  * Each entry is mapped to a hit per channel; pass `denseScore`/`sparseScore`
315
- * undefined to omit a slug from that channel.
333
+ * undefined to omit a slug from that channel. `summaryDenseScore` /
334
+ * `summarySparseScore` route to the summary-side channels — tests that
335
+ * don't care about summary scoring leave them undefined and the summary
336
+ * channel falls back to body-only behavior.
316
337
  */
317
338
  function stageTurn(
318
- hits: Array<{ slug: string; denseScore?: number; sparseScore?: number }>,
339
+ hits: Array<{
340
+ slug: string;
341
+ denseScore?: number;
342
+ sparseScore?: number;
343
+ summaryDenseScore?: number;
344
+ summarySparseScore?: number;
345
+ }>,
319
346
  channels = 4,
320
347
  ): void {
321
348
  // Clear any leftovers from a prior turn before staging this one so unused
@@ -336,6 +363,22 @@ function stageTurn(
336
363
  .filter((h) => h.sparseScore !== undefined)
337
364
  .map((h) => ({ score: h.sparseScore, payload: { slug: h.slug } })),
338
365
  });
366
+ state.queryResponses.dense.push({
367
+ points: hits
368
+ .filter((h) => h.summaryDenseScore !== undefined)
369
+ .map((h) => ({
370
+ score: h.summaryDenseScore,
371
+ payload: { slug: h.slug },
372
+ })),
373
+ });
374
+ state.queryResponses.sparse.push({
375
+ points: hits
376
+ .filter((h) => h.summarySparseScore !== undefined)
377
+ .map((h) => ({
378
+ score: h.summarySparseScore,
379
+ payload: { slug: h.slug },
380
+ })),
381
+ });
339
382
  }
340
383
  }
341
384
 
@@ -395,7 +438,7 @@ describe("injectMemoryV2Block", () => {
395
438
  expect(result.block).not.toContain("<memory>");
396
439
  expect(result.block).not.toContain("</memory>");
397
440
  expect(result.block).not.toContain("## What I Remember Right Now");
398
- expect(result.block).toContain("### alice-vscode");
441
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
399
442
  expect(result.block).toContain("VS Code");
400
443
 
401
444
  // State persisted: alice's activation is above epsilon and recorded;
@@ -484,10 +527,10 @@ describe("injectMemoryV2Block", () => {
484
527
  });
485
528
 
486
529
  expect(result.toInject).toEqual(["carol-jazz"]);
487
- expect(result.block).toContain("### carol-jazz");
530
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
488
531
  // The block only shows the new slug — alice's attachment lives on the
489
532
  // previous turn's user message.
490
- expect(result.block).not.toContain("### alice-vscode");
533
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
491
534
 
492
535
  const persisted = await hydrate(db, "conv-1");
493
536
  expect(persisted!.everInjected).toEqual([
@@ -532,7 +575,7 @@ describe("injectMemoryV2Block", () => {
532
575
  });
533
576
 
534
577
  expect(result.toInject).toEqual(["alice-vscode"]);
535
- expect(result.block).toContain("### alice-vscode");
578
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
536
579
 
537
580
  const persisted = await hydrate(db, "conv-1");
538
581
  expect(persisted!.everInjected).toEqual([
@@ -540,6 +583,74 @@ describe("injectMemoryV2Block", () => {
540
583
  ]);
541
584
  });
542
585
 
586
+ test("page with summary renders as path + summary, no body, with the CRITICAL header", async () => {
587
+ // Pages whose frontmatter carries a `summary` should inject only the
588
+ // summary text behind the path header — the agent reads the full file
589
+ // on demand. The leading `**CRITICAL:**` line tells the agent how to
590
+ // read the block.
591
+ stageTurn([{ slug: "summarized-page", denseScore: 0.9 }]);
592
+
593
+ const result = await injectMemoryV2Block({
594
+ database: db,
595
+ conversationId: "conv-1",
596
+ currentTurn: 1,
597
+ userMessage: "tell me about the summarized page",
598
+ assistantMessage: "",
599
+ nowText: "Now",
600
+ messageId: "msg-1",
601
+ config: makeConfig(),
602
+ });
603
+
604
+ expect(result.block).not.toBeNull();
605
+ expect(result.block).toContain(
606
+ "**CRITICAL:** These are page summaries. Read the page file if it looks relevant.",
607
+ );
608
+ expect(result.block).toContain(
609
+ "# memory/concepts/summarized-page.md\nA short prose description",
610
+ );
611
+ // Body is NOT in the block — the agent must follow up with a read tool.
612
+ expect(result.block).not.toContain("Long-form body content");
613
+ // Frontmatter is also omitted; the path header carries the identifying
614
+ // information by itself, and edges flow through the activation graph.
615
+ expect(result.block).not.toContain("---\nedges:");
616
+ });
617
+
618
+ test("mixed batch — summary page renders short, fallback page renders full", async () => {
619
+ // Both pages rank into top-K. summarized-page has a summary → short
620
+ // form. frontmatter-demo has no summary → full-page fallback. The
621
+ // single CRITICAL header sits at the top regardless.
622
+ stageTurn([
623
+ { slug: "summarized-page", denseScore: 0.95 },
624
+ { slug: "frontmatter-demo", denseScore: 0.85 },
625
+ ]);
626
+
627
+ const result = await injectMemoryV2Block({
628
+ database: db,
629
+ conversationId: "conv-1",
630
+ currentTurn: 1,
631
+ userMessage: "show me everything",
632
+ assistantMessage: "",
633
+ nowText: "Now",
634
+ messageId: "msg-1",
635
+ config: makeConfig(),
636
+ });
637
+
638
+ expect(result.block).not.toBeNull();
639
+ // CRITICAL header appears exactly once.
640
+ const criticalCount = (
641
+ result.block!.match(/\*\*CRITICAL:\*\* These are page summaries\./g) ?? []
642
+ ).length;
643
+ expect(criticalCount).toBe(1);
644
+ // summarized-page → short form (path + summary, no body, no frontmatter).
645
+ expect(result.block).toContain("# memory/concepts/summarized-page.md\nA");
646
+ expect(result.block).not.toContain("Long-form body content");
647
+ // frontmatter-demo → full-page fallback (path + frontmatter + body).
648
+ expect(result.block).toContain(
649
+ "# memory/concepts/frontmatter-demo.md\n---\n",
650
+ );
651
+ expect(result.block).toContain("Demo body content.");
652
+ });
653
+
543
654
  test("includes the page frontmatter (edges, ref_files) in each rendered section", async () => {
544
655
  // The frontmatter (`edges`, `ref_files`) lives on disk above the page
545
656
  // body and is part of the page's content. Injection must reproduce both
@@ -560,8 +671,12 @@ describe("injectMemoryV2Block", () => {
560
671
  });
561
672
 
562
673
  expect(result.block).not.toBeNull();
563
- // Slug header is immediately followed by the frontmatter open delimiter.
564
- expect(result.block).toContain("### frontmatter-demo\n---\n");
674
+ // Path header is immediately followed by the frontmatter open delimiter.
675
+ // The fallback path renders the full page (frontmatter + body) when the
676
+ // page has no `summary` field — `frontmatter-demo` predates the field.
677
+ expect(result.block).toContain(
678
+ "# memory/concepts/frontmatter-demo.md\n---\n",
679
+ );
565
680
  // Both fields render in YAML block style with their populated values.
566
681
  expect(result.block).toContain("edges:\n - alice-vscode");
567
682
  expect(result.block).toContain("ref_files:\n - images/demo.jpg");
@@ -589,8 +704,8 @@ describe("injectMemoryV2Block", () => {
589
704
  });
590
705
 
591
706
  expect(result.toInject).toEqual(["carol-jazz", "alice-vscode"]);
592
- const carolIdx = result.block!.indexOf("### carol-jazz");
593
- const aliceIdx = result.block!.indexOf("### alice-vscode");
707
+ const carolIdx = result.block!.indexOf("# memory/concepts/carol-jazz.md");
708
+ const aliceIdx = result.block!.indexOf("# memory/concepts/alice-vscode.md");
594
709
  expect(carolIdx).toBeGreaterThan(-1);
595
710
  expect(aliceIdx).toBeGreaterThan(-1);
596
711
  expect(carolIdx).toBeLessThan(aliceIdx);
@@ -692,7 +807,7 @@ describe("injectMemoryV2Block", () => {
692
807
  expect(result.block).not.toContain("<memory>");
693
808
  expect(result.block).not.toContain("</memory>");
694
809
  expect(result.block).not.toContain("## What I Remember Right Now");
695
- expect(result.block).not.toContain("### alice-vscode");
810
+ expect(result.block).not.toContain("# memory/concepts/alice-vscode.md");
696
811
  expect(result.block).toContain("### Skills You Can Use");
697
812
  expect(result.block).toContain(
698
813
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
@@ -731,11 +846,13 @@ describe("injectMemoryV2Block", () => {
731
846
  );
732
847
  expect(result.block).not.toBeNull();
733
848
 
734
- const aliceIdx = result.block!.indexOf("### alice-vscode");
849
+ const aliceHeaderIdx = result.block!.indexOf(
850
+ "# memory/concepts/alice-vscode.md",
851
+ );
735
852
  const skillsIdx = result.block!.indexOf("### Skills You Can Use");
736
- expect(aliceIdx).toBeGreaterThan(-1);
853
+ expect(aliceHeaderIdx).toBeGreaterThan(-1);
737
854
  expect(skillsIdx).toBeGreaterThan(-1);
738
- expect(aliceIdx).toBeLessThan(skillsIdx);
855
+ expect(aliceHeaderIdx).toBeLessThan(skillsIdx);
739
856
 
740
857
  expect(result.block).toContain(
741
858
  '- The "Example Skill A" skill (example-skill-a) is available. Helps with examples. → use skill_load to activate',
@@ -864,7 +981,7 @@ describe("injectMemoryV2Block", () => {
864
981
  });
865
982
 
866
983
  expect(result.block).not.toBeNull();
867
- expect(result.block).toContain("### alice-vscode");
984
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
868
985
  // No newly-injected slug — alice was already in everInjected.
869
986
  expect(result.toInject).toEqual([]);
870
987
 
@@ -900,9 +1017,9 @@ describe("injectMemoryV2Block", () => {
900
1017
  });
901
1018
 
902
1019
  expect(result.block).not.toBeNull();
903
- expect(result.block).toContain("### alice-vscode");
904
- expect(result.block).toContain("### bob-coffee");
905
- expect(result.block).toContain("### carol-jazz");
1020
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1021
+ expect(result.block).toContain("# memory/concepts/bob-coffee.md");
1022
+ expect(result.block).toContain("# memory/concepts/carol-jazz.md");
906
1023
  // The seeded directed edges (alice→bob, bob→alice, frontmatter-demo→alice)
907
1024
  // mean alice has two incoming predecessors and bob has one, so directed
908
1025
  // spread normalizes alice's activation more aggressively than bob's. The
@@ -1163,7 +1280,7 @@ describe("injectMemoryV2Block", () => {
1163
1280
  expect(telemetryState.recordCalls.length).toBe(0);
1164
1281
  expect(result.toInject).toEqual(["alice-vscode"]);
1165
1282
  expect(result.block).not.toBeNull();
1166
- expect(result.block).toContain("### alice-vscode");
1283
+ expect(result.block).toContain("# memory/concepts/alice-vscode.md");
1167
1284
 
1168
1285
  const persisted = await hydrate(db, "conv-1");
1169
1286
  expect(persisted!.everInjected).toEqual([