@vellumai/assistant 0.8.9-staging.3 → 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 (90) hide show
  1. package/openapi.yaml +16 -115
  2. package/package.json +1 -1
  3. package/src/__tests__/anthropic-provider.test.ts +23 -8
  4. package/src/__tests__/context-window-manager-compact-retry.test.ts +120 -14
  5. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  6. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  7. package/src/__tests__/llm-resolver.test.ts +73 -0
  8. package/src/__tests__/plugin-external-api.test.ts +68 -0
  9. package/src/__tests__/published-app-updater.test.ts +138 -0
  10. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  11. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  12. package/src/cli/commands/memory-v3.ts +57 -199
  13. package/src/config/call-site-defaults.ts +0 -1
  14. package/src/config/llm-resolver.ts +39 -7
  15. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  16. package/src/config/schemas/call-site-catalog.ts +0 -7
  17. package/src/config/schemas/llm.ts +17 -1
  18. package/src/config/schemas/memory-v3.ts +58 -8
  19. package/src/config/seed-inference-profiles.ts +18 -0
  20. package/src/daemon/conversation-agent-loop.ts +9 -17
  21. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  22. package/src/daemon/tool-side-effects.ts +15 -0
  23. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  24. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  25. package/src/memory/v2/consolidation-job.ts +2 -72
  26. package/src/memory/v2/types.ts +5 -0
  27. package/src/plugins/defaults/compaction/window-manager.ts +56 -0
  28. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  29. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  30. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  31. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  32. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  33. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  34. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  35. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  36. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  37. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  38. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  39. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  40. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  41. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  42. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  43. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  44. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  45. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  46. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  47. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  48. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  49. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  50. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  51. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  52. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  53. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  54. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  55. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  56. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  57. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  58. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  59. package/src/plugins/external-api.ts +114 -0
  60. package/src/providers/anthropic/client.ts +9 -10
  61. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  62. package/src/providers/inference/logit-bias.ts +55 -0
  63. package/src/providers/openai/chat-completions-provider.ts +19 -1
  64. package/src/providers/retry.ts +22 -0
  65. package/src/providers/types.ts +6 -0
  66. package/src/runtime/routes/app-management-routes.ts +3 -0
  67. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  68. package/src/services/published-app-updater.ts +30 -8
  69. package/src/tools/skills/load.ts +1 -1
  70. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  71. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  72. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  73. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  74. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  75. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  76. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  77. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  78. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  79. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  80. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  81. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  82. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  83. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  84. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  85. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  86. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  87. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  88. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  89. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  90. package/src/util/map-limit.ts +0 -27
@@ -1,47 +1,38 @@
1
1
  /**
2
2
  * `assistant memory v3` CLI subgroup.
3
3
  *
4
- * Operator-facing tree-gardening + core-editing subcommands for the v3 memory
5
- * subsystem (leaf-tree activation model). The daemon owns the live tree, so the
6
- * mutating verbs route through `cliIpcCall` to handlers that run inside the
7
- * assistant process (where the in-memory lanes can be invalidated after a
8
- * write). The read-only `health` report and the `set-core` cost preview also
9
- * route through the daemon so it stays the single source of truth for the live
10
- * workspace tree.
4
+ * Operator-facing live-lane maintenance subcommands for the v3 memory subsystem
5
+ * (section-lane retrieval model). The daemon owns the live shadow lanes, so the
6
+ * verbs route through `cliIpcCall` to handlers that run inside the assistant
7
+ * process (where the in-memory lanes can be invalidated/rebuilt after a write).
11
8
  *
12
9
  * Subcommands:
13
10
  *
14
- * - `health` — print the structural health report (read-only).
15
- * - `reconcile` — v1 convergence pass: rewrites any dangling page/core ref to
16
- * the current on-disk tree and prunes stale core entries. It does NOT yet
17
- * detect renames/moves/splits — that requires a captured prior leaf
18
- * snapshot (a follow-up), so `renames` is always empty in v1.
19
- * - `set-core` — add/remove always-on core leaves. Validates that every added
20
- * leaf exists, previews the resulting always-on page count, and writes only
21
- * on `--yes`.
22
11
  * - `rebuild-index` — invalidate the v3 lanes so the next turn rebuilds.
23
- *
24
- * Deferred to follow-ups: thin `add-leaf` / `rename-leaf` / `delete-leaf`
25
- * wrappers (edit the leaf file[s] then reconcile).
12
+ * - `backfill-sections` — one-time: embed every page's sections (including
13
+ * synthetic skill/CLI rows) into the dense store, then advance the maintain
14
+ * checkpoint. For the transition before A/B/cutover, since the collection
15
+ * starts empty and the incremental maintain pass only re-embeds deltas.
26
16
  */
27
17
 
28
18
  import type { Command } from "commander";
29
19
 
30
20
  import { cliIpcCall } from "../../ipc/cli-client.js";
31
21
  import type {
32
- MemoryV3HealthResult,
22
+ MemoryV3BackfillSectionsResult,
33
23
  MemoryV3RebuildIndexResult,
34
- MemoryV3ReconcileResult,
35
- MemoryV3SetCoreResult,
36
24
  } from "../../runtime/routes/memory-v3-routes.js";
37
25
  import { registerCommand } from "../lib/register-command.js";
38
26
  import { log } from "../logger.js";
39
27
 
40
- /** Commander collector for a repeatable option: accumulates each value. */
41
- function collect(val: string, acc: string[]): string[] {
42
- acc.push(val);
43
- return acc;
44
- }
28
+ /**
29
+ * IPC timeout for `backfill-sections`. The one-time full-corpus section embed
30
+ * runs every page's chunks through the embedder sequentially, which easily
31
+ * outlasts `cliIpcCall`'s default 60s — so we give it a generous 30-minute
32
+ * ceiling rather than report a spurious "Request timed out" while the assistant
33
+ * keeps working.
34
+ */
35
+ const BACKFILL_IPC_TIMEOUT_MS = 30 * 60 * 1000;
45
36
 
46
37
  export function registerMemoryV3Command(program: Command): void {
47
38
  // Reuse an existing `memory` parent if some other registrar attached to it
@@ -54,97 +45,80 @@ export function registerMemoryV3Command(program: Command): void {
54
45
  registerCommand(memory, {
55
46
  name: "v3",
56
47
  transport: "ipc",
57
- description: "Memory v3 tree-gardening and core-editing (leaf-tree model)",
48
+ description: "Memory v3 live-lane maintenance (section-lane model)",
58
49
  build: (v3) => {
59
50
  v3.addHelpText(
60
51
  "after",
61
52
  `
62
- The v3 memory subsystem organizes concept pages under a curated leaf tree
63
- (/workspace/memory/v3/data/) and pins an always-on "core" set of leaves.
64
- These commands garden that tree safely: each mutating verb snapshots and
65
- fail-closed validates before committing, and invalidates the live lanes so
66
- the next turn rebuilds from the reconciled state.
53
+ The v3 memory subsystem retrieves concept pages over section-grain lanes and
54
+ caches them as live shadow lanes inside the assistant. These commands maintain
55
+ that live state safely.
67
56
 
68
57
  Examples:
69
- $ assistant memory v3 health
70
- $ assistant memory v3 set-core --add domain-a/topic-x
71
- $ assistant memory v3 set-core --remove domain-a/topic-y --yes
72
- $ assistant memory v3 reconcile
73
- $ assistant memory v3 rebuild-index`,
58
+ $ assistant memory v3 rebuild-index
59
+ $ assistant memory v3 backfill-sections`,
74
60
  );
75
61
 
76
- // ── health ────────────────────────────────────────────────────────────
62
+ // ── rebuild-index ─────────────────────────────────────────────────────
77
63
 
78
- v3.command("health")
79
- .description("Print the v3 structural health report (read-only)")
80
- .option("--json", "Emit raw JSON instead of the rendered report")
64
+ v3.command("rebuild-index")
65
+ .description("Invalidate the v3 lanes so the next turn rebuilds")
81
66
  .addHelpText(
82
67
  "after",
83
68
  `
84
- Loads the live leaf tree + core set and reports where the taxonomy has
85
- drifted from the page graph: unassigned slugs, dangling refs, novel
86
- clusters, and over/under-sized leaves. Read-only — no writes. Prints
87
- "all green" when no maintenance is warranted.
69
+ Drops the assistant's cached v3 shadow lanes so the section index is rebuilt
70
+ from the current on-disk state on the next turn. Useful after editing concept
71
+ pages out-of-band.
88
72
 
89
73
  Examples:
90
- $ assistant memory v3 health
91
- $ assistant memory v3 health --json | jq '.counts'`,
74
+ $ assistant memory v3 rebuild-index`,
92
75
  )
93
- .action(async (opts: { json?: boolean }) => {
94
- const result = await cliIpcCall<MemoryV3HealthResult>(
95
- "memory_v3_health",
76
+ .action(async () => {
77
+ const result = await cliIpcCall<MemoryV3RebuildIndexResult>(
78
+ "memory_v3_rebuild_index",
96
79
  { body: {} },
97
80
  );
98
81
  if (!result.ok) {
99
- log.error(result.error ?? "Failed to compute v3 health");
82
+ log.error(result.error ?? "Failed to invalidate the v3 lanes");
100
83
  process.exitCode = 1;
101
84
  return;
102
85
  }
103
- const payload = result.result!;
104
- if (opts.json === true) {
105
- log.info(JSON.stringify(payload, null, 2));
106
- return;
107
- }
108
- log.info(
109
- payload.rendered === ""
110
- ? "memory-v3 health: all green"
111
- : payload.rendered,
112
- );
86
+ log.info("v3 lanes invalidated; next turn will rebuild.");
113
87
  });
114
88
 
115
- // ── reconcile ─────────────────────────────────────────────────────────
89
+ // ── backfill-sections ─────────────────────────────────────────────────
116
90
 
117
- v3.command("reconcile")
91
+ v3.command("backfill-sections")
118
92
  .description(
119
- "v1 convergence/prune pass over page/core refs (no rename detection yet)",
93
+ "One-time: embed every page's sections into the dense store (incl skills/CLI)",
120
94
  )
121
95
  .option("--json", "Emit raw JSON instead of a formatted summary")
122
96
  .addHelpText(
123
97
  "after",
124
98
  `
125
- Runs a convergence pass over the leaf tree: rewrites any page leaves:
126
- frontmatter or core.json entry that dangles to a path the current on-disk
127
- tree no longer has, and prunes stale core entries. Fail-closed: snapshots
128
- before mutating and rolls back if any dangling reference survives
129
- validation. On success the v3 lanes are invalidated.
99
+ Embeds EVERY concept page's sections — including synthetic skill and CLI
100
+ capability rows — into the section dense store in one pass, then advances
101
+ the maintain checkpoint so the next incremental pass only re-embeds future
102
+ edits. Use this once on an existing install before the section-lane A/B and
103
+ cutover: the dense collection starts empty, and the periodic maintenance
104
+ pass only re-embeds pages edited since its last run (and never the synthetic
105
+ rows), so most of the corpus would otherwise never be embedded.
130
106
 
131
- Limitation: v1 cannot detect renames/moves/splits. The reconciler diffs
132
- the current tree against itself (there is no captured "before" snapshot),
133
- so renames always report as empty and a true rename surfaces as a
134
- delete + add. Full rename/move/split detection requires persisting a prior
135
- leaf snapshot and is a planned follow-up.
107
+ Idempotent and safe to re-run. Runs inside the assistant so it uses the live
108
+ configuration and advances the checkpoint the assistant reads.
136
109
 
137
110
  Examples:
138
- $ assistant memory v3 reconcile
139
- $ assistant memory v3 reconcile --json | jq '.prunedCore'`,
111
+ $ assistant memory v3 backfill-sections
112
+ $ assistant memory v3 backfill-sections --json | jq '.sections'`,
140
113
  )
141
114
  .action(async (opts: { json?: boolean }) => {
142
- const result = await cliIpcCall<MemoryV3ReconcileResult>(
143
- "memory_v3_reconcile",
115
+ const result = await cliIpcCall<MemoryV3BackfillSectionsResult>(
116
+ "memory_v3_backfill_sections",
144
117
  { body: {} },
118
+ { timeoutMs: BACKFILL_IPC_TIMEOUT_MS },
145
119
  );
146
120
  if (!result.ok) {
147
- log.error(result.error ?? "Failed to reconcile the v3 tree");
121
+ log.error(result.error ?? "Failed to backfill section embeddings");
148
122
  process.exitCode = 1;
149
123
  return;
150
124
  }
@@ -154,127 +128,11 @@ Examples:
154
128
  return;
155
129
  }
156
130
  log.info(
157
- `Renamed: ${payload.renames.length === 0 ? "none" : payload.renames.length}`,
158
- );
159
- for (const r of payload.renames) {
160
- log.info(` - ${r.oldPath} → ${r.newPath}`);
161
- }
162
- log.info(
163
- `Deleted: ${payload.deleted.length === 0 ? "none" : payload.deleted.join(", ")}`,
131
+ `Embedded ${payload.sections} sections across ${payload.articles} pages` +
132
+ (payload.failures > 0
133
+ ? ` (${payload.failures} page(s) failed — see assistant logs).`
134
+ : "."),
164
135
  );
165
- log.info(
166
- `Pruned core: ${payload.prunedCore.length === 0 ? "none" : payload.prunedCore.join(", ")}`,
167
- );
168
- });
169
-
170
- // ── set-core ──────────────────────────────────────────────────────────
171
-
172
- v3.command("set-core")
173
- .description(
174
- "Add/remove always-on core leaves (previews cost; writes on --yes)",
175
- )
176
- .option(
177
- "--add <leaf>",
178
- "Leaf path to add to the always-on core set (repeatable)",
179
- collect,
180
- [] as string[],
181
- )
182
- .option(
183
- "--remove <leaf>",
184
- "Leaf path to remove from the always-on core set (repeatable)",
185
- collect,
186
- [] as string[],
187
- )
188
- .option("--yes", "Apply the change (otherwise preview only)")
189
- .option("--json", "Emit raw JSON instead of a formatted summary")
190
- .addHelpText(
191
- "after",
192
- `
193
- Edits the always-on core leaf set in core.json. Every --add entry must
194
- exist in the live tree (unknown leaves are rejected). Without --yes this
195
- PREVIEWS the resulting core set and the number of unique page slugs it
196
- would pin always-on, WITHOUT writing. Pass --yes to persist and invalidate
197
- the lanes.
198
-
199
- Examples:
200
- $ assistant memory v3 set-core --add domain-a/topic-x # preview
201
- $ assistant memory v3 set-core --add domain-a/topic-x --yes # apply
202
- $ assistant memory v3 set-core --remove domain-b/topic-z --yes`,
203
- )
204
- .action(
205
- async (opts: {
206
- add: string[];
207
- remove: string[];
208
- yes?: boolean;
209
- json?: boolean;
210
- }) => {
211
- if (opts.add.length === 0 && opts.remove.length === 0) {
212
- log.error("Pass at least one --add or --remove leaf path.");
213
- process.exitCode = 1;
214
- return;
215
- }
216
-
217
- const result = await cliIpcCall<MemoryV3SetCoreResult>(
218
- "memory_v3_set_core",
219
- {
220
- body: {
221
- add: opts.add,
222
- remove: opts.remove,
223
- write: opts.yes === true,
224
- },
225
- },
226
- );
227
- if (!result.ok) {
228
- log.error(result.error ?? "Failed to update the v3 core set");
229
- process.exitCode = 1;
230
- return;
231
- }
232
- const payload = result.result!;
233
- if (opts.json === true) {
234
- log.info(JSON.stringify(payload, null, 2));
235
- return;
236
- }
237
- log.info(
238
- `Core leaves (${payload.nextCore.length}): ${
239
- payload.nextCore.length === 0
240
- ? "none"
241
- : payload.nextCore.join(", ")
242
- }`,
243
- );
244
- log.info(`Always-on page count: ${payload.alwaysOnPageCount}`);
245
- if (payload.written) {
246
- log.info("Written to core.json; lanes invalidated.");
247
- } else {
248
- log.info("Preview only — re-run with --yes to apply.");
249
- }
250
- },
251
- );
252
-
253
- // ── rebuild-index ─────────────────────────────────────────────────────
254
-
255
- v3.command("rebuild-index")
256
- .description("Invalidate the v3 lanes so the next turn rebuilds")
257
- .addHelpText(
258
- "after",
259
- `
260
- Drops the daemon's cached v3 shadow lanes so the tree + needle are rebuilt
261
- from the current on-disk state on the next turn. Useful after editing leaf
262
- files or assignments out-of-band.
263
-
264
- Examples:
265
- $ assistant memory v3 rebuild-index`,
266
- )
267
- .action(async () => {
268
- const result = await cliIpcCall<MemoryV3RebuildIndexResult>(
269
- "memory_v3_rebuild_index",
270
- { body: {} },
271
- );
272
- if (!result.ok) {
273
- log.error(result.error ?? "Failed to invalidate the v3 lanes");
274
- process.exitCode = 1;
275
- return;
276
- }
277
- log.info("v3 lanes invalidated; next turn will rebuild.");
278
136
  });
279
137
  },
280
138
  });
@@ -25,7 +25,6 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
25
25
  profile: "cost-optimized",
26
26
  contextWindow: { maxInputTokens: 1000000 },
27
27
  },
28
- memoryV3RouteL1: { profile: "balanced", temperature: 0 },
29
28
  memoryV3SelectL2: { profile: "balanced", temperature: 0 },
30
29
  recall: {
31
30
  profile: "balanced",
@@ -78,6 +78,15 @@ export function resolveCallSiteConfig(
78
78
  ): z.infer<typeof LLMConfigBase> {
79
79
  const layers: Mergeable[] = [llm.default as Mergeable];
80
80
 
81
+ // Effective logit-bias preset, tracked outside the deep-merge so it ties to
82
+ // the single highest-precedence *profile* that wins resolution rather than
83
+ // inheriting from a lower one. Profile layers are appended low→high, and each
84
+ // one fully determines the preset (a profile that omits `logitBias` clears
85
+ // any value a lower-precedence profile set), so the last profile appended
86
+ // wins — matching the merge's own precedence and including the implicit
87
+ // call-site default selected by `effectiveDefault`.
88
+ const biasRef: LogitBiasRef = { preset: undefined };
89
+
81
90
  const activeFragment = resolveProfileFragment(llm.activeProfile, llm, opts);
82
91
  const overrideFragment = resolveProfileFragment(
83
92
  opts.overrideProfile,
@@ -89,18 +98,32 @@ export function resolveCallSiteConfig(
89
98
  effectiveDefault(callSite, llm, opts.overrideProfile != null);
90
99
 
91
100
  if (callSite === "mainAgent") {
92
- appendCallSiteLayers(layers, callSite, llm, site, opts);
93
- appendProfileLayer(layers, activeFragment);
94
- appendProfileLayer(layers, overrideFragment);
101
+ appendCallSiteLayers(layers, callSite, llm, site, opts, biasRef);
102
+ appendProfileLayer(layers, activeFragment, biasRef);
103
+ appendProfileLayer(layers, overrideFragment, biasRef);
95
104
  } else {
96
- appendProfileLayer(layers, activeFragment);
97
- appendProfileLayer(layers, overrideFragment);
98
- appendCallSiteLayers(layers, callSite, llm, site, opts);
105
+ appendProfileLayer(layers, activeFragment, biasRef);
106
+ appendProfileLayer(layers, overrideFragment, biasRef);
107
+ appendCallSiteLayers(layers, callSite, llm, site, opts, biasRef);
99
108
  }
100
109
 
101
- return finalize(deepMerge(...layers.map(withImpliedProviderForKnownModel)));
110
+ const resolved = finalize(
111
+ deepMerge(...layers.map(withImpliedProviderForKnownModel)),
112
+ );
113
+ // `logitBias` is profile-scoped: the winning profile is its only source.
114
+ // Overwrite — or clear — whatever the deep-merge may have copied from a
115
+ // non-profile layer (`llm.default` or a call-site fragment), so a preset set
116
+ // outside a profile can't apply to a profile that didn't opt in.
117
+ if (biasRef.preset !== undefined) {
118
+ resolved.logitBias = biasRef.preset;
119
+ } else {
120
+ delete (resolved as { logitBias?: unknown }).logitBias;
121
+ }
122
+ return resolved;
102
123
  }
103
124
 
125
+ type LogitBiasRef = { preset: ProfileEntry["logitBias"] };
126
+
104
127
  // ---------------------------------------------------------------------------
105
128
  // Internal helpers
106
129
  // ---------------------------------------------------------------------------
@@ -261,8 +284,10 @@ function withImpliedProviderForKnownModel(source: Mergeable): Mergeable {
261
284
  function appendProfileLayer(
262
285
  layers: Mergeable[],
263
286
  profile: ProfileEntry | undefined,
287
+ biasRef: LogitBiasRef,
264
288
  ): void {
265
289
  if (profile != null) {
290
+ biasRef.preset = profile.logitBias;
266
291
  layers.push(profileConfigFragment(profile));
267
292
  }
268
293
  }
@@ -273,6 +298,7 @@ function appendCallSiteLayers(
273
298
  llm: z.infer<typeof LLMSchema>,
274
299
  site: z.infer<typeof LLMSchema>["callSites"][LLMCallSite] | undefined,
275
300
  opts: ResolveCallSiteOpts,
301
+ biasRef: LogitBiasRef,
276
302
  ): void {
277
303
  if (site != null) {
278
304
  if (site.profile != null) {
@@ -286,6 +312,7 @@ function appendCallSiteLayers(
286
312
  `LLM call site "${callSite}" references undefined profile "${site.profile}"`,
287
313
  );
288
314
  }
315
+ biasRef.preset = profileFragment.logitBias;
289
316
  layers.push(profileConfigFragment(profileFragment));
290
317
  }
291
318
  // Strip the `profile` discriminator before merging — it isn't a
@@ -304,6 +331,11 @@ function profileConfigFragment(profile: ProfileEntry): Mergeable {
304
331
  // profile before this point), but strip it defensively so it can never
305
332
  // leak into the merged `LLMConfigBase`.
306
333
  mix: _mix,
334
+ // `logitBias` is profile-identity metadata, not inheritable config: a
335
+ // preset must apply only to the profile that opted in, never bleed from a
336
+ // lower-precedence (e.g. active) profile into one that merely inherited it.
337
+ // `RetryProvider` resolves it from the applied profile, not the merge.
338
+ logitBias: _logitBias,
307
339
  ...config
308
340
  } = profile;
309
341
  return config as Mergeable;
@@ -7,19 +7,35 @@ describe("MemoryV3ConfigSchema", () => {
7
7
  const parsed = MemoryV3ConfigSchema.parse({});
8
8
  expect(parsed).toEqual({
9
9
  workingSet: { maxPages: 150, evictWindow: 5 },
10
- l2Concurrency: 16,
10
+ needleK: 100,
11
+ denseK: 100,
12
+ edge: { hubDegree: 30, seedCount: 18, perSeed: 6, cap: 45 },
11
13
  });
12
14
  });
13
15
 
14
- test("accepts an explicit l2Concurrency override", () => {
15
- expect(MemoryV3ConfigSchema.parse({ l2Concurrency: 8 }).l2Concurrency).toBe(
16
- 8,
17
- );
16
+ test("accepts explicit lane-K overrides", () => {
17
+ const parsed = MemoryV3ConfigSchema.parse({ needleK: 50, denseK: 75 });
18
+ expect(parsed.needleK).toBe(50);
19
+ expect(parsed.denseK).toBe(75);
18
20
  });
19
21
 
20
- test("rejects a non-positive or non-integer l2Concurrency", () => {
21
- expect(() => MemoryV3ConfigSchema.parse({ l2Concurrency: 0 })).toThrow();
22
- expect(() => MemoryV3ConfigSchema.parse({ l2Concurrency: -4 })).toThrow();
23
- expect(() => MemoryV3ConfigSchema.parse({ l2Concurrency: 1.5 })).toThrow();
22
+ test("accepts a partial edge override, defaulting the rest", () => {
23
+ const parsed = MemoryV3ConfigSchema.parse({ edge: { hubDegree: 10 } });
24
+ expect(parsed.edge).toEqual({
25
+ hubDegree: 10,
26
+ seedCount: 18,
27
+ perSeed: 6,
28
+ cap: 45,
29
+ });
30
+ });
31
+
32
+ test("rejects non-positive or non-integer lane knobs", () => {
33
+ expect(() => MemoryV3ConfigSchema.parse({ needleK: 0 })).toThrow();
34
+ expect(() => MemoryV3ConfigSchema.parse({ denseK: -4 })).toThrow();
35
+ expect(() => MemoryV3ConfigSchema.parse({ needleK: 1.5 })).toThrow();
36
+ expect(() =>
37
+ MemoryV3ConfigSchema.parse({ edge: { perSeed: 0 } }),
38
+ ).toThrow();
39
+ expect(() => MemoryV3ConfigSchema.parse({ edge: { cap: -1 } })).toThrow();
24
40
  });
25
41
  });
@@ -121,13 +121,6 @@ const CATALOG_RECORD: CatalogRecord = {
121
121
  "Selects which concept pages to inject for the next agent turn by routing over a cached page index.",
122
122
  domain: "memory",
123
123
  },
124
- memoryV3RouteL1: {
125
- id: "memoryV3RouteL1",
126
- displayName: "Memory V3 L1 Router",
127
- description:
128
- "Picks which leaves of the topic tree to open for the next agent turn by routing over a cache-warm static leaf block.",
129
- domain: "memory",
130
- },
131
124
  memoryV3SelectL2: {
132
125
  id: "memoryV3SelectL2",
133
126
  displayName: "Memory V3 L2 Selector",
@@ -50,7 +50,6 @@ export const LLMCallSiteEnum = z.enum([
50
50
  "memoryV2Migration",
51
51
  "memoryV2Sweep",
52
52
  "memoryRouter",
53
- "memoryV3RouteL1",
54
53
  "memoryV3SelectL2",
55
54
  "memoryV2Consolidation",
56
55
  "memoryRetrospective",
@@ -124,6 +123,17 @@ const VerbosityEnum = z.enum(["low", "medium", "high"]);
124
123
  const ModelSchema = z.string().min(1);
125
124
  const MaxTokensSchema = z.number().int().positive();
126
125
  const TemperatureSchema = z.number().min(0).max(2).nullable();
126
+ // Named, code-resolved logit-bias preset a profile may opt into. The value is a
127
+ // preset *name*, not an inline token→bias map, so the workspace config stays
128
+ // small. This is profile-identity metadata, not inheritable config: the resolver
129
+ // strips it from the deep-merge and re-attaches it from the winning profile (see
130
+ // `profileConfigFragment` / `resolveCallSiteConfig`), and `RetryProvider`
131
+ // resolves it to a `logit_bias` map at request time, forwarded only on the
132
+ // Fireworks (OpenAI-compatible) path. Keep these literals in sync with the
133
+ // presets handled by `resolveLogitBiasPreset` in
134
+ // `providers/inference/logit-bias.ts` (kept separate to avoid a schema →
135
+ // provider import cycle).
136
+ const LogitBiasPresetSchema = z.enum(["suppress-cjk"]);
127
137
 
128
138
  // ---------------------------------------------------------------------------
129
139
  // Thinking & ContextWindow
@@ -316,6 +326,11 @@ export const LLMConfigBase = z.object({
316
326
  thinking: ThinkingSchema.default(ThinkingSchema.parse({})),
317
327
  contextWindow: ContextWindowSchema.default(ContextWindowSchema.parse({})),
318
328
  openrouter: OpenRouterSchema.default(OpenRouterSchema.parse({})),
329
+ // Not deep-merged like the other fields: `resolveCallSiteConfig` sets this
330
+ // from the single highest-precedence profile that won resolution (see
331
+ // `profileConfigFragment`, which strips it from the merge), so a preset
332
+ // can't bleed from a lower-precedence profile into one that didn't opt in.
333
+ logitBias: LogitBiasPresetSchema.optional(),
319
334
  });
320
335
  export type LLMConfigBase = z.infer<typeof LLMConfigBase>;
321
336
 
@@ -336,6 +351,7 @@ const LLMConfigFragment = z.object({
336
351
  thinking: ThinkingFragmentSchema.optional(),
337
352
  contextWindow: ContextWindowDeepPartialSchema.optional(),
338
353
  openrouter: OpenRouterDeepPartialSchema.optional(),
354
+ logitBias: LogitBiasPresetSchema.optional(),
339
355
  });
340
356
  type LLMConfigFragment = z.infer<typeof LLMConfigFragment>;
341
357
 
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  /**
4
- * Memory v3 (topic-tree routing) working-set configuration.
4
+ * Memory v3 (section-grain lane retrieval) working-set configuration.
5
5
  *
6
6
  * The working set is the per-conversation set of concept pages carried
7
7
  * forward across turns. Eviction keeps it bounded: pages unseen for longer
@@ -28,20 +28,70 @@ export const MemoryV3WorkingSetSchema = z
28
28
  })
29
29
  .describe("Memory v3 working-set retention/eviction tuning.");
30
30
 
31
+ /**
32
+ * Edge-lane tuning for the link-graph expansion that folds a turn's lexical and
33
+ * dense seeds outward to their first-class neighbours.
34
+ */
35
+ export const MemoryV3EdgeSchema = z
36
+ .object({
37
+ hubDegree: z
38
+ .number({ error: "memory.v3.edge.hubDegree must be a number" })
39
+ .int("memory.v3.edge.hubDegree must be an integer")
40
+ .positive("memory.v3.edge.hubDegree must be a positive integer")
41
+ .default(30)
42
+ .describe(
43
+ "In-degree above which an article is treated as a hub and excluded from edge expansion (too generic to be a useful surface).",
44
+ ),
45
+ seedCount: z
46
+ .number({ error: "memory.v3.edge.seedCount must be a number" })
47
+ .int("memory.v3.edge.seedCount must be an integer")
48
+ .positive("memory.v3.edge.seedCount must be a positive integer")
49
+ .default(18)
50
+ .describe(
51
+ "Number of top needle+dense seeds (in rank order) expanded by the edge lane.",
52
+ ),
53
+ perSeed: z
54
+ .number({ error: "memory.v3.edge.perSeed must be a number" })
55
+ .int("memory.v3.edge.perSeed must be an integer")
56
+ .positive("memory.v3.edge.perSeed must be a positive integer")
57
+ .default(6)
58
+ .describe("Maximum neighbours surfaced per expanded seed."),
59
+ cap: z
60
+ .number({ error: "memory.v3.edge.cap must be a number" })
61
+ .int("memory.v3.edge.cap must be an integer")
62
+ .positive("memory.v3.edge.cap must be a positive integer")
63
+ .default(45)
64
+ .describe(
65
+ "Hard cap on the total number of distinct articles surfaced by the edge lane.",
66
+ ),
67
+ })
68
+ .describe("Memory v3 edge-lane (link-graph expansion) tuning.");
69
+
31
70
  export const MemoryV3ConfigSchema = z
32
71
  .object({
33
72
  workingSet: MemoryV3WorkingSetSchema.default(
34
73
  MemoryV3WorkingSetSchema.parse({}),
35
74
  ),
36
- l2Concurrency: z
37
- .number({ error: "memory.v3.l2Concurrency must be a number" })
38
- .int("memory.v3.l2Concurrency must be an integer")
39
- .positive("memory.v3.l2Concurrency must be a positive integer")
40
- .default(16)
75
+ needleK: z
76
+ .number({ error: "memory.v3.needleK must be a number" })
77
+ .int("memory.v3.needleK must be an integer")
78
+ .positive("memory.v3.needleK must be a positive integer")
79
+ .default(100)
80
+ .describe(
81
+ "Number of section-grain BM25 needle articles folded into the candidate pool each turn.",
82
+ ),
83
+ denseK: z
84
+ .number({ error: "memory.v3.denseK must be a number" })
85
+ .int("memory.v3.denseK must be an integer")
86
+ .positive("memory.v3.denseK must be a positive integer")
87
+ .default(100)
41
88
  .describe(
42
- "Bounded fan-out for the per-leaf L2 selection (number of leaf selector calls in flight at once).",
89
+ "Number of dense-lane articles folded into the candidate pool each turn.",
43
90
  ),
91
+ edge: MemoryV3EdgeSchema.default(MemoryV3EdgeSchema.parse({})),
44
92
  })
45
- .describe("Memory v3 — topic-tree routing with a carry-forward working set");
93
+ .describe(
94
+ "Memory v3 — section-grain lane retrieval with a carry-forward working set",
95
+ );
46
96
 
47
97
  export type MemoryV3Config = z.infer<typeof MemoryV3ConfigSchema>;
@@ -74,6 +74,24 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
74
74
  thinking: { enabled: false, streamThinking: false },
75
75
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
76
76
  },
77
+ // Open-weight economy option: Kimi K2.6 served by Fireworks via managed
78
+ // platform inference. Carries the `suppress-cjk` logit-bias preset to
79
+ // discourage the model from spontaneously emitting Chinese in English
80
+ // output; the preset is profile-scoped and only forwarded on the Fireworks
81
+ // path (see `providers/inference/logit-bias.ts`).
82
+ "balanced-economy": {
83
+ intent: "balanced",
84
+ provider: "fireworks",
85
+ connectionName: "fireworks-managed",
86
+ source: "managed",
87
+ label: "Balanced Economy",
88
+ description: "Strong open model (Kimi K2.6) at a lower price point",
89
+ maxTokens: 16000,
90
+ effort: "high",
91
+ thinking: { enabled: true, streamThinking: true },
92
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
93
+ logitBias: "suppress-cjk",
94
+ },
77
95
  };
78
96
 
79
97
  /**