@vellumai/assistant 0.11.10-dev.202609091820.7951ec9 → 0.11.10-dev.202609092016.c7aa45c

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 (54) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +9 -4
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +5 -3
  4. package/openapi.yaml +216 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/config-schema.test.ts +26 -0
  7. package/src/__tests__/db-readiness-http-gate.test.ts +43 -0
  8. package/src/__tests__/error-handler-friendly-messages.test.ts +15 -0
  9. package/src/__tests__/guardian-gateway-sim.ts +5 -0
  10. package/src/__tests__/helpers/gateway-guardian-requests-store-bridge.ts +3 -2
  11. package/src/__tests__/identity-routes.test.ts +6 -1
  12. package/src/__tests__/tool-grant-request-escalation.test.ts +43 -0
  13. package/src/__tests__/workspace-git-service.test.ts +170 -1
  14. package/src/__tests__/workspace-heartbeat-service.test.ts +41 -0
  15. package/src/config/bundled-skills/conversation-groups/SKILL.md +1 -0
  16. package/src/config/bundled-skills/schedule/SKILL.md +10 -0
  17. package/src/config/schemas/workspace-git.ts +10 -0
  18. package/src/conversations/message-consolidation.ts +4 -0
  19. package/src/daemon/__tests__/daemon-readiness.test.ts +32 -0
  20. package/src/daemon/daemon-readiness.ts +34 -4
  21. package/src/daemon/lifecycle.ts +12 -5
  22. package/src/daemon/shutdown-handlers.ts +2 -0
  23. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +17 -2
  24. package/src/ipc/__tests__/gateway-ipc-errors.test.ts +51 -0
  25. package/src/ipc/__tests__/gateway-validated-call.test.ts +5 -6
  26. package/src/ipc/assistant-server.ts +9 -16
  27. package/src/ipc/gateway-client.test.ts +6 -2
  28. package/src/ipc/gateway-client.ts +53 -14
  29. package/src/ipc/gateway-ipc-errors.ts +16 -4
  30. package/src/ipc/gateway-validated-call.ts +5 -12
  31. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +57 -0
  32. package/src/monitoring/worker.ts +18 -0
  33. package/src/monitoring/workspace-git-heartbeat.ts +45 -0
  34. package/src/notifications/__tests__/notification-utils.test.ts +13 -0
  35. package/src/notifications/__tests__/schedule-result-producer.test.ts +15 -0
  36. package/src/notifications/notification-utils.ts +15 -0
  37. package/src/notifications/schedule-result-producer.ts +2 -1
  38. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +14 -3
  39. package/src/persistence/conversation-crud.ts +201 -0
  40. package/src/persistence/db-init.ts +23 -7
  41. package/src/persistence/migrations/__tests__/run-migrations.test.ts +83 -8
  42. package/src/persistence/migrations/run-migrations.ts +101 -6
  43. package/src/runtime/access-request-helper.ts +5 -1
  44. package/src/runtime/middleware/error-handler.ts +14 -10
  45. package/src/runtime/routes/attachment-list-route.test.ts +961 -0
  46. package/src/runtime/routes/attachment-routes.ts +128 -0
  47. package/src/runtime/routes/debug-routes.ts +64 -0
  48. package/src/runtime/routes/gateway-log-routes.ts +1 -8
  49. package/src/runtime/routes/http-adapter.ts +8 -15
  50. package/src/runtime/routes/identity-routes.ts +13 -0
  51. package/src/runtime/routes/trust-rules-routes.ts +1 -8
  52. package/src/runtime/tool-grant-request-helper.ts +6 -2
  53. package/src/workspace/git-service.ts +217 -57
  54. package/src/workspace/heartbeat-service.ts +34 -9
package/AGENTS.md CHANGED
@@ -30,7 +30,7 @@ DB migrations run asynchronously during startup: the HTTP server binds (so `/hea
30
30
 
31
31
  Existing enforcement, which new code must not bypass:
32
32
 
33
- - **HTTP** requests are gated per-route in `runtime/http-server.ts`; **IPC** methods in `ipc/assistant-server.ts`; both derive their exempt set from `DB_MIGRATION_READINESS_EXEMPT_OPERATIONS` in `daemon-readiness.ts` (health/liveness probes only — anything exempted must never touch the DB).
33
+ - **HTTP** requests are gated per-route in `runtime/http-server.ts`; **IPC** methods in `ipc/assistant-server.ts`; both derive their exempt set from `DB_MIGRATION_READINESS_EXEMPT_OPERATIONS` in `daemon-readiness.ts` (health/liveness probes plus in-memory diagnostics such as `debug/database` — anything exempted must never touch the DB).
34
34
  - **Message sinks** (`processMessage`, `processMessageInBackground`) guard via `assertDbMigrationsReadyForTurn()`.
35
35
  - **Background sweeps** are started by lifecycle only after migrations settle (`startRuntimeHttpServerBackgroundSweeps`).
36
36
  - The **migration-repair surface** (`admin/rollback-migrations` plus all `migrations/import*` / preflight transports and their job-status route) is additionally allowed in the terminal `failed` state only — see `DB_MIGRATION_FAILED_STATE_EXEMPT_OPERATIONS`. Never widen this to the `running` state: a rollback or import would race the in-flight migration runner. A successful repair does not clear the failed latch — the daemon must be restarted to re-run migrations and become ready.
@@ -254,10 +254,15 @@ export type GuardianRequestMutationIpcResponse = z.infer<
254
254
 
255
255
  /**
256
256
  * Request for `guardian_requests_create`. `id` is REQUIRED and
257
- * caller-supplied — request ids are load-bearing (deterministic
258
- * access-request ids; `tool_approval` rows reuse the pending-interaction
259
- * requestId as PK). `requestCode` is generated gateway-side when omitted.
260
- * No `sourceType`: the gateway derives it from `sourceChannel`.
257
+ * caller-supplied. The interaction-promotion paths (`tool_approval`, and
258
+ * `pending_question` for `ask_question`) reuse the pending-interaction
259
+ * requestId as PK so a decision can find its interaction; every other
260
+ * create mints a UUID, including voice `pending_question` rows, which carry
261
+ * their interaction id in `pendingQuestionId` instead. The id is the row's
262
+ * primary key and the insert is strict, so a caller that can create
263
+ * concurrently must mint one unique per call. `requestCode` is generated
264
+ * gateway-side when omitted. No `sourceType`: the gateway derives it from
265
+ * `sourceChannel`.
261
266
  */
262
267
  export const CreateGuardianRequestIpcParamsSchema = z.object({
263
268
  id: z.string().min(1),
@@ -340,7 +340,8 @@ type PendingRequest = {
340
340
  export type PersistentIpcClientOptions = {
341
341
  /**
342
342
  * Additional delays (ms) before each reconnect attempt after a retryable
343
- * connect failure. Pass `[]` to fail on the first miss (tests).
343
+ * connect failure. Pass `[]` to fail on the first miss (tests and callers
344
+ * that already own a retry budget, such as classify_risk).
344
345
  */
345
346
  connectRetryBackoffsMs?: readonly number[];
346
347
  };
@@ -350,8 +351,9 @@ export type PersistentIpcClientOptions = {
350
351
  * reconnection on failure. Multiplexes requests by ID so many concurrent
351
352
  * callers can share one socket.
352
353
  *
353
- * Designed for hot-path calls (e.g. classify_risk) where connecting per call
354
- * adds unacceptable overhead.
354
+ * Designed for multiplexed calls over one socket. Control-plane callers
355
+ * typically keep the default connect retries for sibling boot races.
356
+ * Callers that already own a retry budget pass `connectRetryBackoffsMs: []`.
355
357
  */
356
358
  export class PersistentIpcClient {
357
359
  private socket: Socket | null = null;
package/openapi.yaml CHANGED
@@ -1713,6 +1713,99 @@ paths:
1713
1713
  responses:
1714
1714
  "204":
1715
1715
  description: Successful response
1716
+ get:
1717
+ operationId: attachments_get
1718
+ summary: List conversation attachments
1719
+ description:
1720
+ Return metadata for every attachment linked to a conversation's messages, newest first, across fork
1721
+ lineage. Camera frames kept by live vision are flagged. Bytes come from GET /attachments/{id}/content.
1722
+ tags:
1723
+ - attachments
1724
+ parameters:
1725
+ - name: conversationId
1726
+ in: query
1727
+ required: true
1728
+ schema:
1729
+ type: string
1730
+ description: Conversation whose attachments to list
1731
+ - name: sightFrames
1732
+ in: query
1733
+ required: false
1734
+ schema:
1735
+ type: string
1736
+ enum:
1737
+ - only
1738
+ - exclude
1739
+ description: Restrict to camera frames, or leave them out
1740
+ - name: limit
1741
+ in: query
1742
+ required: false
1743
+ schema:
1744
+ type: integer
1745
+ description: Page size, default 200, max 1000
1746
+ - name: offset
1747
+ in: query
1748
+ required: false
1749
+ schema:
1750
+ type: integer
1751
+ responses:
1752
+ "200":
1753
+ description: Successful response
1754
+ content:
1755
+ application/json:
1756
+ schema:
1757
+ type: object
1758
+ properties:
1759
+ attachments:
1760
+ type: array
1761
+ items:
1762
+ type: object
1763
+ properties:
1764
+ id:
1765
+ type: string
1766
+ filename:
1767
+ type: string
1768
+ mimeType:
1769
+ type: string
1770
+ sizeBytes:
1771
+ type: number
1772
+ kind:
1773
+ type: string
1774
+ messageId:
1775
+ type: string
1776
+ createdAt:
1777
+ type: number
1778
+ description: "Capture time: the carrying message's created_at"
1779
+ fileBacked:
1780
+ type: boolean
1781
+ thumbnailData:
1782
+ type: string
1783
+ sightFrame:
1784
+ type: boolean
1785
+ ambientKeep:
1786
+ type: boolean
1787
+ required:
1788
+ - id
1789
+ - filename
1790
+ - mimeType
1791
+ - sizeBytes
1792
+ - kind
1793
+ - messageId
1794
+ - createdAt
1795
+ - sightFrame
1796
+ - ambientKeep
1797
+ additionalProperties: false
1798
+ total:
1799
+ type: number
1800
+ hasMore:
1801
+ type: boolean
1802
+ required:
1803
+ - attachments
1804
+ - total
1805
+ - hasMore
1806
+ additionalProperties: false
1807
+ "400":
1808
+ description: Missing or invalid query
1716
1809
  post:
1717
1810
  operationId: attachments_post
1718
1811
  summary: Upload attachment
@@ -11081,6 +11174,71 @@ paths:
11081
11174
  - exitCode
11082
11175
  - timedOut
11083
11176
  additionalProperties: false
11177
+ /v1/debug/database:
11178
+ get:
11179
+ operationId: debug_database_get
11180
+ summary: Database migration diagnostics
11181
+ description:
11182
+ Return the in-memory DB migration latch, including failed and deferred steps. Does not query SQLite, so it
11183
+ stays answerable while migrations are running or have failed.
11184
+ tags:
11185
+ - debug
11186
+ responses:
11187
+ "200":
11188
+ description: Successful response
11189
+ content:
11190
+ application/json:
11191
+ schema:
11192
+ type: object
11193
+ properties:
11194
+ ready:
11195
+ type: boolean
11196
+ state:
11197
+ type: string
11198
+ enum:
11199
+ - not_started
11200
+ - running
11201
+ - failed
11202
+ - ready
11203
+ reason:
11204
+ type: string
11205
+ error:
11206
+ type: string
11207
+ failed:
11208
+ type: array
11209
+ items:
11210
+ type: object
11211
+ properties:
11212
+ name:
11213
+ type: string
11214
+ error:
11215
+ type: string
11216
+ required:
11217
+ - name
11218
+ additionalProperties: false
11219
+ deferred:
11220
+ type: array
11221
+ items:
11222
+ type: object
11223
+ properties:
11224
+ name:
11225
+ type: string
11226
+ missing:
11227
+ type: array
11228
+ items:
11229
+ type: string
11230
+ required:
11231
+ - name
11232
+ - missing
11233
+ additionalProperties: false
11234
+ validationError:
11235
+ type: string
11236
+ required:
11237
+ - ready
11238
+ - state
11239
+ - failed
11240
+ - deferred
11241
+ additionalProperties: false
11084
11242
  /v1/defer/cancel:
11085
11243
  post:
11086
11244
  operationId: defer_cancel_post
@@ -13442,6 +13600,35 @@ paths:
13442
13600
  type: string
13443
13601
  error:
13444
13602
  type: string
13603
+ failedMigrations:
13604
+ type: array
13605
+ items:
13606
+ type: object
13607
+ properties:
13608
+ name:
13609
+ type: string
13610
+ error:
13611
+ type: string
13612
+ required:
13613
+ - name
13614
+ additionalProperties: false
13615
+ deferredMigrations:
13616
+ type: array
13617
+ items:
13618
+ type: object
13619
+ properties:
13620
+ name:
13621
+ type: string
13622
+ missing:
13623
+ type: array
13624
+ items:
13625
+ type: string
13626
+ required:
13627
+ - name
13628
+ - missing
13629
+ additionalProperties: false
13630
+ validationError:
13631
+ type: string
13445
13632
  required:
13446
13633
  - ready
13447
13634
  - state
@@ -13647,6 +13834,35 @@ paths:
13647
13834
  type: string
13648
13835
  error:
13649
13836
  type: string
13837
+ failedMigrations:
13838
+ type: array
13839
+ items:
13840
+ type: object
13841
+ properties:
13842
+ name:
13843
+ type: string
13844
+ error:
13845
+ type: string
13846
+ required:
13847
+ - name
13848
+ additionalProperties: false
13849
+ deferredMigrations:
13850
+ type: array
13851
+ items:
13852
+ type: object
13853
+ properties:
13854
+ name:
13855
+ type: string
13856
+ missing:
13857
+ type: array
13858
+ items:
13859
+ type: string
13860
+ required:
13861
+ - name
13862
+ - missing
13863
+ additionalProperties: false
13864
+ validationError:
13865
+ type: string
13650
13866
  required:
13651
13867
  - ready
13652
13868
  - state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.10-dev.202609091820.7951ec9",
3
+ "version": "0.11.10-dev.202609092016.c7aa45c",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -757,6 +757,7 @@ describe("AssistantConfigSchema", () => {
757
757
  maxFileSizeBytes: 256000,
758
758
  historyCompaction: { enabled: true },
759
759
  interactiveGitTimeoutMs: 10000,
760
+ stageBatchSize: 1000,
760
761
  enrichmentQueueSize: 50,
761
762
  enrichmentConcurrency: 1,
762
763
  enrichmentJobTimeoutMs: 30000,
@@ -811,6 +812,31 @@ describe("AssistantConfigSchema", () => {
811
812
  expect(result.success).toBe(false);
812
813
  });
813
814
 
815
+ test("applies workspaceGit.stageBatchSize default", () => {
816
+ expect(AssistantConfigSchema.parse({}).workspaceGit.stageBatchSize).toBe(
817
+ 1000,
818
+ );
819
+ });
820
+
821
+ test("accepts custom workspaceGit.stageBatchSize", () => {
822
+ const result = AssistantConfigSchema.parse({
823
+ workspaceGit: { stageBatchSize: 250 },
824
+ });
825
+ expect(result.workspaceGit.stageBatchSize).toBe(250);
826
+ });
827
+
828
+ test("rejects non-positive workspaceGit.stageBatchSize", () => {
829
+ const zeroResult = AssistantConfigSchema.safeParse({
830
+ workspaceGit: { stageBatchSize: 0 },
831
+ });
832
+ expect(zeroResult.success).toBe(false);
833
+
834
+ const negativeResult = AssistantConfigSchema.safeParse({
835
+ workspaceGit: { stageBatchSize: -1 },
836
+ });
837
+ expect(negativeResult.success).toBe(false);
838
+ });
839
+
814
840
  test("applies sight defaults", () => {
815
841
  expect(AssistantConfigSchema.parse({}).sight).toEqual({
816
842
  keepLatestFrames: 2,
@@ -123,6 +123,49 @@ describe("DB migration readiness HTTP gate", () => {
123
123
  expect(body.reason).toBe("db_migrations_failed");
124
124
  });
125
125
 
126
+ test("continues serving debug/database while migrations are running", async () => {
127
+ setDbMigrating();
128
+ await startServer();
129
+
130
+ const response = await fetch(url("/debug/database"));
131
+ expect(response.status).toBe(200);
132
+
133
+ const body = (await response.json()) as Record<string, unknown>;
134
+ expect(body.ready).toBe(false);
135
+ expect(body.state).toBe("running");
136
+ expect(body.reason).toBe("db_migrations_running");
137
+ expect(body.failed).toEqual([]);
138
+ expect(body.deferred).toEqual([]);
139
+ });
140
+
141
+ test("continues serving debug/database when migrations have failed", async () => {
142
+ setDbMigrationFailed(new Error("boom"), {
143
+ failedMigrations: [{ name: "flakyStep", error: "transient failure" }],
144
+ deferredMigrations: [{ name: "dependentStep", missing: ["flakyStep"] }],
145
+ validationError: "schema mismatch",
146
+ });
147
+ await startServer();
148
+
149
+ const response = await fetch(url("/debug/database"));
150
+ expect(response.status).toBe(200);
151
+
152
+ const body = (await response.json()) as Record<string, unknown>;
153
+ expect(body.ready).toBe(false);
154
+ expect(body.state).toBe("failed");
155
+ expect(body.reason).toBe("db_migrations_failed");
156
+ expect(body.error).toBe("boom");
157
+ expect(body.failed).toEqual([
158
+ { name: "flakyStep", error: "transient failure" },
159
+ ]);
160
+ expect(body.deferred).toEqual([
161
+ { name: "dependentStep", missing: ["flakyStep"] },
162
+ ]);
163
+ expect(body.validationError).toBe("schema mismatch");
164
+
165
+ const conversations = await fetch(url("/conversations"));
166
+ expect(conversations.status).toBe(503);
167
+ });
168
+
126
169
  test("blocks config schema while migrations are running", async () => {
127
170
  setDbMigrating();
128
171
  await startServer();
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { IpcConnectError } from "@vellumai/gateway-client/ipc-client";
4
+
3
5
  import { withErrorHandling } from "../runtime/middleware/error-handler.js";
4
6
  import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
5
7
 
@@ -42,4 +44,17 @@ describe("withErrorHandling – friendly error messages", () => {
42
44
  };
43
45
  expect(body.error.message).toBe("Twilio phone number not configured.");
44
46
  });
47
+
48
+ test("IpcConnectError surfaces as 503 SERVICE_UNAVAILABLE", async () => {
49
+ const response = await withErrorHandling("test", async () => {
50
+ throw new IpcConnectError("connect ENOENT", "ENOENT");
51
+ });
52
+
53
+ expect(response.status).toBe(503);
54
+ const body = (await response.json()) as {
55
+ error: { code: string; message: string };
56
+ };
57
+ expect(body.error.code).toBe("SERVICE_UNAVAILABLE");
58
+ expect(body.error.message).toContain("Gateway is not reachable over IPC");
59
+ });
45
60
  });
@@ -564,6 +564,11 @@ export function createGuardianGatewaySim() {
564
564
  async function createGuardianRequest(
565
565
  params: SeedRequestParams & { id: string; guardianPrincipalId: string },
566
566
  ): Promise<SimGuardianRequest> {
567
+ // The gateway insert is strict on the primary key; a silent overwrite
568
+ // here would hide a caller minting the same id twice.
569
+ if (requests.has(params.id)) {
570
+ throw new Error("UNIQUE constraint failed: guardian_requests.id");
571
+ }
567
572
  return seedRequest(params);
568
573
  }
569
574
 
@@ -49,8 +49,9 @@ async function createGuardianRequest(
49
49
  params: CreateGuardianRequestIpcParams,
50
50
  ): Promise<SimGuardianRequest> {
51
51
  // The schema requires `guardianPrincipalId`, mirroring the gateway store's
52
- // integrity guard (every contract kind is decisionable).
53
- return bridgeState.seedRequest(
52
+ // integrity guard (every contract kind is decisionable). The sim's create
53
+ // then rejects a duplicate id, mirroring the store's strict insert.
54
+ return bridgeState.module.createGuardianRequest(
54
55
  CreateGuardianRequestIpcParamsSchema.parse(params),
55
56
  );
56
57
  }
@@ -483,13 +483,18 @@ describe("identity routes — /readyz readiness gate", () => {
483
483
  });
484
484
 
485
485
  test("returns 503 when DB migrations fail", async () => {
486
- setDbMigrationFailed(new Error("migration failed"));
486
+ setDbMigrationFailed(new Error("migration failed"), {
487
+ failedMigrations: [{ name: "flakyStep", error: "transient failure" }],
488
+ });
487
489
  const res = handleReadyz();
488
490
  expect(res.status).toBe(503);
489
491
  const body = (await res.json()) as Record<string, unknown>;
490
492
  expect(body.ready).toBe(false);
491
493
  expect(body.reason).toBe("db_migrations_failed");
492
494
  expect((body.dbMigrations as Record<string, unknown>).state).toBe("failed");
495
+ expect((body.dbMigrations as Record<string, unknown>).failedMigrations).toEqual(
496
+ [{ name: "flakyStep", error: "transient failure" }],
497
+ );
493
498
  });
494
499
 
495
500
  test("returns 200 even if CES is down", async () => {
@@ -282,6 +282,49 @@ describe("ToolApprovalHandler / grant-miss escalation", () => {
282
282
  });
283
283
  expect(requests.length).toBe(0);
284
284
  });
285
+
286
+ test("sibling tool calls escalating concurrently each get their own request", async () => {
287
+ // The agent loop runs a turn's tool_use blocks under Promise.all, so two
288
+ // grant-gated calls reach the gateway create in the same tick. Each is a
289
+ // distinct invocation (different input digest), so each needs its own
290
+ // row; the create must never collide on the primary key.
291
+ const context = makeContext({ trustClass: "trusted_contact" });
292
+ const results = await Promise.all([
293
+ handler.checkPreExecutionGates(
294
+ "bash",
295
+ { command: "ls -la" },
296
+ context,
297
+ "high",
298
+ Date.now(),
299
+ ),
300
+ handler.checkPreExecutionGates(
301
+ "bash",
302
+ { command: "cat README.md" },
303
+ context,
304
+ "high",
305
+ Date.now(),
306
+ ),
307
+ ]);
308
+
309
+ // Both escalate and time out on the short inline wait: a denial for each,
310
+ // never a thrown persistence error that would fail the whole turn.
311
+ for (const result of results) {
312
+ expect(result.allowed).toBe(false);
313
+ if (result.allowed) {
314
+ return;
315
+ }
316
+ expect(result.result.content).toContain("guardian approval");
317
+ }
318
+
319
+ const requests = await sim.module.listGuardianRequestsOrEmpty({
320
+ kind: "tool_grant_request",
321
+ status: "pending",
322
+ });
323
+ expect(requests.length).toBe(2);
324
+ expect(new Set(requests.map((r) => r.id)).size).toBe(2);
325
+ expect(new Set(requests.map((r) => r.inputDigest)).size).toBe(2);
326
+ expect(emittedSignals.length).toBe(2);
327
+ });
285
328
  });
286
329
 
287
330
  // ---------------------------------------------------------------------------