@vellumai/assistant 0.10.3-dev.202606300531.247eff8 → 0.10.3-dev.202606300731.35e9ab8
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/package.json +1 -1
- package/scripts/generate-openapi.ts +80 -58
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +1 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +21 -0
- package/src/__tests__/workspace-migration-117-normalize-stale-lean-memory-v3-defaults.test.ts +221 -0
- package/src/cli/commands/memory/memory-v2.ts +1 -1
- package/src/cli/commands/memory/memory-v3.ts +2 -2
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -11
- package/src/config/schemas/memory-v3.ts +54 -10
- package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +1 -1
- package/src/{runtime → plugins/defaults/memory}/routes/__tests__/memory-v2-routes.test.ts +3 -3
- package/src/{runtime → plugins/defaults/memory}/routes/__tests__/memory-v2-simulate-route.test.ts +11 -11
- package/src/{runtime → plugins/defaults/memory}/routes/memory-eval-routes.ts +12 -9
- package/src/{runtime → plugins/defaults/memory}/routes/memory-item-routes.test.ts +19 -12
- package/src/{runtime → plugins/defaults/memory}/routes/memory-item-routes.ts +17 -13
- package/src/{runtime → plugins/defaults/memory}/routes/memory-v2-routes.ts +23 -23
- package/src/{runtime → plugins/defaults/memory}/routes/memory-v3-routes.ts +10 -7
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +69 -1
- package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +2 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +1 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +44 -1
- package/src/plugins/defaults/memory/v3/__tests__/tuning-profile.test.ts +96 -0
- package/src/plugins/defaults/memory/v3/entity-lane.test.ts +124 -0
- package/src/plugins/defaults/memory/v3/entity-lane.ts +113 -0
- package/src/plugins/defaults/memory/v3/hot-set.test.ts +1 -1
- package/src/plugins/defaults/memory/v3/orchestrate.ts +43 -0
- package/src/plugins/defaults/memory/v3/section-needle.ts +20 -5
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +55 -11
- package/src/plugins/defaults/memory/v3/tuning-profile.ts +91 -0
- package/src/plugins/defaults/memory/v3/types.ts +4 -1
- package/src/runtime/routes/index.ts +4 -4
- package/src/workspace/migrations/117-normalize-stale-lean-memory-v3-defaults.ts +116 -0
- package/src/workspace/migrations/registry.ts +2 -0
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* cd assistant && bun run generate:openapi -- --check # CI: fail if stale
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { readFileSync } from "node:fs";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
20
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
21
21
|
import { join, resolve } from "node:path";
|
|
22
22
|
|
|
@@ -32,6 +32,7 @@ import { createDocument } from "zod-openapi";
|
|
|
32
32
|
|
|
33
33
|
const ROOT = resolve(import.meta.dir, "..");
|
|
34
34
|
const ROUTES_DIR = join(ROOT, "src/runtime/routes");
|
|
35
|
+
const PLUGIN_DEFAULTS_DIR = join(ROOT, "src/plugins/defaults");
|
|
35
36
|
const OUTPUT_PATH = join(ROOT, "openapi.yaml");
|
|
36
37
|
const PKG_PATH = join(ROOT, "package.json");
|
|
37
38
|
|
|
@@ -147,68 +148,89 @@ function resolveSchemaForDocument(schemaSource: unknown): ContentSchema {
|
|
|
147
148
|
// ---------------------------------------------------------------------------
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
151
|
+
* Directories holding `RouteDefinition` modules: the host `runtime/routes`
|
|
152
|
+
* tree plus every default plugin's `routes/` subdirectory. A default plugin
|
|
153
|
+
* that owns model-facing HTTP/IPC routes defines them under
|
|
154
|
+
* `plugins/defaults/<plugin>/routes/` and the host route aggregator registers
|
|
155
|
+
* them at runtime; the generator scans those dirs too so the spec stays
|
|
156
|
+
* complete no matter which package a route lives in. Sorted for reproducible
|
|
157
|
+
* output (see {@link collectRoutesFromModules}).
|
|
158
|
+
*/
|
|
159
|
+
async function routeModuleDirs(): Promise<string[]> {
|
|
160
|
+
const dirs = [ROUTES_DIR];
|
|
161
|
+
for (const entry of await readdir(PLUGIN_DEFAULTS_DIR, {
|
|
162
|
+
withFileTypes: true,
|
|
163
|
+
})) {
|
|
164
|
+
if (!entry.isDirectory()) continue;
|
|
165
|
+
const routesDir = join(PLUGIN_DEFAULTS_DIR, entry.name, "routes");
|
|
166
|
+
if (existsSync(routesDir)) dirs.push(routesDir);
|
|
167
|
+
}
|
|
168
|
+
return dirs.sort();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Dynamically import every route module under each {@link routeModuleDirs}
|
|
173
|
+
* entry and collect all exported `ROUTES` / `*_ROUTES` arrays. Each route
|
|
174
|
+
* module exports a `RouteDefinition[]`; new modules are picked up without
|
|
175
|
+
* manual updates.
|
|
156
176
|
*/
|
|
157
177
|
async function collectRoutesFromModules(): Promise<RouteEntry[]> {
|
|
158
178
|
const routes: RouteEntry[] = [];
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
for (const file of files) {
|
|
182
|
-
const filePath = join(ROUTES_DIR, file);
|
|
183
|
-
let mod: Record<string, unknown>;
|
|
184
|
-
try {
|
|
185
|
-
mod = (await import(filePath)) as Record<string, unknown>;
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.warn(
|
|
188
|
-
`Warning: could not import ${file}: ${err instanceof Error ? err.message : err}`,
|
|
189
|
-
);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Collect every export whose name is `ROUTES` or ends in `_ROUTES`.
|
|
194
|
-
// A handful of route files (e.g. `channel-route-definitions.ts`,
|
|
195
|
-
// `contact-prompt-routes.ts`) export under domain-prefixed names like
|
|
196
|
-
// `CHANNEL_ROUTES` and `CONTACT_PROMPT_ROUTES` rather than the
|
|
197
|
-
// canonical `ROUTES`. Without this fan-out the only way those routes
|
|
198
|
-
// reached the spec was via the `index.ts` barrel — which is excluded
|
|
199
|
-
// above for reproducibility.
|
|
200
|
-
const exportNames = Object.keys(mod)
|
|
201
|
-
.filter((k) => k === "ROUTES" || k.endsWith("_ROUTES"))
|
|
180
|
+
for (const dir of await routeModuleDirs()) {
|
|
181
|
+
// Skip the `index.ts` barrel: it re-exports every other route module's
|
|
182
|
+
// ROUTES into a single combined array, so importing it would double-count
|
|
183
|
+
// every entry. The duplicate `method:endpoint` keys are deduped later by
|
|
184
|
+
// first-seen, but the surviving entry's `sourceModule` (used to derive
|
|
185
|
+
// OpenAPI `tags`) depends on `readdir` order — which is filesystem
|
|
186
|
+
// dependent and diverges between local sandbox and the CI runner, making
|
|
187
|
+
// the generator non-reproducible. Sort the file list as well so directory
|
|
188
|
+
// entry order can never affect the output.
|
|
189
|
+
const files = (await readdir(dir, { recursive: true }))
|
|
190
|
+
.filter(
|
|
191
|
+
(f) =>
|
|
192
|
+
typeof f === "string" &&
|
|
193
|
+
f.endsWith(".ts") &&
|
|
194
|
+
!f.endsWith(".test.ts") &&
|
|
195
|
+
!f.endsWith(".benchmark.test.ts") &&
|
|
196
|
+
!f.includes("node_modules") &&
|
|
197
|
+
f !== "index.ts" &&
|
|
198
|
+
!f.endsWith("/index.ts"),
|
|
199
|
+
)
|
|
202
200
|
.sort();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
201
|
+
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
const filePath = join(dir, file);
|
|
204
|
+
let mod: Record<string, unknown>;
|
|
205
|
+
try {
|
|
206
|
+
mod = (await import(filePath)) as Record<string, unknown>;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.warn(
|
|
209
|
+
`Warning: could not import ${file}: ${err instanceof Error ? err.message : err}`,
|
|
210
|
+
);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Collect every export whose name is `ROUTES` or ends in `_ROUTES`.
|
|
215
|
+
// A handful of route files (e.g. `channel-route-definitions.ts`,
|
|
216
|
+
// `contact-prompt-routes.ts`) export under domain-prefixed names like
|
|
217
|
+
// `CHANNEL_ROUTES` and `CONTACT_PROMPT_ROUTES` rather than the
|
|
218
|
+
// canonical `ROUTES`. Without this fan-out the only way those routes
|
|
219
|
+
// reached the spec was via the `index.ts` barrel — which is excluded
|
|
220
|
+
// above for reproducibility.
|
|
221
|
+
const exportNames = Object.keys(mod)
|
|
222
|
+
.filter((k) => k === "ROUTES" || k.endsWith("_ROUTES"))
|
|
223
|
+
.sort();
|
|
224
|
+
for (const name of exportNames) {
|
|
225
|
+
const arr = mod[name];
|
|
226
|
+
if (!Array.isArray(arr)) continue;
|
|
227
|
+
for (const raw of arr) {
|
|
228
|
+
const result = RouteEntrySchema.safeParse({
|
|
229
|
+
...(typeof raw === "object" && raw !== null ? raw : {}),
|
|
230
|
+
sourceModule: file,
|
|
231
|
+
});
|
|
232
|
+
if (result.success) routes.push(result.data);
|
|
233
|
+
}
|
|
212
234
|
}
|
|
213
235
|
}
|
|
214
236
|
}
|
|
@@ -54,7 +54,7 @@ mock.module("../memory/v2/skill-store.js", () => ({
|
|
|
54
54
|
const { registerMemoryV2Command } =
|
|
55
55
|
await import("../cli/commands/memory/memory-v2.js");
|
|
56
56
|
const { ROUTES: memoryV2Routes, MEMORY_V2_DISABLED_CODE } =
|
|
57
|
-
await import("../
|
|
57
|
+
await import("../plugins/defaults/memory/routes/memory-v2-routes.js");
|
|
58
58
|
const { RouteError } = await import("../runtime/routes/errors.js");
|
|
59
59
|
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
@@ -91,15 +91,29 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
91
91
|
"../../../../daemon/message-types/memory.js",
|
|
92
92
|
"../../../../daemon/trust-context.js",
|
|
93
93
|
"../../../../memory/graph/conversation-graph-memory.js",
|
|
94
|
+
"../../../../memory/graph/store.js",
|
|
95
|
+
"../../../../memory/graph/types.js",
|
|
94
96
|
"../../../../memory/memory-marker.js",
|
|
95
97
|
"../../../../memory/memory-recall-log-store.js",
|
|
98
|
+
"../../../../memory/memory-v2-concept-frequency.js",
|
|
96
99
|
"../../../../memory/prompt-override.js",
|
|
97
100
|
"../../../../memory/v2/cli-command-store.js",
|
|
101
|
+
"../../../../memory/v2/edge-index.js",
|
|
102
|
+
"../../../../memory/v2/harness/compare.js",
|
|
103
|
+
"../../../../memory/v2/harness/retriever.js",
|
|
104
|
+
"../../../../memory/v2/harness/router-retriever.js",
|
|
105
|
+
"../../../../memory/v2/harness/runner.js",
|
|
98
106
|
"../../../../memory/v2/injected-block-slugs.js",
|
|
107
|
+
"../../../../memory/v2/injection-events.js",
|
|
108
|
+
"../../../../memory/v2/now-text.js",
|
|
99
109
|
"../../../../memory/v2/page-index.js",
|
|
100
110
|
"../../../../memory/v2/page-store.js",
|
|
111
|
+
"../../../../memory/v2/prompts/router.js",
|
|
112
|
+
"../../../../memory/v2/router.js",
|
|
101
113
|
"../../../../memory/v2/sim.js",
|
|
102
114
|
"../../../../memory/v2/skill-store.js",
|
|
115
|
+
"../../../../memory/v3-eval/eval-packets.js",
|
|
116
|
+
"../../../../memory/v3-eval/eval-tally.js",
|
|
103
117
|
"../../../../persistence/checkpoints.js",
|
|
104
118
|
"../../../../persistence/conversation-crud.js",
|
|
105
119
|
"../../../../persistence/db-connection.js",
|
|
@@ -108,13 +122,18 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
108
122
|
"../../../../persistence/embeddings/embedding-billing-breaker.js",
|
|
109
123
|
"../../../../persistence/embeddings/embedding-cache.js",
|
|
110
124
|
"../../../../persistence/embeddings/embedding-types.js",
|
|
125
|
+
"../../../../persistence/embeddings/qdrant-circuit-breaker.js",
|
|
111
126
|
"../../../../persistence/embeddings/qdrant-client.js",
|
|
112
127
|
"../../../../persistence/jobs-store.js",
|
|
113
128
|
"../../../../persistence/message-content.js",
|
|
129
|
+
"../../../../persistence/schema/index.js",
|
|
114
130
|
"../../../../providers/cache-control.js",
|
|
115
131
|
"../../../../providers/provider-send-message.js",
|
|
116
132
|
"../../../../providers/types.js",
|
|
117
133
|
"../../../../runtime/assistant-event-hub.js",
|
|
134
|
+
"../../../../runtime/auth/route-policy.js",
|
|
135
|
+
"../../../../runtime/routes/errors.js",
|
|
136
|
+
"../../../../runtime/routes/types.js",
|
|
118
137
|
"../../../../skills/frontmatter.js",
|
|
119
138
|
"../../../../skills/install-meta.js",
|
|
120
139
|
"../../../../tools/skills/delete-managed.js",
|
|
@@ -167,8 +186,10 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
167
186
|
"../injection-presence.js",
|
|
168
187
|
"../injector-order.js",
|
|
169
188
|
"@qdrant/js-client-rest",
|
|
189
|
+
"drizzle-orm",
|
|
170
190
|
"node:crypto",
|
|
171
191
|
"node:fs",
|
|
192
|
+
"node:fs/promises",
|
|
172
193
|
"node:path",
|
|
173
194
|
"uuid",
|
|
174
195
|
"zod",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for workspace migration `117-normalize-stale-lean-memory-v3-defaults`.
|
|
3
|
+
*
|
|
4
|
+
* The first-launch seed persists the fully-defaulted config.json, so assistants
|
|
5
|
+
* created during the lean-default window stored the lean v3 tuning values as if
|
|
6
|
+
* explicit. The migration strips persisted v3 tuning leaves that equal the
|
|
7
|
+
* retired lean defaults so the restored full schema defaults re-apply, while
|
|
8
|
+
* preserving deliberate (non-lean) values, pre-lean full values, and untouched
|
|
9
|
+
* sibling fields. It never creates config.json and is idempotent.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdtempSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
22
|
+
|
|
23
|
+
import { MemoryV3ConfigSchema } from "../config/schemas/memory-v3.js";
|
|
24
|
+
import { normalizeStaleLeanMemoryV3DefaultsMigration } from "../workspace/migrations/117-normalize-stale-lean-memory-v3-defaults.js";
|
|
25
|
+
|
|
26
|
+
let workspaceDir: string;
|
|
27
|
+
let configPath: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-117-test-"));
|
|
31
|
+
configPath = join(workspaceDir, "config.json");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (existsSync(workspaceDir)) {
|
|
36
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function readConfig(): Record<string, unknown> {
|
|
41
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A config.json as the first-launch seed would persist it on the lean build:
|
|
45
|
+
* the full v3 block with every tuning value at its lean default. */
|
|
46
|
+
function leanSeedConfig(): Record<string, unknown> {
|
|
47
|
+
return {
|
|
48
|
+
memory: {
|
|
49
|
+
v3: {
|
|
50
|
+
live: true,
|
|
51
|
+
prune: { maxResidentBytes: 393216, targetResidentBytes: 262144 },
|
|
52
|
+
hotSet: { k: 8, halfLifeDays: 14 },
|
|
53
|
+
freshSet: { k: 8 },
|
|
54
|
+
learnedEdges: {
|
|
55
|
+
halfLifeDays: 30,
|
|
56
|
+
minCount: 3,
|
|
57
|
+
npmiFloor: 0.2,
|
|
58
|
+
maxPerPage: 6,
|
|
59
|
+
perSeed: 3,
|
|
60
|
+
cap: 0,
|
|
61
|
+
},
|
|
62
|
+
spotlight: { n: 6, windowTurns: 2 },
|
|
63
|
+
needleK: 12,
|
|
64
|
+
denseK: 0,
|
|
65
|
+
replyQueryK: 0,
|
|
66
|
+
selectorEnabled: false,
|
|
67
|
+
selectorPromptPath: null,
|
|
68
|
+
edge: { hubDegree: 30, seedCount: 6, perSeed: 1, cap: 6 },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("117-normalize-stale-lean-memory-v3-defaults migration", () => {
|
|
75
|
+
test("has correct id and description", () => {
|
|
76
|
+
expect(normalizeStaleLeanMemoryV3DefaultsMigration.id).toBe(
|
|
77
|
+
"117-normalize-stale-lean-memory-v3-defaults",
|
|
78
|
+
);
|
|
79
|
+
expect(normalizeStaleLeanMemoryV3DefaultsMigration.description).toContain(
|
|
80
|
+
"memory.v3",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("strips lean tuning leaves so the full schema defaults re-apply", () => {
|
|
85
|
+
writeFileSync(configPath, JSON.stringify(leanSeedConfig()), "utf-8");
|
|
86
|
+
|
|
87
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
88
|
+
|
|
89
|
+
const v3 = (readConfig().memory as Record<string, unknown>).v3 as Record<
|
|
90
|
+
string,
|
|
91
|
+
unknown
|
|
92
|
+
>;
|
|
93
|
+
// The switched leaves are gone…
|
|
94
|
+
expect("needleK" in v3).toBe(false);
|
|
95
|
+
expect("denseK" in v3).toBe(false);
|
|
96
|
+
expect("replyQueryK" in v3).toBe(false);
|
|
97
|
+
expect("selectorEnabled" in v3).toBe(false);
|
|
98
|
+
expect("k" in (v3.hotSet as Record<string, unknown>)).toBe(false);
|
|
99
|
+
expect("k" in (v3.freshSet as Record<string, unknown>)).toBe(false);
|
|
100
|
+
expect("cap" in (v3.learnedEdges as Record<string, unknown>)).toBe(false);
|
|
101
|
+
const edge = v3.edge as Record<string, unknown>;
|
|
102
|
+
expect("seedCount" in edge).toBe(false);
|
|
103
|
+
expect("perSeed" in edge).toBe(false);
|
|
104
|
+
expect("cap" in edge).toBe(false);
|
|
105
|
+
|
|
106
|
+
// …untouched siblings survive…
|
|
107
|
+
expect(v3.live).toBe(true);
|
|
108
|
+
expect((v3.hotSet as Record<string, unknown>).halfLifeDays).toBe(14);
|
|
109
|
+
expect(edge.hubDegree).toBe(30);
|
|
110
|
+
expect((v3.learnedEdges as Record<string, unknown>).perSeed).toBe(3);
|
|
111
|
+
|
|
112
|
+
// …and re-parsing yields the restored full profile.
|
|
113
|
+
const parsed = MemoryV3ConfigSchema.parse(v3);
|
|
114
|
+
expect(parsed.needleK).toBe(100);
|
|
115
|
+
expect(parsed.denseK).toBe(100);
|
|
116
|
+
expect(parsed.replyQueryK).toBe(12);
|
|
117
|
+
expect(parsed.selectorEnabled).toBe(true);
|
|
118
|
+
expect(parsed.hotSet.k).toBe(40);
|
|
119
|
+
expect(parsed.freshSet.k).toBe(100);
|
|
120
|
+
expect(parsed.learnedEdges.cap).toBe(20);
|
|
121
|
+
expect(parsed.edge).toEqual({
|
|
122
|
+
hubDegree: 30,
|
|
123
|
+
seedCount: 18,
|
|
124
|
+
perSeed: 6,
|
|
125
|
+
cap: 45,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("leaves a config with any non-lean tuning untouched (not the lean-seed signature)", () => {
|
|
130
|
+
// A lean-seed config the user later edited one field of no longer matches
|
|
131
|
+
// the all-lean signature, so the migration leaves it alone rather than
|
|
132
|
+
// partially rewriting an explicit config.
|
|
133
|
+
const config = leanSeedConfig();
|
|
134
|
+
(
|
|
135
|
+
(config.memory as Record<string, unknown>).v3 as Record<string, unknown>
|
|
136
|
+
).needleK = 50;
|
|
137
|
+
const original = JSON.stringify(config);
|
|
138
|
+
writeFileSync(configPath, original, "utf-8");
|
|
139
|
+
|
|
140
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
141
|
+
|
|
142
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("preserves a deliberate lean-valued override (partial config, not a full seed)", () => {
|
|
146
|
+
// An established assistant that deliberately disables dense retrieval must
|
|
147
|
+
// keep that choice — a lone lean-valued leaf is not the seed signature.
|
|
148
|
+
const original = JSON.stringify({ memory: { v3: { denseK: 0 } } });
|
|
149
|
+
writeFileSync(configPath, original, "utf-8");
|
|
150
|
+
|
|
151
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
152
|
+
|
|
153
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("leaves a pre-lean full config untouched (no write)", () => {
|
|
157
|
+
const fullConfig = {
|
|
158
|
+
memory: {
|
|
159
|
+
v3: {
|
|
160
|
+
needleK: 100,
|
|
161
|
+
denseK: 100,
|
|
162
|
+
replyQueryK: 12,
|
|
163
|
+
selectorEnabled: true,
|
|
164
|
+
hotSet: { k: 40, halfLifeDays: 14 },
|
|
165
|
+
freshSet: { k: 100 },
|
|
166
|
+
learnedEdges: { cap: 20, maxPerPage: 6 },
|
|
167
|
+
edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
const original = JSON.stringify(fullConfig);
|
|
172
|
+
writeFileSync(configPath, original, "utf-8");
|
|
173
|
+
|
|
174
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
175
|
+
|
|
176
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("no-op when config.json is absent (never creates it)", () => {
|
|
180
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
181
|
+
expect(existsSync(configPath)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("no-op when there is no memory.v3 block", () => {
|
|
185
|
+
const original = JSON.stringify({ memory: { enabled: true } });
|
|
186
|
+
writeFileSync(configPath, original, "utf-8");
|
|
187
|
+
|
|
188
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
189
|
+
|
|
190
|
+
expect(readFileSync(configPath, "utf-8")).toBe(original);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("leaves malformed config.json untouched", () => {
|
|
194
|
+
writeFileSync(configPath, "{ not json", "utf-8");
|
|
195
|
+
|
|
196
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
197
|
+
|
|
198
|
+
expect(readFileSync(configPath, "utf-8")).toBe("{ not json");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("is idempotent", () => {
|
|
202
|
+
writeFileSync(configPath, JSON.stringify(leanSeedConfig()), "utf-8");
|
|
203
|
+
|
|
204
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
205
|
+
const afterFirst = readFileSync(configPath, "utf-8");
|
|
206
|
+
|
|
207
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
208
|
+
|
|
209
|
+
expect(readFileSync(configPath, "utf-8")).toBe(afterFirst);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("down is a no-op", () => {
|
|
213
|
+
writeFileSync(configPath, JSON.stringify(leanSeedConfig()), "utf-8");
|
|
214
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.run(workspaceDir);
|
|
215
|
+
const before = readFileSync(configPath, "utf-8");
|
|
216
|
+
|
|
217
|
+
normalizeStaleLeanMemoryV3DefaultsMigration.down(workspaceDir);
|
|
218
|
+
|
|
219
|
+
expect(readFileSync(configPath, "utf-8")).toBe(before);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
MemoryV2ReembedSkillsResult,
|
|
28
28
|
MemoryV2SimulateRouterResult,
|
|
29
29
|
MemoryV2ValidateResult,
|
|
30
|
-
} from "../../../
|
|
30
|
+
} from "../../../plugins/defaults/memory/routes/memory-v2-routes.js";
|
|
31
31
|
import { registerCommand } from "../../lib/register-command.js";
|
|
32
32
|
import { log } from "../../logger.js";
|
|
33
33
|
import {
|
|
@@ -23,11 +23,11 @@ import { cliIpcCall } from "../../../ipc/cli-client.js";
|
|
|
23
23
|
import type {
|
|
24
24
|
MemoryEvalRunResult,
|
|
25
25
|
MemoryEvalTallyResult,
|
|
26
|
-
} from "../../../
|
|
26
|
+
} from "../../../plugins/defaults/memory/routes/memory-eval-routes.js";
|
|
27
27
|
import type {
|
|
28
28
|
MemoryV3BackfillSectionsResult,
|
|
29
29
|
MemoryV3RebuildIndexResult,
|
|
30
|
-
} from "../../../
|
|
30
|
+
} from "../../../plugins/defaults/memory/routes/memory-v3-routes.js";
|
|
31
31
|
import { registerCommand } from "../../lib/register-command.js";
|
|
32
32
|
import { log } from "../../logger.js";
|
|
33
33
|
|
|
@@ -8,23 +8,24 @@ describe("MemoryV3ConfigSchema", () => {
|
|
|
8
8
|
expect(parsed).toEqual({
|
|
9
9
|
live: false,
|
|
10
10
|
prune: { maxResidentBytes: 393216, targetResidentBytes: 262144 },
|
|
11
|
-
hotSet: { k:
|
|
12
|
-
freshSet: { k:
|
|
11
|
+
hotSet: { k: 40, halfLifeDays: 14 },
|
|
12
|
+
freshSet: { k: 100 },
|
|
13
13
|
learnedEdges: {
|
|
14
14
|
halfLifeDays: 30,
|
|
15
15
|
minCount: 3,
|
|
16
16
|
npmiFloor: 0.2,
|
|
17
17
|
maxPerPage: 6,
|
|
18
18
|
perSeed: 3,
|
|
19
|
-
cap:
|
|
19
|
+
cap: 20,
|
|
20
20
|
},
|
|
21
21
|
spotlight: { n: 6, windowTurns: 2 },
|
|
22
|
-
needleK:
|
|
23
|
-
denseK:
|
|
24
|
-
replyQueryK:
|
|
25
|
-
selectorEnabled:
|
|
22
|
+
needleK: 100,
|
|
23
|
+
denseK: 100,
|
|
24
|
+
replyQueryK: 12,
|
|
25
|
+
selectorEnabled: true,
|
|
26
26
|
selectorPromptPath: null,
|
|
27
|
-
edge: { hubDegree: 30, seedCount:
|
|
27
|
+
edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
|
|
28
|
+
entity: { enabled: true, idfFloor: 4, cap: 8 },
|
|
28
29
|
});
|
|
29
30
|
});
|
|
30
31
|
|
|
@@ -105,9 +106,9 @@ describe("MemoryV3ConfigSchema", () => {
|
|
|
105
106
|
const parsed = MemoryV3ConfigSchema.parse({ edge: { hubDegree: 10 } });
|
|
106
107
|
expect(parsed.edge).toEqual({
|
|
107
108
|
hubDegree: 10,
|
|
108
|
-
seedCount:
|
|
109
|
-
perSeed:
|
|
110
|
-
cap:
|
|
109
|
+
seedCount: 18,
|
|
110
|
+
perSeed: 6,
|
|
111
|
+
cap: 45,
|
|
111
112
|
});
|
|
112
113
|
});
|
|
113
114
|
|