@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4

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 (196) hide show
  1. package/docs/activation-funnel-telemetry.md +310 -0
  2. package/openapi.yaml +16 -115
  3. package/package.json +1 -1
  4. package/src/__tests__/activation-early-marking.test.ts +120 -0
  5. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  6. package/src/__tests__/anthropic-provider.test.ts +23 -8
  7. package/src/__tests__/approval-cascade.test.ts +1 -1
  8. package/src/__tests__/compaction-direct.test.ts +32 -18
  9. package/src/__tests__/compaction-events.test.ts +2 -2
  10. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  11. package/src/__tests__/context-overflow-reducer.test.ts +5 -5
  12. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  13. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  14. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  15. package/src/__tests__/conversation-error.test.ts +15 -1
  16. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  17. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  18. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  19. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  20. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  21. package/src/__tests__/conversation-queue.test.ts +1 -1
  22. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  23. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  25. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  29. package/src/__tests__/conversation-undo.test.ts +2 -2
  30. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  31. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  32. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  33. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  34. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  35. package/src/__tests__/history-repair-hook.test.ts +2 -0
  36. package/src/__tests__/llm-resolver.test.ts +73 -0
  37. package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
  38. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  39. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  40. package/src/__tests__/plugin-external-api.test.ts +68 -0
  41. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  42. package/src/__tests__/published-app-updater.test.ts +138 -0
  43. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  44. package/src/__tests__/title-generate-hook.test.ts +2 -0
  45. package/src/__tests__/web-fetch.test.ts +45 -0
  46. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  47. package/src/acp/resolve-agent.test.ts +0 -56
  48. package/src/acp/resolve-agent.ts +10 -38
  49. package/src/agent/loop.ts +13 -27
  50. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  51. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  52. package/src/cli/commands/memory-v3.ts +57 -199
  53. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  54. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  55. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  56. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  57. package/src/cli/lib/install-from-github.ts +258 -41
  58. package/src/cli/lib/plugin-details.ts +20 -13
  59. package/src/cli/lib/plugin-marketplace.ts +23 -5
  60. package/src/cli/lib/search-plugins.ts +14 -8
  61. package/src/config/acp-defaults.ts +3 -3
  62. package/src/config/acp-schema.ts +1 -7
  63. package/src/config/bundled-skills/acp/SKILL.md +4 -17
  64. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  65. package/src/config/call-site-defaults.ts +0 -1
  66. package/src/config/feature-flag-registry.json +3 -18
  67. package/src/config/llm-resolver.ts +39 -7
  68. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  69. package/src/config/schemas/call-site-catalog.ts +0 -7
  70. package/src/config/schemas/llm.ts +17 -1
  71. package/src/config/schemas/memory-v3.ts +58 -8
  72. package/src/config/seed-inference-profiles.ts +18 -0
  73. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  74. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  75. package/src/daemon/conversation-agent-loop.ts +87 -95
  76. package/src/daemon/conversation-error.ts +31 -4
  77. package/src/daemon/conversation-history.ts +1 -1
  78. package/src/daemon/conversation-media-retry.ts +19 -6
  79. package/src/daemon/conversation-messaging.ts +17 -0
  80. package/src/daemon/conversation-process.ts +14 -5
  81. package/src/daemon/conversation-queue-manager.ts +8 -0
  82. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  83. package/src/daemon/conversation-surfaces.ts +141 -3
  84. package/src/daemon/conversation.ts +48 -13
  85. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  86. package/src/daemon/persist-unsendable-image.ts +62 -25
  87. package/src/daemon/process-message.ts +1 -1
  88. package/src/daemon/tool-side-effects.ts +15 -0
  89. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  90. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  91. package/src/memory/activation-session-store.ts +43 -0
  92. package/src/memory/db-init.ts +4 -0
  93. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  94. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  95. package/src/memory/migrations/index.ts +2 -0
  96. package/src/memory/onboarding-events-store.ts +66 -18
  97. package/src/memory/schema/infrastructure.ts +13 -0
  98. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  99. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  100. package/src/memory/v2/consolidation-job.ts +2 -72
  101. package/src/memory/v2/types.ts +5 -0
  102. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  103. package/src/notifications/adapters/telegram.ts +7 -1
  104. package/src/plugin-api/constants.ts +2 -2
  105. package/src/plugin-api/index.ts +2 -2
  106. package/src/plugin-api/types.ts +19 -5
  107. package/src/plugins/defaults/compaction/compact.ts +24 -15
  108. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
  109. package/src/plugins/defaults/compaction/manager-store.ts +1 -1
  110. package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
  111. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
  112. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  113. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  114. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  115. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  116. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  117. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  118. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  119. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  120. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  121. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  122. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  123. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  124. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  125. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  126. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  127. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  128. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  129. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  130. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  131. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  132. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  133. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  134. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  135. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  136. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  137. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  138. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  139. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  140. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  141. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  142. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  143. package/src/plugins/external-api.ts +114 -0
  144. package/src/prompts/system-prompt.ts +61 -10
  145. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  146. package/src/providers/anthropic/client.ts +9 -10
  147. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  148. package/src/providers/inference/logit-bias.ts +55 -0
  149. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  150. package/src/providers/openai/chat-completions-provider.ts +44 -1
  151. package/src/providers/retry.ts +22 -0
  152. package/src/providers/types.ts +6 -0
  153. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  154. package/src/runtime/routes/acp-routes.test.ts +3 -20
  155. package/src/runtime/routes/app-management-routes.ts +3 -0
  156. package/src/runtime/routes/conversation-routes.ts +2 -0
  157. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  158. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  159. package/src/runtime/routes/stt-routes.ts +45 -12
  160. package/src/runtime/routes/workspace-routes.ts +50 -15
  161. package/src/services/published-app-updater.ts +30 -8
  162. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  163. package/src/telemetry/activation-funnel.ts +167 -0
  164. package/src/telemetry/types.ts +13 -0
  165. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  166. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  167. package/src/tools/acp/list-agents.test.ts +2 -18
  168. package/src/tools/acp/list-agents.ts +3 -15
  169. package/src/tools/acp/spawn.test.ts +0 -10
  170. package/src/tools/browser/browser-execution.ts +12 -2
  171. package/src/tools/network/web-fetch.ts +65 -24
  172. package/src/tools/skills/load.ts +1 -1
  173. package/src/tools/ui-surface/definitions.ts +7 -0
  174. package/src/acp/feature-gate.test.ts +0 -48
  175. package/src/acp/feature-gate.ts +0 -34
  176. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  177. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  178. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  179. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  180. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  181. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  182. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  183. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  184. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  185. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  186. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  187. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  188. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  189. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  190. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  191. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  192. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  193. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  194. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  195. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  196. package/src/util/map-limit.ts +0 -27
@@ -1,20 +1,25 @@
1
1
  /**
2
- * Tests for `assistant/src/memory/v3/shadow-plugin.ts`.
2
+ * Tests for `shadow-plugin.ts` (section-lane pipeline).
3
3
  *
4
4
  * The v3 plugin is flag-gated. These tests assert:
5
5
  * - both flags OFF → orchestrate is never called and no DB rows are written;
6
6
  * - either flag ON → orchestrate runs and selection rows land in
7
- * `memory_v3_selections`;
7
+ * `memory_v3_selections` with the new lane source tags;
8
8
  * - shadow-only (live off) → the injector returns `null` (never mutates the
9
9
  * turn) but still logs;
10
10
  * - live on → the injector returns the rendered `<memory>` block;
11
11
  * - an empty selection under live → `null`;
12
- * - lazy-init runs the loaders only once across multiple turns.
12
+ * - lazy-init runs the lane builders only once across multiple turns, and
13
+ * `invalidateLanes` forces exactly one rebuild;
14
+ * - `initLanes` feeds synthetic capability pages (skills / CLI commands) into
15
+ * the section index via `renderCapabilityContent`, so the needle lane ranks
16
+ * them like any other page (they are no longer always-added to the pool).
13
17
  *
14
18
  * All heavy dependencies (config, flag resolver, conversation reads, v2 page
15
- * index + page store, v3 loaders, orchestrate) are mocked BEFORE importing the
16
- * plugin so the module observes them at load time. A real in-memory SQLite DB
17
- * backs the write assertions via the `memory_v3_selections` migration.
19
+ * index + page store, the lane builders, orchestrate) are mocked BEFORE
20
+ * importing the plugin so the module observes them at load time. A real
21
+ * in-memory SQLite DB backs the write assertions via the
22
+ * `memory_v3_selections` migration.
18
23
  */
19
24
 
20
25
  import { Database } from "bun:sqlite";
@@ -24,22 +29,25 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
24
29
 
25
30
  import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
26
31
  import * as schema from "../../../../memory/schema.js";
27
- import type { LeafTree, SelectionSource } from "../types.js";
32
+ import type { CandidateLane, SectionIndex, SelectionSource } from "../types.js";
28
33
 
29
34
  // `mock.module` is process-global and, in Bun, neither `mock.restore()` nor a
30
35
  // re-mock in `afterAll` reverts it for files that load LATER in the same
31
- // `bun test src/memory/v3/` run. Sibling files (orchestrate.test.ts,
32
- // tree.test.ts) import these same modules for real, so an unconditional stub
33
- // here would leak in and break them. Instead each stub below DELEGATES to the
34
- // real implementation unless this test is actively running (`shadowMockActive`,
35
- // toggled in beforeEach/afterAll).
36
+ // `bun test src/plugins/defaults/memory-v3-shadow/` run. Sibling files import
37
+ // these same modules for real, so an unconditional stub here would leak in and
38
+ // break them. Instead each stub below DELEGATES to the real implementation
39
+ // unless this test is actively running (`shadowMockActive`, toggled in
40
+ // beforeEach/afterAll).
36
41
  //
37
42
  // Snapshot the real exports into plain objects NOW: a module namespace object
38
- // is a live view, so reading `realTree.loadLeafTree` *after* the stub is
39
- // installed would resolve back to the stub (infinite recursion).
40
- const realTree = { ...(await import("../tree.js")) };
41
- const realCore = { ...(await import("../core.js")) };
42
- const realNeedle = { ...(await import("../needle.js")) };
43
+ // is a live view, so reading the real export *after* the stub is installed
44
+ // would resolve back to the stub (infinite recursion).
45
+ const realSections = { ...(await import("../sections.js")) };
46
+ const realSectionNeedle = { ...(await import("../section-needle.js")) };
47
+ const realEdge = { ...(await import("../edge.js")) };
48
+ const realSectionDenseStore = {
49
+ ...(await import("../section-dense-store.js")),
50
+ };
43
51
  const realOrchestrate = { ...(await import("../orchestrate.js")) };
44
52
  const realPlatform = { ...(await import("../../../../util/platform.js")) };
45
53
  const realPageStore = {
@@ -48,33 +56,65 @@ const realPageStore = {
48
56
  const realConversationCrud = {
49
57
  ...(await import("../../../../memory/conversation-crud.js")),
50
58
  };
59
+ const realSkillStore = {
60
+ ...(await import("../../../../memory/v2/skill-store.js")),
61
+ };
62
+ const realCliCommandStore = {
63
+ ...(await import("../../../../memory/v2/cli-command-store.js")),
64
+ };
51
65
 
52
66
  let shadowMockActive = false;
53
67
 
54
68
  // ─── mutable test state, read by the mocks below ────────────────────────────
55
69
 
56
- // Per-flag toggles. The plugin reads `memory-v3-live` and `memory-v3-shadow`
57
- // independently, so the mock resolves by flag key rather than a single boolean.
58
70
  let liveEnabled = false;
59
71
  let shadowEnabled = false;
60
72
  let messages: Array<{ role: string; content: string }> = [];
73
+
74
+ // A synthetic skill capability slug the page index carries. Its rendered
75
+ // content holds a distinctive term ("kumquat") so the real needle, built over
76
+ // the section index `initLanes` feeds, ranks it. Generic placeholder only.
77
+ const CAPABILITY_SLUG = "skills/example";
78
+ const CAPABILITY_CONTENT = "use the kumquat skill to do the thing";
79
+
80
+ // The orchestrate result the spy returns. `laneBySlug` records the lane that
81
+ // surfaced each pooled slug: page-1 → "needle", page-2 → "dense", page-3 →
82
+ // "edge"; `attributeSelections` reads it directly. `carried` is a carry-forward
83
+ // slug not re-selected this turn. `sectionBySlug` carries the matched section
84
+ // for the slugs that had one (page-1/page-2) — consumed by the live injector's
85
+ // progressive disclosure, independent of source attribution.
61
86
  const orchestrateSpy = mock(async () => ({
62
- openedLeaves: [],
63
87
  currentSelections: [
64
- { slug: "domain-a/page-1", pinned: true },
65
- { slug: "domain-b/page-2", pinned: false },
88
+ { slug: "page-1", pinned: true },
89
+ { slug: "page-2", pinned: false },
90
+ { slug: "page-3", pinned: false },
66
91
  ],
67
- workingSetUnion: new Set<string>(["domain-a/page-1", "domain-b/page-2"]),
68
- finalInjection: ["domain-a/page-1", "domain-b/page-2", "domain-a/carried"],
92
+ workingSetUnion: new Set<string>(["page-1", "page-2", "page-3"]),
93
+ finalInjection: ["page-1", "page-2", "page-3", "carried"],
94
+ sectionBySlug: new Map([
95
+ ["page-1", { article: "page-1", title: "", text: "x", ordinal: 0 }],
96
+ ["page-2", { article: "page-2", title: "", text: "y", ordinal: 0 }],
97
+ ]),
98
+ laneBySlug: new Map<string, CandidateLane>([
99
+ ["page-1", "needle"],
100
+ ["page-2", "dense"],
101
+ ["page-3", "edge"],
102
+ ]),
69
103
  }));
70
- let treeLoads = 0;
71
- let coreLoads = 0;
104
+
105
+ let sectionBuilds = 0;
72
106
  let needleBuilds = 0;
73
- let configL2Concurrency = 16;
107
+ let edgeBuilds = 0;
108
+ let ensureCollectionCalls = 0;
109
+ let ensureCollectionThrows = false;
110
+
111
+ // The `pageBody` resolver `initLanes` passes to `buildSectionIndex` (its second
112
+ // arg), captured by the stub below so a test can drive it directly: a capability
113
+ // slug must resolve to its rendered capability content, an on-disk slug to its
114
+ // page body.
115
+ let capturedPageBody: ((slug: string) => Promise<string>) | null = null;
74
116
 
75
- // Shared in-memory DB so writes are observable from the test. We hold the raw
76
- // sqlite handle alongside the drizzle wrapper so the test can both read rows
77
- // directly and feed the same handle through the mocked `getSqliteFrom`.
117
+ // Shared in-memory DB so writes are observable from the test.
78
118
  let testSqlite: Database;
79
119
  let testDb = makeDb();
80
120
  function makeDb() {
@@ -85,15 +125,10 @@ function makeDb() {
85
125
  return db;
86
126
  }
87
127
 
88
- // A tree where `domain-a/*` pages are owned by a core leaf and `domain-b/*`
89
- // are not, so attribution maps to core+l2 / l1+l2 respectively.
90
- const FAKE_TREE = {
91
- leaves: new Map([
92
- ["domain-a", { members: ["domain-a/page-1", "domain-a/carried"] }],
93
- ["domain-b", { members: ["domain-b/page-2"] }],
94
- ]),
95
- byPage: new Map(),
96
- } as unknown as LeafTree;
128
+ const FAKE_SECTION_INDEX: SectionIndex = {
129
+ sections: [],
130
+ byArticle: new Map(),
131
+ };
97
132
 
98
133
  // ─── module mocks (installed before the plugin import) ──────────────────────
99
134
 
@@ -111,16 +146,17 @@ mock.module("../../../../config/loader.js", () => ({
111
146
  memory: {
112
147
  v3: {
113
148
  workingSet: { maxPages: 150, evictWindow: 5 },
114
- l2Concurrency: configL2Concurrency,
149
+ needleK: 100,
150
+ denseK: 100,
151
+ edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
115
152
  },
153
+ qdrant: { vectorSize: 8, onDisk: false },
116
154
  },
117
155
  }),
118
156
  }));
119
157
 
120
158
  // Spread the real module so every export the live path transitively imports
121
- // (e.g. `getMessageById` via page-content.ts) stays present — replacing the
122
- // whole module with a bare `{ getMessages }` made those consumers fail to load.
123
- // Only `getMessages` is overridden, since that's the one the plugin reads.
159
+ // stays present; only `getMessages` is overridden.
124
160
  mock.module("../../../../memory/conversation-crud.js", () => ({
125
161
  ...realConversationCrud,
126
162
  getMessages: () => messages.map((m, i) => ({ ...m, id: `m${i}` })),
@@ -131,13 +167,42 @@ mock.module("../../../../memory/db-connection.js", () => ({
131
167
  getSqliteFrom: () => testSqlite,
132
168
  }));
133
169
 
134
- mock.module("../v2/page-index.js", () => ({
135
- getPageIndex: async () => ({ bySlug: new Map() }),
170
+ mock.module("../../../../memory/v2/page-index.js", () => ({
171
+ getPageIndex: async () => ({
172
+ entries: [
173
+ {
174
+ slug: "page-1",
175
+ id: 1,
176
+ summary: "",
177
+ edges: [],
178
+ leaves: [],
179
+ modifiedAt: 0,
180
+ },
181
+ {
182
+ slug: "page-2",
183
+ id: 2,
184
+ summary: "",
185
+ edges: [],
186
+ leaves: [],
187
+ modifiedAt: 0,
188
+ },
189
+ // A synthetic capability row — same shape the v2 page index appends for
190
+ // skills/CLI commands. `initLanes` must route it through the capability
191
+ // resolver, not an on-disk read.
192
+ {
193
+ slug: CAPABILITY_SLUG,
194
+ id: 3,
195
+ summary: "",
196
+ edges: [],
197
+ leaves: [],
198
+ modifiedAt: 0,
199
+ },
200
+ ],
201
+ bySlug: new Map(),
202
+ }),
136
203
  }));
137
204
 
138
205
  // `pageContent` (live mode) reads the full page via `readPage`/`renderPageContent`.
139
- // Stub them to return a deterministic body per slug so the rendered `<memory>`
140
- // block is assertable without touching the filesystem.
141
206
  mock.module("../../../../memory/v2/page-store.js", () => ({
142
207
  ...realPageStore,
143
208
  readPage: async (workspaceDir: string, slug: string) =>
@@ -160,41 +225,74 @@ mock.module("../../../../util/platform.js", () => ({
160
225
  : realPlatform.getWorkspaceDir(),
161
226
  }));
162
227
 
163
- mock.module("../tree.js", () => ({
164
- ...realTree,
165
- resolveDataDir: () =>
166
- shadowMockActive ? "/tmp/shadow-test-data" : realTree.resolveDataDir(),
167
- loadLeafTree: async (dataDir: string) => {
168
- if (!shadowMockActive) return realTree.loadLeafTree(dataDir);
169
- treeLoads++;
170
- return FAKE_TREE;
171
- },
172
- membersOf: (tree: LeafTree, leaf: string) =>
228
+ // Capability stores: `renderCapabilityContent` (reached from `initLanes`' pageBody
229
+ // and from the live injector) resolves synthetic slugs through these. Spread the
230
+ // real module so the prefix predicates (`isSkillSlug`/`isCliCommandSlug`) stay
231
+ // intact; override only the content lookup so the capability slug resolves.
232
+ mock.module("../../../../memory/v2/skill-store.js", () => ({
233
+ ...realSkillStore,
234
+ getSkillCapability: (idOrSlug: string) =>
235
+ shadowMockActive
236
+ ? idOrSlug === CAPABILITY_SLUG
237
+ ? { id: "example", content: CAPABILITY_CONTENT }
238
+ : null
239
+ : realSkillStore.getSkillCapability(idOrSlug),
240
+ }));
241
+
242
+ mock.module("../../../../memory/v2/cli-command-store.js", () => ({
243
+ ...realCliCommandStore,
244
+ getCliCommandCapability: (idOrSlug: string) =>
173
245
  shadowMockActive
174
- ? ((tree.leaves.get(leaf) as unknown as { members?: string[] })
175
- ?.members ?? [])
176
- : realTree.membersOf(tree, leaf),
246
+ ? null
247
+ : realCliCommandStore.getCliCommandCapability(idOrSlug),
177
248
  }));
178
249
 
179
- mock.module("../core.js", () => ({
180
- ...realCore,
181
- loadCore: async (dataDir: string) => {
182
- if (!shadowMockActive) return realCore.loadCore(dataDir);
183
- coreLoads++;
184
- return new Set(["domain-a"]);
250
+ mock.module("../sections.js", () => ({
251
+ ...realSections,
252
+ buildSectionIndex: async (
253
+ ...args: Parameters<typeof realSections.buildSectionIndex>
254
+ ) => {
255
+ if (!shadowMockActive) return realSections.buildSectionIndex(...args);
256
+ sectionBuilds++;
257
+ // Capture the `pageBody` resolver so a test can exercise the capability
258
+ // branch directly. Returning the FAKE index keeps the other tests cheap.
259
+ capturedPageBody = args[1];
260
+ return FAKE_SECTION_INDEX;
185
261
  },
186
262
  }));
187
263
 
188
- mock.module("../needle.js", () => ({
189
- ...realNeedle,
190
- buildNeedleIndex: async (
191
- ...args: Parameters<typeof realNeedle.buildNeedleIndex>
264
+ mock.module("../section-needle.js", () => ({
265
+ ...realSectionNeedle,
266
+ buildSectionNeedle: (
267
+ ...args: Parameters<typeof realSectionNeedle.buildSectionNeedle>
192
268
  ) => {
193
- if (!shadowMockActive) return realNeedle.buildNeedleIndex(...args);
269
+ if (!shadowMockActive) return realSectionNeedle.buildSectionNeedle(...args);
194
270
  needleBuilds++;
195
- return { query: () => [] } as unknown as Awaited<
196
- ReturnType<typeof realNeedle.buildNeedleIndex>
197
- >;
271
+ return { query: () => [], bestSection: () => -1 };
272
+ },
273
+ }));
274
+
275
+ mock.module("../edge.js", () => ({
276
+ ...realEdge,
277
+ buildEdgeGraph: async (
278
+ ...args: Parameters<typeof realEdge.buildEdgeGraph>
279
+ ) => {
280
+ if (!shadowMockActive) return realEdge.buildEdgeGraph(...args);
281
+ edgeBuilds++;
282
+ return { adjacency: new Map(), hubs: new Set(), slugs: new Set() };
283
+ },
284
+ }));
285
+
286
+ mock.module("../section-dense-store.js", () => ({
287
+ ...realSectionDenseStore,
288
+ ensureSectionCollection: async (
289
+ ...args: Parameters<typeof realSectionDenseStore.ensureSectionCollection>
290
+ ) => {
291
+ if (!shadowMockActive) {
292
+ return realSectionDenseStore.ensureSectionCollection(...args);
293
+ }
294
+ ensureCollectionCalls++;
295
+ if (ensureCollectionThrows) throw new Error("qdrant unavailable");
198
296
  },
199
297
  }));
200
298
 
@@ -211,9 +309,6 @@ const { runShadowObservation, resetShadowLanesForTests, invalidateLanes } =
211
309
  await import("../shadow-plugin.js");
212
310
  const { memoryV3Injector } = await import("../injector.js");
213
311
 
214
- // The module stubs above stay installed for the rest of the process (Bun can't
215
- // reliably uninstall them), but `shadowMockActive` gates their fake behavior to
216
- // this file's tests only, so later-loaded sibling files see real behavior.
217
312
  afterAll(() => {
218
313
  shadowMockActive = false;
219
314
  });
@@ -237,10 +332,12 @@ beforeEach(() => {
237
332
  },
238
333
  ];
239
334
  orchestrateSpy.mockClear();
240
- treeLoads = 0;
241
- coreLoads = 0;
335
+ sectionBuilds = 0;
242
336
  needleBuilds = 0;
243
- configL2Concurrency = 16;
337
+ edgeBuilds = 0;
338
+ ensureCollectionCalls = 0;
339
+ ensureCollectionThrows = false;
340
+ capturedPageBody = null;
244
341
  testDb = makeDb();
245
342
  resetShadowLanesForTests();
246
343
  });
@@ -260,22 +357,27 @@ describe("memory-v3 shadow plugin", () => {
260
357
  shadowEnabled = false;
261
358
  await runShadowObservation("conv-1", 0);
262
359
  expect(orchestrateSpy).not.toHaveBeenCalled();
263
- expect(treeLoads).toBe(0);
360
+ expect(sectionBuilds).toBe(0);
264
361
  expect(readRows()).toHaveLength(0);
265
362
  });
266
363
 
267
- test("shadow flag ON → orchestrate runs and rows are written", async () => {
364
+ test("shadow flag ON → orchestrate runs and rows are written with per-lane sources", async () => {
268
365
  shadowEnabled = true;
269
366
  await runShadowObservation("conv-1", 2);
270
367
 
271
368
  expect(orchestrateSpy).toHaveBeenCalledTimes(1);
272
369
  const rows = readRows();
370
+ // Each selection is attributed to the lane that surfaced it
371
+ // (`result.laneBySlug`), not re-derived from section presence — so the
372
+ // dense-only page-2 logs "dense", not "needle".
273
373
  expect(rows).toEqual([
274
- { slug: "domain-a/carried", source: "carry-forward", pinned: 0 },
275
- // page-1 belongs to the core leaf `domain-a` → core+l2, pinned.
276
- { slug: "domain-a/page-1", source: "core+l2", pinned: 1 },
277
- // page-2 belongs to non-core `domain-b` → l1+l2.
278
- { slug: "domain-b/page-2", source: "l1+l2", pinned: 0 },
374
+ { slug: "carried", source: "carry-forward", pinned: 0 },
375
+ // page-1 was surfaced by the needle lane → "needle", pinned.
376
+ { slug: "page-1", source: "needle", pinned: 1 },
377
+ // page-2 was surfaced by the dense lane → "dense".
378
+ { slug: "page-2", source: "dense", pinned: 0 },
379
+ // page-3 was surfaced by the edge lane → "edge".
380
+ { slug: "page-3", source: "edge", pinned: 0 },
279
381
  ]);
280
382
  });
281
383
 
@@ -294,16 +396,21 @@ describe("memory-v3 shadow plugin", () => {
294
396
  expect(turn.currentMessage).toBe("hello world");
295
397
  });
296
398
 
297
- test("orchestrate receives the configured L2 concurrency", async () => {
399
+ test("orchestrate receives the lane deps", async () => {
298
400
  shadowEnabled = true;
299
- configL2Concurrency = 9;
300
401
  await runShadowObservation("conv-1", 0);
301
402
  const deps = (
302
403
  orchestrateSpy.mock.calls as unknown as unknown[][]
303
404
  )[0]![1] as {
304
- l2Concurrency?: number;
405
+ sectionIndex?: unknown;
406
+ needle?: unknown;
407
+ edgeGraph?: unknown;
408
+ workingSet?: unknown;
305
409
  };
306
- expect(deps.l2Concurrency).toBe(9);
410
+ expect(deps.sectionIndex).toBeDefined();
411
+ expect(deps.needle).toBeDefined();
412
+ expect(deps.edgeGraph).toBeDefined();
413
+ expect(deps.workingSet).toBeDefined();
307
414
  });
308
415
 
309
416
  test("both flags OFF → produce returns null, no orchestrate, no writes", async () => {
@@ -332,9 +439,12 @@ describe("memory-v3 shadow plugin", () => {
332
439
  expect(block!.placement).toBe("after-memory-prefix");
333
440
  expect(block!.text.startsWith("<memory>\n")).toBe(true);
334
441
  expect(block!.text.endsWith("\n</memory>")).toBe(true);
335
- // finalInjection slugs are rendered into the block in order.
336
- expect(block!.text).toContain("body for domain-a/page-1");
337
- expect(block!.text).toContain("body for domain-a/carried");
442
+ // Progressive disclosure: a slug with a matched section renders that
443
+ // section's text (page-1 → "x"), NOT the full page body.
444
+ expect(block!.text).toContain("# memory/concepts/page-1.md\nx");
445
+ expect(block!.text).not.toContain("body for page-1");
446
+ // A slug with no matched section (carry-forward) falls back to the full body.
447
+ expect(block!.text).toContain("body for carried");
338
448
  // Selections are still logged in live mode.
339
449
  expect(readRows().length).toBeGreaterThan(0);
340
450
  });
@@ -343,76 +453,87 @@ describe("memory-v3 shadow plugin", () => {
343
453
  liveEnabled = true;
344
454
  shadowEnabled = false;
345
455
  orchestrateSpy.mockImplementationOnce(async () => ({
346
- openedLeaves: [],
347
456
  currentSelections: [],
348
457
  workingSetUnion: new Set<string>(),
349
458
  finalInjection: [],
459
+ sectionBySlug: new Map(),
460
+ laneBySlug: new Map(),
350
461
  }));
351
462
  const block = await produce("conv-1", 0);
352
463
  expect(block).toBeNull();
353
- // Orchestration still ran (and logged nothing, since there were no rows).
354
464
  expect(orchestrateSpy).toHaveBeenCalledTimes(1);
355
465
  expect(readRows()).toHaveLength(0);
356
466
  });
357
467
 
358
- test("lazy-init runs the loaders only once across turns", async () => {
468
+ test("lazy-init runs the lane builders only once across turns", async () => {
359
469
  shadowEnabled = true;
360
470
  await runShadowObservation("conv-1", 0);
361
471
  await runShadowObservation("conv-1", 1);
362
472
  await runShadowObservation("conv-1", 2);
363
- expect(treeLoads).toBe(1);
364
- expect(coreLoads).toBe(1);
473
+ expect(sectionBuilds).toBe(1);
365
474
  expect(needleBuilds).toBe(1);
475
+ expect(edgeBuilds).toBe(1);
476
+ expect(ensureCollectionCalls).toBe(1);
366
477
  expect(orchestrateSpy).toHaveBeenCalledTimes(3);
367
478
  });
368
479
 
480
+ test("a Qdrant section-collection failure does not disable the in-memory lanes", async () => {
481
+ shadowEnabled = true;
482
+ ensureCollectionThrows = true;
483
+ // initLanes still builds the needle + edge lanes and orchestrate runs — a
484
+ // Qdrant outage degrades only the dense lane, it does not take down all of v3
485
+ // (and does not poison the memoized lanes by rejecting init).
486
+ await runShadowObservation("conv-1", 0);
487
+ expect(needleBuilds).toBe(1);
488
+ expect(edgeBuilds).toBe(1);
489
+ expect(ensureCollectionCalls).toBe(1);
490
+ expect(orchestrateSpy).toHaveBeenCalledTimes(1);
491
+ });
492
+
369
493
  test("invalidateLanes forces a one-time rebuild on the next turn", async () => {
370
494
  shadowEnabled = true;
371
495
  await runShadowObservation("conv-1", 0);
372
496
  await runShadowObservation("conv-1", 1);
373
- // One build so far; invalidation drops it.
374
- expect(treeLoads).toBe(1);
497
+ expect(sectionBuilds).toBe(1);
375
498
  expect(needleBuilds).toBe(1);
376
499
 
377
500
  invalidateLanes();
378
501
 
379
- // The next turn rebuilds (fresh tree load + fresh needle index)...
380
502
  await runShadowObservation("conv-1", 2);
381
- expect(treeLoads).toBe(2);
382
- expect(coreLoads).toBe(2);
503
+ expect(sectionBuilds).toBe(2);
383
504
  expect(needleBuilds).toBe(2);
505
+ expect(edgeBuilds).toBe(2);
384
506
 
385
507
  // ...and the rebuild is memoized again — no further builds until the next
386
508
  // invalidation.
387
509
  await runShadowObservation("conv-1", 3);
388
- expect(treeLoads).toBe(2);
510
+ expect(sectionBuilds).toBe(2);
389
511
  expect(needleBuilds).toBe(2);
390
512
  });
391
513
 
392
514
  test("resetShadowLanesForTests invalidates like invalidateLanes", async () => {
393
515
  shadowEnabled = true;
394
516
  await runShadowObservation("conv-1", 0);
395
- expect(treeLoads).toBe(1);
517
+ expect(sectionBuilds).toBe(1);
396
518
 
397
519
  resetShadowLanesForTests();
398
520
 
399
521
  await runShadowObservation("conv-1", 1);
400
- expect(treeLoads).toBe(2);
522
+ expect(sectionBuilds).toBe(2);
401
523
  });
402
524
 
403
525
  test("concurrent first turns after invalidation share a single build", async () => {
404
526
  shadowEnabled = true;
405
527
  await runShadowObservation("conv-1", 0);
406
- expect(treeLoads).toBe(1);
528
+ expect(sectionBuilds).toBe(1);
407
529
 
408
530
  invalidateLanes();
409
531
 
410
- // Both turns race the rebuild; memoization must collapse them to one build.
411
532
  await Promise.all([
412
533
  runShadowObservation("conv-1", 1),
413
534
  runShadowObservation("conv-1", 2),
414
535
  ]);
415
- expect(treeLoads).toBe(2);
536
+ expect(sectionBuilds).toBe(2);
416
537
  expect(needleBuilds).toBe(2);
417
538
  });
418
539
 
@@ -428,4 +549,44 @@ describe("memory-v3 shadow plugin", () => {
428
549
  expect(orchestrateSpy).not.toHaveBeenCalled();
429
550
  expect(readRows()).toHaveLength(0);
430
551
  });
552
+
553
+ describe("initLanes feeds synthetic capability pages into the section index", () => {
554
+ test("the pageBody resolver returns capability content for a synthetic slug and disk body otherwise", async () => {
555
+ shadowEnabled = true;
556
+ await runShadowObservation("conv-1", 0);
557
+
558
+ // `initLanes` ran the real pageBody-building closure and handed it to
559
+ // `buildSectionIndex`; the stub captured it.
560
+ expect(capturedPageBody).not.toBeNull();
561
+ const pageBody = capturedPageBody!;
562
+
563
+ // A capability slug resolves to its rendered capability content (NOT an
564
+ // on-disk read, which would miss), so the needle can rank it.
565
+ const capBody = await pageBody(CAPABILITY_SLUG);
566
+ expect(capBody).toContain("kumquat");
567
+ // On-disk slugs still resolve to their page body.
568
+ expect(await pageBody("page-1")).toBe("body for page-1");
569
+ });
570
+
571
+ test("a synthetic slug's capability content yields a section the needle ranks", async () => {
572
+ shadowEnabled = true;
573
+ await runShadowObservation("conv-1", 0);
574
+ const pageBody = capturedPageBody!;
575
+
576
+ // Feed the captured resolver through the REAL section builder + needle:
577
+ // the capability slug yields ≥1 section and is ranked on a term from its
578
+ // capability content — the path that makes synthetic pages lane-rankable.
579
+ const index = await realSections.buildSectionIndex(
580
+ [CAPABILITY_SLUG],
581
+ pageBody,
582
+ );
583
+ expect(index.byArticle.get(CAPABILITY_SLUG)?.length ?? 0).toBeGreaterThan(
584
+ 0,
585
+ );
586
+
587
+ const needle = realSectionNeedle.buildSectionNeedle(index);
588
+ const hits = needle.query("kumquat", 5);
589
+ expect(hits.map((h) => h.article)).toContain(CAPABILITY_SLUG);
590
+ });
591
+ });
431
592
  });
@@ -1,10 +1,6 @@
1
1
  import { expect, test } from "bun:test";
2
2
 
3
3
  import type {
4
- LeafFrontmatter,
5
- LeafNode,
6
- LeafPath,
7
- LeafTree,
8
4
  MemoryRoutingTurn,
9
5
  SelectionSource,
10
6
  Slug,
@@ -12,24 +8,8 @@ import type {
12
8
  } from "../types.js";
13
9
 
14
10
  test("v3 core types instantiate", () => {
15
- const path: LeafPath = "domain/topic/leaf";
16
11
  const slug: Slug = "page-123";
17
12
 
18
- const frontmatter: LeafFrontmatter = { path, in_core: true };
19
-
20
- const node: LeafNode = {
21
- path,
22
- frontmatter,
23
- description: "an example leaf node",
24
- members: [slug],
25
- domain: "example",
26
- };
27
-
28
- const tree: LeafTree = {
29
- leaves: new Map<LeafPath, LeafNode>([[path, node]]),
30
- byPage: new Map<Slug, LeafPath[]>([[slug, [path]]]),
31
- };
32
-
33
13
  const entry: WorkingSetEntry = {
34
14
  slug,
35
15
  selectedAtTurn: 1,
@@ -46,8 +26,6 @@ test("v3 core types instantiate", () => {
46
26
 
47
27
  const source: SelectionSource = "carry-forward";
48
28
 
49
- expect(tree.leaves.get(path)).toBe(node);
50
- expect(tree.byPage.get(slug)).toEqual([path]);
51
29
  expect(entry.slug).toBe(slug);
52
30
  expect(turnContext.turnNumber).toBe(3);
53
31
  expect(source).toBe("carry-forward");