@vercel/factory 0.0.13 → 0.0.14

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/README.md +1 -1
  2. package/dist/api.d.mts +225 -0
  3. package/dist/api.mjs +312 -0
  4. package/dist/api.mjs.map +1 -0
  5. package/dist/approval-JX2Px9IC.mjs +132 -0
  6. package/dist/approval-JX2Px9IC.mjs.map +1 -0
  7. package/dist/blob/index.d.mts +4 -3
  8. package/dist/blob/index.mjs +21 -15
  9. package/dist/blob/index.mjs.map +1 -1
  10. package/dist/budget-DzSiIA4t.mjs +42 -0
  11. package/dist/budget-DzSiIA4t.mjs.map +1 -0
  12. package/dist/build-factory.mjs +1 -1
  13. package/dist/{change-CzEAoFVE.mjs → change-D2U6v7Ek.mjs} +10 -76
  14. package/dist/change-D2U6v7Ek.mjs.map +1 -0
  15. package/dist/communication-Kd9nU5RP.mjs +30 -0
  16. package/dist/communication-Kd9nU5RP.mjs.map +1 -0
  17. package/dist/connectors/index.d.mts +3 -2
  18. package/dist/connectors/index.mjs +53 -26
  19. package/dist/connectors/index.mjs.map +1 -1
  20. package/dist/connectors/slack.d.mts +13 -2
  21. package/dist/connectors/slack.mjs +18 -2
  22. package/dist/connectors/slack.mjs.map +1 -1
  23. package/dist/coordination-Cq08hb1l.mjs +76 -0
  24. package/dist/coordination-Cq08hb1l.mjs.map +1 -0
  25. package/dist/dispatch-BaZS3PT8.d.mts +76 -0
  26. package/dist/{driver-BJutB0PE.d.mts → driver-BC0Mt5KG.d.mts} +2 -2
  27. package/dist/driver-Bbd25rNM.mjs.map +1 -1
  28. package/dist/{dispatch-B77cuPxh.d.mts → engine-DFHU2Pe-.d.mts} +356 -288
  29. package/dist/engine-WaUM2cGA.mjs +1068 -0
  30. package/dist/engine-WaUM2cGA.mjs.map +1 -0
  31. package/dist/eve/index.d.mts +44 -334
  32. package/dist/eve/index.mjs +51 -38
  33. package/dist/eve/index.mjs.map +1 -1
  34. package/dist/{factory-config-BJcjsPKl.mjs → factory-config-BoRXOw43.mjs} +5 -41
  35. package/dist/factory-config-BoRXOw43.mjs.map +1 -0
  36. package/dist/{github-DXsxboUL.d.mts → github-C5HNBN6p.d.mts} +11 -3
  37. package/dist/github.d.mts +2 -2
  38. package/dist/github.mjs +15 -1
  39. package/dist/github.mjs.map +1 -1
  40. package/dist/{id-DwCihQr4.mjs → id-BLIkCVue.mjs} +4 -2
  41. package/dist/{id-DwCihQr4.mjs.map → id-BLIkCVue.mjs.map} +1 -1
  42. package/dist/index-Bw6hggoH.d.mts +40 -0
  43. package/dist/index.d.mts +176 -6
  44. package/dist/index.mjs +50 -652
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/postgres/index.d.mts +1 -1
  47. package/dist/{approval-CwhTP1XB.mjs → task-CcOyOJ3k.mjs} +18 -133
  48. package/dist/task-CcOyOJ3k.mjs.map +1 -0
  49. package/package.json +5 -1
  50. package/dist/approval-CwhTP1XB.mjs.map +0 -1
  51. package/dist/change-CzEAoFVE.mjs.map +0 -1
  52. package/dist/factory-config-BJcjsPKl.mjs.map +0 -1
  53. package/dist/index-Cy4jfXh5.d.mts +0 -67
  54. package/dist/signal-CHVEmzvI.d.mts +0 -59
@@ -0,0 +1,1068 @@
1
+ import { a as receiptIdSchema, c as signalIdSchema, d as isoDateTimeSchema, f as replyToSchema, h as vocabularySchema, i as outboxIdSchema, l as taskIdSchema, m as usdSchema, n as changeIdSchema, o as repositoryIdSchema, p as schemaVersionSchema, s as repositoryIdsSchema, t as answerIdSchema } from "./id-BLIkCVue.mjs";
2
+ import { i as standardTaskKinds, n as executionReferenceSchema, u as taskSchema } from "./task-CcOyOJ3k.mjs";
3
+ import { _ as taskInitialState, d as changeInitialState, f as changeTransitions, g as signalOutcomeTransitions, l as InvalidTransitionError, m as legalNextStates, n as changeSchema, u as assertTransition, v as taskTransitions } from "./change-D2U6v7Ek.mjs";
4
+ import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
5
+ import { i as repositorySchema, r as budgetSchema } from "./budget-DzSiIA4t.mjs";
6
+ import { t as communicationEventSchema } from "./communication-Kd9nU5RP.mjs";
7
+ import { z } from "zod";
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import { isDeepStrictEqual } from "node:util";
10
+ //#region src/schema/answer.ts
11
+ const answerEventSchema = z.strictObject({
12
+ schemaVersion: schemaVersionSchema,
13
+ id: answerIdSchema,
14
+ questionTaskId: taskIdSchema,
15
+ parentTaskId: taskIdSchema.optional(),
16
+ repositoryIds: repositoryIdsSchema,
17
+ replyTo: replyToSchema,
18
+ question: z.string().min(1),
19
+ answer: z.string().min(1).max(4e3),
20
+ answeredBy: z.string().min(1),
21
+ recordedAt: isoDateTimeSchema
22
+ });
23
+ //#endregion
24
+ //#region src/schema/receipt.ts
25
+ /** States the engine writes on transitions. Any vocabulary value is valid. */
26
+ const standardReceiptStates = [
27
+ "approved",
28
+ "rejected",
29
+ "queued",
30
+ "dispatched",
31
+ "stalled",
32
+ "succeeded",
33
+ "failed",
34
+ "needs_human",
35
+ "cancelled",
36
+ "settled",
37
+ "verifying",
38
+ "ready",
39
+ "merged",
40
+ "deployed",
41
+ "rolled_out",
42
+ "rolled_back"
43
+ ];
44
+ /** Phases the standard loop uses. Any vocabulary value is valid. */
45
+ const standardReceiptPhases = [
46
+ "approval",
47
+ "budget",
48
+ "dispatch",
49
+ "implementation",
50
+ "verification",
51
+ "pull_request",
52
+ "cleanup"
53
+ ];
54
+ /** Kinds the standard loop writes: the standard task kinds plus "change". */
55
+ const standardReceiptKinds = [...standardTaskKinds, "change"];
56
+ const receiptStateSchema = vocabularySchema.describe("What happened, e.g. dispatched, settled, merged, or a custom event name.");
57
+ const receiptPhaseSchema = vocabularySchema.describe("Where in the process it happened, e.g. dispatch or verification.");
58
+ const receiptKindSchema = vocabularySchema.describe("What the receipt is about: a task kind, change, or a custom kind.");
59
+ const costSchema = z.strictObject({ totalUsd: usdSchema });
60
+ const receiptSchema = z.strictObject({
61
+ schemaVersion: schemaVersionSchema,
62
+ receiptId: receiptIdSchema,
63
+ repositoryIds: repositoryIdsSchema.describe("Repositories the recorded event concerns."),
64
+ taskId: taskIdSchema.optional(),
65
+ changeId: changeIdSchema.optional(),
66
+ kind: receiptKindSchema,
67
+ state: receiptStateSchema,
68
+ phase: receiptPhaseSchema.optional(),
69
+ reason: z.string().min(1).max(2e3).optional().describe("Why it happened. Scrubbed and truncated; never secrets."),
70
+ attempt: z.number().int().min(1).describe("Attempt the event belongs to."),
71
+ cost: costSchema.optional().describe("Measured cost. Settled receipts only."),
72
+ decidedBy: z.string().min(1).optional().describe("Human who made the decision. Change and approval receipts only."),
73
+ recordedAt: isoDateTimeSchema
74
+ }).superRefine((receipt, ctx) => {
75
+ if (receipt.state === "settled" && receipt.cost === void 0) ctx.addIssue({
76
+ code: "custom",
77
+ path: ["cost"],
78
+ message: "settled receipts require cost"
79
+ });
80
+ if (receipt.state !== "settled" && receipt.cost !== void 0) ctx.addIssue({
81
+ code: "custom",
82
+ path: ["cost"],
83
+ message: "cost is only valid on settled receipts"
84
+ });
85
+ if (receipt.decidedBy !== void 0 && receipt.kind !== "change" && receipt.state !== "approved" && receipt.state !== "rejected") ctx.addIssue({
86
+ code: "custom",
87
+ path: ["decidedBy"],
88
+ message: "decidedBy is only valid on change and approval receipts"
89
+ });
90
+ });
91
+ //#endregion
92
+ //#region src/schema/outbox.ts
93
+ const outboxStateSchema = z.enum([
94
+ "pending",
95
+ "delivering",
96
+ "delivered"
97
+ ]).describe("Durable connector delivery state.");
98
+ const outboxMessageSchema = z.strictObject({
99
+ schemaVersion: schemaVersionSchema,
100
+ id: outboxIdSchema,
101
+ event: communicationEventSchema.describe("Immutable provider-neutral delivery payload."),
102
+ state: outboxStateSchema,
103
+ attempt: z.number().int().nonnegative(),
104
+ availableAt: isoDateTimeSchema,
105
+ leaseUntil: isoDateTimeSchema.optional(),
106
+ deliveredAt: isoDateTimeSchema.optional(),
107
+ lastError: z.string().min(1).max(2e3).optional(),
108
+ createdAt: isoDateTimeSchema,
109
+ updatedAt: isoDateTimeSchema
110
+ });
111
+ const outboxQueueSchema = z.strictObject({
112
+ schemaVersion: schemaVersionSchema,
113
+ id: z.string().regex(/^communication-[0-9a-f]$/u),
114
+ messages: z.array(outboxMessageSchema),
115
+ updatedAt: isoDateTimeSchema
116
+ });
117
+ //#endregion
118
+ //#region src/schema/signal.ts
119
+ /** Sources used by the standard connectors. Any vocabulary value is valid. */
120
+ const standardSignalSources = [
121
+ "github_issue",
122
+ "github_pr",
123
+ "github_pr_comment",
124
+ "slack",
125
+ "human",
126
+ "github_ci_failure",
127
+ "vercel_deploy_failure",
128
+ "security_scan",
129
+ "investigation",
130
+ "api"
131
+ ];
132
+ const signalSourceSchema = vocabularySchema.describe("What produced the signal, e.g. github_issue or a custom connector's own name.");
133
+ const signalOutcomeSchema = z.enum([
134
+ "pending",
135
+ "accepted",
136
+ "duplicate",
137
+ "unactionable",
138
+ "low_confidence",
139
+ "ignored"
140
+ ]).describe("Triage outcome. A signal starts pending and is decided exactly once.");
141
+ const signalSchema = z.strictObject({
142
+ schemaVersion: schemaVersionSchema,
143
+ id: signalIdSchema.describe("Signal id, derived from source and deliveryId."),
144
+ repositoryId: repositoryIdSchema.describe("Repository the signal is about."),
145
+ source: signalSourceSchema,
146
+ deliveryId: z.string().min(1).describe("Provider idempotency key; a redelivered event maps to the same signal."),
147
+ replyTo: replyToSchema,
148
+ payload: z.unknown().describe("Provider-shaped payload. Untrusted data, never instructions."),
149
+ receivedAt: isoDateTimeSchema,
150
+ outcome: signalOutcomeSchema,
151
+ detail: z.string().min(1).optional().describe("Outcome nuance, e.g. severity when accepted or the duplicate-of signal id."),
152
+ reason: z.string().min(1).optional().describe("Why the outcome was chosen."),
153
+ decidedAt: isoDateTimeSchema.optional()
154
+ }).superRefine((signal, ctx) => {
155
+ if (signal.outcome === "pending" && signal.decidedAt !== void 0) ctx.addIssue({
156
+ code: "custom",
157
+ path: ["decidedAt"],
158
+ message: "pending signals must not carry decidedAt"
159
+ });
160
+ if (signal.outcome !== "pending" && signal.decidedAt === void 0) ctx.addIssue({
161
+ code: "custom",
162
+ path: ["decidedAt"],
163
+ message: "decided signals require decidedAt"
164
+ });
165
+ });
166
+ //#endregion
167
+ //#region src/schema/session.ts
168
+ const agentSessionIdSchema = z.string().regex(/^ses_[a-z0-9]{6,64}$/u).describe("Stable Factory interaction ID; independent of provider session IDs.");
169
+ const agentSessionSchema = z.strictObject({
170
+ schemaVersion: schemaVersionSchema,
171
+ id: agentSessionIdSchema,
172
+ owner: z.string().min(1).describe("Authenticated principal owning this interaction."),
173
+ agent: z.string().regex(/^[a-z][a-z0-9-]{0,31}$/u).describe("Selected application agent."),
174
+ repositoryId: repositoryIdSchema.optional().describe("Configured repository selected for this interaction."),
175
+ routeId: z.string().min(1).optional().describe("Declared workflow route for specialist work."),
176
+ message: z.string().min(1).max(16e3).describe("Initial request, retained for idempotent admission and recovery."),
177
+ changeId: changeIdSchema.optional().describe("Existing change selected for this work."),
178
+ taskId: taskIdSchema.optional().describe("Workflow-owned task; its execution reference is authoritative."),
179
+ execution: executionReferenceSchema.optional().describe("Provider reference for taskless conversations only."),
180
+ notifications: z.array(communicationEventSchema).default([]).describe("Durable connector deliveries, deduplicated by eventId."),
181
+ createdAt: isoDateTimeSchema
182
+ }).refine((session) => session.taskId === void 0 || session.execution === void 0, "A session binds either a task or a conversation execution");
183
+ //#endregion
184
+ //#region src/store/engine.ts
185
+ /** An approval decision that does not apply to the task's current approval or state. */
186
+ var ApprovalError = class extends Error {
187
+ taskId;
188
+ reason;
189
+ constructor(taskId, reason) {
190
+ super(`approval decision on "${taskId}" refused: ${reason}`);
191
+ this.name = "ApprovalError";
192
+ this.taskId = taskId;
193
+ this.reason = reason;
194
+ }
195
+ };
196
+ /** A settlement that does not apply to the task's current state. */
197
+ var SettlementError = class extends Error {
198
+ taskId;
199
+ reason;
200
+ constructor(taskId, reason) {
201
+ super(`settlement on "${taskId}" refused: ${reason}`);
202
+ this.name = "SettlementError";
203
+ this.taskId = taskId;
204
+ this.reason = reason;
205
+ }
206
+ };
207
+ var ChangeAssociationError = class extends Error {
208
+ taskId;
209
+ repositoryId;
210
+ reason;
211
+ constructor(taskId, repositoryId, reason) {
212
+ super(`change for task "${taskId}" refused: ${reason}`);
213
+ this.name = "ChangeAssociationError";
214
+ this.taskId = taskId;
215
+ this.repositoryId = repositoryId;
216
+ this.reason = reason;
217
+ }
218
+ };
219
+ var OutboxEventConflictError = class extends Error {
220
+ constructor(eventId) {
221
+ super(`outbox event "${eventId}" was reused with different content`);
222
+ this.name = "OutboxEventConflictError";
223
+ }
224
+ };
225
+ var AnswerEventConflictError = class extends Error {
226
+ constructor(questionTaskId) {
227
+ super(`question "${questionTaskId}" already has a different answer event`);
228
+ this.name = "AnswerEventConflictError";
229
+ }
230
+ };
231
+ var SessionConflictError = class extends Error {};
232
+ function sameStoredValue(left, right) {
233
+ return isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
234
+ }
235
+ const taskReceiptStates = {
236
+ queued: "queued",
237
+ running: "dispatched",
238
+ stalled: "stalled",
239
+ succeeded: "succeeded",
240
+ failed: "failed",
241
+ needs_human: "needs_human",
242
+ cancelled: "cancelled"
243
+ };
244
+ const changeReceiptStates = {
245
+ verifying: "verifying",
246
+ ready: "ready",
247
+ merged: "merged",
248
+ deployed: "deployed",
249
+ rolled_out: "rolled_out",
250
+ rolled_back: "rolled_back"
251
+ };
252
+ function randomId(prefix) {
253
+ return `${prefix}_${randomUUID().replaceAll("-", "")}`;
254
+ }
255
+ function derivedId(prefix, key) {
256
+ return `${prefix}_${createHash("sha256").update(key).digest("hex").slice(0, 32)}`;
257
+ }
258
+ function bumpsAttempt(from, to) {
259
+ return to === "queued" || from === "verifying" && to === "running";
260
+ }
261
+ const emptyUsage = {
262
+ costUsd: 0,
263
+ inputTokens: 0,
264
+ outputTokens: 0,
265
+ cacheReadTokens: 0,
266
+ cacheWriteTokens: 0,
267
+ steps: {}
268
+ };
269
+ const unverifiedEvidence = {
270
+ reproVerified: false,
271
+ testsAdded: 0,
272
+ checksGreen: false,
273
+ attackPassed: false,
274
+ scopeMatches: false
275
+ };
276
+ function addUsd(a, b) {
277
+ return (Math.round(a * 1e6) + Math.round(b * 1e6)) / 1e6;
278
+ }
279
+ function createStores(options) {
280
+ const { driver } = options;
281
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
282
+ async function read(collection, id, schema) {
283
+ const record = await driver.get(collection, id);
284
+ if (record === null) return null;
285
+ return {
286
+ value: schema.parse(record.value),
287
+ version: record.version
288
+ };
289
+ }
290
+ async function mustRead(collection, id, schema) {
291
+ const record = await read(collection, id, schema);
292
+ if (record === null) throw new RecordNotFoundError(collection, id);
293
+ return record;
294
+ }
295
+ async function listAll(collection, schema) {
296
+ return (await driver.list(collection)).map((entry) => schema.parse(entry.record.value));
297
+ }
298
+ async function insertReceipt(receiptId, receipt) {
299
+ const full = receiptSchema.parse({
300
+ ...receipt,
301
+ schemaVersion: 1,
302
+ receiptId,
303
+ recordedAt: now()
304
+ });
305
+ try {
306
+ await driver.insert("receipts", receiptId, full);
307
+ return full;
308
+ } catch (error) {
309
+ if (error instanceof RecordExistsError) return (await mustRead("receipts", receiptId, receiptSchema)).value;
310
+ throw error;
311
+ }
312
+ }
313
+ async function appendReceipt(key, receipt) {
314
+ return insertReceipt(derivedId("rcpt", key), receipt);
315
+ }
316
+ const stores = {
317
+ driverName: driver.name,
318
+ sessions: {
319
+ async create({ operationId, ...input }) {
320
+ if (operationId.trim().length === 0 || operationId.length > 200) throw new Error("operationId must contain 1–200 characters");
321
+ const id = derivedId("ses", JSON.stringify([input.owner, operationId]));
322
+ const candidate = agentSessionSchema.parse({
323
+ ...input,
324
+ id,
325
+ schemaVersion: 1,
326
+ createdAt: now()
327
+ });
328
+ try {
329
+ await driver.insert("sessions", id, candidate);
330
+ return candidate;
331
+ } catch (error) {
332
+ if (!(error instanceof RecordExistsError)) throw error;
333
+ const existing = (await mustRead("sessions", id, agentSessionSchema)).value;
334
+ const { createdAt: _at, execution: _execution, taskId: _task, notifications: _notifications, ...intent } = existing;
335
+ const { createdAt: _newAt, notifications: _newNotifications, ...nextIntent } = candidate;
336
+ if (!sameStoredValue(intent, nextIntent)) throw new SessionConflictError("Session operationId was reused with different input");
337
+ return existing;
338
+ }
339
+ },
340
+ async get(id) {
341
+ return (await read("sessions", id, agentSessionSchema))?.value ?? null;
342
+ },
343
+ list() {
344
+ return listAll("sessions", agentSessionSchema);
345
+ },
346
+ async bind(id, reference) {
347
+ for (;;) {
348
+ const existing = await mustRead("sessions", id, agentSessionSchema);
349
+ if ("taskId" in reference) {
350
+ const task = (await mustRead("tasks", reference.taskId, taskSchema)).value;
351
+ if (task.origin.operator !== existing.value.owner || task.replyTo.channel !== "api" || task.replyTo.address !== id || task.repositoryIds[0] !== existing.value.repositoryId) throw new SessionConflictError("Task does not belong to this session");
352
+ }
353
+ const previous = existing.value.taskId === void 0 ? existing.value.execution === void 0 ? void 0 : { execution: existing.value.execution } : { taskId: existing.value.taskId };
354
+ if (previous !== void 0) {
355
+ if (!sameStoredValue(previous, reference)) throw new SessionConflictError("Session execution binding cannot be replaced");
356
+ return existing.value;
357
+ }
358
+ const next = agentSessionSchema.parse({
359
+ ...existing.value,
360
+ ...reference
361
+ });
362
+ try {
363
+ await driver.update("sessions", id, next, existing.version);
364
+ return next;
365
+ } catch (error) {
366
+ if (!(error instanceof VersionConflictError)) throw error;
367
+ }
368
+ }
369
+ },
370
+ async notify(input) {
371
+ const event = communicationEventSchema.parse(input);
372
+ if (event.replyTo.channel !== "api") throw new SessionConflictError("Expected an API notification");
373
+ for (;;) {
374
+ const record = await mustRead("sessions", event.replyTo.address, agentSessionSchema);
375
+ const existing = record.value.notifications.find((item) => item.eventId === event.eventId);
376
+ if (existing !== void 0) {
377
+ if (!sameStoredValue(existing, event)) throw new SessionConflictError("Notification eventId was reused");
378
+ return;
379
+ }
380
+ const next = agentSessionSchema.parse({
381
+ ...record.value,
382
+ notifications: [...record.value.notifications, event]
383
+ });
384
+ try {
385
+ await driver.update("sessions", next.id, next, record.version);
386
+ return;
387
+ } catch (error) {
388
+ if (!(error instanceof VersionConflictError)) throw error;
389
+ }
390
+ }
391
+ }
392
+ },
393
+ repositories: {
394
+ async put(repository) {
395
+ const parsed = repositorySchema.parse(repository);
396
+ const existing = await read("repositories", parsed.id, repositorySchema);
397
+ if (existing === null) await driver.insert("repositories", parsed.id, parsed);
398
+ else await driver.update("repositories", parsed.id, parsed, existing.version);
399
+ return parsed;
400
+ },
401
+ async get(id) {
402
+ const record = await read("repositories", id, repositorySchema);
403
+ return record === null ? null : record.value;
404
+ },
405
+ list() {
406
+ return listAll("repositories", repositorySchema);
407
+ }
408
+ },
409
+ signals: {
410
+ async create(input) {
411
+ const id = derivedId("sig", `${input.source}:${input.deliveryId}`);
412
+ const signal = signalSchema.parse({
413
+ schemaVersion: 1,
414
+ id,
415
+ repositoryId: input.repositoryId,
416
+ source: input.source,
417
+ deliveryId: input.deliveryId,
418
+ replyTo: input.replyTo,
419
+ payload: input.payload,
420
+ receivedAt: now(),
421
+ outcome: "pending"
422
+ });
423
+ try {
424
+ await driver.insert("signals", id, signal);
425
+ return signal;
426
+ } catch (error) {
427
+ if (error instanceof RecordExistsError) return (await mustRead("signals", id, signalSchema)).value;
428
+ throw error;
429
+ }
430
+ },
431
+ async get(id) {
432
+ const record = await read("signals", id, signalSchema);
433
+ return record === null ? null : record.value;
434
+ },
435
+ async decide(id, outcome, decideOptions = {}) {
436
+ const existing = await mustRead("signals", id, signalSchema);
437
+ assertTransition(signalOutcomeTransitions, existing.value.outcome, outcome);
438
+ const next = signalSchema.parse({
439
+ ...existing.value,
440
+ outcome,
441
+ detail: decideOptions.detail,
442
+ reason: decideOptions.reason,
443
+ decidedAt: now()
444
+ });
445
+ await driver.update("signals", id, next, existing.version);
446
+ return next;
447
+ },
448
+ list() {
449
+ return listAll("signals", signalSchema);
450
+ }
451
+ },
452
+ tasks: {
453
+ async create(input) {
454
+ const timestamp = now();
455
+ const id = input.dedupeKey === void 0 ? randomId("task") : derivedId("task", input.dedupeKey);
456
+ const task = taskSchema.parse({
457
+ schemaVersion: 1,
458
+ id,
459
+ repositoryIds: input.repositoryIds,
460
+ kind: input.kind,
461
+ area: input.area,
462
+ state: taskInitialState,
463
+ approval: input.approval,
464
+ origin: input.origin,
465
+ dependsOn: input.dependsOn,
466
+ replyTo: input.replyTo,
467
+ attempt: 1,
468
+ question: input.question,
469
+ changeId: input.changeId,
470
+ createdAt: timestamp,
471
+ updatedAt: timestamp
472
+ });
473
+ try {
474
+ await driver.insert("tasks", task.id, task);
475
+ return task;
476
+ } catch (error) {
477
+ if (input.dedupeKey !== void 0 && error instanceof RecordExistsError) return (await mustRead("tasks", task.id, taskSchema)).value;
478
+ throw error;
479
+ }
480
+ },
481
+ async approve(id, decision) {
482
+ const existing = await mustRead("tasks", id, taskSchema);
483
+ const task = existing.value;
484
+ if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
485
+ if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
486
+ const next = taskSchema.parse({
487
+ ...task,
488
+ approval: "granted",
489
+ updatedAt: now()
490
+ });
491
+ if (next.repositoryIds !== void 0) await appendReceipt(`task:${id}:approved:${next.attempt}`, {
492
+ repositoryIds: next.repositoryIds,
493
+ taskId: id,
494
+ kind: next.kind,
495
+ state: "approved",
496
+ phase: "approval",
497
+ reason: decision.reason,
498
+ attempt: next.attempt,
499
+ decidedBy: decision.by
500
+ });
501
+ await driver.update("tasks", id, next, existing.version);
502
+ return next;
503
+ },
504
+ async reject(id, decision) {
505
+ const existing = await mustRead("tasks", id, taskSchema);
506
+ const task = existing.value;
507
+ if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
508
+ if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
509
+ const next = taskSchema.parse({
510
+ ...task,
511
+ approval: "denied",
512
+ state: "cancelled",
513
+ updatedAt: now()
514
+ });
515
+ if (next.repositoryIds !== void 0) {
516
+ await appendReceipt(`task:${id}:rejected:${next.attempt}`, {
517
+ repositoryIds: next.repositoryIds,
518
+ taskId: id,
519
+ kind: next.kind,
520
+ state: "rejected",
521
+ phase: "approval",
522
+ reason: decision.reason,
523
+ attempt: next.attempt,
524
+ decidedBy: decision.by
525
+ });
526
+ await appendReceipt(`task:${id}:cancelled:${next.attempt}`, {
527
+ repositoryIds: next.repositoryIds,
528
+ taskId: id,
529
+ kind: next.kind,
530
+ state: "cancelled",
531
+ reason: decision.reason ?? `rejected by ${decision.by}`,
532
+ attempt: next.attempt
533
+ });
534
+ }
535
+ await driver.update("tasks", id, next, existing.version);
536
+ return next;
537
+ },
538
+ async recordExecution(id, execution) {
539
+ const existing = await mustRead("tasks", id, taskSchema);
540
+ const task = existing.value;
541
+ if (task.state !== "running") throw new InvalidTransitionError(task.state, "running", legalNextStates(taskTransitions, task.state));
542
+ const timestamp = now();
543
+ const next = taskSchema.parse({
544
+ ...task,
545
+ execution,
546
+ lastProgressAt: timestamp,
547
+ updatedAt: timestamp
548
+ });
549
+ await driver.update("tasks", id, next, existing.version);
550
+ return next;
551
+ },
552
+ async recordUsage(id, step) {
553
+ for (;;) {
554
+ const existing = await mustRead("tasks", id, taskSchema);
555
+ const task = existing.value;
556
+ const usage = task.usage ?? emptyUsage;
557
+ const recordedIndex = usage.steps[step.turnId];
558
+ if (recordedIndex !== void 0 && step.stepIndex <= recordedIndex) return task;
559
+ const timestamp = now();
560
+ const next = taskSchema.parse({
561
+ ...task,
562
+ lastProgressAt: timestamp,
563
+ usage: {
564
+ costUsd: addUsd(usage.costUsd, step.costUsd ?? 0),
565
+ inputTokens: usage.inputTokens + (step.inputTokens ?? 0),
566
+ outputTokens: usage.outputTokens + (step.outputTokens ?? 0),
567
+ cacheReadTokens: usage.cacheReadTokens + (step.cacheReadTokens ?? 0),
568
+ cacheWriteTokens: usage.cacheWriteTokens + (step.cacheWriteTokens ?? 0),
569
+ steps: {
570
+ ...usage.steps,
571
+ [step.turnId]: step.stepIndex
572
+ }
573
+ },
574
+ updatedAt: timestamp
575
+ });
576
+ try {
577
+ await driver.update("tasks", id, next, existing.version);
578
+ return next;
579
+ } catch (error) {
580
+ if (!(error instanceof VersionConflictError)) throw error;
581
+ }
582
+ }
583
+ },
584
+ async settle(id, cost, settleOptions = {}) {
585
+ const existing = await mustRead("tasks", id, taskSchema);
586
+ const task = existing.value;
587
+ if (task.settledUsd !== void 0) return task;
588
+ if (task.state !== "succeeded" && task.state !== "failed" && task.state !== "cancelled") throw new SettlementError(id, `task is ${task.state}, not terminal`);
589
+ const validated = costSchema.parse(cost);
590
+ const normalized = { totalUsd: Math.round(validated.totalUsd * 1e6) / 1e6 };
591
+ let recorded = normalized;
592
+ if (task.repositoryIds !== void 0) recorded = (await appendReceipt(`task:${id}:settled:${task.attempt}`, {
593
+ repositoryIds: task.repositoryIds,
594
+ taskId: id,
595
+ changeId: task.changeId,
596
+ kind: task.kind,
597
+ state: "settled",
598
+ phase: "budget",
599
+ reason: settleOptions.reason,
600
+ attempt: task.attempt,
601
+ cost: normalized
602
+ })).cost ?? normalized;
603
+ const next = taskSchema.parse({
604
+ ...task,
605
+ settledUsd: recorded.totalUsd,
606
+ updatedAt: now()
607
+ });
608
+ await driver.update("tasks", id, next, existing.version);
609
+ return next;
610
+ },
611
+ async get(id) {
612
+ const record = await read("tasks", id, taskSchema);
613
+ return record === null ? null : record.value;
614
+ },
615
+ async answerQuestion(input) {
616
+ const question = await mustRead("tasks", input.questionTaskId, taskSchema);
617
+ if (question.value.kind !== "question" || question.value.question === void 0) throw new Error(`task ${input.questionTaskId} is not a question`);
618
+ if (question.value.state !== "needs_human" && question.value.state !== "succeeded") throw new Error(`question task ${input.questionTaskId} is ${question.value.state}, not waiting`);
619
+ const questionDetails = question.value.question;
620
+ const id = answerIdSchema.parse(derivedId("ans", input.questionTaskId));
621
+ const candidate = answerEventSchema.parse({
622
+ schemaVersion: 1,
623
+ id,
624
+ questionTaskId: input.questionTaskId,
625
+ parentTaskId: question.value.origin.parentTaskId,
626
+ repositoryIds: question.value.repositoryIds,
627
+ replyTo: question.value.replyTo,
628
+ question: questionDetails.text,
629
+ answer: input.answer,
630
+ answeredBy: input.answeredBy,
631
+ recordedAt: now()
632
+ });
633
+ let answerEvent = candidate;
634
+ try {
635
+ await driver.insert("answers", id, candidate);
636
+ } catch (error) {
637
+ if (!(error instanceof RecordExistsError)) throw error;
638
+ const existing = await mustRead("answers", id, answerEventSchema);
639
+ const { recordedAt: _existingAt, ...existingIntent } = existing.value;
640
+ const { recordedAt: _candidateAt, ...candidateIntent } = candidate;
641
+ if (!isDeepStrictEqual(existingIntent, candidateIntent)) throw new AnswerEventConflictError(input.questionTaskId);
642
+ answerEvent = existing.value;
643
+ }
644
+ let parentTask = null;
645
+ if (answerEvent.parentTaskId !== void 0) {
646
+ parentTask = await stores.tasks.get(answerEvent.parentTaskId);
647
+ if (parentTask === null) throw new Error(`parent task ${answerEvent.parentTaskId} does not exist`);
648
+ for (;;) {
649
+ const parent = await mustRead("tasks", answerEvent.parentTaskId, taskSchema);
650
+ if (parent.value.answerEventIds?.includes(answerEvent.id) === true) {
651
+ parentTask = parent.value;
652
+ break;
653
+ }
654
+ const projected = taskSchema.parse({
655
+ ...parent.value,
656
+ answerEventIds: [...parent.value.answerEventIds ?? [], answerEvent.id],
657
+ updatedAt: now()
658
+ });
659
+ try {
660
+ await driver.update("tasks", parent.value.id, projected, parent.version);
661
+ parentTask = projected;
662
+ break;
663
+ } catch (error) {
664
+ if (!(error instanceof VersionConflictError)) throw error;
665
+ }
666
+ }
667
+ if (parentTask.state === "needs_human") parentTask = await stores.tasks.transition(parentTask.id, "queued", { reason: `question ${answerEvent.questionTaskId} answered by ${answerEvent.answeredBy}` });
668
+ }
669
+ let questionTask = await mustRead("tasks", input.questionTaskId, taskSchema).then((record) => record.value);
670
+ if (questionTask.state === "needs_human") questionTask = await stores.tasks.transition(questionTask.id, "succeeded", {
671
+ patch: { question: {
672
+ ...questionDetails,
673
+ answer: answerEvent.answer,
674
+ answeredBy: answerEvent.answeredBy
675
+ } },
676
+ reason: "question answered through Foreman"
677
+ });
678
+ else if (questionTask.state !== "succeeded" || questionTask.question?.answer !== answerEvent.answer || questionTask.question.answeredBy !== answerEvent.answeredBy) throw new Error(`question task ${questionTask.id} is ${questionTask.state}, not waiting`);
679
+ return {
680
+ answerEvent,
681
+ questionTask,
682
+ parentTask
683
+ };
684
+ },
685
+ async transition(id, to, transitionOptions = {}) {
686
+ if (transitionOptions.patch !== void 0 && "settledUsd" in transitionOptions.patch) throw new SettlementError(id, "settledUsd is set only by settle, not a transition");
687
+ if (transitionOptions.patch !== void 0 && "usage" in transitionOptions.patch) throw new SettlementError(id, "usage is set only by recordUsage, not a transition");
688
+ const existing = await mustRead("tasks", id, taskSchema);
689
+ if (transitionOptions.expectFrom !== void 0 && existing.value.state !== transitionOptions.expectFrom) throw new InvalidTransitionError(existing.value.state, to, legalNextStates(taskTransitions, existing.value.state));
690
+ assertTransition(taskTransitions, existing.value.state, to);
691
+ if (to === "needs_human" && existing.value.kind !== "question") {
692
+ if (transitionOptions.questionTaskId === void 0) throw new Error(`task ${id} cannot enter needs_human without a QuestionTask id`);
693
+ const question = await mustRead("tasks", transitionOptions.questionTaskId, taskSchema);
694
+ if (question.value.kind !== "question" || question.value.state !== "needs_human" || question.value.origin.parentTaskId !== id) throw new Error(`task ${id} cannot enter needs_human for question ${question.value.id}`);
695
+ }
696
+ const bumps = bumpsAttempt(existing.value.state, to);
697
+ const timestamp = now();
698
+ const { stalledAt: _stalledAt, stalledReason: _stalledReason, questionTaskId: _questionTaskId, ...cleared } = existing.value;
699
+ const next = taskSchema.parse({
700
+ ...cleared,
701
+ ...bumps ? { execution: void 0 } : {},
702
+ ...transitionOptions.patch,
703
+ ...to === "needs_human" && existing.value.kind !== "question" ? { questionTaskId: transitionOptions.questionTaskId } : {},
704
+ ...to === "stalled" ? {
705
+ stalledAt: timestamp,
706
+ stalledReason: transitionOptions.reason
707
+ } : {},
708
+ ...to === "running" ? { lastProgressAt: timestamp } : {},
709
+ state: to,
710
+ attempt: bumps ? existing.value.attempt + 1 : existing.value.attempt,
711
+ updatedAt: timestamp
712
+ });
713
+ const receiptState = taskReceiptStates[to];
714
+ if (receiptState !== void 0 && next.repositoryIds !== void 0) await appendReceipt(JSON.stringify([
715
+ "task",
716
+ id,
717
+ receiptState,
718
+ next.attempt,
719
+ next.changeId ?? null,
720
+ transitionOptions.reason ?? null
721
+ ]), {
722
+ repositoryIds: next.repositoryIds,
723
+ taskId: id,
724
+ changeId: next.changeId,
725
+ kind: next.kind,
726
+ state: receiptState,
727
+ reason: transitionOptions.reason,
728
+ attempt: next.attempt
729
+ });
730
+ await driver.update("tasks", id, next, existing.version);
731
+ return next;
732
+ },
733
+ list() {
734
+ return listAll("tasks", taskSchema);
735
+ }
736
+ },
737
+ changes: {
738
+ async create(input) {
739
+ const timestamp = now();
740
+ const id = derivedId("chg", `${input.repositoryId}:github-pr:${input.prNumber}`);
741
+ const change = changeSchema.parse({
742
+ schemaVersion: 1,
743
+ id,
744
+ repositoryId: input.repositoryId,
745
+ taskId: input.taskId,
746
+ groupId: input.groupId,
747
+ prNumber: input.prNumber,
748
+ prUrl: input.prUrl,
749
+ author: input.author,
750
+ risk: input.risk,
751
+ confidence: input.confidence,
752
+ evidence: input.evidence,
753
+ state: changeInitialState,
754
+ createdAt: timestamp,
755
+ updatedAt: timestamp
756
+ });
757
+ await mustRead("repositories", change.repositoryId, repositorySchema);
758
+ if (change.taskId !== void 0) {
759
+ const task = (await mustRead("tasks", change.taskId, taskSchema)).value;
760
+ if (task.kind !== "code_change") throw new ChangeAssociationError(change.taskId, change.repositoryId, "changes can only be produced by code_change tasks");
761
+ const taskRepositoryId = task.repositoryIds[0];
762
+ if (taskRepositoryId !== change.repositoryId) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
763
+ }
764
+ try {
765
+ await driver.insert("changes", change.id, change);
766
+ return change;
767
+ } catch (error) {
768
+ if (error instanceof RecordExistsError) return (await mustRead("changes", change.id, changeSchema)).value;
769
+ throw error;
770
+ }
771
+ },
772
+ async get(id) {
773
+ const record = await read("changes", id, changeSchema);
774
+ return record === null ? null : record.value;
775
+ },
776
+ async transition(id, to, transitionOptions = {}) {
777
+ const existing = await mustRead("changes", id, changeSchema);
778
+ assertTransition(changeTransitions, existing.value.state, to);
779
+ const invalidated = existing.value.state === "ready" && to === "verifying";
780
+ const next = changeSchema.parse({
781
+ ...existing.value,
782
+ ...transitionOptions.patch,
783
+ ...invalidated ? {
784
+ confidence: "low",
785
+ evidence: unverifiedEvidence
786
+ } : {},
787
+ state: to,
788
+ updatedAt: now()
789
+ });
790
+ const receiptState = changeReceiptStates[to];
791
+ if (receiptState !== void 0) await appendReceipt(`change:${id}:${receiptState}`, {
792
+ repositoryIds: [next.repositoryId],
793
+ taskId: next.taskId,
794
+ changeId: id,
795
+ kind: "change",
796
+ state: receiptState,
797
+ reason: transitionOptions.reason,
798
+ attempt: 1,
799
+ decidedBy: next.decidedBy
800
+ });
801
+ await driver.update("changes", id, next, existing.version);
802
+ return next;
803
+ },
804
+ list() {
805
+ return listAll("changes", changeSchema);
806
+ }
807
+ },
808
+ receipts: {
809
+ append(input) {
810
+ const { dedupeKey, attempt, ...fields } = input;
811
+ return insertReceipt(dedupeKey === void 0 ? randomId("rcpt") : derivedId("rcpt", `custom:${dedupeKey}`), {
812
+ ...fields,
813
+ attempt: attempt ?? 1
814
+ });
815
+ },
816
+ async get(id) {
817
+ const record = await read("receipts", id, receiptSchema);
818
+ return record === null ? null : record.value;
819
+ },
820
+ list() {
821
+ return listAll("receipts", receiptSchema);
822
+ }
823
+ },
824
+ budgets: {
825
+ async ensure(input) {
826
+ const budget = budgetSchema.parse({
827
+ schemaVersion: 1,
828
+ factoryId: input.factoryId,
829
+ month: input.month,
830
+ limits: input.limits,
831
+ notices: {}
832
+ });
833
+ try {
834
+ await driver.insert("budgets", input.month, budget);
835
+ return budget;
836
+ } catch (error) {
837
+ if (!(error instanceof RecordExistsError)) throw error;
838
+ }
839
+ const existing = await mustRead("budgets", input.month, budgetSchema);
840
+ const refreshed = budgetSchema.parse({
841
+ ...existing.value,
842
+ limits: input.limits
843
+ });
844
+ if (JSON.stringify(refreshed) === JSON.stringify(existing.value)) return existing.value;
845
+ try {
846
+ await driver.update("budgets", input.month, refreshed, existing.version);
847
+ return refreshed;
848
+ } catch (error) {
849
+ if (error instanceof VersionConflictError) return (await mustRead("budgets", input.month, budgetSchema)).value;
850
+ throw error;
851
+ }
852
+ },
853
+ async get(month) {
854
+ const record = await read("budgets", month, budgetSchema);
855
+ return record === null ? null : record.value;
856
+ },
857
+ async stampNotice(month, notice) {
858
+ const field = notice === "warning" ? "warningAt" : "exhaustedAt";
859
+ for (;;) {
860
+ const existing = await mustRead("budgets", month, budgetSchema);
861
+ if (existing.value.notices[field] !== void 0) return {
862
+ budget: existing.value,
863
+ stamped: false
864
+ };
865
+ const next = budgetSchema.parse({
866
+ ...existing.value,
867
+ notices: {
868
+ ...existing.value.notices,
869
+ [field]: now()
870
+ }
871
+ });
872
+ try {
873
+ await driver.update("budgets", month, next, existing.version);
874
+ return {
875
+ budget: next,
876
+ stamped: true
877
+ };
878
+ } catch (error) {
879
+ if (!(error instanceof VersionConflictError)) throw error;
880
+ }
881
+ }
882
+ }
883
+ },
884
+ answers: { async get(id) {
885
+ return (await read("answers", id, answerEventSchema))?.value ?? null;
886
+ } },
887
+ outbox: {
888
+ async enqueue(input) {
889
+ const event = communicationEventSchema.parse(input);
890
+ const id = outboxIdSchema.parse(derivedId("out", event.eventId));
891
+ const delivered = await read("outboxDeliveries", id, outboxMessageSchema);
892
+ if (delivered !== null) {
893
+ if (!isDeepStrictEqual(delivered.value.event, event)) throw new OutboxEventConflictError(event.eventId);
894
+ return delivered.value;
895
+ }
896
+ const queueId = `communication-${id.slice(4, 5)}`;
897
+ for (;;) {
898
+ const existing = await read("outboxQueues", queueId, outboxQueueSchema);
899
+ const duplicate = existing?.value.messages.find((message) => message.id === id);
900
+ if (duplicate !== void 0) {
901
+ if (!isDeepStrictEqual(duplicate.event, event)) throw new OutboxEventConflictError(event.eventId);
902
+ return duplicate;
903
+ }
904
+ const timestamp = now();
905
+ const message = outboxMessageSchema.parse({
906
+ schemaVersion: 1,
907
+ id,
908
+ event,
909
+ state: "pending",
910
+ attempt: 0,
911
+ availableAt: timestamp,
912
+ createdAt: timestamp,
913
+ updatedAt: timestamp
914
+ });
915
+ const queue = outboxQueueSchema.parse({
916
+ schemaVersion: 1,
917
+ id: queueId,
918
+ messages: [...existing?.value.messages ?? [], message],
919
+ updatedAt: timestamp
920
+ });
921
+ try {
922
+ if (existing === null) await driver.insert("outboxQueues", queueId, queue);
923
+ else await driver.update("outboxQueues", queueId, queue, existing.version);
924
+ return message;
925
+ } catch (error) {
926
+ if (!(error instanceof RecordExistsError) && !(error instanceof VersionConflictError)) throw error;
927
+ }
928
+ }
929
+ },
930
+ async get(id) {
931
+ const delivered = await read("outboxDeliveries", id, outboxMessageSchema);
932
+ if (delivered !== null) return delivered.value;
933
+ return (await read("outboxQueues", `communication-${id.slice(4, 5)}`, outboxQueueSchema))?.value.messages.find((message) => message.id === id) ?? null;
934
+ },
935
+ async claim(id, leaseMs = 6e4) {
936
+ if (!Number.isSafeInteger(leaseMs) || leaseMs <= 0) throw new Error("outbox leaseMs must be a positive safe integer");
937
+ const queueId = `communication-${id.slice(4, 5)}`;
938
+ for (;;) {
939
+ const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
940
+ const index = queue.value.messages.findIndex((message) => message.id === id);
941
+ if (index < 0) return null;
942
+ if (await read("outboxDeliveries", id, outboxMessageSchema) !== null) {
943
+ const messages = queue.value.messages.filter((message) => message.id !== id);
944
+ try {
945
+ await driver.update("outboxQueues", queueId, outboxQueueSchema.parse({
946
+ ...queue.value,
947
+ messages,
948
+ updatedAt: now()
949
+ }), queue.version);
950
+ return null;
951
+ } catch (error) {
952
+ if (!(error instanceof VersionConflictError)) throw error;
953
+ continue;
954
+ }
955
+ }
956
+ const message = queue.value.messages[index];
957
+ const timestamp = now();
958
+ if (!(message.state === "pending" && message.availableAt <= timestamp || message.state === "delivering" && message.leaseUntil !== void 0 && message.leaseUntil <= timestamp)) return null;
959
+ const next = outboxMessageSchema.parse({
960
+ ...message,
961
+ state: "delivering",
962
+ attempt: message.attempt + 1,
963
+ leaseUntil: new Date(Date.parse(timestamp) + leaseMs).toISOString(),
964
+ updatedAt: timestamp
965
+ });
966
+ const messages = [...queue.value.messages];
967
+ messages[index] = next;
968
+ try {
969
+ await driver.update("outboxQueues", queueId, outboxQueueSchema.parse({
970
+ ...queue.value,
971
+ messages,
972
+ updatedAt: timestamp
973
+ }), queue.version);
974
+ return next;
975
+ } catch (error) {
976
+ if (!(error instanceof VersionConflictError)) throw error;
977
+ }
978
+ }
979
+ },
980
+ async retry(id, attempt, reason, delayMs) {
981
+ if (!Number.isSafeInteger(delayMs) || delayMs < 0) throw new Error("outbox delayMs must be a nonnegative safe integer");
982
+ const queueId = `communication-${id.slice(4, 5)}`;
983
+ for (;;) {
984
+ const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
985
+ const index = queue.value.messages.findIndex((message) => message.id === id);
986
+ if (index < 0) return null;
987
+ const message = queue.value.messages[index];
988
+ if (message.state !== "delivering" || message.attempt !== attempt) return null;
989
+ const timestamp = now();
990
+ const { leaseUntil: _leaseUntil, ...leased } = message;
991
+ const next = outboxMessageSchema.parse({
992
+ ...leased,
993
+ state: "pending",
994
+ availableAt: new Date(Date.parse(timestamp) + delayMs).toISOString(),
995
+ lastError: reason.trim().slice(0, 2e3) || "delivery failed",
996
+ updatedAt: timestamp
997
+ });
998
+ const messages = [...queue.value.messages];
999
+ messages[index] = next;
1000
+ try {
1001
+ await driver.update("outboxQueues", queueId, outboxQueueSchema.parse({
1002
+ ...queue.value,
1003
+ messages,
1004
+ updatedAt: timestamp
1005
+ }), queue.version);
1006
+ return next;
1007
+ } catch (error) {
1008
+ if (!(error instanceof VersionConflictError)) throw error;
1009
+ }
1010
+ }
1011
+ },
1012
+ async complete(id, attempt) {
1013
+ const queueId = `communication-${id.slice(4, 5)}`;
1014
+ let delivered = await read("outboxDeliveries", id, outboxMessageSchema);
1015
+ for (;;) {
1016
+ const queue = await mustRead("outboxQueues", queueId, outboxQueueSchema);
1017
+ const index = queue.value.messages.findIndex((message) => message.id === id);
1018
+ if (index < 0) return delivered?.value ?? null;
1019
+ const message = queue.value.messages[index];
1020
+ const timestamp = now();
1021
+ if (delivered === null) {
1022
+ if (message.state !== "delivering" || message.attempt !== attempt) return null;
1023
+ const { leaseUntil: _leaseUntil, lastError: _lastError, ...leased } = message;
1024
+ const next = outboxMessageSchema.parse({
1025
+ ...leased,
1026
+ state: "delivered",
1027
+ deliveredAt: timestamp,
1028
+ updatedAt: timestamp
1029
+ });
1030
+ try {
1031
+ await driver.insert("outboxDeliveries", id, next);
1032
+ delivered = {
1033
+ value: next,
1034
+ version: 1
1035
+ };
1036
+ } catch (error) {
1037
+ if (!(error instanceof RecordExistsError)) throw error;
1038
+ delivered = await mustRead("outboxDeliveries", id, outboxMessageSchema);
1039
+ if (!isDeepStrictEqual(delivered.value.event, message.event)) throw new OutboxEventConflictError(message.event.eventId);
1040
+ }
1041
+ }
1042
+ const messages = queue.value.messages.filter((candidate) => candidate.id !== id);
1043
+ try {
1044
+ await driver.update("outboxQueues", queueId, outboxQueueSchema.parse({
1045
+ ...queue.value,
1046
+ messages,
1047
+ updatedAt: timestamp
1048
+ }), queue.version);
1049
+ return delivered.value;
1050
+ } catch (error) {
1051
+ if (!(error instanceof VersionConflictError)) throw error;
1052
+ }
1053
+ }
1054
+ },
1055
+ async pending(limit = 100) {
1056
+ if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("outbox pending limit must be a positive safe integer");
1057
+ const queues = await Promise.all("0123456789abcdef".split("").map((shard) => read("outboxQueues", `communication-${shard}`, outboxQueueSchema)));
1058
+ const timestamp = now();
1059
+ return queues.flatMap((queue) => queue?.value.messages ?? []).filter((message) => message.state === "pending" && message.availableAt <= timestamp || message.state === "delivering" && message.leaseUntil !== void 0 && message.leaseUntil <= timestamp).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).slice(0, limit);
1060
+ }
1061
+ }
1062
+ };
1063
+ return stores;
1064
+ }
1065
+ //#endregion
1066
+ export { standardReceiptPhases as C, standardReceiptKinds as S, answerEventSchema as T, costSchema as _, SessionConflictError as a, receiptSchema as b, agentSessionIdSchema as c, signalSchema as d, signalSourceSchema as f, outboxStateSchema as g, outboxQueueSchema as h, OutboxEventConflictError as i, agentSessionSchema as l, outboxMessageSchema as m, ApprovalError as n, SettlementError as o, standardSignalSources as p, ChangeAssociationError as r, createStores as s, AnswerEventConflictError as t, signalOutcomeSchema as u, receiptKindSchema as v, standardReceiptStates as w, receiptStateSchema as x, receiptPhaseSchema as y };
1067
+
1068
+ //# sourceMappingURL=engine-WaUM2cGA.mjs.map