@vercel/factory 0.0.12 → 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.
- package/README.md +1 -1
- package/dist/api.d.mts +225 -0
- package/dist/api.mjs +312 -0
- package/dist/api.mjs.map +1 -0
- package/dist/{approval-DdvEKLiE.mjs → approval-JX2Px9IC.mjs} +2 -2
- package/dist/{approval-DdvEKLiE.mjs.map → approval-JX2Px9IC.mjs.map} +1 -1
- package/dist/blob/index.d.mts +4 -3
- package/dist/blob/index.mjs +21 -15
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget-DzSiIA4t.mjs +42 -0
- package/dist/budget-DzSiIA4t.mjs.map +1 -0
- package/dist/build-factory.d.mts +1 -1
- package/dist/build-factory.mjs +1 -1
- package/dist/change-D2U6v7Ek.mjs +554 -0
- package/dist/change-D2U6v7Ek.mjs.map +1 -0
- package/dist/communication-Kd9nU5RP.mjs +30 -0
- package/dist/communication-Kd9nU5RP.mjs.map +1 -0
- package/dist/connectors/index.d.mts +3 -2
- package/dist/connectors/index.mjs +53 -26
- package/dist/connectors/index.mjs.map +1 -1
- package/dist/connectors/slack.d.mts +17 -6
- package/dist/connectors/slack.mjs +99 -32
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/coordination-Cq08hb1l.mjs +76 -0
- package/dist/coordination-Cq08hb1l.mjs.map +1 -0
- package/dist/dispatch-BaZS3PT8.d.mts +76 -0
- package/dist/{driver-CrZsy7sm.d.mts → driver-BC0Mt5KG.d.mts} +2 -2
- package/dist/driver-Bbd25rNM.mjs.map +1 -1
- package/dist/engine-DFHU2Pe-.d.mts +781 -0
- package/dist/engine-WaUM2cGA.mjs +1068 -0
- package/dist/engine-WaUM2cGA.mjs.map +1 -0
- package/dist/eve/index.d.mts +289 -153
- package/dist/eve/index.mjs +378 -89
- package/dist/eve/index.mjs.map +1 -1
- package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BoRXOw43.mjs} +23 -22
- package/dist/factory-config-BoRXOw43.mjs.map +1 -0
- package/dist/{factory-config-Bu4mklm9.d.mts → factory-config-D2yU3G0c.d.mts} +21 -2
- package/dist/{github-wThBfbm-.d.mts → github-C5HNBN6p.d.mts} +34 -3
- package/dist/github.d.mts +2 -2
- package/dist/github.mjs +61 -1
- package/dist/github.mjs.map +1 -1
- package/dist/{id-DwCihQr4.mjs → id-BLIkCVue.mjs} +4 -2
- package/dist/{id-DwCihQr4.mjs.map → id-BLIkCVue.mjs.map} +1 -1
- package/dist/index-Bw6hggoH.d.mts +40 -0
- package/dist/index.d.mts +291 -61
- package/dist/index.mjs +140 -783
- package/dist/index.mjs.map +1 -1
- package/dist/postgres/index.d.mts +1 -1
- package/dist/task-CcOyOJ3k.mjs +154 -0
- package/dist/task-CcOyOJ3k.mjs.map +1 -0
- package/package.json +5 -1
- package/dist/approval-CIXB8IF8.d.mts +0 -522
- package/dist/change-CFNGXVqD.mjs +0 -230
- package/dist/change-CFNGXVqD.mjs.map +0 -1
- package/dist/factory-config-ifd3JxiU.mjs.map +0 -1
- package/dist/index-Cy4jfXh5.d.mts +0 -67
- 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
|