@vellumai/assistant 0.10.3-dev.202606271631.97b63cd → 0.10.3-dev.202606271833.9783ef3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606271631.97b63cd",
3
+ "version": "0.10.3-dev.202606271833.9783ef3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -42,6 +42,7 @@ import {
42
42
  } from "./schemas/logging.js";
43
43
  import { McpConfigSchema } from "./schemas/mcp.js";
44
44
  import { MemoryConfigSchema } from "./schemas/memory.js";
45
+ import { MigrationsConfigSchema } from "./schemas/migrations.js";
45
46
  import { NotificationsConfigSchema } from "./schemas/notifications.js";
46
47
  import {
47
48
  DaemonConfigSchema,
@@ -63,6 +64,7 @@ export const AssistantConfigSchema = z
63
64
  .object({
64
65
  services: ServicesSchema.default(ServicesSchema.parse({})),
65
66
  memory: MemoryConfigSchema.default(MemoryConfigSchema.parse({})),
67
+ migrations: MigrationsConfigSchema.default(MigrationsConfigSchema.parse({})),
66
68
  dataDir: z
67
69
  .string({ error: "dataDir must be a string" })
68
70
  .default(getDataDir())
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+
3
+ export const MigrationsWorkerConfigSchema = z
4
+ .object({
5
+ enabled: z
6
+ .boolean({ error: "migrations.worker.enabled must be a boolean" })
7
+ .default(false)
8
+ .describe(
9
+ "Enable the async migration worker. While false, migrations that are too expensive to run inside the synchronous startup migration runner skip their work and pass, deferring until the async runner is in place.",
10
+ ),
11
+ })
12
+ .describe("Async migration worker configuration");
13
+
14
+ export const MigrationsConfigSchema = z
15
+ .object({
16
+ worker: MigrationsWorkerConfigSchema.default(
17
+ MigrationsWorkerConfigSchema.parse({}),
18
+ ),
19
+ })
20
+ .describe("Workspace/database migration configuration");
21
+
22
+ export type MigrationsWorkerConfig = z.infer<
23
+ typeof MigrationsWorkerConfigSchema
24
+ >;
25
+ export type MigrationsConfig = z.infer<typeof MigrationsConfigSchema>;
@@ -1,3 +1,4 @@
1
+ import { getConfig } from "../../config/loader.js";
1
2
  import { getLogger } from "../../util/logger.js";
2
3
  import { getDbPath } from "../../util/platform.js";
3
4
  import { runAsyncSqlite } from "../db-async-query.js";
@@ -124,6 +125,19 @@ function windowSql(lo: number, hi: number): string {
124
125
  export async function migrateStripThinkingFromConsolidated(
125
126
  database: DrizzleDb,
126
127
  ): Promise<void> {
128
+ // Gated on the async migration worker. This sweep proved too expensive for
129
+ // the synchronous startup migration runner — on a large `messages` table it
130
+ // can hold the daemon for minutes and trip the startup probe into a restart
131
+ // loop. While `migrations.worker.enabled` is false, skip the work and let the
132
+ // step pass so startup is never blocked; performing the sweep is deferred to
133
+ // the new async migration runner once it takes shape.
134
+ if (!getConfig().migrations.worker.enabled) {
135
+ log.info(
136
+ "Migration 209: skipped — migrations.worker.enabled is false; deferring the strip-thinking sweep to the async migration runner",
137
+ );
138
+ return;
139
+ }
140
+
127
141
  const raw = getSqliteFrom(database);
128
142
  const dbPath = getDbPath();
129
143
 
@@ -1,12 +1,16 @@
1
+ import { getConfig } from "../../config/loader.js";
1
2
  import {
2
3
  PLACEHOLDER_BLOCKS_OMITTED,
3
4
  PLACEHOLDER_EMPTY_TURN,
4
5
  } from "../../providers/placeholder-sentinels.js";
6
+ import { getLogger } from "../../util/logger.js";
5
7
  import { getDbPath } from "../../util/platform.js";
6
8
  import { runAsyncSqlite } from "../db-async-query.js";
7
9
  import type { DrizzleDb } from "../db-connection.js";
8
10
  import { getSqliteFrom } from "../db-connection.js";
9
11
 
12
+ const log = getLogger("db-init");
13
+
10
14
  /**
11
15
  * Number of `rowid` values swept per `runAsyncSqlite` dispatch. Each window is
12
16
  * one off-thread subprocess transaction. Keeping it well below the row count of
@@ -123,6 +127,19 @@ function windowSql(lo: number, hi: number): string {
123
127
  export async function migrateStripPlaceholderSentinelsFromMessages(
124
128
  database: DrizzleDb,
125
129
  ): Promise<void> {
130
+ // Gated on the async migration worker. This full-table sweep proved too
131
+ // expensive for the synchronous startup migration runner — on a large
132
+ // `messages` table it can hold the daemon's event loop and write lock for
133
+ // minutes. While `migrations.worker.enabled` is false, skip the work and let
134
+ // the step pass so startup is never blocked; performing the sweep is deferred
135
+ // to the new async migration runner once it takes shape.
136
+ if (!getConfig().migrations.worker.enabled) {
137
+ log.info(
138
+ "Migration 222: skipped — migrations.worker.enabled is false; deferring the placeholder-sentinel sweep to the async migration runner",
139
+ );
140
+ return;
141
+ }
142
+
126
143
  const raw = getSqliteFrom(database);
127
144
  const dbPath = getDbPath();
128
145
 
@@ -17,9 +17,18 @@ const {
17
17
  ROWID_WINDOW,
18
18
  WINDOW_TIMEOUT_MS,
19
19
  } = await import("../209-strip-thinking-from-consolidated.js");
20
+ const { loadRawConfig, saveRawConfig } =
21
+ await import("../../../config/loader.js");
20
22
 
21
23
  await initializeDb();
22
24
 
25
+ // The sweep is gated behind `migrations.worker.enabled` (default false), under
26
+ // which it short-circuits and passes. Enable it so the migration does its work
27
+ // for these tests.
28
+ const rawConfig = loadRawConfig();
29
+ rawConfig.migrations = { worker: { enabled: true } };
30
+ saveRawConfig(rawConfig);
31
+
23
32
  const CONV = "conv-209";
24
33
  getSqlite()
25
34
  .query(
@@ -283,6 +292,28 @@ describe("migration 209 — strip thinking from consolidated assistant messages"
283
292
  expect(watermark()).toBeNull();
284
293
  });
285
294
 
295
+ test("skips the sweep and passes when migrations.worker.enabled is false", async () => {
296
+ const original = JSON.stringify([
297
+ { type: "thinking", thinking: "secret", signature: "s" },
298
+ { type: "text", text: "gated" },
299
+ ]);
300
+ const { id } = insert("assistant", original);
301
+
302
+ const disabled = loadRawConfig();
303
+ disabled.migrations = { worker: { enabled: false } };
304
+ saveRawConfig(disabled);
305
+ try {
306
+ // Resolves without throwing and leaves the row untouched — the work is
307
+ // deferred to the async migration runner.
308
+ await migrateStripThinkingFromConsolidated(getDb());
309
+ expect(content(id)).toBe(original);
310
+ } finally {
311
+ const enabled = loadRawConfig();
312
+ enabled.migrations = { worker: { enabled: true } };
313
+ saveRawConfig(enabled);
314
+ }
315
+ });
316
+
286
317
  test("keeps the sweep window and timeout bounded so it cannot overrun a whole table", () => {
287
318
  // Regression guard: ROWID_WINDOW was 100_000, larger than a typical
288
319
  // messages table, so the entire table swept in one subprocess that overran
@@ -20,9 +20,18 @@ const {
20
20
  ROWID_WINDOW,
21
21
  WINDOW_TIMEOUT_MS,
22
22
  } = await import("../222-strip-placeholder-sentinels-from-messages.js");
23
+ const { loadRawConfig, saveRawConfig } =
24
+ await import("../../../config/loader.js");
23
25
 
24
26
  await initializeDb();
25
27
 
28
+ // The sweep is gated behind `migrations.worker.enabled` (default false), under
29
+ // which it short-circuits and passes. Enable it so the migration does its work
30
+ // for these tests.
31
+ const rawConfig = loadRawConfig();
32
+ rawConfig.migrations = { worker: { enabled: true } };
33
+ saveRawConfig(rawConfig);
34
+
26
35
  const CONV = "conv-222";
27
36
  getSqlite()
28
37
  .query(
@@ -235,6 +244,28 @@ describe("migration 222 — strip placeholder sentinels from assistant messages"
235
244
  expect(blocks(second.id)).toEqual([{ type: "text", text: "second" }]);
236
245
  });
237
246
 
247
+ test("skips the sweep and passes when migrations.worker.enabled is false", async () => {
248
+ const original = JSON.stringify([
249
+ { type: "text", text: PLACEHOLDER_EMPTY_TURN },
250
+ { type: "text", text: "gated" },
251
+ ]);
252
+ const { id } = insert("assistant", original);
253
+
254
+ const disabled = loadRawConfig();
255
+ disabled.migrations = { worker: { enabled: false } };
256
+ saveRawConfig(disabled);
257
+ try {
258
+ // Resolves without throwing and leaves the row untouched — the work is
259
+ // deferred to the async migration runner.
260
+ await migrateStripPlaceholderSentinelsFromMessages(getDb());
261
+ expect(content(id)).toBe(original);
262
+ } finally {
263
+ const enabled = loadRawConfig();
264
+ enabled.migrations = { worker: { enabled: true } };
265
+ saveRawConfig(enabled);
266
+ }
267
+ });
268
+
238
269
  test("keeps the sweep window and timeout bounded so it cannot overrun a whole table", () => {
239
270
  expect(ROWID_WINDOW).toBeLessThanOrEqual(10_000);
240
271
  expect(WINDOW_TIMEOUT_MS).toBeLessThan(60 * 60 * 1000);