@vellumai/assistant 0.11.4-staging.2 → 0.11.4-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 (209) hide show
  1. package/AGENTS.md +8 -2
  2. package/ARCHITECTURE.md +2 -0
  3. package/docs/architecture/memory.md +15 -0
  4. package/docs/browser-use-architecture-phase2.md +128 -56
  5. package/docs/flux-turn-detection-spike.md +11 -6
  6. package/docs/guardian-request-flow.md +35 -0
  7. package/knip.json +3 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  10. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  11. package/openapi.yaml +136 -76
  12. package/package.json +1 -1
  13. package/scripts/write-plugin-api-shim.ts +10 -0
  14. package/src/__tests__/app-control-flow.test.ts +1 -0
  15. package/src/__tests__/approval-routes-http.test.ts +2 -2
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  17. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  18. package/src/__tests__/compaction-events.test.ts +8 -10
  19. package/src/__tests__/conversation-agent-loop.test.ts +4 -1
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  21. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  22. package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
  23. package/src/__tests__/conversation-queue.test.ts +39 -62
  24. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  27. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  28. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
  30. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  31. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  36. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  37. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  38. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  39. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  40. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  41. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  42. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  43. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  44. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  45. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  46. package/src/__tests__/document-sync-tags.test.ts +0 -75
  47. package/src/__tests__/file-ops-service.test.ts +163 -30
  48. package/src/__tests__/filesystem-tools.test.ts +23 -24
  49. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  50. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  51. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  52. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  53. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  54. package/src/__tests__/media-generate-image.test.ts +131 -21
  55. package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
  56. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  57. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
  58. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  59. package/src/__tests__/qdrant-manager.test.ts +14 -1
  60. package/src/__tests__/run-due-schedules.test.ts +21 -0
  61. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  62. package/src/__tests__/schedule-routes.test.ts +23 -0
  63. package/src/__tests__/schedule-store.test.ts +17 -0
  64. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  65. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  66. package/src/__tests__/starter-task-flow.test.ts +5 -4
  67. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  68. package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
  69. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  70. package/src/__tests__/subagent-tools.test.ts +81 -101
  71. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  72. package/src/__tests__/tool-executor.test.ts +5 -1
  73. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  74. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  75. package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
  76. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  77. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  78. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  79. package/src/acp/session-manager.ts +8 -1
  80. package/src/api/events/host-file.ts +2 -2
  81. package/src/api/surfaces.ts +5 -0
  82. package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
  83. package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
  84. package/src/calls/voice-session-bridge.ts +44 -25
  85. package/src/calls/voice-triage-escalate.ts +1 -0
  86. package/src/cli/bundled-modules.ts +29 -0
  87. package/src/cli/commands/db/repair.ts +4 -8
  88. package/src/cli/commands/domain.ts +6 -3
  89. package/src/cli/commands/email.ts +6 -3
  90. package/src/cli/commands/keys.ts +8 -3
  91. package/src/cli/commands/plugins.ts +85 -36
  92. package/src/cli/commands/schedules.ts +35 -1
  93. package/src/cli/lib/bundled-marketplace.json +1 -1
  94. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  95. package/src/config/assistant-feature-flags.ts +36 -15
  96. package/src/config/balanced-model-experiment.ts +35 -0
  97. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  98. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  99. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  100. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  101. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  102. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  103. package/src/config/call-site-defaults.ts +7 -0
  104. package/src/config/default-profile-catalog.ts +96 -4
  105. package/src/config/feature-flag-registry.json +11 -11
  106. package/src/config/skills.ts +9 -2
  107. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  108. package/src/daemon/conversation-agent-loop.ts +14 -13
  109. package/src/daemon/conversation-notifiers.ts +11 -9
  110. package/src/daemon/conversation-process.ts +0 -27
  111. package/src/daemon/conversation-runtime-assembly.ts +9 -2
  112. package/src/daemon/conversation-store.ts +4 -4
  113. package/src/daemon/conversation-surfaces.ts +27 -13
  114. package/src/daemon/conversation-tool-setup.ts +2 -4
  115. package/src/daemon/conversation.ts +104 -51
  116. package/src/daemon/doordash-steps.ts +2 -2
  117. package/src/daemon/lifecycle.ts +14 -1
  118. package/src/daemon/process-message.ts +0 -13
  119. package/src/daemon/windows-compiled-entry.ts +4 -0
  120. package/src/documents/document-store.ts +5 -235
  121. package/src/hooks/types.ts +5 -0
  122. package/src/ipc/gateway-flag-listener.ts +17 -3
  123. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
  124. package/src/live-voice/live-voice-manager.ts +16 -3
  125. package/src/live-voice/live-voice-session.ts +63 -9
  126. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  127. package/src/monitoring/control.ts +1 -0
  128. package/src/monitoring/db-integrity-sample.ts +4 -5
  129. package/src/notifications/AGENTS.md +2 -0
  130. package/src/notifications/approval-card-data.ts +33 -0
  131. package/src/permissions/prompter.ts +1 -5
  132. package/src/persistence/conversation-queries.ts +66 -16
  133. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  134. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  135. package/src/persistence/schema/documents.ts +4 -4
  136. package/src/plugin-api/constants.ts +8 -0
  137. package/src/plugin-api/index.ts +5 -1
  138. package/src/plugin-api/webhook-url.ts +13 -11
  139. package/src/plugins/defaults/main.ts +6 -7
  140. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  141. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  142. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  143. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  144. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  145. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  146. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  147. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  148. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  149. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  150. package/src/plugins/mtime-cache.ts +17 -0
  151. package/src/prompts/templates/system-sections.ts +0 -7
  152. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  153. package/src/providers/__tests__/retry-callsite.test.ts +20 -0
  154. package/src/providers/openai/chat-completions-provider.ts +11 -1
  155. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
  156. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
  157. package/src/routes/control.ts +1 -0
  158. package/src/routes/route-host-client.ts +1 -0
  159. package/src/runtime/AGENTS.md +16 -17
  160. package/src/runtime/agent-wake.ts +15 -12
  161. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
  162. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  163. package/src/runtime/routes/conversation-list-routes.ts +54 -22
  164. package/src/runtime/routes/conversation-management-routes.ts +2 -3
  165. package/src/runtime/routes/conversation-routes.ts +11 -13
  166. package/src/runtime/routes/documents-routes.ts +3 -222
  167. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  168. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  169. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  170. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  171. package/src/runtime/routes/schedule-routes.ts +94 -7
  172. package/src/runtime/routes/workspace-routes.ts +0 -9
  173. package/src/runtime/routes/workspace-utils.ts +3 -13
  174. package/src/runtime/services/conversation-serializer.ts +7 -2
  175. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  176. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  177. package/src/schedule/plugin-schedule-availability.ts +58 -0
  178. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  179. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  180. package/src/schedule/schedule-store.ts +5 -1
  181. package/src/schedule/scheduler.ts +9 -4
  182. package/src/schedule/worker-control.ts +1 -0
  183. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  184. package/src/subagent/consult-context.ts +11 -11
  185. package/src/subagent/consult-prompt.ts +26 -35
  186. package/src/subagent/manager.ts +20 -37
  187. package/src/subagent/notify.ts +7 -1
  188. package/src/subagent/types.ts +15 -13
  189. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  190. package/src/tools/acp/spawn.ts +6 -4
  191. package/src/tools/filesystem/read.ts +27 -10
  192. package/src/tools/host-filesystem/read.ts +27 -15
  193. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  194. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  195. package/src/tools/shared/filesystem/types.ts +5 -5
  196. package/src/tools/skills/scaffold-managed.ts +25 -7
  197. package/src/tools/subagent/spawn.ts +28 -88
  198. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  199. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  200. package/src/util/logger.ts +16 -0
  201. package/src/util/worker-process.ts +37 -4
  202. package/src/windows-compiled-cli.ts +32 -0
  203. package/src/windows-compiled-entry.ts +4 -0
  204. package/src/windows-compiled-logger.ts +6 -0
  205. package/src/windows-compiled-worker-entry.ts +29 -0
  206. package/src/__tests__/document-workspace-file.test.ts +0 -467
  207. package/src/daemon/interactive-turn-sender.ts +0 -59
  208. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  209. package/src/subagent/consult-transcript.ts +0 -90
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { resolveWorkerCommand } from "../worker-process.js";
4
+
5
+ const entry = new URL("file:///source/monitoring/worker.ts");
6
+
7
+ describe("resolveWorkerCommand", () => {
8
+ test("uses the packaged Windows worker executable when present", () => {
9
+ expect(
10
+ resolveWorkerCommand(entry, "monitoring", {
11
+ platform: "win32",
12
+ execPath: "/runtime/vellum-daemon.exe",
13
+ executableExists: () => true,
14
+ }),
15
+ ).toEqual(["/runtime/vellum-worker.exe", "monitoring"]);
16
+ });
17
+
18
+ test("routes integrity checks through the packaged worker", () => {
19
+ expect(
20
+ resolveWorkerCommand(entry, "db-integrity", {
21
+ platform: "win32",
22
+ execPath: "/runtime/vellum-worker.exe",
23
+ executableExists: () => true,
24
+ }),
25
+ ).toEqual(["/runtime/vellum-worker.exe", "db-integrity"]);
26
+ });
27
+
28
+ test("falls back to the source entry outside a packaged runtime", () => {
29
+ expect(
30
+ resolveWorkerCommand(entry, "monitoring", {
31
+ platform: "win32",
32
+ execPath: "/runtime/vellum-daemon.exe",
33
+ executableExists: () => false,
34
+ }),
35
+ ).toEqual(["bun", "--smol", "run", "/source/monitoring/worker.ts"]);
36
+ });
37
+ });
@@ -19,6 +19,16 @@ import { logSerializers } from "./log-redact.js";
19
19
  import { getLogsDir } from "./platform.js";
20
20
 
21
21
  const loadModule = createRequire(import.meta.url);
22
+ let bundledPino: typeof import("pino") | undefined;
23
+ let bundledPinoPretty: typeof import("pino-pretty") | undefined;
24
+
25
+ export function setBundledLoggerModules(
26
+ pino: typeof import("pino"),
27
+ pinoPretty: typeof import("pino-pretty"),
28
+ ): void {
29
+ bundledPino = pino;
30
+ bundledPinoPretty = pinoPretty;
31
+ }
22
32
 
23
33
  /**
24
34
  * pino loads on first logger construction, not import, so CLI processes that
@@ -26,11 +36,17 @@ const loadModule = createRequire(import.meta.url);
26
36
  * the real dual-export and ESM-shaped test mocks.
27
37
  */
28
38
  function loadPino(): typeof import("pino") {
39
+ if (bundledPino) {
40
+ return bundledPino;
41
+ }
29
42
  const mod = loadModule("pino") as { default?: unknown };
30
43
  return (mod.default ?? mod) as typeof import("pino");
31
44
  }
32
45
 
33
46
  function loadPinoPretty(): typeof import("pino-pretty") {
47
+ if (bundledPinoPretty) {
48
+ return bundledPinoPretty;
49
+ }
34
50
  const mod = loadModule("pino-pretty") as { default?: unknown };
35
51
  return (mod.default ?? mod) as typeof import("pino-pretty");
36
52
  }
@@ -16,7 +16,7 @@ import {
16
16
  readFileSync,
17
17
  unlinkSync,
18
18
  } from "node:fs";
19
- import { dirname } from "node:path";
19
+ import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
 
22
22
  import { getCurrentLogFilePath } from "./logger.js";
@@ -122,6 +122,37 @@ export interface SpawnWorkerProcessOptions {
122
122
  detached?: boolean;
123
123
  }
124
124
 
125
+ export type PackagedWorkerEntry =
126
+ | "monitoring"
127
+ | "schedule"
128
+ | "memory"
129
+ | "routes"
130
+ | "db-integrity";
131
+
132
+ interface WorkerCommandRuntime {
133
+ platform: NodeJS.Platform;
134
+ execPath: string;
135
+ executableExists: (path: string) => boolean;
136
+ }
137
+
138
+ export function resolveWorkerCommand(
139
+ entry: URL,
140
+ packagedEntry: PackagedWorkerEntry | undefined,
141
+ runtime: WorkerCommandRuntime = {
142
+ platform: process.platform,
143
+ execPath: process.execPath,
144
+ executableExists: existsSync,
145
+ },
146
+ ): string[] {
147
+ if (runtime.platform === "win32" && packagedEntry) {
148
+ const executable = join(dirname(runtime.execPath), "vellum-worker.exe");
149
+ if (runtime.executableExists(executable)) {
150
+ return [executable, packagedEntry];
151
+ }
152
+ }
153
+ return ["bun", "--smol", "run", fileURLToPath(entry)];
154
+ }
155
+
125
156
  type WorkerReadyOutcome = "ready" | "exited" | "timeout";
126
157
 
127
158
  /**
@@ -178,6 +209,8 @@ export async function spawnWorkerProcess(args: {
178
209
  pidPath: string;
179
210
  /** Worker entry script, e.g. `new URL("./worker.ts", import.meta.url)`. */
180
211
  entry: URL;
212
+ /** Entry bundled into vellum-worker.exe for packaged Windows runtimes. */
213
+ packagedEntry?: PackagedWorkerEntry;
181
214
  /** Human-readable name used in spawn-failure messages, e.g. "Memory worker". */
182
215
  workerLabel: string;
183
216
  options?: SpawnWorkerProcessOptions;
@@ -208,15 +241,15 @@ export async function spawnWorkerProcess(args: {
208
241
  // output is at least visible to the spawning process.
209
242
  }
210
243
 
211
- // Workers are latency-insensitive, so run them in bun's small-heap mode
212
- // with a RAM-size hint sized to the container — see worker-memory.ts.
244
+ // Source workers use bun's small-heap mode. Packaged Windows workers use the
245
+ // compiled worker executable. Both receive the RAM hint from worker-memory.
213
246
  //
214
247
  // `fileURLToPath`, not `.pathname`: a URL's pathname is percent-encoded, so
215
248
  // an install path containing a space (every macOS desktop install lives
216
249
  // under "Application Support") would reach bun as "Application%20Support"
217
250
  // and the entry would not be found.
218
251
  const child = Bun.spawn({
219
- cmd: ["bun", "--smol", "run", fileURLToPath(args.entry)],
252
+ cmd: resolveWorkerCommand(args.entry, args.packagedEntry),
220
253
  env: workerMemoryEnv(),
221
254
  stdio: ["ignore", "ignore", stderrFd],
222
255
  detached,
@@ -0,0 +1,32 @@
1
+ import { setBundledCliModules } from "./cli/bundled-modules.js";
2
+ import * as pluginDiff from "./cli/lib/diff-plugin.js";
3
+ import * as pluginInspect from "./cli/lib/inspect-plugin.js";
4
+ import * as pluginInstallGitHub from "./cli/lib/install-from-github.js";
5
+ import * as pluginInstallPlatform from "./cli/lib/install-from-platform.js";
6
+ import * as pluginInstalled from "./cli/lib/list-installed-plugins.js";
7
+ import * as pluginCatalogCache from "./cli/lib/plugin-catalog-cache.js";
8
+ import * as pluginCatalogLocal from "./cli/lib/plugin-catalog-local.js";
9
+ import * as pluginPinHistory from "./cli/lib/plugin-pin-history.js";
10
+ import * as pluginSurfaces from "./cli/lib/plugin-surfaces.js";
11
+ import * as pluginSearch from "./cli/lib/search-plugins.js";
12
+ import * as pluginUninstall from "./cli/lib/uninstall-plugin.js";
13
+ import * as pluginUpgrade from "./cli/lib/upgrade-plugin.js";
14
+ import * as configEnv from "./config/env.js";
15
+ import * as providerSecretCatalog from "./providers/provider-secret-catalog.js";
16
+
17
+ setBundledCliModules({
18
+ configEnv,
19
+ providerSecretCatalog,
20
+ pluginCatalogCache,
21
+ pluginCatalogLocal,
22
+ pluginDiff,
23
+ pluginInspect,
24
+ pluginInstallGitHub,
25
+ pluginInstallPlatform,
26
+ pluginInstalled,
27
+ pluginPinHistory,
28
+ pluginSearch,
29
+ pluginSurfaces,
30
+ pluginUninstall,
31
+ pluginUpgrade,
32
+ });
@@ -0,0 +1,4 @@
1
+ import "./windows-compiled-logger.js";
2
+ import "./windows-compiled-cli.js";
3
+
4
+ await import("./index.js");
@@ -0,0 +1,6 @@
1
+ import pino from "pino";
2
+ import pinoPretty from "pino-pretty";
3
+
4
+ import { setBundledLoggerModules } from "./util/logger.js";
5
+
6
+ setBundledLoggerModules(pino, pinoPretty);
@@ -0,0 +1,29 @@
1
+ import "./windows-compiled-logger.js";
2
+
3
+ const worker = process.argv[2];
4
+
5
+ switch (worker) {
6
+ case "monitoring":
7
+ await import("./monitoring/worker.js");
8
+ break;
9
+ case "schedule":
10
+ await import("./schedule/worker.js");
11
+ break;
12
+ case "memory":
13
+ await (
14
+ await import("./plugins/defaults/worker-entrypoints.js")
15
+ ).loadDefaultMemoryWorker();
16
+ break;
17
+ case "routes":
18
+ await import("./embedded/plugin-api.js");
19
+ await import("./routes/worker.js");
20
+ break;
21
+ case "db-integrity": {
22
+ const { runIntegrityCheck } =
23
+ await import("./monitoring/db-integrity-check.js");
24
+ console.log(JSON.stringify(runIntegrityCheck(process.argv[3] ?? "")));
25
+ break;
26
+ }
27
+ default:
28
+ throw new Error(`Unknown Windows worker entry: ${worker ?? "missing"}`);
29
+ }
@@ -1,467 +0,0 @@
1
- /**
2
- * Tests for file-backed documents: the find-or-create route that gives a
3
- * workspace markdown file a document identity, and the write-through that
4
- * keeps the file in step with every document content write.
5
- */
6
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
- import { join } from "node:path";
8
- import { beforeEach, describe, expect, test } from "bun:test";
9
-
10
- import {
11
- createFileBackedDocument,
12
- getDocumentById,
13
- getDocumentByWorkspacePath,
14
- rebindDocumentsToRenamedPath,
15
- } from "../documents/document-store.js";
16
- import { getDb, getSqlite } from "../persistence/db-connection.js";
17
- import { migrateAddDocumentWorkspacePath } from "../persistence/migrations/360-add-document-workspace-path.js";
18
- import { ROUTES as DOCUMENT_ROUTES } from "../runtime/routes/documents-routes.js";
19
- import {
20
- BadRequestError,
21
- NotFoundError,
22
- UnprocessableEntityError,
23
- } from "../runtime/routes/errors.js";
24
- import type { RouteDefinition } from "../runtime/routes/types.js";
25
- import { ROUTES as WORKSPACE_ROUTES } from "../runtime/routes/workspace-routes.js";
26
- import {
27
- executeDocumentReplaceText,
28
- executeDocumentUpdate,
29
- } from "../tools/document/document-tool.js";
30
- import type { ToolContext } from "../tools/types.js";
31
- import { resetDbForTesting } from "./db-test-helpers.js";
32
-
33
- const workspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
34
- const notesDir = join(workspaceDir, "notes");
35
-
36
- const CONVERSATION_ID = "conv-file-docs";
37
-
38
- interface DocumentPayload {
39
- success: boolean;
40
- surfaceId: string;
41
- title: string;
42
- content: string;
43
- wordCount: number;
44
- workspacePath: string | null;
45
- }
46
-
47
- function getRoute(operationId: string): RouteDefinition {
48
- const route = DOCUMENT_ROUTES.find((r) => r.operationId === operationId);
49
- if (!route) {
50
- throw new Error(`No document route found for operationId: ${operationId}`);
51
- }
52
- return route;
53
- }
54
-
55
- /**
56
- * Open a workspace file as a document. Returns the route payload so callers
57
- * can assert on the same shape `GET documents/{id}` serves.
58
- */
59
- async function openWorkspaceFile(
60
- path: string,
61
- conversationId = CONVERSATION_ID,
62
- ): Promise<DocumentPayload> {
63
- const route = getRoute("documentForWorkspaceFile");
64
- const result = await route.handler({ body: { path, conversationId } });
65
- return result as DocumentPayload;
66
- }
67
-
68
- /** Save a document through the client's document save route. */
69
- async function saveViaRoute(
70
- surfaceId: string,
71
- title: string,
72
- content: string,
73
- ): Promise<void> {
74
- const route = getRoute("saveDocument");
75
- await route.handler({
76
- body: {
77
- surfaceId,
78
- conversationId: CONVERSATION_ID,
79
- title,
80
- content,
81
- wordCount: content.split(/\s+/).filter((w) => w.length > 0).length,
82
- },
83
- });
84
- }
85
-
86
- function makeToolContext(): ToolContext {
87
- return {
88
- workingDir: workspaceDir,
89
- conversationId: CONVERSATION_ID,
90
- trustClass: "guardian",
91
- executionChannel: "web",
92
- };
93
- }
94
-
95
- /**
96
- * Rebuild the document tables at the pre-360 shape, then apply the migration —
97
- * the same order a real upgrade takes, so these tests exercise the migrated
98
- * schema rather than a hand-written copy of it.
99
- */
100
- function bootstrapDocumentTables(): void {
101
- resetDbForTesting();
102
- const raw = getSqlite();
103
- raw.exec(/*sql*/ `
104
- DROP TABLE IF EXISTS document_conversations;
105
- DROP TABLE IF EXISTS documents;
106
- DROP TABLE IF EXISTS conversations;
107
-
108
- CREATE TABLE conversations (
109
- id TEXT PRIMARY KEY,
110
- created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
111
- );
112
-
113
- CREATE TABLE documents (
114
- surface_id TEXT PRIMARY KEY,
115
- conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
116
- title TEXT NOT NULL,
117
- content TEXT NOT NULL,
118
- word_count INTEGER NOT NULL DEFAULT 0,
119
- created_at INTEGER NOT NULL,
120
- updated_at INTEGER NOT NULL
121
- );
122
-
123
- CREATE TABLE document_conversations (
124
- surface_id TEXT NOT NULL,
125
- conversation_id TEXT NOT NULL,
126
- created_at INTEGER NOT NULL,
127
- PRIMARY KEY (surface_id, conversation_id),
128
- FOREIGN KEY (surface_id) REFERENCES documents(surface_id) ON DELETE CASCADE
129
- );
130
- `);
131
- migrateAddDocumentWorkspacePath(getDb());
132
- raw
133
- .query(`INSERT OR IGNORE INTO conversations (id, created_at) VALUES (?, ?)`)
134
- .run(CONVERSATION_ID, Date.now());
135
- raw
136
- .query(`INSERT OR IGNORE INTO conversations (id, created_at) VALUES (?, ?)`)
137
- .run("conv-other", Date.now());
138
- }
139
-
140
- beforeEach(() => {
141
- bootstrapDocumentTables();
142
- rmSync(notesDir, { recursive: true, force: true });
143
- mkdirSync(notesDir, { recursive: true });
144
- });
145
-
146
- describe("POST documents/for-workspace-file", () => {
147
- test("creates a document seeded from the file, then returns the same one", async () => {
148
- writeFileSync(join(notesDir, "plan.md"), "# Plan\n\nFirst draft.");
149
-
150
- const first = await openWorkspaceFile("notes/plan.md");
151
- expect(first.success).toBe(true);
152
- expect(first.title).toBe("plan.md");
153
- expect(first.content).toBe("# Plan\n\nFirst draft.");
154
- expect(first.workspacePath).toBe("notes/plan.md");
155
-
156
- const second = await openWorkspaceFile("notes/plan.md");
157
- expect(second.surfaceId).toBe(first.surfaceId);
158
-
159
- // Non-canonical spellings of the same path resolve to the same document.
160
- const third = await openWorkspaceFile("./notes/../notes/plan.md");
161
- expect(third.surfaceId).toBe(first.surfaceId);
162
- });
163
-
164
- test("refreshes stored content from disk when the file changed underneath", async () => {
165
- const filePath = join(notesDir, "plan.md");
166
- writeFileSync(filePath, "original body");
167
-
168
- const first = await openWorkspaceFile("notes/plan.md");
169
- expect(first.content).toBe("original body");
170
-
171
- writeFileSync(filePath, "edited outside the editor");
172
-
173
- const reopened = await openWorkspaceFile("notes/plan.md");
174
- expect(reopened.surfaceId).toBe(first.surfaceId);
175
- expect(reopened.content).toBe("edited outside the editor");
176
- expect(reopened.wordCount).toBe(4);
177
- expect(getDocumentById(first.surfaceId)?.content).toBe(
178
- "edited outside the editor",
179
- );
180
- });
181
-
182
- test("associates the document with every conversation that opens it", async () => {
183
- writeFileSync(join(notesDir, "plan.md"), "shared");
184
-
185
- const first = await openWorkspaceFile("notes/plan.md");
186
- await openWorkspaceFile("notes/plan.md", "conv-other");
187
-
188
- const rows = getSqlite()
189
- .query(
190
- `SELECT conversation_id FROM document_conversations WHERE surface_id = ? ORDER BY conversation_id`,
191
- )
192
- .all(first.surfaceId) as Array<{ conversation_id: string }>;
193
- expect(rows.map((r) => r.conversation_id)).toEqual([
194
- CONVERSATION_ID,
195
- "conv-other",
196
- ]);
197
- });
198
-
199
- test("rejects paths that escape the workspace", async () => {
200
- await expect(openWorkspaceFile("../escape.md")).rejects.toBeInstanceOf(
201
- BadRequestError,
202
- );
203
- await expect(openWorkspaceFile("/etc/passwd.md")).rejects.toBeInstanceOf(
204
- BadRequestError,
205
- );
206
- await expect(
207
- openWorkspaceFile("notes/../../escape.md"),
208
- ).rejects.toBeInstanceOf(BadRequestError);
209
- });
210
-
211
- test("rejects non-markdown files", async () => {
212
- writeFileSync(join(notesDir, "data.txt"), "plain text");
213
-
214
- await expect(openWorkspaceFile("notes/data.txt")).rejects.toBeInstanceOf(
215
- UnprocessableEntityError,
216
- );
217
- });
218
-
219
- test("requires path and conversationId", () => {
220
- const route = getRoute("documentForWorkspaceFile");
221
- expect(() => route.handler({ body: {} })).toThrow(BadRequestError);
222
- expect(() => route.handler({ body: { path: "notes/plan.md" } })).toThrow(
223
- BadRequestError,
224
- );
225
- });
226
-
227
- test("404s for a file that was never there", async () => {
228
- await expect(openWorkspaceFile("notes/missing.md")).rejects.toBeInstanceOf(
229
- NotFoundError,
230
- );
231
- });
232
-
233
- test("404s distinctly for a bound document whose file was deleted", async () => {
234
- const filePath = join(notesDir, "plan.md");
235
- writeFileSync(filePath, "will be deleted");
236
- const created = await openWorkspaceFile("notes/plan.md");
237
- rmSync(filePath);
238
-
239
- await expect(openWorkspaceFile("notes/plan.md")).rejects.toThrow(
240
- /file backing this document no longer exists/,
241
- );
242
-
243
- // The document row survives; the file is never resurrected from it.
244
- expect(getDocumentById(created.surfaceId)?.content).toBe("will be deleted");
245
- expect(() => readFileSync(filePath, "utf-8")).toThrow();
246
- });
247
- });
248
-
249
- describe("write-through to the backing file", () => {
250
- test("the client's document save rewrites the file", async () => {
251
- const filePath = join(notesDir, "plan.md");
252
- writeFileSync(filePath, "original body");
253
- const doc = await openWorkspaceFile("notes/plan.md");
254
-
255
- await saveViaRoute(doc.surfaceId, "plan.md", "saved from the editor");
256
-
257
- expect(readFileSync(filePath, "utf-8")).toBe("saved from the editor");
258
- expect(getDocumentById(doc.surfaceId)?.content).toBe(
259
- "saved from the editor",
260
- );
261
- });
262
-
263
- test("a document_update tool edit rewrites the file", async () => {
264
- const filePath = join(notesDir, "plan.md");
265
- writeFileSync(filePath, "original body");
266
- const doc = await openWorkspaceFile("notes/plan.md");
267
-
268
- const result = executeDocumentUpdate(
269
- {
270
- surface_id: doc.surfaceId,
271
- content: "appended by the assistant",
272
- mode: "append",
273
- },
274
- { ...makeToolContext(), sendToClient: () => {} },
275
- );
276
- expect(result.isError).toBe(false);
277
-
278
- const expected = "original body\n\nappended by the assistant";
279
- expect(readFileSync(filePath, "utf-8")).toBe(expected);
280
- expect(getDocumentById(doc.surfaceId)?.content).toBe(expected);
281
- });
282
-
283
- test("a document_replace_text tool edit rewrites the file", async () => {
284
- const filePath = join(notesDir, "plan.md");
285
- writeFileSync(filePath, "hello world");
286
- const doc = await openWorkspaceFile("notes/plan.md");
287
-
288
- const result = executeDocumentReplaceText(
289
- { surface_id: doc.surfaceId, find: "world", replace: "there" },
290
- { ...makeToolContext(), sendToClient: () => {} },
291
- );
292
- expect(result.isError).toBe(false);
293
-
294
- expect(readFileSync(filePath, "utf-8")).toBe("hello there");
295
- expect(getDocumentById(doc.surfaceId)?.content).toBe("hello there");
296
- });
297
-
298
- test("a failed file write fails the update instead of diverging", async () => {
299
- const filePath = join(notesDir, "plan.md");
300
- writeFileSync(filePath, "original body");
301
- const doc = await openWorkspaceFile("notes/plan.md");
302
-
303
- // Replace the file with a directory: the write primitive refuses it, so
304
- // the row must stay on the content that still matches what is on disk.
305
- rmSync(filePath);
306
- mkdirSync(filePath);
307
-
308
- const result = executeDocumentUpdate(
309
- { surface_id: doc.surfaceId, content: "never lands", mode: "replace" },
310
- { ...makeToolContext(), sendToClient: () => {} },
311
- );
312
-
313
- expect(result.isError).toBe(true);
314
- expect(getDocumentById(doc.surfaceId)?.content).toBe("original body");
315
- });
316
-
317
- test("documents with no backing file are untouched by the write-through", async () => {
318
- const route = getRoute("saveDocument");
319
- await route.handler({
320
- body: {
321
- surfaceId: "doc-plain",
322
- conversationId: CONVERSATION_ID,
323
- title: "Plain",
324
- content: "db only",
325
- wordCount: 2,
326
- },
327
- });
328
-
329
- const stored = getDocumentById("doc-plain");
330
- expect(stored?.content).toBe("db only");
331
- expect(stored?.workspacePath).toBeNull();
332
- });
333
- });
334
-
335
- // ===========================================================================
336
- // Rename rebinding
337
- // ===========================================================================
338
-
339
- /** Bind a surface ID to a workspace-relative path with throwaway content. */
340
- function bindDocument(surfaceId: string, workspacePath: string): void {
341
- const created = createFileBackedDocument({
342
- surfaceId,
343
- conversationId: CONVERSATION_ID,
344
- title: workspacePath,
345
- content: "body",
346
- workspacePath,
347
- });
348
- expect(created.success).toBe(true);
349
- }
350
-
351
- describe("rebindDocumentsToRenamedPath", () => {
352
- test("moves the document bound to a renamed file", () => {
353
- bindDocument("doc-renamed", "notes/plan.md");
354
-
355
- rebindDocumentsToRenamedPath({
356
- oldPath: "notes/plan.md",
357
- newPath: "notes/roadmap.md",
358
- });
359
-
360
- expect(getDocumentById("doc-renamed")?.workspacePath).toBe(
361
- "notes/roadmap.md",
362
- );
363
- expect(getDocumentByWorkspacePath("notes/plan.md")).toBeNull();
364
- expect(getDocumentByWorkspacePath("notes/roadmap.md")?.surfaceId).toBe(
365
- "doc-renamed",
366
- );
367
- });
368
-
369
- test("moves documents nested under a renamed directory", () => {
370
- bindDocument("doc-top", "notes/plan.md");
371
- bindDocument("doc-nested", "notes/deep/spec.md");
372
-
373
- rebindDocumentsToRenamedPath({ oldPath: "notes", newPath: "archive" });
374
-
375
- expect(getDocumentById("doc-top")?.workspacePath).toBe("archive/plan.md");
376
- expect(getDocumentById("doc-nested")?.workspacePath).toBe(
377
- "archive/deep/spec.md",
378
- );
379
- });
380
-
381
- test("leaves documents outside the renamed subtree alone", () => {
382
- bindDocument("doc-moved", "notes/plan.md");
383
- bindDocument("doc-sibling", "notes-archive/plan.md");
384
- bindDocument("doc-unrelated", "other/plan.md");
385
-
386
- rebindDocumentsToRenamedPath({ oldPath: "notes", newPath: "archive" });
387
-
388
- expect(getDocumentById("doc-moved")?.workspacePath).toBe("archive/plan.md");
389
- expect(getDocumentById("doc-sibling")?.workspacePath).toBe(
390
- "notes-archive/plan.md",
391
- );
392
- expect(getDocumentById("doc-unrelated")?.workspacePath).toBe(
393
- "other/plan.md",
394
- );
395
- });
396
-
397
- test("keeps both bindings when the destination is already bound", () => {
398
- bindDocument("doc-source", "notes/plan.md");
399
- bindDocument("doc-destination", "notes/roadmap.md");
400
-
401
- expect(() =>
402
- rebindDocumentsToRenamedPath({
403
- oldPath: "notes/plan.md",
404
- newPath: "notes/roadmap.md",
405
- }),
406
- ).not.toThrow();
407
-
408
- expect(getDocumentById("doc-source")?.workspacePath).toBe("notes/plan.md");
409
- expect(getDocumentById("doc-destination")?.workspacePath).toBe(
410
- "notes/roadmap.md",
411
- );
412
- });
413
-
414
- test("one blocked row does not strand the rest of a directory rename", () => {
415
- bindDocument("doc-blocked", "notes/plan.md");
416
- bindDocument("doc-occupier", "archive/plan.md");
417
- bindDocument("doc-free", "notes/spec.md");
418
-
419
- rebindDocumentsToRenamedPath({ oldPath: "notes", newPath: "archive" });
420
-
421
- expect(getDocumentById("doc-blocked")?.workspacePath).toBe("notes/plan.md");
422
- expect(getDocumentById("doc-occupier")?.workspacePath).toBe(
423
- "archive/plan.md",
424
- );
425
- expect(getDocumentById("doc-free")?.workspacePath).toBe("archive/spec.md");
426
- });
427
- });
428
-
429
- describe("POST workspace/rename keeps documents bound", () => {
430
- const renameRoute = WORKSPACE_ROUTES.find(
431
- (r) => r.operationId === "workspace_rename",
432
- )!;
433
-
434
- test("a renamed file carries its document to the new path", async () => {
435
- writeFileSync(join(notesDir, "plan.md"), "# Plan");
436
- const doc = await openWorkspaceFile("notes/plan.md");
437
- expect(doc.workspacePath).toBe("notes/plan.md");
438
-
439
- await renameRoute.handler({
440
- body: { oldPath: "notes/plan.md", newPath: "notes/roadmap.md" },
441
- });
442
-
443
- expect(getDocumentById(doc.surfaceId)?.workspacePath).toBe(
444
- "notes/roadmap.md",
445
- );
446
-
447
- // Reopening the file at its new name yields the same document identity,
448
- // so comments and history survive the rename.
449
- const reopened = await openWorkspaceFile("notes/roadmap.md");
450
- expect(reopened.surfaceId).toBe(doc.surfaceId);
451
- });
452
-
453
- test("a renamed directory carries the documents beneath it", async () => {
454
- writeFileSync(join(notesDir, "plan.md"), "# Plan");
455
- const doc = await openWorkspaceFile("notes/plan.md");
456
-
457
- await renameRoute.handler({
458
- body: { oldPath: "notes", newPath: "archive" },
459
- });
460
-
461
- expect(getDocumentById(doc.surfaceId)?.workspacePath).toBe(
462
- "archive/plan.md",
463
- );
464
-
465
- rmSync(join(workspaceDir, "archive"), { recursive: true, force: true });
466
- });
467
- });