@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
@@ -4,9 +4,9 @@
4
4
  * SCOPE / ALTITUDE. A full daemon-assembly integration (plugin registry → flag
5
5
  * read → runtime assembly → provider call) is too heavy and too mock-fragile
6
6
  * for a unit test. Instead this composes the REAL v3 live-path units with a
7
- * mocked routing/selection provider and synthetic fixtures:
7
+ * mocked select provider + stubbed dense lane and synthetic fixtures:
8
8
  *
9
- * orchestrate (routeL1 + selectAcrossLeaves over a shared WorkingSet)
9
+ * orchestrate (needle ∪ dense ∪ edge → selectPool over a shared WorkingSet)
10
10
  * → renderMemoryBlock (the rendered `<memory>` working-set block)
11
11
  * → stripAllMemoryInjections (all-turns history strip)
12
12
  *
@@ -16,35 +16,26 @@
16
16
  * message so exactly one block exists. Driving these real units across turns
17
17
  * exercises carry-forward, eviction, single-source, and strip-all end-to-end at
18
18
  * the v3 layer without the daemon. The provider is stubbed (no network).
19
- *
20
- * Mock-leak safety: the only `mock.module` here stub the provider + logger,
21
- * which are pure inputs to orchestrate and carry no real behavior siblings
22
- * depend on (orchestrate.test.ts installs the identical stubs). No
23
- * snapshot-real-modules dance is needed because nothing is partially stubbed.
24
19
  */
25
20
 
26
- import { beforeEach, describe, expect, mock, test } from "bun:test";
21
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
27
22
 
28
23
  import { stripAllMemoryInjections } from "../../../../memory/graph/conversation-graph-memory.js";
24
+ import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
29
25
  import type {
30
26
  ContentBlock,
31
27
  Message,
32
28
  Provider,
33
29
  ProviderResponse,
34
30
  } from "../../../../providers/types.js";
35
- import type { NeedleIndex } from "../needle.js";
36
- import type {
37
- LeafNode,
38
- LeafPath,
39
- LeafTree,
40
- MemoryRoutingTurn,
41
- Slug,
42
- } from "../types.js";
43
- import liveTurns from "./fixtures/live-turns.json" with { type: "json" };
31
+ import type { EdgeGraph } from "../edge.js";
32
+ import { buildEdgeGraph } from "../edge.js";
33
+ import { buildSectionNeedle } from "../section-needle.js";
34
+ import { buildSectionIndex } from "../sections.js";
35
+ import type { MemoryRoutingTurn, SectionIndex, Slug } from "../types.js";
44
36
 
45
37
  // ---------------------------------------------------------------------------
46
- // Provider mock installed BEFORE the orchestrator import so router.ts and
47
- // selector.ts observe it at load time. Mirrors orchestrate.test.ts.
38
+ // Module stubs installed BEFORE the orchestrator import.
48
39
  // ---------------------------------------------------------------------------
49
40
 
50
41
  let providerStub: Provider | null = null;
@@ -62,60 +53,61 @@ mock.module("../../../../util/logger.js", () => ({
62
53
  }),
63
54
  }));
64
55
 
56
+ // The dense lane never hits in this fixture — the needle drives the pool. The
57
+ // stub DELEGATES to the real `denseLane` unless this file's tests are running
58
+ // (`denseMockActive`), so the process-global `mock.module` cannot leak fake
59
+ // behavior into dense.test.ts (which exercises the real lane).
60
+ const realDense = { ...(await import("../dense.js")) };
61
+ let denseMockActive = false;
62
+ mock.module("../dense.js", () => ({
63
+ ...realDense,
64
+ denseLane: async (...args: Parameters<typeof realDense.denseLane>) =>
65
+ denseMockActive ? [] : realDense.denseLane(...args),
66
+ }));
67
+
65
68
  const { orchestrate } = await import("../orchestrate.js");
66
69
  const { WorkingSet } = await import("../working-set.js");
67
70
  const { renderMemoryBlock } = await import("../render-injection.js");
68
71
 
69
72
  // ---------------------------------------------------------------------------
70
- // Fixture types + helpers.
73
+ // Fixtures: a tiny corpus whose section text contains a distinctive term per
74
+ // page so a query selects exactly that page via the needle.
71
75
  // ---------------------------------------------------------------------------
72
76
 
73
- interface LeafSelection {
74
- ids: number[];
75
- pinned_ids: number[];
76
- }
77
- interface LiveTurn {
78
- name: string;
79
- currentMessage: string;
80
- routeIds: number[];
81
- leafSelections: Record<LeafPath, LeafSelection>;
82
- }
83
- const TURNS = (liveTurns as unknown as { turns: LiveTurn[] }).turns;
84
-
85
- function makeLeaf(path: LeafPath, members: Slug[]): LeafNode {
86
- return {
87
- path,
88
- frontmatter: { path, in_core: false },
89
- description: `description for ${path}`,
90
- members,
91
- domain: path.split("/")[0],
92
- };
77
+ const PAGES: Record<Slug, string> = {
78
+ "page-a": "lead a\n## Body\napple content for page a",
79
+ "page-b": "lead b\n## Body\nbanana content for page b",
80
+ "page-c": "lead c\n## Body\ncherry content for page c",
81
+ };
82
+ const SLUGS = Object.keys(PAGES);
83
+
84
+ function makeEntries(): PageIndexEntry[] {
85
+ return SLUGS.map((slug, i) => ({
86
+ id: i + 1,
87
+ slug,
88
+ summary: `summary of ${slug}`,
89
+ edges: [],
90
+ leaves: [],
91
+ modifiedAt: 0,
92
+ }));
93
93
  }
94
94
 
95
- /**
96
- * Synthetic tree. Sorted leaf order is [domain-a/topic-x, domain-a/topic-y],
97
- * so L1 id 1 → topic-x, id 2 → topic-y.
98
- */
99
- function makeTree(): LeafTree {
100
- const leaves: LeafNode[] = [
101
- makeLeaf("domain-a/topic-x", ["page-a", "page-b"]),
102
- makeLeaf("domain-a/topic-y", ["page-c"]),
103
- ];
104
- const byPage = new Map<Slug, LeafPath[]>();
105
- for (const leaf of leaves) {
106
- for (const slug of leaf.members) {
107
- byPage.set(slug, [...(byPage.get(slug) ?? []), leaf.path]);
108
- }
109
- }
110
- return { leaves: new Map(leaves.map((n) => [n.path, n])), byPage };
111
- }
112
-
113
- const summaryOf = async (slug: Slug): Promise<string> => `summary of ${slug}`;
114
- /** Deterministic per-slug body so the rendered `<memory>` block is assertable. */
95
+ const config = {} as never;
115
96
  const contentOf = async (slug: Slug): Promise<string> => `body for ${slug}`;
116
97
 
117
- /** Needle that never hits — routing alone drives the open set in this fixture. */
118
- const emptyNeedle: NeedleIndex = { query: () => [] };
98
+ async function buildLanes(): Promise<{
99
+ sectionIndex: SectionIndex;
100
+ needle: ReturnType<typeof buildSectionNeedle>;
101
+ edgeGraph: EdgeGraph;
102
+ }> {
103
+ const sectionIndex = await buildSectionIndex(SLUGS, async (s) => PAGES[s]!);
104
+ const needle = buildSectionNeedle(sectionIndex);
105
+ const edgeGraph = await buildEdgeGraph(
106
+ makeEntries(),
107
+ async (s) => PAGES[s]!, // no `links:` frontmatter → edge graph is empty
108
+ );
109
+ return { sectionIndex, needle, edgeGraph };
110
+ }
119
111
 
120
112
  function makeTurn(
121
113
  turnNumber: number,
@@ -129,71 +121,72 @@ function makeTurn(
129
121
  };
130
122
  }
131
123
 
132
- function toolUseResponse(
133
- name: string,
134
- input: Record<string, unknown>,
135
- ): ProviderResponse {
124
+ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
136
125
  return {
137
126
  model: "stub-model",
138
127
  stopReason: "tool_use",
139
128
  usage: { inputTokens: 0, outputTokens: 0 },
140
- content: [{ type: "tool_use", id: "tu-1", name, input }],
129
+ content: [{ type: "tool_use", id: "tu-1", name: "select_pages", input }],
141
130
  };
142
131
  }
143
132
 
144
- /** Last `<leaf>X</leaf>` tag found in an L2 request, or undefined. */
145
- function leafFromMessages(messages: Message[]): LeafPath | undefined {
133
+ /** Parse the numbered `<candidates>` block into an ordered slug list. */
134
+ function candidateSlugs(messages: Message[]): Slug[] {
146
135
  for (const msg of messages) {
147
136
  for (const block of msg.content) {
148
- if (block.type === "text") {
149
- const m = /<leaf>([^<]+)<\/leaf>/.exec(block.text);
150
- if (m) return m[1];
151
- }
137
+ if (block.type !== "text") continue;
138
+ const m = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(block.text);
139
+ if (!m) continue;
140
+ return m[1]
141
+ .split("\n")
142
+ .map((line) => /^\[\d+\] (\S+) —/.exec(line)?.[1])
143
+ .filter((s): s is string => !!s);
152
144
  }
153
145
  }
154
- return undefined;
146
+ return [];
155
147
  }
156
148
 
157
- /**
158
- * Build a provider that answers L1 `open_leaves` with `routeIds` and each L2
159
- * `select_pages` call with the per-leaf selection from the fixture turn.
160
- */
161
- function providerForTurn(turn: LiveTurn): Provider {
149
+ /** Provider that selects (and optionally pins) the pooled candidates in `keep`. */
150
+ function selectProvider(keep: Slug[], pin: Slug[] = []): Provider {
162
151
  return {
163
152
  name: "stub",
164
- sendMessage: async (messages, options) => {
165
- const toolName = options?.tools?.[0]?.name;
166
- if (toolName === "open_leaves") {
167
- return toolUseResponse("open_leaves", { ids: turn.routeIds });
168
- }
169
- const leaf = leafFromMessages(messages);
170
- const sel = (leaf ? turn.leafSelections[leaf] : undefined) ?? {
171
- ids: [],
172
- pinned_ids: [],
173
- };
174
- return toolUseResponse("select_pages", {
175
- ids: sel.ids,
176
- pinned_ids: sel.pinned_ids,
153
+ sendMessage: async (messages) => {
154
+ const pool = candidateSlugs(messages);
155
+ const ids: number[] = [];
156
+ const pinned_ids: number[] = [];
157
+ pool.forEach((slug, i) => {
158
+ if (keep.includes(slug)) ids.push(i + 1);
159
+ if (pin.includes(slug)) pinned_ids.push(i + 1);
177
160
  });
161
+ return toolUseResponse({ ids, pinned_ids });
178
162
  },
179
163
  };
180
164
  }
181
165
 
182
166
  /** Run one turn through orchestrate over the shared working set + render it. */
183
167
  async function runTurn(
184
- turn: LiveTurn,
185
168
  turnNumber: number,
186
- deps: { tree: LeafTree; workingSet: InstanceType<typeof WorkingSet> },
169
+ query: string,
170
+ keep: Slug[],
171
+ pin: Slug[],
172
+ deps: {
173
+ lanes: Awaited<ReturnType<typeof buildLanes>>;
174
+ workingSet: InstanceType<typeof WorkingSet>;
175
+ },
187
176
  ) {
188
- providerStub = providerForTurn(turn);
189
- const result = await orchestrate(makeTurn(turnNumber, turn.currentMessage), {
190
- tree: deps.tree,
191
- core: new Set(),
192
- needle: emptyNeedle,
177
+ providerStub = selectProvider(keep, pin);
178
+ const result = await orchestrate(makeTurn(turnNumber, query), {
179
+ sectionIndex: deps.lanes.sectionIndex,
180
+ needle: deps.lanes.needle,
181
+ denseConfig: config,
182
+ edgeGraph: deps.lanes.edgeGraph,
193
183
  workingSet: deps.workingSet,
194
- pageSummary: summaryOf,
195
184
  });
196
- const block = await renderMemoryBlock(result.finalInjection, contentOf);
185
+ const block = await renderMemoryBlock(
186
+ result.finalInjection,
187
+ result.sectionBySlug,
188
+ contentOf,
189
+ );
197
190
  return { result, block };
198
191
  }
199
192
 
@@ -203,9 +196,14 @@ function countMemoryBlocks(text: string): number {
203
196
  }
204
197
 
205
198
  beforeEach(() => {
199
+ denseMockActive = true;
206
200
  providerStub = null;
207
201
  });
208
202
 
203
+ afterAll(() => {
204
+ denseMockActive = false;
205
+ });
206
+
209
207
  // ---------------------------------------------------------------------------
210
208
  // Carry-forward: a page pinned in turn 1 appears in turn 2's injected block
211
209
  // WITHOUT being re-selected in turn 2 (same WorkingSet reused across turns).
@@ -213,20 +211,23 @@ beforeEach(() => {
213
211
 
214
212
  describe("memory-v3 live — carry-forward across turns", () => {
215
213
  test("a page pinned in turn 1 is injected in turn 2 without re-selection", async () => {
216
- const tree = makeTree();
214
+ const lanes = await buildLanes();
217
215
  const workingSet = new WorkingSet();
218
216
 
219
- const t1 = await runTurn(TURNS[0], 1, { tree, workingSet });
220
- // page-a was selected (and pinned) this turn and rendered into the block.
217
+ const t1 = await runTurn(1, "apple", ["page-a"], ["page-a"], {
218
+ lanes,
219
+ workingSet,
220
+ });
221
221
  expect(t1.result.currentSelections.map((s) => s.slug)).toContain("page-a");
222
222
  expect(t1.block).toContain("body for page-a");
223
223
 
224
- const t2 = await runTurn(TURNS[1], 2, { tree, workingSet });
225
- // Turn 2 opens a DIFFERENT leaf (topic-y) and never re-selects page-a…
224
+ const t2 = await runTurn(2, "cherry", ["page-c"], [], {
225
+ lanes,
226
+ workingSet,
227
+ });
226
228
  expect(t2.result.currentSelections.map((s) => s.slug)).not.toContain(
227
229
  "page-a",
228
230
  );
229
- // …yet page-a carries forward into the injected block via the working set.
230
231
  expect(t2.result.finalInjection).toContain("page-a");
231
232
  expect(t2.block).toContain("body for page-a");
232
233
  });
@@ -239,24 +240,36 @@ describe("memory-v3 live — carry-forward across turns", () => {
239
240
 
240
241
  describe("memory-v3 live — eviction reflected in the injected block", () => {
241
242
  test("a stale non-pinned page drops out; the pinned page persists", async () => {
242
- const tree = makeTree();
243
+ const lanes = await buildLanes();
243
244
  // Small window: a non-pinned entry unseen for >2 turns evicts. page-b is
244
245
  // selected only in turn 1, so by turn 4 (4-1=3 > 2) it ages out; pinned
245
246
  // page-a never evicts; page-c is re-selected every later turn.
246
247
  const workingSet = new WorkingSet(150, 2);
247
248
 
248
- const t1 = await runTurn(TURNS[0], 1, { tree, workingSet });
249
+ const t1 = await runTurn(
250
+ 1,
251
+ "apple banana",
252
+ ["page-a", "page-b"],
253
+ ["page-a"],
254
+ { lanes, workingSet },
255
+ );
249
256
  expect(t1.result.finalInjection).toContain("page-b");
250
257
  expect(t1.block).toContain("body for page-b");
251
258
 
252
- // Turns 2–3 keep page-b inside the window (3-1=2, not > 2).
253
- await runTurn(TURNS[1], 2, { tree, workingSet });
254
- const t3 = await runTurn(TURNS[2], 3, { tree, workingSet });
259
+ // Turns 2–3 keep page-b inside the window (3-1=2, not > 2); re-select page-c.
260
+ await runTurn(2, "cherry", ["page-c"], [], { lanes, workingSet });
261
+ const t3 = await runTurn(3, "cherry", ["page-c"], [], {
262
+ lanes,
263
+ workingSet,
264
+ });
255
265
  expect(t3.result.finalInjection).toContain("page-b");
256
266
 
257
267
  // Turn 4: page-b is now stale (4-1=3 > 2) and must be gone from the block;
258
268
  // pinned page-a and freshly-selected page-c remain.
259
- const t4 = await runTurn(TURNS[3], 4, { tree, workingSet });
269
+ const t4 = await runTurn(4, "cherry", ["page-c"], [], {
270
+ lanes,
271
+ workingSet,
272
+ });
260
273
  expect(t4.result.finalInjection).not.toContain("page-b");
261
274
  expect(t4.block).not.toContain("body for page-b");
262
275
  expect(t4.result.finalInjection).toContain("page-a");
@@ -266,17 +279,20 @@ describe("memory-v3 live — eviction reflected in the injected block", () => {
266
279
 
267
280
  // ---------------------------------------------------------------------------
268
281
  // Single source: orchestrate → render produces exactly one coherent `<memory>`
269
- // block per turn. (Assembly-level v2 suppression is covered by the assembly
270
- // tests; here we assert the v3 producer never emits more than one block.)
282
+ // block per turn.
271
283
  // ---------------------------------------------------------------------------
272
284
 
273
285
  describe("memory-v3 live — single memory source", () => {
274
286
  test("each turn renders exactly one <memory> block", async () => {
275
- const tree = makeTree();
287
+ const lanes = await buildLanes();
276
288
  const workingSet = new WorkingSet();
277
289
 
278
- for (const [i, turn] of TURNS.entries()) {
279
- const { block } = await runTurn(turn, i + 1, { tree, workingSet });
290
+ const queries = ["apple", "banana", "cherry"];
291
+ for (const [i, query] of queries.entries()) {
292
+ const { block } = await runTurn(i + 1, query, [SLUGS[i]!], [], {
293
+ lanes,
294
+ workingSet,
295
+ });
280
296
  expect(countMemoryBlocks(block)).toBe(1);
281
297
  expect(block.startsWith("<memory>\n")).toBe(true);
282
298
  expect(block.endsWith("\n</memory>")).toBe(true);
@@ -291,10 +307,9 @@ describe("memory-v3 live — single memory source", () => {
291
307
 
292
308
  describe("memory-v3 live — all-turns history strip", () => {
293
309
  test("historical <memory> blocks strip back to byte-stable user history", async () => {
294
- const tree = makeTree();
310
+ const lanes = await buildLanes();
295
311
  const workingSet = new WorkingSet();
296
312
 
297
- // The canonical (un-injected) user history the strip must converge to.
298
313
  const baseHistory: Message[] = [
299
314
  { role: "user", content: [{ type: "text", text: "first user message" }] },
300
315
  { role: "assistant", content: [{ type: "text", text: "ok" }] },
@@ -306,17 +321,14 @@ describe("memory-v3 live — all-turns history strip", () => {
306
321
  { role: "user", content: [{ type: "text", text: "third user message" }] },
307
322
  ];
308
323
 
309
- // Simulate prior turns having injected a v3 `<memory>` block onto EVERY
310
- // historical user message (prepended, as the live path does).
311
324
  const memBlock = (text: string): ContentBlock => ({ type: "text", text });
325
+ const queries = ["apple", "banana", "cherry"];
312
326
  let injected: Message[] = baseHistory;
313
- for (let turnNumber = 1; turnNumber <= TURNS.length; turnNumber++) {
314
- const { block } = await runTurn(TURNS[turnNumber - 1], turnNumber, {
315
- tree,
327
+ for (const [i, query] of queries.entries()) {
328
+ const { block } = await runTurn(i + 1, query, [SLUGS[i]!], [], {
329
+ lanes,
316
330
  workingSet,
317
331
  });
318
- // Re-inject this turn's freshly-rendered block onto each user message,
319
- // after stripping the prior turn's block (what the live assembly does).
320
332
  const stripped = stripAllMemoryInjections(injected);
321
333
  injected = stripped.map((m) =>
322
334
  m.role === "user"
@@ -325,8 +337,6 @@ describe("memory-v3 live — all-turns history strip", () => {
325
337
  );
326
338
  }
327
339
 
328
- // After a final all-turns strip, history is byte-identical to the canonical
329
- // base — every injected block was recognized and removed.
330
340
  const finalStripped = stripAllMemoryInjections(injected);
331
341
  expect(finalStripped).toEqual(baseHistory);
332
342
  });