@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
@@ -1,278 +1,32 @@
1
1
  /**
2
- * Memory v3 route definitions — tree-gardening + core-editing operations for
3
- * the leaf-tree memory model.
2
+ * Memory v3 route definitions — live-lane maintenance operations for the
3
+ * section-lane memory model.
4
4
  *
5
- * The daemon owns the live tree, so the *mutating* gardening verbs
6
- * (`reconcile`, `set-core --write`, `rebuild-index`) run here, inside the
7
- * daemon process, where the in-memory shadow 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.
5
+ * The daemon owns the live shadow lanes, so the maintenance verbs run here,
6
+ * inside the daemon process, where the in-memory lanes can be invalidated or
7
+ * rebuilt after a write:
11
8
  *
12
- * Each route's behavior lives in a small DI-friendly `handle*` function that
13
- * takes an optional `MemoryV3Deps` (filesystem overrides) so tests can drive it
14
- * against a temp workspace without mocking module globals. The exported
15
- * `RouteDefinition`s are thin `RouteHandlerArgs` adapters over those handlers.
9
+ * - `rebuild-index` — drop the cached lanes so the next turn rebuilds.
10
+ * - `backfill-sections` — one-time full embed of every page's sections into
11
+ * the dense store, advancing the maintain high-water mark.
12
+ *
13
+ * Each route's behavior lives in a small DI-friendly `handle*` function (with an
14
+ * injectable config seam) so tests can drive it without mocking module globals.
15
+ * The exported `RouteDefinition`s are thin adapters over those handlers.
16
16
  */
17
17
 
18
- import { writeFile } from "node:fs/promises";
19
- import { join } from "node:path";
20
-
21
18
  import { z } from "zod";
22
19
 
23
- import { getPageIndex } from "../../memory/v2/page-index.js";
24
- import { loadCore } from "../../plugins/defaults/memory-v3-shadow/core.js";
25
- import {
26
- computeV3Health,
27
- renderV3Health,
28
- } from "../../plugins/defaults/memory-v3-shadow/health.js";
29
- import {
30
- type LeafRef,
31
- reconcileTree,
32
- } from "../../plugins/defaults/memory-v3-shadow/reconcile.js";
20
+ import { getConfig } from "../../config/loader.js";
21
+ import type { AssistantConfig } from "../../config/types.js";
22
+ import { backfillAllSections } from "../../plugins/defaults/memory-v3-shadow/maintain-job.js";
33
23
  import { invalidateLanes } from "../../plugins/defaults/memory-v3-shadow/shadow-plugin.js";
34
- import {
35
- coreSlugs,
36
- loadLeafTree,
37
- resolveDataDir,
38
- } from "../../plugins/defaults/memory-v3-shadow/tree.js";
39
- import type {
40
- LeafPath,
41
- LeafTree,
42
- Slug,
43
- } from "../../plugins/defaults/memory-v3-shadow/types.js";
44
24
  import { getLogger } from "../../util/logger.js";
45
- import { getWorkspaceDir } from "../../util/platform.js";
46
25
  import { ACTOR_PRINCIPALS, type RoutePolicy } from "../auth/route-policy.js";
47
- import { RouteError } from "./errors.js";
48
- import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
26
+ import type { RouteDefinition } from "./types.js";
49
27
 
50
28
  const log = getLogger("memory-v3-routes");
51
29
 
52
- /**
53
- * Filesystem location overrides. Production callers omit these and the handlers
54
- * resolve the live workspace; tests inject a temp workspace + data dir to
55
- * exercise the handlers without mocking module globals.
56
- */
57
- export interface MemoryV3Deps {
58
- dataDir?: string;
59
- workspaceDir?: string;
60
- }
61
-
62
- // ---------------------------------------------------------------------------
63
- // Shared loading helpers
64
- // ---------------------------------------------------------------------------
65
-
66
- /**
67
- * Load the live leaf tree + its full slug universe.
68
- *
69
- * Page `leaves:` frontmatter is the authoritative source of page→leaf
70
- * membership: we build a `pageLeaves` map from the page index and union it over
71
- * `assignments.json` via `loadLeafTree(dataDir, pageLeaves)`, and derive the
72
- * slug universe (`allSlugs`) from the page index itself. This mirrors
73
- * `maybePrependV3Health` in `memory/v2/consolidation-job.ts` so the CLI
74
- * `health` / `set-core` cost computations agree with the consolidation-injected
75
- * health block instead of disagreeing by reading stale assignments.json only.
76
- */
77
- async function loadTreeAndSlugs(deps?: MemoryV3Deps): Promise<{
78
- dataDir: string;
79
- tree: LeafTree;
80
- allSlugs: Slug[];
81
- }> {
82
- const workspaceDir = deps?.workspaceDir ?? getWorkspaceDir();
83
- const dataDir = deps?.dataDir ?? resolveDataDir();
84
-
85
- // Each page contributes its `leaves:` frontmatter as the authoritative leaf
86
- // set for that slug; the slug universe is every page the index knows about.
87
- const pageIndex = await getPageIndex(workspaceDir);
88
- const pageLeaves = new Map<Slug, LeafPath[]>();
89
- const allSlugs: Slug[] = [];
90
- for (const entry of pageIndex.entries) {
91
- pageLeaves.set(entry.slug, entry.leaves);
92
- allSlugs.push(entry.slug);
93
- }
94
-
95
- const tree = await loadLeafTree(dataDir, pageLeaves);
96
- return { dataDir, tree, allSlugs };
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // health
101
- // ---------------------------------------------------------------------------
102
-
103
- const MemoryV3HealthResultSchema = z.object({
104
- /** Pre-rendered, human-readable report. Empty string when all-green. */
105
- rendered: z.string(),
106
- /** The structural counts, for `--json` consumers. */
107
- counts: z.object({
108
- unassigned: z.number(),
109
- danglingRefs: z.number(),
110
- novelClusters: z.number(),
111
- oversizedLeaves: z.number(),
112
- tinyLeaves: z.number(),
113
- }),
114
- });
115
- export type MemoryV3HealthResult = z.infer<typeof MemoryV3HealthResultSchema>;
116
-
117
- export async function handleMemoryV3Health(
118
- deps?: MemoryV3Deps,
119
- ): Promise<MemoryV3HealthResult> {
120
- const { dataDir, tree, allSlugs } = await loadTreeAndSlugs(deps);
121
- const core = await loadCore(dataDir);
122
- const report = computeV3Health({ tree, allSlugs, core });
123
- return {
124
- rendered: renderV3Health(report),
125
- counts: {
126
- unassigned: report.unassigned.length,
127
- danglingRefs: report.danglingRefs.length,
128
- novelClusters: report.novelClusters.length,
129
- oversizedLeaves: report.oversizedLeaves.length,
130
- tinyLeaves: report.tinyLeaves.length,
131
- },
132
- };
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // set-core
137
- // ---------------------------------------------------------------------------
138
-
139
- const MemoryV3SetCoreBodySchema = z.object({
140
- /** Leaves to add to the always-on core set. */
141
- add: z.array(z.string()).optional(),
142
- /** Leaves to remove from the always-on core set. */
143
- remove: z.array(z.string()).optional(),
144
- /**
145
- * When true, persist the new core to `core.json` and invalidate the lanes.
146
- * When false (default), compute the preview WITHOUT writing.
147
- */
148
- write: z.boolean().optional(),
149
- });
150
- export type MemoryV3SetCoreBody = z.infer<typeof MemoryV3SetCoreBodySchema>;
151
-
152
- const MemoryV3SetCoreResultSchema = z.object({
153
- /** The core leaf set that would result (or did result, when `write`). */
154
- nextCore: z.array(z.string()),
155
- /** Number of unique page slugs the new core set pins always-on. */
156
- alwaysOnPageCount: z.number(),
157
- /** Whether `core.json` was written. */
158
- written: z.boolean(),
159
- });
160
- export type MemoryV3SetCoreResult = z.infer<typeof MemoryV3SetCoreResultSchema>;
161
-
162
- /** Wire-format error code for a `set-core` add referencing an unknown leaf. */
163
- export const MEMORY_V3_UNKNOWN_LEAF_CODE = "MEMORY_V3_UNKNOWN_LEAF";
164
-
165
- /** Thrown when a `set-core` add entry does not exist in the live tree. */
166
- export class UnknownLeafError extends Error {
167
- constructor(public readonly unknown: LeafPath[]) {
168
- super(
169
- `Unknown leaf path(s) — not present in the tree: ${unknown.join(", ")}`,
170
- );
171
- this.name = "UnknownLeafError";
172
- }
173
- }
174
-
175
- /** Persist the always-on core set back to `<dataDir>/core.json`. */
176
- async function writeCore(dataDir: string, alwaysOn: LeafPath[]): Promise<void> {
177
- await writeFile(
178
- join(dataDir, "core.json"),
179
- `${JSON.stringify({ alwaysOn }, null, 2)}\n`,
180
- );
181
- }
182
-
183
- export async function handleMemoryV3SetCore(
184
- body: MemoryV3SetCoreBody,
185
- deps?: MemoryV3Deps,
186
- ): Promise<MemoryV3SetCoreResult> {
187
- const add = body.add ?? [];
188
- const remove = body.remove ?? [];
189
- const { dataDir, tree } = await loadTreeAndSlugs(deps);
190
-
191
- // Validate every ADD entry exists in the live tree. Removing an entry that is
192
- // already absent is a no-op (idempotent), so only adds are validated.
193
- const unknown = add.filter((leaf) => !tree.leaves.has(leaf));
194
- if (unknown.length > 0) throw new UnknownLeafError(unknown);
195
-
196
- const core = await loadCore(dataDir);
197
- for (const leaf of remove) core.delete(leaf);
198
- for (const leaf of add) core.add(leaf);
199
-
200
- // Drop any pre-existing core entry that no longer maps to a live leaf, so the
201
- // preview reflects what the tree can actually pin. Sorted for stable output.
202
- const nextCore = [...core].filter((leaf) => tree.leaves.has(leaf)).sort();
203
- const nextCoreSet = new Set<LeafPath>(nextCore);
204
-
205
- // Cost preview: the number of UNIQUE page slugs the new core pins always-on.
206
- const alwaysOnPageCount = coreSlugs(tree, nextCoreSet).size;
207
-
208
- if (body.write === true) {
209
- await writeCore(dataDir, nextCore);
210
- invalidateLanes();
211
- log.info(
212
- { coreSize: nextCore.length, alwaysOnPageCount },
213
- "memory-v3 core updated",
214
- );
215
- }
216
-
217
- return { nextCore, alwaysOnPageCount, written: body.write === true };
218
- }
219
-
220
- // ---------------------------------------------------------------------------
221
- // reconcile
222
- // ---------------------------------------------------------------------------
223
-
224
- const MemoryV3ReconcileResultSchema = z.object({
225
- renames: z.array(
226
- z.object({
227
- id: z.string().optional(),
228
- oldPath: z.string(),
229
- newPath: z.string(),
230
- }),
231
- ),
232
- deleted: z.array(z.string()),
233
- prunedCore: z.array(z.string()),
234
- });
235
- export type MemoryV3ReconcileResult = z.infer<
236
- typeof MemoryV3ReconcileResultSchema
237
- >;
238
-
239
- /**
240
- * Reconcile page + core references against the live on-disk tree.
241
- *
242
- * `prevLeaves` describes the tree as it was BEFORE the maintainer's on-disk
243
- * restructuring. We derive it from the current leaf set, which makes reconcile
244
- * a safe convergence pass: renames/deletes already applied on disk diff to
245
- * nothing, while it still rewrites any page/core ref left dangling and prunes
246
- * stale core entries (and fail-closed restores on a residual dangling ref).
247
- * `reconcileTree` snapshots, applies, validates, and invalidates the lanes.
248
- */
249
- export async function handleMemoryV3Reconcile(
250
- deps?: MemoryV3Deps,
251
- ): Promise<MemoryV3ReconcileResult> {
252
- const workspaceDir = deps?.workspaceDir ?? getWorkspaceDir();
253
- const dataDir = deps?.dataDir ?? resolveDataDir();
254
- // v1: prev == current is intentional. Without a captured prior leaf snapshot
255
- // we cannot detect renames/moves/splits, so this runs as a convergence /
256
- // dangling-ref prune pass. Full rename detection is a follow-up.
257
- const prevLeaves = await loadPrevLeaves(dataDir);
258
-
259
- const result = await reconcileTree({ prevLeaves, dataDir, workspaceDir });
260
- return {
261
- renames: result.renames,
262
- deleted: result.deleted,
263
- prunedCore: result.prunedCore,
264
- };
265
- }
266
-
267
- /** The current leaf set as `LeafRef`s (path + stable id when present). */
268
- async function loadPrevLeaves(dataDir: string): Promise<LeafRef[]> {
269
- const tree = await loadLeafTree(dataDir);
270
- return [...tree.leaves.values()].map((node) => ({
271
- path: node.path,
272
- ...(node.frontmatter.id ? { id: node.frontmatter.id } : {}),
273
- }));
274
- }
275
-
276
30
  // ---------------------------------------------------------------------------
277
31
  // rebuild-index
278
32
  // ---------------------------------------------------------------------------
@@ -285,7 +39,7 @@ export type MemoryV3RebuildIndexResult = z.infer<
285
39
  >;
286
40
 
287
41
  /**
288
- * Invalidate the v3 shadow lanes so the next turn rebuilds the tree/needle
42
+ * Invalidate the v3 shadow lanes so the next turn rebuilds the section index
289
43
  * from the current on-disk state. Runs in-daemon so it acts on the live
290
44
  * process's cached lanes (an in-CLI call would invalidate nothing).
291
45
  */
@@ -296,21 +50,47 @@ export async function handleMemoryV3RebuildIndex(): Promise<MemoryV3RebuildIndex
296
50
  }
297
51
 
298
52
  // ---------------------------------------------------------------------------
299
- // Route definitions (RouteHandlerArgs adapters over the handlers above)
53
+ // backfill-sections
300
54
  // ---------------------------------------------------------------------------
301
55
 
302
- /** Read-only verbs (the `health` report) require only `settings.read`. */
303
- const READ_POLICY: RoutePolicy = {
304
- requiredScopes: ["settings.read"],
305
- allowedPrincipalTypes: ACTOR_PRINCIPALS,
306
- };
56
+ const MemoryV3BackfillSectionsResultSchema = z.object({
57
+ /** Pages whose sections were embedded this pass. */
58
+ articles: z.number(),
59
+ /** Total section points upserted across all articles. */
60
+ sections: z.number(),
61
+ /** Pages whose embed threw (and was contained). */
62
+ failures: z.number(),
63
+ });
64
+ export type MemoryV3BackfillSectionsResult = z.infer<
65
+ typeof MemoryV3BackfillSectionsResultSchema
66
+ >;
307
67
 
308
68
  /**
309
- * Mutating verbs require `settings.write`. `set-core --write`, `reconcile`, and
310
- * `rebuild-index` write `core.json`, rewrite page frontmatter, and invalidate
311
- * the live lanes, so a `settings.read`-only principal must not reach them.
312
- * (`set-core` without `write` is a preview, but it shares this route, so the
313
- * route as a whole is gated on write.)
69
+ * One-time full backfill of the section dense store: embed EVERY page in the
70
+ * index — including synthetic skill/CLI rows the incremental maintain pass
71
+ * skips — into the `memory_v3_sections` collection, then advance the maintain
72
+ * high-water mark so the next incremental run deltas from here. Runs in-daemon
73
+ * so it uses the live config and so the maintain checkpoint it advances is the
74
+ * one the daemon's incremental pass reads.
75
+ *
76
+ * `config` is injectable for tests; production resolves the live config.
77
+ */
78
+ export async function handleMemoryV3BackfillSections(
79
+ config: AssistantConfig = getConfig(),
80
+ ): Promise<MemoryV3BackfillSectionsResult> {
81
+ const outcome = await backfillAllSections(config);
82
+ log.info(outcome, "memory-v3 section backfill complete (route)");
83
+ return outcome;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Route definitions (RouteHandlerArgs adapters over the handlers above)
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Mutating verbs require `settings.write`. `rebuild-index` invalidates the live
92
+ * lanes and `backfill-sections` writes the dense store + advances the maintain
93
+ * checkpoint, so a `settings.read`-only principal must not reach them.
314
94
  */
315
95
  const WRITE_POLICY: RoutePolicy = {
316
96
  requiredScopes: ["settings.write"],
@@ -319,54 +99,24 @@ const WRITE_POLICY: RoutePolicy = {
319
99
 
320
100
  export const ROUTES: RouteDefinition[] = [
321
101
  {
322
- operationId: "memory_v3_health",
323
- method: "POST",
324
- policy: READ_POLICY,
325
- endpoint: "memory/v3/health",
326
- handler: () => handleMemoryV3Health(),
327
- summary: "Print the v3 structural health report (read-only)",
328
- tags: ["memory"],
329
- responseBody: MemoryV3HealthResultSchema,
330
- },
331
- {
332
- operationId: "memory_v3_set_core",
102
+ operationId: "memory_v3_rebuild_index",
333
103
  method: "POST",
334
104
  policy: WRITE_POLICY,
335
- endpoint: "memory/v3/set-core",
336
- handler: async ({ body = {} }: RouteHandlerArgs) => {
337
- try {
338
- return await handleMemoryV3SetCore(body as MemoryV3SetCoreBody);
339
- } catch (err) {
340
- if (err instanceof UnknownLeafError) {
341
- throw new RouteError(err.message, MEMORY_V3_UNKNOWN_LEAF_CODE, 422);
342
- }
343
- throw err;
344
- }
345
- },
346
- summary: "Add/remove always-on core leaves (validates + previews cost)",
105
+ endpoint: "memory/v3/rebuild-index",
106
+ handler: () => handleMemoryV3RebuildIndex(),
107
+ summary: "Invalidate the v3 lanes so the next turn rebuilds",
347
108
  tags: ["memory"],
348
- requestBody: MemoryV3SetCoreBodySchema,
349
- responseBody: MemoryV3SetCoreResultSchema,
109
+ responseBody: MemoryV3RebuildIndexResultSchema,
350
110
  },
351
111
  {
352
- operationId: "memory_v3_reconcile",
112
+ operationId: "memory_v3_backfill_sections",
353
113
  method: "POST",
354
114
  policy: WRITE_POLICY,
355
- endpoint: "memory/v3/reconcile",
356
- handler: () => handleMemoryV3Reconcile(),
115
+ endpoint: "memory/v3/backfill-sections",
116
+ handler: () => handleMemoryV3BackfillSections(),
357
117
  summary:
358
- "v1 convergence/prune pass over page+core refs (no rename detection without a prior snapshot)",
118
+ "One-time: embed every page's sections (incl synthetic skill/CLI rows) into the dense store",
359
119
  tags: ["memory"],
360
- responseBody: MemoryV3ReconcileResultSchema,
361
- },
362
- {
363
- operationId: "memory_v3_rebuild_index",
364
- method: "POST",
365
- policy: WRITE_POLICY,
366
- endpoint: "memory/v3/rebuild-index",
367
- handler: () => handleMemoryV3RebuildIndex(),
368
- summary: "Invalidate the v3 lanes so the next turn rebuilds",
369
- tags: ["memory"],
370
- responseBody: MemoryV3RebuildIndexResultSchema,
120
+ responseBody: MemoryV3BackfillSectionsResultSchema,
371
121
  },
372
122
  ];
@@ -18,8 +18,8 @@ mock.module("../helpers.js", () => ({
18
18
  addPlaygroundMessage: async () => ({ id: "msg-test" }),
19
19
  }));
20
20
 
21
- import type { ContextWindowResult } from "../../../../context/window-manager.js";
22
21
  import type { Conversation } from "../../../../daemon/conversation.js";
22
+ import type { ContextWindowResult } from "../../../../plugins/defaults/compaction/window-manager.js";
23
23
  import type { Message } from "../../../../providers/types.js";
24
24
  import { RouteError } from "../../errors.js";
25
25
  import { ROUTES } from "../index.js";
@@ -17,6 +17,7 @@ import { listProviderEntries } from "../../providers/speech-to-text/provider-cat
17
17
  import { resolveBatchTranscriber } from "../../providers/speech-to-text/resolve.js";
18
18
  import { normalizeSttError } from "../../stt/daemon-batch-transcriber.js";
19
19
  import type { BatchTranscriber, SttErrorCategory } from "../../stt/types.js";
20
+ import { SttError } from "../../stt/types.js";
20
21
  import { getLogger } from "../../util/logger.js";
21
22
  import {
22
23
  FFMPEG_TRANSCODE_TIMEOUT_MS,
@@ -141,6 +142,27 @@ async function toWav(inputPath: string, isVideo: boolean): Promise<string> {
141
142
  return wavPath;
142
143
  }
143
144
 
145
+ /**
146
+ * Call the provider and tag any failure as an {@link SttError} so callers can
147
+ * distinguish provider failures (categorized HTTP statuses) from local
148
+ * preprocessing failures (ffmpeg/ffprobe), which stay on the bad-gateway path.
149
+ */
150
+ async function transcribeChunk(
151
+ transcriber: BatchTranscriber,
152
+ audioBuffer: Buffer,
153
+ ): Promise<string> {
154
+ try {
155
+ const result = await transcriber.transcribe({
156
+ audio: audioBuffer,
157
+ mimeType: "audio/wav",
158
+ signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
159
+ });
160
+ return result.text;
161
+ } catch (err) {
162
+ throw normalizeSttError(err);
163
+ }
164
+ }
165
+
144
166
  async function transcribeWithProvider(
145
167
  audioPath: string,
146
168
  transcriber: BatchTranscriber,
@@ -151,12 +173,7 @@ async function transcribeWithProvider(
151
173
  // If small enough, send directly
152
174
  if (fileSize <= STT_CHUNK_MAX_BYTES) {
153
175
  const audioBuffer = await readFile(audioPath);
154
- const result = await transcriber.transcribe({
155
- audio: audioBuffer,
156
- mimeType: "audio/wav",
157
- signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
158
- });
159
- return result.text;
176
+ return transcribeChunk(transcriber, audioBuffer);
160
177
  }
161
178
 
162
179
  // Split into chunks for large files
@@ -175,12 +192,8 @@ async function transcribeWithProvider(
175
192
  ` Transcribing chunk ${i + 1}/${chunks.length}...\n`,
176
193
  );
177
194
  const audioBuffer = await readFile(chunks[i]);
178
- const result = await transcriber.transcribe({
179
- audio: audioBuffer,
180
- mimeType: "audio/wav",
181
- signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
182
- });
183
- if (result.text) parts.push(result.text);
195
+ const text = await transcribeChunk(transcriber, audioBuffer);
196
+ if (text) parts.push(text);
184
197
  }
185
198
 
186
199
  return parts.join(" ");
@@ -363,6 +376,26 @@ async function handleTranscribeFile({ body }: RouteHandlerArgs) {
363
376
  durationSeconds,
364
377
  };
365
378
  } catch (err) {
379
+ // Only provider failures (tagged as SttError by transcribeChunk) are
380
+ // categorized into HTTP statuses. Local preprocessing failures (ffmpeg/
381
+ // ffprobe) stay on the bad-gateway path so their stderr never decides the
382
+ // category (e.g. a path under a dir named "403" must not become a 401).
383
+ if (err instanceof SttError) {
384
+ // Auth/rate-limit/timeout/invalid-audio are user-config or transient
385
+ // conditions, not daemon defects, so log them at warn to keep them out
386
+ // of error telemetry. Reserve error level (with the raw err for a Sentry
387
+ // stack) for genuine provider/unexpected failures.
388
+ if (err.category === "provider-error") {
389
+ log.error({ err, filePath }, "File transcription failed");
390
+ } else {
391
+ log.warn(
392
+ { category: err.category, message: err.message, filePath },
393
+ "File transcription failed",
394
+ );
395
+ }
396
+ throw STT_ERROR_MAP[err.category]();
397
+ }
398
+
366
399
  log.error({ err, filePath }, "File transcription failed");
367
400
  throw new BadGatewayError(
368
401
  err instanceof Error ? err.message : "Transcription failed",
@@ -46,14 +46,24 @@ const workspaceTreeEntrySchema = z.object({
46
46
 
47
47
  type TreeEntry = z.infer<typeof workspaceTreeEntrySchema>;
48
48
 
49
- // Total number of filesystem entries we're willing to traverse when computing
50
- // recursive directory sizes for a single tree listing. The budget is shared
51
- // across every directory in the response so a giant `repos/` checkout cannot
52
- // starve the request — once exhausted, remaining directories report `size:
53
- // null` (caller renders an unknown-size placeholder). 50k is generous for
54
- // typical workspaces (sounds, db, logs) while bailing fast on multi-repo
55
- // trees.
56
- const DIR_SIZE_TOTAL_ENTRY_BUDGET = 50_000;
49
+ // Recursive directory sizing walks the filesystem synchronously, so it is
50
+ // bounded by two caps that work together:
51
+ //
52
+ // - `DIR_SIZE_PER_DIR_ENTRY_BUDGET` caps a single directory. No one giant
53
+ // subtree (e.g. a `node_modules/` or `repos/` checkout) can consume the whole
54
+ // listing's budget and starve its siblings; only the oversized directory
55
+ // itself reports `size: null` (the caller renders an unknown-size
56
+ // placeholder) while every other folder still shows an accurate size.
57
+ // - `DIR_SIZE_TOTAL_ENTRY_BUDGET` caps the listing as a whole, so a workspace
58
+ // with many large sibling directories still can't block the daemon event loop
59
+ // for an unbounded amount of time. Once it is exhausted, the remaining
60
+ // directories report `size: null`.
61
+ //
62
+ // Each directory is allotted `min(per-dir cap, remaining listing budget)`
63
+ // entries, and whatever it actually consumes is deducted from the listing
64
+ // budget.
65
+ const DIR_SIZE_PER_DIR_ENTRY_BUDGET = 250_000;
66
+ const DIR_SIZE_TOTAL_ENTRY_BUDGET = 1_000_000;
57
67
 
58
68
  interface DirSizeBudget {
59
69
  remaining: number;
@@ -61,13 +71,13 @@ interface DirSizeBudget {
61
71
 
62
72
  /**
63
73
  * Recursively sum the byte size of every regular file under `absPath`,
64
- * sharing a single entry budget across all directories in the same listing.
74
+ * traversing up to `budget.remaining` filesystem entries.
65
75
  *
66
76
  * Returns:
67
77
  * - the total size in bytes when the entire subtree was traversed within
68
78
  * budget
69
- * - `null` if the shared budget was exhausted before completion (the caller
70
- * surfaces that as an unknown size in the UI)
79
+ * - `null` if the budget was exhausted before completion (the caller surfaces
80
+ * that as an unknown size in the UI)
71
81
  *
72
82
  * Symlinks are not followed. We rely on `withFileTypes` so we never `stat`
73
83
  * directories purely to discover their type.
@@ -108,6 +118,27 @@ function computeDirSize(absPath: string, budget: DirSizeBudget): number | null {
108
118
  return total;
109
119
  }
110
120
 
121
+ /**
122
+ * Compute a directory's recursive size while drawing from a shared listing
123
+ * budget. The directory is allotted at most `DIR_SIZE_PER_DIR_ENTRY_BUDGET`
124
+ * entries — and never more than the listing has left — and whatever it consumes
125
+ * is deducted from `listingBudget` so the next sibling sees the reduced
126
+ * remainder. Returns `null` (unknown size) when the allotment is exhausted.
127
+ */
128
+ function computeDirSizeWithinListing(
129
+ absPath: string,
130
+ listingBudget: DirSizeBudget,
131
+ ): number | null {
132
+ const allotment = Math.min(
133
+ DIR_SIZE_PER_DIR_ENTRY_BUDGET,
134
+ listingBudget.remaining,
135
+ );
136
+ const dirBudget: DirSizeBudget = { remaining: allotment };
137
+ const size = computeDirSize(absPath, dirBudget);
138
+ listingBudget.remaining -= allotment - dirBudget.remaining;
139
+ return size;
140
+ }
141
+
111
142
  const SOUNDS_WORKSPACE_PATH = "data/sounds";
112
143
 
113
144
  function normaliseWorkspacePathForSync(path: string): string {
@@ -153,9 +184,11 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
153
184
  const dirents = readdirSync(resolved, { withFileTypes: true });
154
185
  const workspaceDir = getWorkspaceDir();
155
186
 
156
- // Shared budget across every directory in this listing. When opted out,
157
- // we never construct one, so directories report `size: null` as before.
158
- const dirSizeBudget: DirSizeBudget | undefined = includeDirSizes
187
+ // Listing-wide budget, only constructed when sizes are requested. Each
188
+ // directory draws a per-directory allotment from it so one oversized
189
+ // subtree can't starve its siblings, while the shared remainder still caps
190
+ // total synchronous traversal for the whole listing.
191
+ const listingBudget: DirSizeBudget | undefined = includeDirSizes
159
192
  ? { remaining: DIR_SIZE_TOTAL_ENTRY_BUDGET }
160
193
  : undefined;
161
194
 
@@ -176,7 +209,9 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
176
209
  const relativePath = fullPath.slice(workspaceDir.length + 1);
177
210
 
178
211
  const dirSize =
179
- isDir && dirSizeBudget ? computeDirSize(fullPath, dirSizeBudget) : null;
212
+ isDir && listingBudget
213
+ ? computeDirSizeWithinListing(fullPath, listingBudget)
214
+ : null;
180
215
 
181
216
  entries.push({
182
217
  name: entry.name,