@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
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Tests for `pool-select.ts` — the single forced-tool selector over the unified
3
+ * candidate pool.
4
+ *
5
+ * Coverage matrix:
6
+ * - Numbered candidates render with their descriptors (truncated).
7
+ * - Returned IDs map to the right candidate slugs by 1-based index, with
8
+ * `pinned` driven by `pinned_ids`.
9
+ * - Omitted `ids` → keep ALL candidates (recall-safe).
10
+ * - Explicit `ids: []` → keep none (deliberate abstention).
11
+ * - Out-of-range / duplicate IDs ignored without throwing.
12
+ * - No provider / missing tool_use / schema mismatch / throw → keep none
13
+ * (degrade to deterministic lanes), the last three after a re-prompt retry.
14
+ * - One forced-tool `select_pages` call on the v3 L2 call site, with NO cache
15
+ * breakpoint (the pool is dynamic per turn).
16
+ *
17
+ * The provider is stubbed so no network calls fire; mirrors selector.test.ts.
18
+ */
19
+
20
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
21
+
22
+ import type {
23
+ Message,
24
+ Provider,
25
+ ProviderResponse,
26
+ SendMessageOptions,
27
+ ToolUseContent,
28
+ } from "../../../../providers/types.js";
29
+ import type { MemoryRoutingTurn } from "../types.js";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Mocks installed BEFORE the pool-select import so the module observes them at
33
+ // load time.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ let providerStub: Provider | null = null;
37
+
38
+ interface ProviderCall {
39
+ messages: Message[];
40
+ options: SendMessageOptions | undefined;
41
+ }
42
+ const providerCalls: ProviderCall[] = [];
43
+
44
+ mock.module("../../../../providers/provider-send-message.js", () => ({
45
+ getConfiguredProvider: async () => providerStub,
46
+ extractToolUse: (response: ProviderResponse) =>
47
+ response.content.find((b): b is ToolUseContent => b.type === "tool_use"),
48
+ }));
49
+
50
+ mock.module("../../../../util/logger.js", () => ({
51
+ getLogger: () =>
52
+ new Proxy({} as Record<string, unknown>, {
53
+ get: (_t, prop) => (prop === "child" ? () => ({}) : () => {}),
54
+ }),
55
+ }));
56
+
57
+ const { selectPool } = await import("../pool-select.js");
58
+ type PoolCandidate = Parameters<typeof selectPool>[0][number];
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers.
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function makeProvider(response: ProviderResponse): Provider {
65
+ return {
66
+ name: "stub",
67
+ sendMessage: async (messages, options) => {
68
+ providerCalls.push({ messages, options });
69
+ return response;
70
+ },
71
+ };
72
+ }
73
+
74
+ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
75
+ return {
76
+ model: "stub-model",
77
+ stopReason: "tool_use",
78
+ usage: { inputTokens: 0, outputTokens: 0 },
79
+ content: [{ type: "tool_use", id: "tu-1", name: "select_pages", input }],
80
+ };
81
+ }
82
+
83
+ /** A 200 response that carries no tool_use — the malformed-but-successful case
84
+ * the re-prompt retry exists to recover from. */
85
+ function noToolResponse(): ProviderResponse {
86
+ return {
87
+ model: "stub-model",
88
+ stopReason: "end_turn",
89
+ usage: { inputTokens: 0, outputTokens: 0 },
90
+ content: [{ type: "text", text: "no tool call" }],
91
+ };
92
+ }
93
+
94
+ /** Provider returning a different response per call (the i-th call returns
95
+ * responses[i], or the last entry once exhausted). */
96
+ function makeSequenceProvider(responses: ProviderResponse[]): Provider {
97
+ let i = 0;
98
+ return {
99
+ name: "sequence",
100
+ sendMessage: async (messages, options) => {
101
+ providerCalls.push({ messages, options });
102
+ const response = responses[Math.min(i, responses.length - 1)];
103
+ i += 1;
104
+ return response;
105
+ },
106
+ };
107
+ }
108
+
109
+ /** Provider that records each call and then throws — the throw-after-retries
110
+ * path (the provider's own RetryProvider has already exhausted its backoff). */
111
+ function makeThrowingProvider(): Provider {
112
+ return {
113
+ name: "throwing",
114
+ sendMessage: async (messages, options) => {
115
+ providerCalls.push({ messages, options });
116
+ throw new Error("boom");
117
+ },
118
+ };
119
+ }
120
+
121
+ function makePool(): PoolCandidate[] {
122
+ return [
123
+ { slug: "page-a", descriptor: "section: the alpha rollout plan" },
124
+ { slug: "page-b", descriptor: "section: beta metrics dashboard" },
125
+ { slug: "topic-x", descriptor: "linked page about topic x" },
126
+ ];
127
+ }
128
+
129
+ function makeTurn(currentMessage: string): MemoryRoutingTurn {
130
+ return {
131
+ conversationId: "conv-xyz",
132
+ turnNumber: 1,
133
+ currentMessage,
134
+ recentContext: "earlier we talked about the timeline",
135
+ };
136
+ }
137
+
138
+ beforeEach(() => {
139
+ providerStub = null;
140
+ providerCalls.length = 0;
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // selectPool — id mapping.
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe("selectPool — id mapping", () => {
148
+ test("returned IDs map to candidate slugs, pinned driven by pinned_ids", async () => {
149
+ providerStub = makeProvider(
150
+ toolUseResponse({ ids: [3, 1], pinned_ids: [1] }),
151
+ );
152
+ const result = await selectPool(makePool(), makeTurn("how's the rollout?"));
153
+ expect(result).toEqual([
154
+ { slug: "topic-x", pinned: false },
155
+ { slug: "page-a", pinned: true },
156
+ ]);
157
+ });
158
+
159
+ test("omitted ids keeps ALL candidates (recall-safe)", async () => {
160
+ providerStub = makeProvider(toolUseResponse({}));
161
+ const result = await selectPool(makePool(), makeTurn("anything"));
162
+ expect(result).toEqual([
163
+ { slug: "page-a", pinned: false },
164
+ { slug: "page-b", pinned: false },
165
+ { slug: "topic-x", pinned: false },
166
+ ]);
167
+ });
168
+
169
+ test("explicit empty ids keeps no candidates (abstention)", async () => {
170
+ providerStub = makeProvider(toolUseResponse({ ids: [] }));
171
+ const result = await selectPool(makePool(), makeTurn("nothing relevant"));
172
+ expect(result).toEqual([]);
173
+ });
174
+
175
+ test("out-of-range and duplicate IDs are ignored without throwing", async () => {
176
+ providerStub = makeProvider(toolUseResponse({ ids: [2, 99, 0, -1, 2] }));
177
+ const result = await selectPool(makePool(), makeTurn("the metrics"));
178
+ expect(result).toEqual([{ slug: "page-b", pinned: false }]);
179
+ });
180
+
181
+ test("empty pool returns no pages and never calls the provider", async () => {
182
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
183
+ const result = await selectPool([], makeTurn("hi"));
184
+ expect(result).toEqual([]);
185
+ expect(providerCalls).toHaveLength(0);
186
+ });
187
+ });
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // selectPool — recall-safe fallbacks.
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe("selectPool — degradation on failure", () => {
194
+ test("no provider → no pages, without calling the provider", async () => {
195
+ providerStub = null;
196
+ const result = await selectPool(makePool(), makeTurn("x"));
197
+ expect(result).toEqual([]);
198
+ expect(providerCalls).toHaveLength(0);
199
+ });
200
+
201
+ test("missing tool_use → no pages after retrying", async () => {
202
+ providerStub = makeProvider(noToolResponse());
203
+ const result = await selectPool(makePool(), makeTurn("x"));
204
+ expect(result).toEqual([]);
205
+ expect(providerCalls).toHaveLength(3);
206
+ });
207
+
208
+ test("schema mismatch → no pages after retrying", async () => {
209
+ providerStub = makeProvider(toolUseResponse({ ids: "not-an-array" }));
210
+ const result = await selectPool(makePool(), makeTurn("x"));
211
+ expect(result).toEqual([]);
212
+ expect(providerCalls).toHaveLength(3);
213
+ });
214
+
215
+ test("provider throw → no pages after retrying", async () => {
216
+ providerStub = makeThrowingProvider();
217
+ const result = await selectPool(makePool(), makeTurn("x"));
218
+ expect(result).toEqual([]);
219
+ expect(providerCalls).toHaveLength(3);
220
+ });
221
+
222
+ test("a malformed response that recovers on retry returns its pages", async () => {
223
+ providerStub = makeSequenceProvider([
224
+ noToolResponse(),
225
+ toolUseResponse({ ids: [2] }),
226
+ ]);
227
+ const result = await selectPool(makePool(), makeTurn("the metrics"));
228
+ expect(result).toEqual([{ slug: "page-b", pinned: false }]);
229
+ expect(providerCalls).toHaveLength(2);
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // selectPool — request shape (no cache breakpoint; pool is dynamic per turn).
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("selectPool — request shape", () => {
238
+ test("forces tool_choice to select_pages with the v3 L2 call site", async () => {
239
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
240
+ await selectPool(makePool(), makeTurn("rollout?"));
241
+
242
+ expect(providerCalls).toHaveLength(1);
243
+ const [call] = providerCalls;
244
+ const cfg = call.options?.config as Record<string, unknown>;
245
+ expect(cfg?.callSite).toBe("memoryV3SelectL2");
246
+ expect(cfg?.tool_choice).toEqual({ type: "tool", name: "select_pages" });
247
+ expect(call.options?.tools?.[0]?.name).toBe("select_pages");
248
+ });
249
+
250
+ test("renders numbered candidates with descriptors and NO cache breakpoint", async () => {
251
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
252
+ await selectPool(makePool(), makeTurn("rollout?"));
253
+
254
+ const content = providerCalls[0].messages[0].content as Array<{
255
+ type: string;
256
+ text: string;
257
+ cache_control?: unknown;
258
+ }>;
259
+ // A single text block — the dynamic pool has no static prefix to cache.
260
+ expect(content).toHaveLength(1);
261
+ const [block] = content;
262
+ expect(block.type).toBe("text");
263
+ expect(block.text).toContain(
264
+ "[1] page-a — section: the alpha rollout plan",
265
+ );
266
+ expect(block.text).toContain(
267
+ "[2] page-b — section: beta metrics dashboard",
268
+ );
269
+ expect(block.text).toContain("[3] topic-x — linked page about topic x");
270
+ expect(block.text).toContain("<current_message>rollout?</current_message>");
271
+ expect(block.text).toContain("<recent_context>");
272
+ expect(block.cache_control).toBeUndefined();
273
+ });
274
+
275
+ test("long descriptors are truncated in the candidate list", async () => {
276
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
277
+ const longDescriptor = "z".repeat(1000);
278
+ await selectPool(
279
+ [{ slug: "page-a", descriptor: longDescriptor }],
280
+ makeTurn("x"),
281
+ );
282
+ const text = (providerCalls[0].messages[0].content[0] as { text: string })
283
+ .text;
284
+ expect(text).toContain("...");
285
+ expect(text).not.toContain("z".repeat(1000));
286
+ });
287
+
288
+ test("situational context renders in the user message when present", async () => {
289
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
290
+ await selectPool(makePool(), {
291
+ ...makeTurn("rollout?"),
292
+ situationalContext: "Today is Saturday. The launch is today.",
293
+ });
294
+ const text = (providerCalls[0].messages[0].content[0] as { text: string })
295
+ .text;
296
+ expect(text).toContain(
297
+ "<situation>Today is Saturday. The launch is today.</situation>",
298
+ );
299
+ });
300
+
301
+ test("system prompt mentions pinned (locks the pinning commitment)", async () => {
302
+ providerStub = makeProvider(toolUseResponse({ ids: [1] }));
303
+ await selectPool(makePool(), makeTurn("x"));
304
+ expect(providerCalls[0].options?.systemPrompt).toMatch(/pinned/);
305
+ });
306
+ });
@@ -2,47 +2,76 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { wrapMemoryBlock } from "../../../../memory/memory-marker.js";
4
4
  import { renderMemoryBlock } from "../render-injection.js";
5
- import { Slug } from "../types.js";
5
+ import type { Section, Slug } from "../types.js";
6
6
 
7
+ function section(article: Slug, text: string): Section {
8
+ return { article, title: "", text, ordinal: 0 };
9
+ }
10
+
11
+ /**
12
+ * Stub resolver mirroring `renderV3SectionContent`'s branch: render the matched
13
+ * section's text when present, otherwise fall back to a "full body" stand-in.
14
+ */
7
15
  const resolver =
8
- (pages: Record<Slug, string>) =>
9
- async (slug: Slug): Promise<string> =>
10
- pages[slug] ?? "";
16
+ (fullBodies: Record<Slug, string>) =>
17
+ async (slug: Slug, sec: Section | undefined): Promise<string> =>
18
+ sec ? sec.text : (fullBodies[slug] ?? "");
11
19
 
12
20
  describe("renderMemoryBlock", () => {
13
- test("wraps the selection in a single <memory> block, in order", async () => {
14
- const pages: Record<Slug, string> = {
15
- "page-a": "alpha body",
16
- "page-b": "beta body",
17
- };
21
+ test("renders the matched section (not the full body) for each slug", async () => {
22
+ const sections = new Map<Slug, Section>([
23
+ ["page-a", section("page-a", "matched section A")],
24
+ ["topic-x", section("topic-x", "matched section X")],
25
+ ]);
26
+
27
+ const block = await renderMemoryBlock(
28
+ ["page-a", "topic-x"],
29
+ sections,
30
+ resolver({ "page-a": "FULL body A", "topic-x": "FULL body X" }),
31
+ );
32
+
33
+ expect(block).toBe(
34
+ "<memory>\nmatched section A\nmatched section X\n</memory>",
35
+ );
36
+ expect(block).not.toContain("FULL body");
37
+ });
38
+
39
+ test("falls back to the full/lead body for a slug with no matched section", async () => {
40
+ const sections = new Map<Slug, Section>([
41
+ ["page-a", section("page-a", "matched section A")],
42
+ ]);
18
43
 
19
44
  const block = await renderMemoryBlock(
20
- ["page-a", "page-b"],
21
- resolver(pages),
45
+ ["page-a", "topic-x"],
46
+ sections,
47
+ resolver({ "topic-x": "lead body X" }),
22
48
  );
23
49
 
24
- expect(block).toBe("<memory>\nalpha body\nbeta body\n</memory>");
50
+ // page-a renders its matched section; topic-x (no entry) falls back.
51
+ expect(block).toBe("<memory>\nmatched section A\nlead body X\n</memory>");
25
52
  });
26
53
 
27
54
  test("empty selection renders the empty string", async () => {
28
- const block = await renderMemoryBlock([], async () => "unused");
55
+ const block = await renderMemoryBlock([], new Map(), async () => "unused");
29
56
  expect(block).toBe("");
30
57
  });
31
58
 
32
59
  test("preserves input order deterministically", async () => {
33
- const pages: Record<Slug, string> = {
34
- "page-a": "A",
35
- "page-b": "B",
36
- "page-c": "C",
37
- };
60
+ const sections = new Map<Slug, Section>([
61
+ ["page-a", section("page-a", "A")],
62
+ ["page-b", section("page-b", "B")],
63
+ ["page-c", section("page-c", "C")],
64
+ ]);
38
65
 
39
66
  const forward = await renderMemoryBlock(
40
67
  ["page-a", "page-b", "page-c"],
41
- resolver(pages),
68
+ sections,
69
+ resolver({}),
42
70
  );
43
71
  const reversed = await renderMemoryBlock(
44
72
  ["page-c", "page-b", "page-a"],
45
- resolver(pages),
73
+ sections,
74
+ resolver({}),
46
75
  );
47
76
 
48
77
  expect(forward).toBe("<memory>\nA\nB\nC\n</memory>");
@@ -52,7 +81,8 @@ describe("renderMemoryBlock", () => {
52
81
  test("emits the shared wrapMemoryBlock marker the v2 stripper recognizes", async () => {
53
82
  const block = await renderMemoryBlock(
54
83
  ["page-a"],
55
- resolver({ "page-a": "x" }),
84
+ new Map([["page-a", section("page-a", "x")]]),
85
+ resolver({}),
56
86
  );
57
87
  // Guard the v2/v3 marker contract: the rendered block must be byte-identical
58
88
  // to wrapping the joined content with the shared helper.