@vellumai/assistant 0.10.3-dev.202606291342.c7a83ea → 0.10.3-dev.202606291517.f0c68f7

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/openapi.yaml CHANGED
@@ -12,9 +12,26 @@ paths:
12
12
  /healthz:
13
13
  get:
14
14
  operationId: healthz_get
15
+ summary: Liveness probe
16
+ description:
17
+ Trivial liveness/startup probe. Returns { status, version } the instant the HTTP server is up, with zero
18
+ DB/CES/lifecycle access.
15
19
  responses:
16
20
  "200":
17
21
  description: Successful response
22
+ content:
23
+ application/json:
24
+ schema:
25
+ type: object
26
+ properties:
27
+ status:
28
+ type: string
29
+ version:
30
+ type: string
31
+ required:
32
+ - status
33
+ - version
34
+ additionalProperties: false
18
35
  /pages/{id}:
19
36
  get:
20
37
  operationId: pages__id__get
@@ -30,9 +47,52 @@ paths:
30
47
  /readyz:
31
48
  get:
32
49
  operationId: readyz_get
50
+ summary: Readiness probe
51
+ description:
52
+ Strict readiness probe. Returns 503 with { status, ready, notReady } until the daemon's synchronous startup
53
+ latches (isStartupComplete() && isDbReady()) are set, then a stable 200. notReady lists the gates still failing
54
+ ("startup", "db"). CES is informational and never gates readiness.
33
55
  responses:
34
56
  "200":
35
57
  description: Successful response
58
+ content:
59
+ application/json:
60
+ schema:
61
+ type: object
62
+ properties:
63
+ status:
64
+ type: string
65
+ ready:
66
+ type: boolean
67
+ notReady:
68
+ type: array
69
+ items:
70
+ type: string
71
+ required:
72
+ - status
73
+ - ready
74
+ - notReady
75
+ additionalProperties: false
76
+ "503":
77
+ description: Not ready — startup incomplete or database not ready.
78
+ content:
79
+ application/json:
80
+ schema:
81
+ type: object
82
+ properties:
83
+ status:
84
+ type: string
85
+ ready:
86
+ type: boolean
87
+ notReady:
88
+ type: array
89
+ items:
90
+ type: string
91
+ required:
92
+ - status
93
+ - ready
94
+ - notReady
95
+ additionalProperties: false
36
96
  /v1/acp/{id}/cancel:
37
97
  post:
38
98
  operationId: acp_by_id_cancel_post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606291342.c7a83ea",
3
+ "version": "0.10.3-dev.202606291517.f0c68f7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -216,13 +216,69 @@ async function collectRoutesFromModules(): Promise<RouteEntry[]> {
216
216
  return routes;
217
217
  }
218
218
 
219
+ /**
220
+ * Trivial liveness/startup probe response. `/healthz` is the k8s startup +
221
+ * liveness target and stays intentionally minimal: a static `{ status, version }`
222
+ * answered the instant the HTTP server is up, with zero DB/CES/lifecycle access.
223
+ */
224
+ const trivialHealthSchema = z.object({
225
+ status: z.string(),
226
+ version: z.string(),
227
+ });
228
+
229
+ /**
230
+ * Strict readiness probe response. `/readyz` gates on the daemon's synchronous
231
+ * startup latches (`isStartupComplete() && isDbReady()`), returning 503 with
232
+ * `ready: false` until both are set, then a stable 200. `notReady` lists the
233
+ * gates still failing (`"startup"`, `"db"`). CES is never consulted.
234
+ */
235
+ const readyzSchema = z.object({
236
+ status: z.string(),
237
+ ready: z.boolean(),
238
+ notReady: z.array(z.string()),
239
+ });
240
+
219
241
  /**
220
242
  * Top-level routes outside the /v1/ namespace.
221
243
  * These are added to the spec separately.
222
244
  */
223
- const NON_V1_ROUTES: Array<{ method: string; path: string }> = [
224
- { method: "GET", path: "/healthz" },
225
- { method: "GET", path: "/readyz" },
245
+ const NON_V1_ROUTES: Array<{
246
+ method: string;
247
+ path: string;
248
+ summary?: string;
249
+ description?: string;
250
+ responseBody?: z.ZodType;
251
+ additionalResponses?: Record<
252
+ string,
253
+ { description: string; schema?: unknown }
254
+ >;
255
+ }> = [
256
+ {
257
+ method: "GET",
258
+ path: "/healthz",
259
+ summary: "Liveness probe",
260
+ description:
261
+ "Trivial liveness/startup probe. Returns { status, version } the instant " +
262
+ "the HTTP server is up, with zero DB/CES/lifecycle access.",
263
+ responseBody: trivialHealthSchema,
264
+ },
265
+ {
266
+ method: "GET",
267
+ path: "/readyz",
268
+ summary: "Readiness probe",
269
+ description:
270
+ "Strict readiness probe. Returns 503 with { status, ready, notReady } until " +
271
+ "the daemon's synchronous startup latches (isStartupComplete() && isDbReady()) " +
272
+ "are set, then a stable 200. notReady lists the gates still failing " +
273
+ '("startup", "db"). CES is informational and never gates readiness.',
274
+ responseBody: readyzSchema,
275
+ additionalResponses: {
276
+ "503": {
277
+ description: "Not ready — startup incomplete or database not ready.",
278
+ schema: readyzSchema,
279
+ },
280
+ },
281
+ },
226
282
  { method: "GET", path: "/pages/{id}" },
227
283
  ];
228
284
 
@@ -322,7 +378,16 @@ function buildSpec(
322
378
  path: r.path,
323
379
  method: r.method,
324
380
  endpoint: r.path,
325
- entry: { method: r.method, endpoint: r.path },
381
+ entry: {
382
+ method: r.method,
383
+ endpoint: r.path,
384
+ ...(r.summary ? { summary: r.summary } : {}),
385
+ ...(r.description ? { description: r.description } : {}),
386
+ ...(r.responseBody ? { responseBody: r.responseBody } : {}),
387
+ ...(r.additionalResponses
388
+ ? { additionalResponses: r.additionalResponses }
389
+ : {}),
390
+ },
326
391
  });
327
392
  }
328
393
  }
@@ -22,13 +22,20 @@ mock.module("../util/logger.js", () => ({
22
22
  }),
23
23
  }));
24
24
 
25
+ import {
26
+ resetReadinessForTest,
27
+ setDbReady,
28
+ setStartupComplete,
29
+ } from "../daemon/daemon-readiness.js";
25
30
  import {
26
31
  handleDetailedHealth,
32
+ handleHealth,
27
33
  handleReadyz,
28
34
  ROUTES,
29
35
  } from "../runtime/routes/identity-routes.js";
30
36
  import { setCesClient } from "../security/secure-keys.js";
31
37
  import { getWorkspaceDir } from "../util/platform.js";
38
+ import { APP_VERSION } from "../version.js";
32
39
  import {
33
40
  getHatchedSidecarPath,
34
41
  resolveHatchedAtReadOnly,
@@ -196,31 +203,6 @@ describe("identity routes — health endpoint", () => {
196
203
  setCesClient(undefined);
197
204
  });
198
205
 
199
- test("readyz returns 200 and logs warning when CES is unavailable", () => {
200
- const res = handleReadyz();
201
- expect(res.status).toBe(200);
202
- });
203
-
204
- test("readyz returns 200 when CES is connected and ready", () => {
205
- const mockClient = {
206
- isReady: () => true,
207
- close: () => {},
208
- } as unknown as import("../credential-execution/client.js").CesClient;
209
- setCesClient(mockClient);
210
- const res = handleReadyz();
211
- expect(res.status).toBe(200);
212
- });
213
-
214
- test("readyz returns 200 when CES client exists but is not ready", () => {
215
- const mockClient = {
216
- isReady: () => false,
217
- close: () => {},
218
- } as unknown as import("../credential-execution/client.js").CesClient;
219
- setCesClient(mockClient);
220
- const res = handleReadyz();
221
- expect(res.status).toBe(200);
222
- });
223
-
224
206
  test("/v1/health reports ces.connected=true when CES is ready", async () => {
225
207
  const mockClient = {
226
208
  isReady: () => true,
@@ -407,6 +389,112 @@ describe("identity routes — health endpoint", () => {
407
389
  });
408
390
  });
409
391
 
392
+ describe("identity routes — trivial /healthz liveness probe", () => {
393
+ test("returns 200 with { status: 'ok', version } carrying APP_VERSION", async () => {
394
+ const res = handleHealth();
395
+ expect(res.status).toBe(200);
396
+
397
+ const body = (await res.json()) as Record<string, unknown>;
398
+ expect(body.status).toBe("ok");
399
+ expect(typeof body.version).toBe("string");
400
+ expect(body.version).not.toBe("");
401
+ expect(body.version).toBe(APP_VERSION);
402
+ });
403
+
404
+ test("never touches CES/lifecycle state — a throwing CES client is never consulted", async () => {
405
+ // If the trivial probe touched CES at all this would throw.
406
+ const explodingClient = {
407
+ isReady: () => {
408
+ throw new Error("handleHealth must not access CES");
409
+ },
410
+ close: () => {},
411
+ } as unknown as import("../credential-execution/client.js").CesClient;
412
+ setCesClient(explodingClient);
413
+
414
+ const res = handleHealth();
415
+ expect(res.status).toBe(200);
416
+ const body = (await res.json()) as Record<string, unknown>;
417
+ expect(body).toEqual({ status: "ok", version: APP_VERSION });
418
+
419
+ setCesClient(undefined);
420
+ });
421
+
422
+ test("answers with CES uninitialized", async () => {
423
+ setCesClient(undefined);
424
+ const res = handleHealth();
425
+ expect(res.status).toBe(200);
426
+ const body = (await res.json()) as Record<string, unknown>;
427
+ expect(body).toEqual({ status: "ok", version: APP_VERSION });
428
+ });
429
+ });
430
+
431
+ describe("identity routes — /readyz readiness gate", () => {
432
+ afterEach(() => {
433
+ resetReadinessForTest();
434
+ setCesClient(undefined);
435
+ });
436
+
437
+ test("returns 503 with notReady:['startup','db'] before startup", async () => {
438
+ resetReadinessForTest();
439
+ const res = handleReadyz();
440
+ expect(res.status).toBe(503);
441
+ const body = (await res.json()) as Record<string, unknown>;
442
+ expect(body).toEqual({
443
+ status: "unready",
444
+ ready: false,
445
+ notReady: ["startup", "db"],
446
+ });
447
+ });
448
+
449
+ test("returns 503 with notReady:['startup'] when db is ready but startup isn't", async () => {
450
+ resetReadinessForTest();
451
+ setDbReady(true);
452
+ const res = handleReadyz();
453
+ expect(res.status).toBe(503);
454
+ const body = (await res.json()) as Record<string, unknown>;
455
+ expect(body.ready).toBe(false);
456
+ expect(body.notReady).toEqual(["startup"]);
457
+ });
458
+
459
+ test("returns 503 with notReady:['db'] when started but db not ready", async () => {
460
+ resetReadinessForTest();
461
+ setStartupComplete();
462
+ const res = handleReadyz();
463
+ expect(res.status).toBe(503);
464
+ const body = (await res.json()) as Record<string, unknown>;
465
+ expect(body.ready).toBe(false);
466
+ expect(body.notReady).toEqual(["db"]);
467
+ });
468
+
469
+ test("returns 200 when started and db ready even if CES is down", async () => {
470
+ resetReadinessForTest();
471
+ setStartupComplete();
472
+ setDbReady(true);
473
+ const explodingClient = {
474
+ isReady: () => {
475
+ throw new Error("/readyz must not consult CES");
476
+ },
477
+ close: () => {},
478
+ } as unknown as import("../credential-execution/client.js").CesClient;
479
+ setCesClient(explodingClient);
480
+
481
+ const res = handleReadyz();
482
+ expect(res.status).toBe(200);
483
+ const body = (await res.json()) as Record<string, unknown>;
484
+ expect(body.ready).toBe(true);
485
+ });
486
+
487
+ test("returns 200 with the ok body when fully ready", async () => {
488
+ resetReadinessForTest();
489
+ setStartupComplete();
490
+ setDbReady(true);
491
+ const res = handleReadyz();
492
+ expect(res.status).toBe(200);
493
+ const body = (await res.json()) as Record<string, unknown>;
494
+ expect(body).toEqual({ status: "ok", ready: true, notReady: [] });
495
+ });
496
+ });
497
+
410
498
  describe("identity routes — createdAt selection", () => {
411
499
  test("falls back to mtime when birthtime is the Unix epoch", () => {
412
500
  const mtime = new Date("2026-05-01T14:49:47.519Z");
@@ -0,0 +1,51 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ isDbReady,
5
+ isStartupComplete,
6
+ resetReadinessForTest,
7
+ setDbReady,
8
+ setStartupComplete,
9
+ } from "../daemon-readiness.js";
10
+
11
+ describe("daemon-readiness", () => {
12
+ beforeEach(() => {
13
+ resetReadinessForTest();
14
+ });
15
+
16
+ afterEach(() => {
17
+ resetReadinessForTest();
18
+ });
19
+
20
+ test("defaults are false", () => {
21
+ expect(isDbReady()).toBe(false);
22
+ expect(isStartupComplete()).toBe(false);
23
+ });
24
+
25
+ test("setDbReady flips state both ways", () => {
26
+ setDbReady(true);
27
+ expect(isDbReady()).toBe(true);
28
+ setDbReady(false);
29
+ expect(isDbReady()).toBe(false);
30
+ });
31
+
32
+ test("setStartupComplete latches startup state", () => {
33
+ expect(isStartupComplete()).toBe(false);
34
+ setStartupComplete();
35
+ expect(isStartupComplete()).toBe(true);
36
+ });
37
+
38
+ test("setStartupComplete is monotonic", () => {
39
+ setStartupComplete();
40
+ setStartupComplete();
41
+ expect(isStartupComplete()).toBe(true);
42
+ });
43
+
44
+ test("resetReadinessForTest clears both latches", () => {
45
+ setDbReady(true);
46
+ setStartupComplete();
47
+ resetReadinessForTest();
48
+ expect(isDbReady()).toBe(false);
49
+ expect(isStartupComplete()).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,33 @@
1
+ // Module-level readiness state for the daemon, readable synchronously from any
2
+ // request handler with no `await`. The `/readyz` probe gates on these latches
3
+ // to report whether the daemon can actually serve requests.
4
+ //
5
+ // CES is intentionally not latched here: it is read live
6
+ // (`getCesClient()?.isReady()`) only when reported in a response body, and must
7
+ // never gate readiness.
8
+
9
+ let dbReady = false;
10
+ let startupComplete = false;
11
+
12
+ export function setDbReady(v: boolean): void {
13
+ dbReady = v;
14
+ }
15
+
16
+ export function isDbReady(): boolean {
17
+ return dbReady;
18
+ }
19
+
20
+ // One-way latch: once startup completes it stays complete for the process
21
+ // lifetime.
22
+ export function setStartupComplete(): void {
23
+ startupComplete = true;
24
+ }
25
+
26
+ export function isStartupComplete(): boolean {
27
+ return startupComplete;
28
+ }
29
+
30
+ export function resetReadinessForTest(): void {
31
+ dbReady = false;
32
+ startupComplete = false;
33
+ }
@@ -95,6 +95,7 @@ import { startAppSourceWatcher } from "./app-source-watcher.js";
95
95
  import { startConfigWatcher } from "./config-watcher.js";
96
96
  import { startConversationEvictor } from "./conversation-store.js";
97
97
  import { writePid } from "./daemon-control.js";
98
+ import { setDbReady, setStartupComplete } from "./daemon-readiness.js";
98
99
  import {
99
100
  evaluateDiskPressureNow,
100
101
  startDiskPressureGuard,
@@ -270,15 +271,35 @@ export async function runDaemon(): Promise<void> {
270
271
  // depend on DB migrations having run (e.g. the inline-attachment-to-disk
271
272
  // backfill that populates attachment filePaths).
272
273
  //
273
- // If DB initialization fails (e.g. a migration error), the daemon
274
- // continues in a degraded state DB-dependent features won't work but
275
- // the HTTP server and config-based subsystems still start so the process
276
- // remains reachable for health checks and diagnostics.
274
+ // The daemon continues in a degraded state on either DB failure mode:
275
+ // (a) initializeDb() throws (e.g. the DB can't be opened), or (b) the DB
276
+ // opens but one or more migrations failed (initializeDb resolves with
277
+ // migrationsOk:false rather than throwing, per the daemon-never-blocks
278
+ // philosophy). In both cases DB-dependent features won't work, but the
279
+ // HTTP server and config-based subsystems still start so the process
280
+ // remains reachable for diagnostics. The trivial /healthz and detailed
281
+ // /v1/health(z) endpoints still answer 200, but setDbReady(true) runs only
282
+ // when migrations all applied, so the readiness latch stays unset and
283
+ // /readyz reports not-ready (503) for the rest of the process lifetime.
284
+ // That 503 is intentional: a DB-broken daemon should fail readiness so it
285
+ // is not routed user traffic.
286
+ //
287
+ // The local `dbReady` and the module-level readiness latch intentionally
288
+ // diverge on the migration-failure path: `dbReady` stays true to allow the
289
+ // downstream best-effort seeding / workspace migrations, while the latch
290
+ // stays unset to keep /readyz 503.
277
291
  let dbReady = false;
278
292
  try {
279
- await initializeDb();
293
+ const { migrationsOk } = await initializeDb();
280
294
  dbReady = true;
281
- log.info("Daemon startup: DB initialized");
295
+ if (migrationsOk) {
296
+ setDbReady(true);
297
+ log.info("Daemon startup: DB initialized");
298
+ } else {
299
+ log.error(
300
+ "Daemon startup: DB opened but one or more migrations failed — /readyz will remain unready (degraded mode)",
301
+ );
302
+ }
282
303
  } catch (err) {
283
304
  log.error(
284
305
  { err },
@@ -1166,6 +1187,12 @@ export async function runDaemon(): Promise<void> {
1166
1187
 
1167
1188
  installShutdownHandlers({ server });
1168
1189
 
1190
+ // The critical startup await-chain has completed and the daemon can serve
1191
+ // requests, so latch readiness before logging "Daemon started". Any fatal
1192
+ // failure earlier in startup propagates out of runDaemon before this line,
1193
+ // so the latch is never set on a failed start.
1194
+ setStartupComplete();
1195
+
1169
1196
  log.info(
1170
1197
  {
1171
1198
  durationMs: Date.now() - startupStartedAt,
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tests for initializeDb()'s readiness-gating return contract.
3
+ *
4
+ * The daemon's readiness latch (setDbReady) is gated on initializeDb resolving
5
+ * with migrationsOk:true, so a clean init must report all migrations applied.
6
+ * Migration-step failures are caught-and-logged inside the runner (not thrown),
7
+ * so the boolean is the only signal lifecycle has to keep /readyz unready on a
8
+ * partially-migrated schema — see assistant/src/daemon/lifecycle.ts.
9
+ */
10
+
11
+ import { describe, expect, test } from "bun:test";
12
+
13
+ import { initializeDb } from "../db-init.js";
14
+
15
+ describe("initializeDb — migrationsOk return contract", () => {
16
+ test("resolves to { migrationsOk: true } on a clean init", async () => {
17
+ const result = await initializeDb();
18
+ expect(result).toEqual({ migrationsOk: true });
19
+ });
20
+ });
@@ -215,9 +215,10 @@ export async function checkpointWalBeforeOpen(): Promise<void> {
215
215
 
216
216
  // ---------------------------------------------------------------------------
217
217
 
218
- export async function initializeDb(): Promise<void> {
218
+ export async function initializeDb(): Promise<{ migrationsOk: boolean }> {
219
219
  if (process.env.BUN_TEST === "1" && tryRestoreTemplate()) {
220
- return;
220
+ // A restored template is fully migrated.
221
+ return { migrationsOk: true };
221
222
  }
222
223
 
223
224
  // Fold any post-crash WAL back into the database off the main event loop
@@ -260,13 +261,22 @@ export async function initializeDb(): Promise<void> {
260
261
  );
261
262
  }
262
263
 
264
+ // A passing post-run validation is part of readiness: validateMigrationState
265
+ // flags schema inconsistencies (e.g. a completed step missing a declared
266
+ // dependsOn checkpoint) that no individual step body surfaces as a failure.
267
+ let validationOk = true;
263
268
  try {
264
269
  validateMigrationState(database, migrationSteps);
265
270
  } catch (err) {
271
+ validationOk = false;
266
272
  log.error({ err }, "validateMigrationState failed");
267
273
  }
268
274
 
269
275
  if (process.env.BUN_TEST === "1") {
270
276
  saveTemplate();
271
277
  }
278
+
279
+ // migrationsOk reflects BOTH no failed migration steps AND a passing
280
+ // post-run validation, so an inconsistent schema keeps /readyz at 503.
281
+ return { migrationsOk: failed.length === 0 && validationOk };
272
282
  }
@@ -8,6 +8,7 @@ import { availableParallelism, cpus, totalmem } from "node:os";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
11
+ import { isDbReady, isStartupComplete } from "../../daemon/daemon-readiness.js";
11
12
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
12
13
  import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
13
14
  import { getMaxRollbackVersion } from "../../persistence/migrations/run-migrations.js";
@@ -17,7 +18,6 @@ import {
17
18
  getDiskUsageInfo,
18
19
  parseK8sMemoryBytes,
19
20
  } from "../../util/disk-usage.js";
20
- import { getLogger } from "../../util/logger.js";
21
21
  import { getWorkspacePromptPath } from "../../util/platform.js";
22
22
  import { APP_VERSION } from "../../version.js";
23
23
  import { resolveHatchedAtReadOnly } from "../../workspace/hatched-date.js";
@@ -314,8 +314,16 @@ function getCpuInfo(): CpuInfo {
314
314
  };
315
315
  }
316
316
 
317
+ /**
318
+ * Trivial liveness/startup probe (`GET /healthz`).
319
+ *
320
+ * This is the k8s startup + liveness probe target: it must answer the instant
321
+ * the HTTP server is up and must NEVER touch DB, CES, migrations, or any other
322
+ * lifecycle state. Keep it to a static `{ status, version }` payload — no
323
+ * syscalls, no disk/memory/cpu reads, no async work.
324
+ */
317
325
  export function handleHealth(): Response {
318
- return Response.json({ status: "ok" });
326
+ return Response.json({ status: "ok", version: APP_VERSION });
319
327
  }
320
328
 
321
329
  function getDetailedHealth() {
@@ -354,17 +362,28 @@ export function handleDetailedHealth(): Response {
354
362
  return Response.json(getDetailedHealth());
355
363
  }
356
364
 
365
+ /**
366
+ * Strict readiness probe (`GET /readyz`).
367
+ *
368
+ * Reports whether the daemon can actually serve requests, gating on two
369
+ * monotonic startup latches read SYNCHRONOUSLY (no `await`, DB query, or
370
+ * subprocess) so the probe stays cheap on a ~10s interval. Returns 503 until
371
+ * both latches are set, then a stable 200 for the rest of the process lifetime.
372
+ *
373
+ * CES is intentionally never consulted: it is informational only. Gating on it
374
+ * would leave a pod permanently NotReady whenever CES never handshakes, even
375
+ * though the daemon serves degraded with an encrypted-store fallback.
376
+ */
357
377
  export function handleReadyz(): Response {
358
- const cesClient = getCesClient();
359
- if (!cesClient?.isReady()) {
360
- // TODO: Return 503 once we confirm via logs that this won't cause
361
- // regressions in the K8s readinessProbe.
362
- getLogger("health").warn(
363
- { reason: cesClient ? "ces_not_ready" : "ces_unavailable" },
364
- "CES not ready pod would be unready if 503 were enabled",
365
- );
366
- }
367
- return Response.json({ status: "ok" });
378
+ const notReady: string[] = [];
379
+ if (!isStartupComplete()) notReady.push("startup");
380
+ if (!isDbReady()) notReady.push("db");
381
+ const ready = notReady.length === 0;
382
+
383
+ return Response.json(
384
+ { status: ready ? "ok" : "unready", ready, notReady },
385
+ { status: ready ? 200 : 503 },
386
+ );
368
387
  }
369
388
 
370
389
  function getIdentity() {