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