@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
package/dist/index.mjs CHANGED
@@ -1,10 +1,14 @@
1
- import { a as repositoryIdsSchema, c as SCHEMA_VERSION, d as schemaVersionSchema, f as usdSchema, i as repositoryIdSchema, l as isoDateTimeSchema, n as factoryIdSchema, o as signalIdSchema, p as vocabularySchema, r as receiptIdSchema, s as taskIdSchema, t as changeIdSchema, u as replyToSchema } from "./id-DwCihQr4.mjs";
2
- import { C as taskUsageSchema, S as taskStateSchema, _ as taskExecutionRepositoryIdsSchema, a as findingCategorySchema, b as taskQuestionSchema, c as findingSchema, d as findingVerificationSchema, f as coordinationRepositoryIdsSchema, g as taskApprovalSchema, h as standardTaskKinds, i as requiresApproval, l as findingVerificationLevelSchema, m as harnessSchema, n as approvalRuleSchema, o as findingFingerprint, p as executionReferenceSchema, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema, v as taskKindSchema, x as taskSchema, y as taskOriginSchema } from "./approval-CwhTP1XB.mjs";
3
- import { C as budgetStatus, D as sweepBudget, E as deriveFactoryId, O as sweepSettlements, S as budgetMonthOf, T as computeMonthSpendUsd, _ as taskInitialState, a as BudgetExhaustedError, b as createCoordination, c as dispatchTask, d as changeInitialState, f as changeTransitions, g as signalOutcomeTransitions, h as signalInitialOutcome, i as levelSchema, l as InvalidTransitionError, m as legalNextStates, n as changeSchema, o as DEFAULT_MAX_LAUNCH_ATTEMPTS, p as isTransitionAllowed, r as changeStateSchema, s as DispatchRefusedError, t as changeEvidenceSchema, u as assertTransition, v as taskTransitions, w as canReserve, x as reconcileCoordination, y as terminalStates } from "./change-CzEAoFVE.mjs";
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, r as factoryIdSchema, s as repositoryIdsSchema, t as answerIdSchema, u as SCHEMA_VERSION } from "./id-BLIkCVue.mjs";
2
+ import { a as taskApprovalSchema, c as taskOriginSchema, d as taskStateSchema, f as taskUsageSchema, i as standardTaskKinds, l as taskQuestionSchema, n as executionReferenceSchema, o as taskExecutionRepositoryIdsSchema, r as harnessSchema, s as taskKindSchema, t as coordinationRepositoryIdsSchema, u as taskSchema } from "./task-CcOyOJ3k.mjs";
3
+ import { a as findingCategorySchema, c as findingSchema, d as findingVerificationSchema, i as requiresApproval, l as findingVerificationLevelSchema, n as approvalRuleSchema, o as findingFingerprint, r as defaultApprovalPolicy, s as findingPrioritySchema, t as approvalPolicySchema, u as findingVerificationMethodSchema } from "./approval-JX2Px9IC.mjs";
4
+ import { C as computeMonthSpendUsd, E as sweepSettlements, S as canReserve, T as sweepBudget, _ as taskInitialState, a as BudgetExhaustedError, b as budgetMonthOf, c as dispatchTask, d as changeInitialState, f as changeTransitions, g as signalOutcomeTransitions, h as signalInitialOutcome, i as levelSchema, l as InvalidTransitionError, m as legalNextStates, n as changeSchema, o as DEFAULT_MAX_LAUNCH_ATTEMPTS, p as isTransitionAllowed, r as changeStateSchema, s as DispatchRefusedError, t as changeEvidenceSchema, u as assertTransition, v as taskTransitions, w as deriveFactoryId, x as budgetStatus, y as terminalStates } from "./change-D2U6v7Ek.mjs";
4
5
  import { n as RecordNotFoundError, r as VersionConflictError, t as RecordExistsError } from "./driver-Bbd25rNM.mjs";
5
- import { a as factoryConfigSchema, c as budgetSchema, i as factoryConfigRepositorySchema, l as repositorySchema, n as defineFactory, o as budgetLimitsSchema, r as executionPolicySchema, s as budgetMonthSchema, t as defaultExecutionPolicy, u as repositorySlugSchema } from "./factory-config-BJcjsPKl.mjs";
6
+ import { n as reconcileCoordination, t as createCoordination } from "./coordination-Cq08hb1l.mjs";
7
+ import { a as repositorySlugSchema, i as repositorySchema, n as budgetMonthSchema, r as budgetSchema, t as budgetLimitsSchema } from "./budget-DzSiIA4t.mjs";
8
+ import { C as standardReceiptPhases, S as standardReceiptKinds, T as answerEventSchema, _ as costSchema, a as SessionConflictError, b as receiptSchema, c as agentSessionIdSchema, d as signalSchema, f as signalSourceSchema, g as outboxStateSchema, h as outboxQueueSchema, i as OutboxEventConflictError, l as agentSessionSchema, m as outboxMessageSchema, n as ApprovalError, o as SettlementError, p as standardSignalSources, r as ChangeAssociationError, s as createStores, t as AnswerEventConflictError, u as signalOutcomeSchema, v as receiptKindSchema, w as standardReceiptStates, x as receiptStateSchema, y as receiptPhaseSchema } from "./engine-WaUM2cGA.mjs";
9
+ import { n as communicationOutcomeSchema, r as communicationOutcomes, t as communicationEventSchema } from "./communication-Kd9nU5RP.mjs";
10
+ import { a as factoryConfigSchema, i as factoryConfigRepositorySchema, n as defineFactory, r as executionPolicySchema, t as defaultExecutionPolicy } from "./factory-config-BoRXOw43.mjs";
6
11
  import { z } from "zod";
7
- import { createHash, randomUUID } from "node:crypto";
8
12
  //#region src/agent-routes.ts
9
13
  const agentRouteSchema = z.strictObject({
10
14
  id: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/u).describe("Stable route selected by a conversational agent or workflow."),
@@ -82,8 +86,6 @@ function verificationBrief(task, repository, change) {
82
86
  //#endregion
83
87
  //#region src/changes.ts
84
88
  async function adoptPullRequest(stores, input) {
85
- const existing = (await stores.changes.list()).find((change) => change.repositoryId === input.repositoryId && change.prNumber === input.prNumber && (change.state === "verifying" || change.state === "ready"));
86
- if (existing !== void 0) return existing;
87
89
  return stores.changes.create({
88
90
  ...input,
89
91
  risk: "low",
@@ -232,6 +234,7 @@ function computeRepositoryHealth(rows, options = {}) {
232
234
  const dayMs = 864e5;
233
235
  const red = [];
234
236
  const yellow = [];
237
+ const tasksById = new Map(rows.tasks.map((task) => [task.id, task]));
235
238
  for (const receipt of rows.receipts) {
236
239
  if (now - Date.parse(receipt.recordedAt) >= dayMs) continue;
237
240
  if (receipt.state === "rolled_back") red.push(`rollback in the last 24h (${receipt.receiptId})`);
@@ -241,6 +244,12 @@ function computeRepositoryHealth(rows, options = {}) {
241
244
  if (openQuestions > 0) yellow.push(`${openQuestions} open question(s)`);
242
245
  const needsHuman = rows.tasks.filter((task) => task.kind !== "question" && task.state === "needs_human").length;
243
246
  if (needsHuman > 0) yellow.push(`${needsHuman} task(s) waiting on a human`);
247
+ const maxQueuedMinutes = options.maxQueuedMinutes ?? 5;
248
+ const staleQueued = rows.tasks.filter((task) => {
249
+ if (task.state !== "queued" || task.kind === "question" || task.kind === "coordination" || task.approval === "required") return false;
250
+ return (task.dependsOn ?? []).every((dependencyId) => tasksById.get(dependencyId)?.changeId !== void 0) && now - Date.parse(task.updatedAt) >= maxQueuedMinutes * 60 * 1e3;
251
+ }).length;
252
+ if (staleQueued > 0) yellow.push(`${staleQueued} eligible task(s) queued for at least ${maxQueuedMinutes} minutes`);
244
253
  const blocked = rows.changes.filter((change) => openChangeStates.has(change.state) && reviewBucket(change.risk, change.confidence) === "blocked").length;
245
254
  if (blocked > 0) yellow.push(`${blocked} change(s) in the blocked bucket`);
246
255
  if (options.maxOpenChanges !== void 0) {
@@ -261,121 +270,6 @@ function computeRepositoryHealth(rows, options = {}) {
261
270
  };
262
271
  }
263
272
  //#endregion
264
- //#region src/schema/receipt.ts
265
- /** States the engine writes on transitions. Any vocabulary value is valid. */
266
- const standardReceiptStates = [
267
- "approved",
268
- "rejected",
269
- "dispatched",
270
- "stalled",
271
- "succeeded",
272
- "failed",
273
- "needs_human",
274
- "cancelled",
275
- "settled",
276
- "ready",
277
- "merged",
278
- "deployed",
279
- "rolled_out",
280
- "rolled_back"
281
- ];
282
- /** Phases the standard loop uses. Any vocabulary value is valid. */
283
- const standardReceiptPhases = [
284
- "approval",
285
- "budget",
286
- "dispatch",
287
- "implementation",
288
- "verification",
289
- "pull_request",
290
- "cleanup"
291
- ];
292
- /** Kinds the standard loop writes: the standard task kinds plus "change". */
293
- const standardReceiptKinds = [...standardTaskKinds, "change"];
294
- const receiptStateSchema = vocabularySchema.describe("What happened, e.g. dispatched, settled, merged, or a custom event name.");
295
- const receiptPhaseSchema = vocabularySchema.describe("Where in the process it happened, e.g. dispatch or verification.");
296
- const receiptKindSchema = vocabularySchema.describe("What the receipt is about: a task kind, change, or a custom kind.");
297
- const costSchema = z.strictObject({ totalUsd: usdSchema });
298
- const receiptSchema = z.strictObject({
299
- schemaVersion: schemaVersionSchema,
300
- receiptId: receiptIdSchema,
301
- repositoryIds: repositoryIdsSchema.describe("Repositories the recorded event concerns."),
302
- taskId: taskIdSchema.optional(),
303
- changeId: changeIdSchema.optional(),
304
- kind: receiptKindSchema,
305
- state: receiptStateSchema,
306
- phase: receiptPhaseSchema.optional(),
307
- reason: z.string().min(1).max(2e3).optional().describe("Why it happened. Scrubbed and truncated; never secrets."),
308
- attempt: z.number().int().min(1).describe("Attempt the event belongs to."),
309
- cost: costSchema.optional().describe("Measured cost. Settled receipts only."),
310
- decidedBy: z.string().min(1).optional().describe("Human who made the decision. Change and approval receipts only."),
311
- recordedAt: isoDateTimeSchema
312
- }).superRefine((receipt, ctx) => {
313
- if (receipt.state === "settled" && receipt.cost === void 0) ctx.addIssue({
314
- code: "custom",
315
- path: ["cost"],
316
- message: "settled receipts require cost"
317
- });
318
- if (receipt.state !== "settled" && receipt.cost !== void 0) ctx.addIssue({
319
- code: "custom",
320
- path: ["cost"],
321
- message: "cost is only valid on settled receipts"
322
- });
323
- if (receipt.decidedBy !== void 0 && receipt.kind !== "change" && receipt.state !== "approved" && receipt.state !== "rejected") ctx.addIssue({
324
- code: "custom",
325
- path: ["decidedBy"],
326
- message: "decidedBy is only valid on change and approval receipts"
327
- });
328
- });
329
- //#endregion
330
- //#region src/schema/signal.ts
331
- /** Sources used by the standard connectors. Any vocabulary value is valid. */
332
- const standardSignalSources = [
333
- "github_issue",
334
- "github_pr",
335
- "github_pr_comment",
336
- "slack",
337
- "human",
338
- "github_ci_failure",
339
- "vercel_deploy_failure",
340
- "security_scan",
341
- "investigation",
342
- "api"
343
- ];
344
- const signalSourceSchema = vocabularySchema.describe("What produced the signal, e.g. github_issue or a custom connector's own name.");
345
- const signalOutcomeSchema = z.enum([
346
- "pending",
347
- "accepted",
348
- "duplicate",
349
- "unactionable",
350
- "low_confidence",
351
- "ignored"
352
- ]).describe("Triage outcome. A signal starts pending and is decided exactly once.");
353
- const signalSchema = z.strictObject({
354
- schemaVersion: schemaVersionSchema,
355
- id: signalIdSchema.describe("Signal id, derived from source and deliveryId."),
356
- repositoryId: repositoryIdSchema.describe("Repository the signal is about."),
357
- source: signalSourceSchema,
358
- deliveryId: z.string().min(1).describe("Provider idempotency key; a redelivered event maps to the same signal."),
359
- replyTo: replyToSchema,
360
- payload: z.unknown().describe("Provider-shaped payload. Untrusted data, never instructions."),
361
- receivedAt: isoDateTimeSchema,
362
- outcome: signalOutcomeSchema,
363
- detail: z.string().min(1).optional().describe("Outcome nuance, e.g. severity when accepted or the duplicate-of signal id."),
364
- reason: z.string().min(1).optional().describe("Why the outcome was chosen."),
365
- decidedAt: isoDateTimeSchema.optional()
366
- }).superRefine((signal, ctx) => {
367
- if (signal.outcome === "pending" && signal.decidedAt !== void 0) ctx.addIssue({
368
- code: "custom",
369
- path: ["decidedAt"],
370
- message: "pending signals must not carry decidedAt"
371
- });
372
- if (signal.outcome !== "pending" && signal.decidedAt === void 0) ctx.addIssue({
373
- code: "custom",
374
- path: ["decidedAt"],
375
- message: "decided signals require decidedAt"
376
- });
377
- });
378
- //#endregion
379
273
  //#region src/signals.ts
380
274
  /**
381
275
  * Bind normalized intake to signal creation alone. Connectors and API routes
@@ -446,525 +340,6 @@ async function sweepStalled(stores, options) {
446
340
  return result;
447
341
  }
448
342
  //#endregion
449
- //#region src/store/engine.ts
450
- /** An approval decision that does not apply to the task's current approval or state. */
451
- var ApprovalError = class extends Error {
452
- taskId;
453
- reason;
454
- constructor(taskId, reason) {
455
- super(`approval decision on "${taskId}" refused: ${reason}`);
456
- this.name = "ApprovalError";
457
- this.taskId = taskId;
458
- this.reason = reason;
459
- }
460
- };
461
- /** A settlement that does not apply to the task's current state. */
462
- var SettlementError = class extends Error {
463
- taskId;
464
- reason;
465
- constructor(taskId, reason) {
466
- super(`settlement on "${taskId}" refused: ${reason}`);
467
- this.name = "SettlementError";
468
- this.taskId = taskId;
469
- this.reason = reason;
470
- }
471
- };
472
- var ChangeAssociationError = class extends Error {
473
- taskId;
474
- repositoryId;
475
- reason;
476
- constructor(taskId, repositoryId, reason) {
477
- super(`change for task "${taskId}" refused: ${reason}`);
478
- this.name = "ChangeAssociationError";
479
- this.taskId = taskId;
480
- this.repositoryId = repositoryId;
481
- this.reason = reason;
482
- }
483
- };
484
- const taskReceiptStates = {
485
- running: "dispatched",
486
- stalled: "stalled",
487
- succeeded: "succeeded",
488
- failed: "failed",
489
- needs_human: "needs_human",
490
- cancelled: "cancelled"
491
- };
492
- const changeReceiptStates = {
493
- ready: "ready",
494
- merged: "merged",
495
- deployed: "deployed",
496
- rolled_out: "rolled_out",
497
- rolled_back: "rolled_back"
498
- };
499
- function randomId(prefix) {
500
- return `${prefix}_${randomUUID().replaceAll("-", "")}`;
501
- }
502
- function derivedId(prefix, key) {
503
- return `${prefix}_${createHash("sha256").update(key).digest("hex").slice(0, 32)}`;
504
- }
505
- function bumpsAttempt(from, to) {
506
- return to === "queued" || from === "verifying" && to === "running";
507
- }
508
- const emptyUsage = {
509
- costUsd: 0,
510
- inputTokens: 0,
511
- outputTokens: 0,
512
- cacheReadTokens: 0,
513
- cacheWriteTokens: 0,
514
- steps: {}
515
- };
516
- function addUsd(a, b) {
517
- return (Math.round(a * 1e6) + Math.round(b * 1e6)) / 1e6;
518
- }
519
- function createStores(options) {
520
- const { driver } = options;
521
- const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
522
- async function read(collection, id, schema) {
523
- const record = await driver.get(collection, id);
524
- if (record === null) return null;
525
- return {
526
- value: schema.parse(record.value),
527
- version: record.version
528
- };
529
- }
530
- async function mustRead(collection, id, schema) {
531
- const record = await read(collection, id, schema);
532
- if (record === null) throw new RecordNotFoundError(collection, id);
533
- return record;
534
- }
535
- async function listAll(collection, schema) {
536
- return (await driver.list(collection)).map((entry) => schema.parse(entry.record.value));
537
- }
538
- async function insertReceipt(receiptId, receipt) {
539
- const full = receiptSchema.parse({
540
- ...receipt,
541
- schemaVersion: 1,
542
- receiptId,
543
- recordedAt: now()
544
- });
545
- try {
546
- await driver.insert("receipts", receiptId, full);
547
- return full;
548
- } catch (error) {
549
- if (error instanceof RecordExistsError) return (await mustRead("receipts", receiptId, receiptSchema)).value;
550
- throw error;
551
- }
552
- }
553
- async function appendReceipt(key, receipt) {
554
- return insertReceipt(derivedId("rcpt", key), receipt);
555
- }
556
- return {
557
- driverName: driver.name,
558
- repositories: {
559
- async put(repository) {
560
- const parsed = repositorySchema.parse(repository);
561
- const existing = await read("repositories", parsed.id, repositorySchema);
562
- if (existing === null) await driver.insert("repositories", parsed.id, parsed);
563
- else await driver.update("repositories", parsed.id, parsed, existing.version);
564
- return parsed;
565
- },
566
- async get(id) {
567
- const record = await read("repositories", id, repositorySchema);
568
- return record === null ? null : record.value;
569
- },
570
- list() {
571
- return listAll("repositories", repositorySchema);
572
- }
573
- },
574
- signals: {
575
- async create(input) {
576
- const id = derivedId("sig", `${input.source}:${input.deliveryId}`);
577
- const signal = signalSchema.parse({
578
- schemaVersion: 1,
579
- id,
580
- repositoryId: input.repositoryId,
581
- source: input.source,
582
- deliveryId: input.deliveryId,
583
- replyTo: input.replyTo,
584
- payload: input.payload,
585
- receivedAt: now(),
586
- outcome: "pending"
587
- });
588
- try {
589
- await driver.insert("signals", id, signal);
590
- return signal;
591
- } catch (error) {
592
- if (error instanceof RecordExistsError) return (await mustRead("signals", id, signalSchema)).value;
593
- throw error;
594
- }
595
- },
596
- async get(id) {
597
- const record = await read("signals", id, signalSchema);
598
- return record === null ? null : record.value;
599
- },
600
- async decide(id, outcome, decideOptions = {}) {
601
- const existing = await mustRead("signals", id, signalSchema);
602
- assertTransition(signalOutcomeTransitions, existing.value.outcome, outcome);
603
- const next = signalSchema.parse({
604
- ...existing.value,
605
- outcome,
606
- detail: decideOptions.detail,
607
- reason: decideOptions.reason,
608
- decidedAt: now()
609
- });
610
- await driver.update("signals", id, next, existing.version);
611
- return next;
612
- },
613
- list() {
614
- return listAll("signals", signalSchema);
615
- }
616
- },
617
- tasks: {
618
- async create(input) {
619
- const timestamp = now();
620
- const id = input.dedupeKey === void 0 ? randomId("task") : derivedId("task", input.dedupeKey);
621
- const task = taskSchema.parse({
622
- schemaVersion: 1,
623
- id,
624
- repositoryIds: input.repositoryIds,
625
- kind: input.kind,
626
- area: input.area,
627
- state: taskInitialState,
628
- approval: input.approval,
629
- origin: input.origin,
630
- dependsOn: input.dependsOn,
631
- replyTo: input.replyTo,
632
- attempt: 1,
633
- question: input.question,
634
- changeId: input.changeId,
635
- createdAt: timestamp,
636
- updatedAt: timestamp
637
- });
638
- try {
639
- await driver.insert("tasks", task.id, task);
640
- return task;
641
- } catch (error) {
642
- if (input.dedupeKey !== void 0 && error instanceof RecordExistsError) return (await mustRead("tasks", task.id, taskSchema)).value;
643
- throw error;
644
- }
645
- },
646
- async approve(id, decision) {
647
- const existing = await mustRead("tasks", id, taskSchema);
648
- const task = existing.value;
649
- if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
650
- if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
651
- const next = taskSchema.parse({
652
- ...task,
653
- approval: "granted",
654
- updatedAt: now()
655
- });
656
- if (next.repositoryIds !== void 0) await appendReceipt(`task:${id}:approved:${next.attempt}`, {
657
- repositoryIds: next.repositoryIds,
658
- taskId: id,
659
- kind: next.kind,
660
- state: "approved",
661
- phase: "approval",
662
- reason: decision.reason,
663
- attempt: next.attempt,
664
- decidedBy: decision.by
665
- });
666
- await driver.update("tasks", id, next, existing.version);
667
- return next;
668
- },
669
- async reject(id, decision) {
670
- const existing = await mustRead("tasks", id, taskSchema);
671
- const task = existing.value;
672
- if (task.approval !== "required") throw new ApprovalError(id, `approval is ${task.approval ?? "not required"}`);
673
- if (task.state !== "queued") throw new ApprovalError(id, `task is ${task.state}, not queued`);
674
- const next = taskSchema.parse({
675
- ...task,
676
- approval: "denied",
677
- state: "cancelled",
678
- updatedAt: now()
679
- });
680
- if (next.repositoryIds !== void 0) {
681
- await appendReceipt(`task:${id}:rejected:${next.attempt}`, {
682
- repositoryIds: next.repositoryIds,
683
- taskId: id,
684
- kind: next.kind,
685
- state: "rejected",
686
- phase: "approval",
687
- reason: decision.reason,
688
- attempt: next.attempt,
689
- decidedBy: decision.by
690
- });
691
- await appendReceipt(`task:${id}:cancelled:${next.attempt}`, {
692
- repositoryIds: next.repositoryIds,
693
- taskId: id,
694
- kind: next.kind,
695
- state: "cancelled",
696
- reason: decision.reason ?? `rejected by ${decision.by}`,
697
- attempt: next.attempt
698
- });
699
- }
700
- await driver.update("tasks", id, next, existing.version);
701
- return next;
702
- },
703
- async recordExecution(id, execution) {
704
- const existing = await mustRead("tasks", id, taskSchema);
705
- const task = existing.value;
706
- if (task.state !== "running") throw new InvalidTransitionError(task.state, "running", legalNextStates(taskTransitions, task.state));
707
- const timestamp = now();
708
- const next = taskSchema.parse({
709
- ...task,
710
- execution,
711
- lastProgressAt: timestamp,
712
- updatedAt: timestamp
713
- });
714
- await driver.update("tasks", id, next, existing.version);
715
- return next;
716
- },
717
- async recordUsage(id, step) {
718
- for (;;) {
719
- const existing = await mustRead("tasks", id, taskSchema);
720
- const task = existing.value;
721
- const usage = task.usage ?? emptyUsage;
722
- const recordedIndex = usage.steps[step.turnId];
723
- if (recordedIndex !== void 0 && step.stepIndex <= recordedIndex) return task;
724
- const timestamp = now();
725
- const next = taskSchema.parse({
726
- ...task,
727
- lastProgressAt: timestamp,
728
- usage: {
729
- costUsd: addUsd(usage.costUsd, step.costUsd ?? 0),
730
- inputTokens: usage.inputTokens + (step.inputTokens ?? 0),
731
- outputTokens: usage.outputTokens + (step.outputTokens ?? 0),
732
- cacheReadTokens: usage.cacheReadTokens + (step.cacheReadTokens ?? 0),
733
- cacheWriteTokens: usage.cacheWriteTokens + (step.cacheWriteTokens ?? 0),
734
- steps: {
735
- ...usage.steps,
736
- [step.turnId]: step.stepIndex
737
- }
738
- },
739
- updatedAt: timestamp
740
- });
741
- try {
742
- await driver.update("tasks", id, next, existing.version);
743
- return next;
744
- } catch (error) {
745
- if (!(error instanceof VersionConflictError)) throw error;
746
- }
747
- }
748
- },
749
- async settle(id, cost, settleOptions = {}) {
750
- const existing = await mustRead("tasks", id, taskSchema);
751
- const task = existing.value;
752
- if (task.settledUsd !== void 0) return task;
753
- if (task.state !== "succeeded" && task.state !== "failed" && task.state !== "cancelled") throw new SettlementError(id, `task is ${task.state}, not terminal`);
754
- const validated = costSchema.parse(cost);
755
- const normalized = { totalUsd: Math.round(validated.totalUsd * 1e6) / 1e6 };
756
- let recorded = normalized;
757
- if (task.repositoryIds !== void 0) recorded = (await appendReceipt(`task:${id}:settled:${task.attempt}`, {
758
- repositoryIds: task.repositoryIds,
759
- taskId: id,
760
- changeId: task.changeId,
761
- kind: task.kind,
762
- state: "settled",
763
- phase: "budget",
764
- reason: settleOptions.reason,
765
- attempt: task.attempt,
766
- cost: normalized
767
- })).cost ?? normalized;
768
- const next = taskSchema.parse({
769
- ...task,
770
- settledUsd: recorded.totalUsd,
771
- updatedAt: now()
772
- });
773
- await driver.update("tasks", id, next, existing.version);
774
- return next;
775
- },
776
- async get(id) {
777
- const record = await read("tasks", id, taskSchema);
778
- return record === null ? null : record.value;
779
- },
780
- async transition(id, to, transitionOptions = {}) {
781
- if (transitionOptions.patch !== void 0 && "settledUsd" in transitionOptions.patch) throw new SettlementError(id, "settledUsd is set only by settle, not a transition");
782
- if (transitionOptions.patch !== void 0 && "usage" in transitionOptions.patch) throw new SettlementError(id, "usage is set only by recordUsage, not a transition");
783
- const existing = await mustRead("tasks", id, taskSchema);
784
- if (transitionOptions.expectFrom !== void 0 && existing.value.state !== transitionOptions.expectFrom) throw new InvalidTransitionError(existing.value.state, to, legalNextStates(taskTransitions, existing.value.state));
785
- assertTransition(taskTransitions, existing.value.state, to);
786
- if (to === "needs_human" && existing.value.kind !== "question") {
787
- if (!(await listAll("tasks", taskSchema)).some((task) => task.kind === "question" && task.state === "needs_human" && task.origin.parentTaskId === id)) throw new Error(`task ${id} cannot enter needs_human without an open QuestionTask`);
788
- }
789
- const bumps = bumpsAttempt(existing.value.state, to);
790
- const timestamp = now();
791
- const { stalledAt: _stalledAt, stalledReason: _stalledReason, ...cleared } = existing.value;
792
- const next = taskSchema.parse({
793
- ...cleared,
794
- ...bumps ? { execution: void 0 } : {},
795
- ...transitionOptions.patch,
796
- ...to === "stalled" ? {
797
- stalledAt: timestamp,
798
- stalledReason: transitionOptions.reason
799
- } : {},
800
- ...to === "running" ? { lastProgressAt: timestamp } : {},
801
- state: to,
802
- attempt: bumps ? existing.value.attempt + 1 : existing.value.attempt,
803
- updatedAt: timestamp
804
- });
805
- const receiptState = taskReceiptStates[to];
806
- if (receiptState !== void 0 && next.repositoryIds !== void 0) await appendReceipt(JSON.stringify([
807
- "task",
808
- id,
809
- receiptState,
810
- next.attempt,
811
- next.changeId ?? null,
812
- transitionOptions.reason ?? null
813
- ]), {
814
- repositoryIds: next.repositoryIds,
815
- taskId: id,
816
- changeId: next.changeId,
817
- kind: next.kind,
818
- state: receiptState,
819
- reason: transitionOptions.reason,
820
- attempt: next.attempt
821
- });
822
- await driver.update("tasks", id, next, existing.version);
823
- return next;
824
- },
825
- list() {
826
- return listAll("tasks", taskSchema);
827
- }
828
- },
829
- changes: {
830
- async create(input) {
831
- const timestamp = now();
832
- const change = changeSchema.parse({
833
- schemaVersion: 1,
834
- id: randomId("chg"),
835
- repositoryId: input.repositoryId,
836
- taskId: input.taskId,
837
- groupId: input.groupId,
838
- prNumber: input.prNumber,
839
- prUrl: input.prUrl,
840
- author: input.author,
841
- risk: input.risk,
842
- confidence: input.confidence,
843
- evidence: input.evidence,
844
- state: changeInitialState,
845
- createdAt: timestamp,
846
- updatedAt: timestamp
847
- });
848
- await mustRead("repositories", change.repositoryId, repositorySchema);
849
- if (change.taskId !== void 0) {
850
- const task = (await mustRead("tasks", change.taskId, taskSchema)).value;
851
- if (task.kind !== "code_change") throw new ChangeAssociationError(change.taskId, change.repositoryId, "changes can only be produced by code_change tasks");
852
- const taskRepositoryId = task.repositoryIds[0];
853
- if (taskRepositoryId !== change.repositoryId) throw new ChangeAssociationError(change.taskId, change.repositoryId, `repository "${change.repositoryId}" does not match task repository "${taskRepositoryId}"`);
854
- }
855
- await driver.insert("changes", change.id, change);
856
- return change;
857
- },
858
- async get(id) {
859
- const record = await read("changes", id, changeSchema);
860
- return record === null ? null : record.value;
861
- },
862
- async transition(id, to, transitionOptions = {}) {
863
- const existing = await mustRead("changes", id, changeSchema);
864
- assertTransition(changeTransitions, existing.value.state, to);
865
- const next = changeSchema.parse({
866
- ...existing.value,
867
- ...transitionOptions.patch,
868
- state: to,
869
- updatedAt: now()
870
- });
871
- const receiptState = changeReceiptStates[to];
872
- if (receiptState !== void 0) await appendReceipt(`change:${id}:${receiptState}`, {
873
- repositoryIds: [next.repositoryId],
874
- taskId: next.taskId,
875
- changeId: id,
876
- kind: "change",
877
- state: receiptState,
878
- reason: transitionOptions.reason,
879
- attempt: 1,
880
- decidedBy: next.decidedBy
881
- });
882
- await driver.update("changes", id, next, existing.version);
883
- return next;
884
- },
885
- list() {
886
- return listAll("changes", changeSchema);
887
- }
888
- },
889
- receipts: {
890
- append(input) {
891
- const { dedupeKey, attempt, ...fields } = input;
892
- return insertReceipt(dedupeKey === void 0 ? randomId("rcpt") : derivedId("rcpt", `custom:${dedupeKey}`), {
893
- ...fields,
894
- attempt: attempt ?? 1
895
- });
896
- },
897
- async get(id) {
898
- const record = await read("receipts", id, receiptSchema);
899
- return record === null ? null : record.value;
900
- },
901
- list() {
902
- return listAll("receipts", receiptSchema);
903
- }
904
- },
905
- budgets: {
906
- async ensure(input) {
907
- const budget = budgetSchema.parse({
908
- schemaVersion: 1,
909
- factoryId: input.factoryId,
910
- month: input.month,
911
- limits: input.limits,
912
- notices: {}
913
- });
914
- try {
915
- await driver.insert("budgets", input.month, budget);
916
- return budget;
917
- } catch (error) {
918
- if (!(error instanceof RecordExistsError)) throw error;
919
- }
920
- const existing = await mustRead("budgets", input.month, budgetSchema);
921
- const refreshed = budgetSchema.parse({
922
- ...existing.value,
923
- limits: input.limits
924
- });
925
- if (JSON.stringify(refreshed) === JSON.stringify(existing.value)) return existing.value;
926
- try {
927
- await driver.update("budgets", input.month, refreshed, existing.version);
928
- return refreshed;
929
- } catch (error) {
930
- if (error instanceof VersionConflictError) return (await mustRead("budgets", input.month, budgetSchema)).value;
931
- throw error;
932
- }
933
- },
934
- async get(month) {
935
- const record = await read("budgets", month, budgetSchema);
936
- return record === null ? null : record.value;
937
- },
938
- async stampNotice(month, notice) {
939
- const field = notice === "warning" ? "warningAt" : "exhaustedAt";
940
- for (;;) {
941
- const existing = await mustRead("budgets", month, budgetSchema);
942
- if (existing.value.notices[field] !== void 0) return {
943
- budget: existing.value,
944
- stamped: false
945
- };
946
- const next = budgetSchema.parse({
947
- ...existing.value,
948
- notices: {
949
- ...existing.value.notices,
950
- [field]: now()
951
- }
952
- });
953
- try {
954
- await driver.update("budgets", month, next, existing.version);
955
- return {
956
- budget: next,
957
- stamped: true
958
- };
959
- } catch (error) {
960
- if (!(error instanceof VersionConflictError)) throw error;
961
- }
962
- }
963
- }
964
- }
965
- };
966
- }
967
- //#endregion
968
343
  //#region src/store/memory.ts
969
344
  function createInMemoryDriver() {
970
345
  const collections = /* @__PURE__ */ new Map();
@@ -1013,6 +388,27 @@ function createInMemoryDriver() {
1013
388
  }
1014
389
  //#endregion
1015
390
  //#region src/sweep.ts
391
+ async function failUnrouteable(stores, task, reason, result) {
392
+ try {
393
+ await stores.tasks.transition(task.id, "failed", {
394
+ reason,
395
+ expectFrom: "queued"
396
+ });
397
+ result.failed.push({
398
+ taskId: task.id,
399
+ reason
400
+ });
401
+ } catch (error) {
402
+ if (error instanceof InvalidTransitionError || error instanceof VersionConflictError) {
403
+ result.skipped.push({
404
+ taskId: task.id,
405
+ reason: "task changed during routing"
406
+ });
407
+ return;
408
+ }
409
+ throw error;
410
+ }
411
+ }
1016
412
  /**
1017
413
  * One pass of the factory's dispatcher, meant to run on a schedule inside the
1018
414
  * deployment: every queued task that is allowed to run is started, in
@@ -1027,7 +423,10 @@ async function sweepQueued(stores, options) {
1027
423
  skipped: [],
1028
424
  failed: []
1029
425
  };
1030
- const queued = (await stores.tasks.list()).filter((task) => task.state === "queued" && task.kind !== "question").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
426
+ const maxDispatches = options.maxDispatches ?? Number.POSITIVE_INFINITY;
427
+ if (!(Number.isInteger(maxDispatches) || maxDispatches === Number.POSITIVE_INFINITY) || maxDispatches < 1) throw new Error("maxDispatches must be a positive integer");
428
+ let attempted = 0;
429
+ const queued = (await stores.tasks.list()).filter((task) => task.state === "queued" && task.kind !== "question").sort((a, b) => b.attempt - a.attempt || a.createdAt.localeCompare(b.createdAt));
1031
430
  for (const task of queued) {
1032
431
  if (task.kind === "coordination") {
1033
432
  result.skipped.push({
@@ -1038,10 +437,7 @@ async function sweepQueued(stores, options) {
1038
437
  }
1039
438
  const executionRepositoryIds = taskExecutionRepositoryIdsSchema.safeParse(task.repositoryIds);
1040
439
  if (!executionRepositoryIds.success) {
1041
- result.skipped.push({
1042
- taskId: task.id,
1043
- reason: "task execution requires exactly one repository"
1044
- });
440
+ await failUnrouteable(stores, task, "task execution requires exactly one repository", result);
1045
441
  continue;
1046
442
  }
1047
443
  if (task.approval === "required") {
@@ -1053,20 +449,22 @@ async function sweepQueued(stores, options) {
1053
449
  }
1054
450
  const launcher = options.launchers[task.kind];
1055
451
  if (launcher === void 0) {
1056
- result.skipped.push({
1057
- taskId: task.id,
1058
- reason: `no agent handles ${task.kind} tasks`
1059
- });
452
+ await failUnrouteable(stores, task, `no registered launcher handles ${task.kind} tasks`, result);
1060
453
  continue;
1061
454
  }
1062
455
  const repository = await stores.repositories.get(executionRepositoryIds.data[0]);
1063
456
  if (repository === null) {
457
+ await failUnrouteable(stores, task, "task has no repository", result);
458
+ continue;
459
+ }
460
+ if (attempted >= maxDispatches) {
1064
461
  result.skipped.push({
1065
462
  taskId: task.id,
1066
- reason: "task has no repository"
463
+ reason: "per-sweep dispatch limit reached"
1067
464
  });
1068
465
  continue;
1069
466
  }
467
+ attempted += 1;
1070
468
  try {
1071
469
  await dispatchTask(stores, task.id, {
1072
470
  launch: (dispatched) => launcher(dispatched, repository),
@@ -1119,6 +517,6 @@ async function ensureVerificationTasks(stores) {
1119
517
  return result;
1120
518
  }
1121
519
  //#endregion
1122
- export { ApprovalError, BudgetExhaustedError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, SettlementError, VersionConflictError, adoptPullRequest, agentRouteSchema, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetLimitsSchema, budgetMonthOf, budgetMonthSchema, budgetSchema, budgetStatus, canReserve, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, computeLedgerMetrics, computeMonthSpendUsd, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createCoordination, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defaultExecutionPolicy, defineAgentRoutes, defineFactory, deriveFactoryId, dispatchTask, ensureVerificationTasks, executionPolicySchema, executionReferenceSchema, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, reconcileCoordination, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, stallWindowMinutes, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepBudget, sweepQueued, sweepSettlements, sweepStalled, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema, terminalStates, usdSchema, verificationBrief, vocabularySchema };
520
+ export { AnswerEventConflictError, ApprovalError, BudgetExhaustedError, ChangeAssociationError, DEFAULT_MAX_LAUNCH_ATTEMPTS, DispatchRefusedError, InvalidTransitionError, OutboxEventConflictError, RecordExistsError, RecordNotFoundError, SCHEMA_VERSION, SessionConflictError, SettlementError, VersionConflictError, adoptPullRequest, agentRouteSchema, agentSessionIdSchema, agentSessionSchema, answerEventSchema, answerIdSchema, approvalPolicySchema, approvalRuleSchema, assertTransition, budgetLimitsSchema, budgetMonthOf, budgetMonthSchema, budgetSchema, budgetStatus, canReserve, changeEvidenceSchema, changeIdSchema, changeInitialState, changeSchema, changeStateSchema, changeTransitions, communicationEventSchema, communicationOutcomeSchema, communicationOutcomes, computeLedgerMetrics, computeMonthSpendUsd, computeRepositoryHealth, coordinationRepositoryIdsSchema, costSchema, createCoordination, createInMemoryDriver, createSignalCreator, createStores, defaultApprovalPolicy, defaultExecutionPolicy, defineAgentRoutes, defineFactory, deriveFactoryId, dispatchTask, ensureVerificationTasks, executionPolicySchema, executionReferenceSchema, factoryConfigRepositorySchema, factoryConfigSchema, factoryIdSchema, findingCategorySchema, findingFingerprint, findingForTask, findingPrioritySchema, findingSchema, findingVerificationLevelSchema, findingVerificationMethodSchema, findingVerificationSchema, fixBrief, harnessSchema, isTransitionAllowed, isoDateTimeSchema, legalNextStates, levelSchema, outboxIdSchema, outboxMessageSchema, outboxQueueSchema, outboxStateSchema, receiptIdSchema, receiptKindSchema, receiptPhaseSchema, receiptSchema, receiptStateSchema, reconcileCoordination, replyToSchema, repositoryIdSchema, repositoryIdsSchema, repositorySchema, repositorySlugSchema, requiresApproval, reviewBucket, scanBrief, schemaVersionSchema, signalIdSchema, signalInitialOutcome, signalOutcomeSchema, signalOutcomeTransitions, signalSchema, signalSourceSchema, stallWindowMinutes, standardReceiptKinds, standardReceiptPhases, standardReceiptStates, standardSignalSources, standardTaskKinds, sweepBudget, sweepQueued, sweepSettlements, sweepStalled, taskApprovalSchema, taskExecutionRepositoryIdsSchema, taskIdSchema, taskInitialState, taskKindSchema, taskOriginSchema, taskQuestionSchema, taskSchema, taskStateSchema, taskTransitions, taskUsageSchema, terminalStates, usdSchema, verificationBrief, vocabularySchema };
1123
521
 
1124
522
  //# sourceMappingURL=index.mjs.map