@mastra/factory 0.10.0-alpha.2 → 0.10.0-alpha.3

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 (47) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/factory.js +2 -2
  3. package/dist/integrations/github/rules.d.ts.map +1 -1
  4. package/dist/integrations/github/rules.js +193 -127
  5. package/dist/integrations/github/rules.js.map +1 -1
  6. package/dist/routes/attention.d.ts +17 -0
  7. package/dist/routes/attention.d.ts.map +1 -0
  8. package/dist/routes/attention.js +305 -0
  9. package/dist/routes/attention.js.map +1 -0
  10. package/dist/routes/surface.d.ts +2 -2
  11. package/dist/routes/surface.d.ts.map +1 -1
  12. package/dist/routes/surface.js +56 -35
  13. package/dist/routes/surface.js.map +1 -1
  14. package/dist/routes/work-items.d.ts.map +1 -1
  15. package/dist/routes/work-items.js +34 -13
  16. package/dist/routes/work-items.js.map +1 -1
  17. package/dist/rules/dispatch-errors.d.ts +13 -0
  18. package/dist/rules/dispatch-errors.d.ts.map +1 -0
  19. package/dist/rules/dispatch-errors.js +77 -0
  20. package/dist/rules/dispatch-errors.js.map +1 -0
  21. package/dist/rules/dispatcher.d.ts.map +1 -1
  22. package/dist/rules/dispatcher.js +52 -31
  23. package/dist/rules/dispatcher.js.map +1 -1
  24. package/dist/rules/processor.d.ts.map +1 -1
  25. package/dist/rules/processor.js +2 -3
  26. package/dist/rules/processor.js.map +1 -1
  27. package/dist/rules/terminal-cleanup.d.ts +1 -1
  28. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  29. package/dist/rules/terminal-cleanup.js +2 -2
  30. package/dist/rules/terminal-cleanup.js.map +1 -1
  31. package/dist/rules/types.d.ts +2 -0
  32. package/dist/rules/types.d.ts.map +1 -1
  33. package/dist/rules/types.js +9 -1
  34. package/dist/rules/types.js.map +1 -1
  35. package/dist/session/factory-session.d.ts +4 -0
  36. package/dist/session/factory-session.d.ts.map +1 -1
  37. package/dist/session/factory-session.js +10 -2
  38. package/dist/session/factory-session.js.map +1 -1
  39. package/dist/storage/domains/source-control/base.d.ts +3 -0
  40. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  41. package/dist/storage/domains/source-control/base.js +8 -2
  42. package/dist/storage/domains/source-control/base.js.map +1 -1
  43. package/dist/storage/domains/work-items/base.d.ts +99 -10
  44. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  45. package/dist/storage/domains/work-items/base.js +449 -35
  46. package/dist/storage/domains/work-items/base.js.map +1 -1
  47. package/package.json +9 -9
@@ -1,3 +1,4 @@
1
+ import { isTerminalFactoryRuleStage } from "../../../rules/types.js";
1
2
  import { createHash } from "crypto";
2
3
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
3
4
  //#region src/storage/domains/work-items/base.ts
@@ -20,6 +21,7 @@ function factoryDecisionHash(decision) {
20
21
  }
21
22
  /** Dispatcher upsert idempotency token — server bookkeeping, dropped from the read wire. */
22
23
  const FACTORY_RULE_MATERIALIZATION_KEY = "factoryRuleMaterializationKey";
24
+ const FACTORY_PULL_REQUEST_RECONCILIATION_KEY = "factoryPullRequestReconciliation";
23
25
  /**
24
26
  * Whether an actor id marks a move an agent run performed: the binding id the
25
27
  * transition tool stamps (`agent:*`), or the rule that fires off a bound run's
@@ -34,6 +36,35 @@ function isAgentActor(by) {
34
36
  if (by === void 0) return false;
35
37
  return by.startsWith("agent:") || by === "factory-tool-result-rule";
36
38
  }
39
+ const FACTORY_DISPATCH_FAILURE_CODES = [
40
+ "session_unavailable",
41
+ "source_control_missing",
42
+ "source_repository_missing",
43
+ "unsupported_provider_item",
44
+ "notification_delivery_failed",
45
+ "repository_git_missing",
46
+ "repository_egress_blocked",
47
+ "repository_clone_failed",
48
+ "repository_pull_failed",
49
+ "repository_push_failed",
50
+ "repository_commit_failed",
51
+ "repository_cli_missing",
52
+ "repository_pr_failed",
53
+ "unknown"
54
+ ];
55
+ function isFactoryDispatchFailureCode(value) {
56
+ return FACTORY_DISPATCH_FAILURE_CODES.some((code) => code === value);
57
+ }
58
+ function factoryDecisionAttentionIdentity(decisionId, failureOccurrence) {
59
+ return {
60
+ kind: "automation-failed",
61
+ sourceId: decisionId,
62
+ occurrence: failureOccurrence
63
+ };
64
+ }
65
+ function factoryAttentionKey(factoryProjectId, identity) {
66
+ return `factory:${factoryProjectId}:attention:${identity.kind}:${identity.sourceId}:${identity.occurrence}`;
67
+ }
37
68
  /**
38
69
  * Stages in which a bound run can still act on its work item. Mirrors the
39
70
  * non-terminal subset of `FACTORY_RULE_STAGES` (rules/types.ts); bindings for
@@ -223,6 +254,8 @@ function applyUpdate({ current, userId, input }) {
223
254
  updated_at: now
224
255
  };
225
256
  }
257
+ const ATTENTION_RECEIPT_QUERY_BATCH_SIZE = 200;
258
+ const ATTENTION_RECEIPT_WRITE_BATCH_SIZE = 25;
226
259
  const projectRelationLocks = /* @__PURE__ */ new Map();
227
260
  function withInProcessProjectLock(key, fn) {
228
261
  const result = (projectRelationLocks.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -240,6 +273,13 @@ function decisionSourceKey(decision) {
240
273
  if (record.type !== "upsertLinkedWorkItem" || typeof record.sourceKey !== "string") return null;
241
274
  return record.sourceKey;
242
275
  }
276
+ function deferredDecisionType(decision) {
277
+ return typeof decision.decision.type === "string" ? decision.decision.type : void 0;
278
+ }
279
+ function ingressWasAccepted(row) {
280
+ if (!row || typeof row.result !== "object" || row.result === null || Array.isArray(row.result)) return false;
281
+ return "status" in row.result && row.result.status === "accepted";
282
+ }
243
283
  const FACTORY_GOVERNANCE_SCHEMAS = [
244
284
  {
245
285
  name: "factory_rule_ingress",
@@ -315,6 +355,10 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
315
355
  decision: { type: "json" },
316
356
  status: { type: "text" },
317
357
  attempts: { type: "integer" },
358
+ failure_occurrence: {
359
+ type: "integer",
360
+ default: 0
361
+ },
318
362
  available_at: { type: "timestamp" },
319
363
  lease_owner: {
320
364
  type: "text",
@@ -328,6 +372,10 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
328
372
  type: "text",
329
373
  nullable: true
330
374
  },
375
+ failure_code: {
376
+ type: "text",
377
+ nullable: true
378
+ },
331
379
  approved_at: {
332
380
  type: "timestamp",
333
381
  nullable: true
@@ -347,9 +395,80 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
347
395
  "idempotency_key"
348
396
  ]
349
397
  }],
398
+ indexes: [
399
+ {
400
+ name: "factory_deferred_decisions_claim_idx",
401
+ columns: ["status", "created_at"]
402
+ },
403
+ {
404
+ name: "factory_deferred_decisions_tenant_status_created_idx",
405
+ columns: [
406
+ "org_id",
407
+ "factory_project_id",
408
+ "status",
409
+ "created_at",
410
+ "id"
411
+ ]
412
+ },
413
+ {
414
+ name: "factory_deferred_decisions_tenant_status_failed_idx",
415
+ columns: [
416
+ "org_id",
417
+ "factory_project_id",
418
+ "status",
419
+ "updated_at",
420
+ "id"
421
+ ]
422
+ }
423
+ ]
424
+ },
425
+ {
426
+ name: "factory_attention_receipts",
427
+ columns: {
428
+ id: { type: "uuid-pk" },
429
+ org_id: { type: "text" },
430
+ factory_project_id: { type: "text" },
431
+ user_id: { type: "text" },
432
+ kind: { type: "text" },
433
+ source_id: { type: "text" },
434
+ occurrence: { type: "integer" },
435
+ state: { type: "text" },
436
+ read_at: { type: "timestamp" },
437
+ archived_at: {
438
+ type: "timestamp",
439
+ nullable: true
440
+ },
441
+ created_at: { type: "timestamp" },
442
+ updated_at: { type: "timestamp" }
443
+ },
444
+ uniqueIndexes: [{
445
+ name: "factory_attention_receipts_user_source_unique",
446
+ columns: [
447
+ "org_id",
448
+ "factory_project_id",
449
+ "user_id",
450
+ "kind",
451
+ "source_id",
452
+ "occurrence"
453
+ ]
454
+ }],
350
455
  indexes: [{
351
- name: "factory_deferred_decisions_claim_idx",
352
- columns: ["status", "created_at"]
456
+ name: "factory_attention_receipts_user_state_idx",
457
+ columns: [
458
+ "org_id",
459
+ "factory_project_id",
460
+ "user_id",
461
+ "state"
462
+ ]
463
+ }, {
464
+ name: "factory_attention_receipts_source_idx",
465
+ columns: [
466
+ "org_id",
467
+ "factory_project_id",
468
+ "kind",
469
+ "source_id",
470
+ "occurrence"
471
+ ]
353
472
  }]
354
473
  },
355
474
  {
@@ -477,16 +596,50 @@ function toDeferredDecision(row) {
477
596
  decision: row.decision,
478
597
  status: row.status,
479
598
  attempts: Number(row.attempts),
599
+ failureOccurrence: Number(row.failure_occurrence ?? 0),
480
600
  availableAt: row.available_at,
481
601
  leaseOwner: row.lease_owner ?? null,
482
602
  leaseExpiresAt: row.lease_expires_at ?? null,
483
603
  lastError: row.last_error ?? null,
604
+ failureCode: isFactoryDispatchFailureCode(row.failure_code) ? row.failure_code : null,
484
605
  approvedAt: row.approved_at ?? null,
485
606
  completedAt: row.completed_at ?? null,
486
607
  createdAt: row.created_at,
487
608
  updatedAt: row.updated_at
488
609
  };
489
610
  }
611
+ function attentionReceiptKind(value) {
612
+ if (value === "automation-failed") return value;
613
+ throw new Error(`Unsupported attention receipt kind '${String(value)}'.`);
614
+ }
615
+ function attentionReceiptState(value) {
616
+ if (value === "read" || value === "archived") return value;
617
+ throw new Error(`Unsupported attention receipt state '${String(value)}'.`);
618
+ }
619
+ function toAttentionReceipt(row) {
620
+ if (typeof row.source_id !== "string" || !row.source_id) throw new Error("Attention receipt source_id must be a non-empty string.");
621
+ const occurrence = Number(row.occurrence);
622
+ if (!Number.isSafeInteger(occurrence) || occurrence < 0) throw new Error("Attention receipt occurrence must be a non-negative safe integer.");
623
+ if (!(row.read_at instanceof Date)) throw new Error("Attention receipt read_at must be a timestamp.");
624
+ const archivedAt = row.archived_at;
625
+ if (archivedAt !== null && archivedAt !== void 0 && !(archivedAt instanceof Date)) throw new Error("Attention receipt archived_at must be a timestamp or null.");
626
+ if (!(row.created_at instanceof Date)) throw new Error("Attention receipt created_at must be a timestamp.");
627
+ if (!(row.updated_at instanceof Date)) throw new Error("Attention receipt updated_at must be a timestamp.");
628
+ return {
629
+ id: row.id,
630
+ orgId: row.org_id,
631
+ factoryProjectId: String(row.factory_project_id),
632
+ userId: String(row.user_id),
633
+ kind: attentionReceiptKind(row.kind),
634
+ sourceId: row.source_id,
635
+ occurrence,
636
+ state: attentionReceiptState(row.state),
637
+ readAt: row.read_at,
638
+ archivedAt: archivedAt ?? null,
639
+ createdAt: row.created_at,
640
+ updatedAt: row.updated_at
641
+ };
642
+ }
490
643
  function toPendingStart(row) {
491
644
  return {
492
645
  id: row.id,
@@ -512,6 +665,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
512
665
  }
513
666
  async init() {
514
667
  await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
668
+ await this.repairLegacyAttentionState();
515
669
  }
516
670
  async dangerouslyClearAll() {
517
671
  await this.ops.deleteMany("work_items", {});
@@ -519,6 +673,11 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
519
673
  get #db() {
520
674
  return this.ops;
521
675
  }
676
+ async #countRows(collection, where) {
677
+ const count = this.#db.count;
678
+ if (!count) throw new Error("[WorkItemsStorage] storage backend does not support collection counts.");
679
+ return count.call(this.#db, collection, where);
680
+ }
522
681
  async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
523
682
  return withInProcessProjectLock(`work-items:${orgId}:${factoryProjectId}`, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
524
683
  }
@@ -612,7 +771,9 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
612
771
  lease_owner: null,
613
772
  lease_expires_at: null,
614
773
  last_error: input.lastError,
774
+ ...table === "factory_deferred_decisions" ? { failure_code: input.failureCode } : {},
615
775
  completed_at: input.terminal ? input.now : null,
776
+ ...table === "factory_deferred_decisions" && input.terminal ? { failure_occurrence: Number(current.failure_occurrence ?? 0) + 1 } : {},
616
777
  updated_at: input.now
617
778
  };
618
779
  });
@@ -628,6 +789,14 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
628
789
  async list({ orgId, factoryProjectId }) {
629
790
  return this.#listWithOps(this.#db, orgId, factoryProjectId);
630
791
  }
792
+ async listByIds({ orgId, factoryProjectId, ids }) {
793
+ if (ids.length === 0) return [];
794
+ return (await this.#db.findMany("work_items", {
795
+ org_id: orgId,
796
+ factory_project_id: factoryProjectId,
797
+ id: { in: [...new Set(ids)] }
798
+ })).map(toWorkItem);
799
+ }
631
800
  async get({ orgId, id }) {
632
801
  const row = await this.#db.findOne("work_items", {
633
802
  org_id: orgId,
@@ -934,6 +1103,143 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
934
1103
  hasMore: rows.length > input.limit
935
1104
  };
936
1105
  }
1106
+ async listFailedDecisionPage(input) {
1107
+ const rows = await this.#db.findMany("factory_deferred_decisions", {
1108
+ org_id: input.orgId,
1109
+ factory_project_id: input.factoryProjectId,
1110
+ status: "failed"
1111
+ }, {
1112
+ orderBy: [["updated_at", "desc"], ["id", "desc"]],
1113
+ limit: input.limit + 1,
1114
+ ...input.before ? { cursor: { values: [input.before.occurredAt, input.before.id] } } : {}
1115
+ });
1116
+ return {
1117
+ decisions: rows.slice(0, input.limit).map(toDeferredDecision),
1118
+ hasMore: rows.length > input.limit
1119
+ };
1120
+ }
1121
+ async countDeferredDecisionsByStatuses({ orgId, factoryProjectId, statuses }) {
1122
+ return this.#countRows("factory_deferred_decisions", {
1123
+ org_id: orgId,
1124
+ factory_project_id: factoryProjectId,
1125
+ status: { in: statuses }
1126
+ });
1127
+ }
1128
+ async getDeferredDecision(orgId, factoryProjectId, decisionId) {
1129
+ const row = await this.#db.findOne("factory_deferred_decisions", {
1130
+ id: decisionId,
1131
+ org_id: orgId,
1132
+ factory_project_id: factoryProjectId
1133
+ });
1134
+ return row ? toDeferredDecision(row) : null;
1135
+ }
1136
+ async listAttentionReceipts({ orgId, factoryProjectId, userId, identities }) {
1137
+ if (identities.length === 0) return [];
1138
+ const requested = new Set(identities.map((identity) => `${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`));
1139
+ const sourceIds = [...new Set(identities.map((identity) => identity.sourceId))];
1140
+ const kinds = [...new Set(identities.map((identity) => identity.kind))];
1141
+ const receipts = [];
1142
+ for (let index = 0; index < sourceIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) {
1143
+ const rows = await this.#db.findMany("factory_attention_receipts", {
1144
+ org_id: orgId,
1145
+ factory_project_id: factoryProjectId,
1146
+ user_id: userId,
1147
+ kind: { in: kinds },
1148
+ source_id: { in: sourceIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1149
+ });
1150
+ receipts.push(...rows.map(toAttentionReceipt).filter((receipt) => requested.has(`${receipt.kind}\0${receipt.sourceId}\0${receipt.occurrence}`)));
1151
+ }
1152
+ return receipts;
1153
+ }
1154
+ async countAttentionReceipts({ orgId, factoryProjectId, userId, state }) {
1155
+ return this.#countRows("factory_attention_receipts", {
1156
+ org_id: orgId,
1157
+ factory_project_id: factoryProjectId,
1158
+ user_id: userId,
1159
+ kind: "automation-failed",
1160
+ ...state ? { state } : {}
1161
+ });
1162
+ }
1163
+ async deleteAttentionReceipts({ orgId, factoryProjectId, identities }) {
1164
+ const unique = [...new Map(identities.map((identity) => [`${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`, identity])).values()];
1165
+ for (let index = 0; index < unique.length; index += ATTENTION_RECEIPT_WRITE_BATCH_SIZE) await Promise.all(unique.slice(index, index + ATTENTION_RECEIPT_WRITE_BATCH_SIZE).map((identity) => this.#db.deleteMany("factory_attention_receipts", {
1166
+ org_id: orgId,
1167
+ factory_project_id: factoryProjectId,
1168
+ kind: identity.kind,
1169
+ source_id: identity.sourceId,
1170
+ occurrence: identity.occurrence
1171
+ })));
1172
+ }
1173
+ async setAttentionReceipt(input) {
1174
+ return this.#retryAttentionReceiptWrite(() => this.storage.withTransaction((ops) => this.#setAttentionReceiptWithOps(ops, input)));
1175
+ }
1176
+ async #retryAttentionReceiptWrite(write) {
1177
+ try {
1178
+ return await write();
1179
+ } catch (error) {
1180
+ if (!(error instanceof UniqueViolationError)) throw error;
1181
+ return write();
1182
+ }
1183
+ }
1184
+ async #setAttentionReceiptWithOps(ops, { orgId, factoryProjectId, userId, decisionId, failureOccurrence, action, now }) {
1185
+ const identity = factoryDecisionAttentionIdentity(decisionId, failureOccurrence);
1186
+ let currentOccurrence = false;
1187
+ if (!await ops.updateAtomic("factory_deferred_decisions", {
1188
+ id: decisionId,
1189
+ org_id: orgId,
1190
+ factory_project_id: factoryProjectId
1191
+ }, (current) => {
1192
+ currentOccurrence = current.status === "failed" && Number(current.failure_occurrence ?? 0) === failureOccurrence;
1193
+ return null;
1194
+ }) || !currentOccurrence) return null;
1195
+ const where = {
1196
+ org_id: orgId,
1197
+ factory_project_id: factoryProjectId,
1198
+ user_id: userId,
1199
+ kind: identity.kind,
1200
+ source_id: identity.sourceId,
1201
+ occurrence: identity.occurrence
1202
+ };
1203
+ const existing = await ops.findOne("factory_attention_receipts", where);
1204
+ if (!existing) return toAttentionReceipt(await ops.insertOne("factory_attention_receipts", {
1205
+ ...where,
1206
+ state: action === "archive" ? "archived" : "read",
1207
+ read_at: now,
1208
+ archived_at: action === "archive" ? now : null,
1209
+ created_at: now,
1210
+ updated_at: now
1211
+ }));
1212
+ const row = await ops.updateAtomic("factory_attention_receipts", {
1213
+ id: existing.id,
1214
+ ...where
1215
+ }, (current) => {
1216
+ const preserveArchive = action === "read" && current.state === "archived";
1217
+ const archived = action === "archive" || preserveArchive;
1218
+ return {
1219
+ state: archived ? "archived" : "read",
1220
+ read_at: current.read_at,
1221
+ archived_at: action === "archive" ? now : archived ? current.archived_at ?? now : null,
1222
+ updated_at: now
1223
+ };
1224
+ });
1225
+ return row ? toAttentionReceipt(row) : null;
1226
+ }
1227
+ async markAttentionReceiptsRead({ orgId, factoryProjectId, userId, occurrences, now }) {
1228
+ const uniqueOccurrences = [...new Map(occurrences.map((occurrence) => [`${occurrence.decisionId}:${occurrence.failureOccurrence}`, occurrence])).values()];
1229
+ for (let index = 0; index < uniqueOccurrences.length; index += ATTENTION_RECEIPT_WRITE_BATCH_SIZE) {
1230
+ const batch = uniqueOccurrences.slice(index, index + ATTENTION_RECEIPT_WRITE_BATCH_SIZE);
1231
+ await this.#retryAttentionReceiptWrite(() => this.storage.withTransaction(async (ops) => {
1232
+ for (const occurrence of batch) await this.#setAttentionReceiptWithOps(ops, {
1233
+ orgId,
1234
+ factoryProjectId,
1235
+ userId,
1236
+ ...occurrence,
1237
+ action: "read",
1238
+ now
1239
+ });
1240
+ }));
1241
+ }
1242
+ }
937
1243
  async claimDeferredDecisions(input) {
938
1244
  return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
939
1245
  }
@@ -1014,27 +1320,38 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1014
1320
  });
1015
1321
  }
1016
1322
  /**
1017
- * Retire proposals parked on a work item. A merged pull request (or an item
1018
- * closed out any other way) leaves its queued runs with nothing left to do,
1019
- * and they would otherwise sit on the card forever asking to be answered.
1020
- *
1021
- * Pass `role` to retire only the proposals a run now starting has overtaken:
1022
- * a parked "start triage" is moot the moment triage is actually running.
1323
+ * Automatically retire proposed or failed work that a newer run or terminal
1324
+ * work-item state has overtaken. Human dismissal remains `dismissed`.
1023
1325
  */
1024
- async dismissProposalsForWorkItem(input) {
1326
+ async supersedeDecisionsForWorkItem(input) {
1025
1327
  const rows = await this.#db.findMany("factory_deferred_decisions", {
1026
1328
  org_id: input.orgId,
1027
1329
  factory_project_id: input.factoryProjectId,
1028
1330
  work_item_id: input.workItemId,
1029
- status: "proposed"
1331
+ status: { in: ["proposed", "failed"] }
1030
1332
  });
1031
- const dismissed = [];
1333
+ const superseded = [];
1032
1334
  for (const row of rows) {
1033
- if (input.role !== void 0 && toDeferredDecision(row).decision.role !== input.role) continue;
1034
- const record = await this.dismissDeferredDecision(input.orgId, input.factoryProjectId, row.id, input.dismissedAt);
1035
- if (record) dismissed.push(record);
1335
+ const decision = toDeferredDecision(row);
1336
+ if (input.role !== void 0 && decision.decision.role !== input.role) continue;
1337
+ const record = decision.status === "proposed" ? await this.#settleProposedDecision({
1338
+ orgId: input.orgId,
1339
+ factoryProjectId: input.factoryProjectId,
1340
+ decisionId: decision.id
1341
+ }, {
1342
+ status: "superseded",
1343
+ updated_at: input.supersededAt,
1344
+ completed_at: input.supersededAt
1345
+ }) : await this.#resolveFailedDecision({
1346
+ orgId: input.orgId,
1347
+ factoryProjectId: input.factoryProjectId,
1348
+ decisionId: decision.id,
1349
+ status: "superseded",
1350
+ now: input.supersededAt
1351
+ });
1352
+ if (record) superseded.push(record);
1036
1353
  }
1037
- return dismissed;
1354
+ return superseded;
1038
1355
  }
1039
1356
  async #settleProposedDecision({ orgId, factoryProjectId, decisionId }, patch) {
1040
1357
  let settled = false;
@@ -1049,28 +1366,118 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1049
1366
  });
1050
1367
  return settled && row ? toDeferredDecision(row) : null;
1051
1368
  }
1369
+ async #resolveFailedDecision({ orgId, factoryProjectId, decisionId, status, now }) {
1370
+ return this.storage.withTransaction(async (ops) => {
1371
+ let resolved = false;
1372
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1373
+ id: decisionId,
1374
+ org_id: orgId,
1375
+ factory_project_id: factoryProjectId
1376
+ }, (current) => {
1377
+ if (current.status !== "failed") return null;
1378
+ resolved = true;
1379
+ return {
1380
+ status,
1381
+ updated_at: now
1382
+ };
1383
+ });
1384
+ if (!resolved || !row) return null;
1385
+ await ops.deleteMany("factory_attention_receipts", {
1386
+ org_id: orgId,
1387
+ factory_project_id: factoryProjectId,
1388
+ kind: "automation-failed",
1389
+ source_id: decisionId,
1390
+ occurrence: Number(row.failure_occurrence ?? 0)
1391
+ });
1392
+ return toDeferredDecision(row);
1393
+ });
1394
+ }
1395
+ async supersedeTerminalDecisionsForWorkItem(input) {
1396
+ await this.supersedeDecisionsForWorkItem(input);
1397
+ }
1398
+ async repairLegacyAttentionState() {
1399
+ let cursor;
1400
+ const inspectedWorkItems = /* @__PURE__ */ new Set();
1401
+ while (true) {
1402
+ const rows = await this.#db.findMany("factory_deferred_decisions", { status: { in: ["failed", "proposed"] } }, {
1403
+ orderBy: [["created_at", "asc"], ["id", "asc"]],
1404
+ limit: 201,
1405
+ ...cursor ? { cursor } : {}
1406
+ });
1407
+ const page = rows.slice(0, ATTENTION_RECEIPT_QUERY_BATCH_SIZE);
1408
+ for (const row of page) {
1409
+ const decision = toDeferredDecision(row);
1410
+ if (decision.status === "failed" && decision.failureOccurrence === 0 && deferredDecisionType(decision) === "transition") {
1411
+ if (ingressWasAccepted(await this.#db.findOne("factory_rule_ingress", {
1412
+ org_id: decision.orgId,
1413
+ factory_project_id: decision.factoryProjectId,
1414
+ identity: `decision:${decision.idempotencyKey}`
1415
+ }))) {
1416
+ await this.#resolveFailedDecision({
1417
+ orgId: decision.orgId,
1418
+ factoryProjectId: decision.factoryProjectId,
1419
+ decisionId: decision.id,
1420
+ status: "succeeded",
1421
+ now: /* @__PURE__ */ new Date()
1422
+ });
1423
+ continue;
1424
+ }
1425
+ }
1426
+ if (!decision.workItemId) continue;
1427
+ const itemKey = `${decision.orgId}\0${decision.factoryProjectId}\0${decision.workItemId}`;
1428
+ if (inspectedWorkItems.has(itemKey)) continue;
1429
+ inspectedWorkItems.add(itemKey);
1430
+ const item = await this.get({
1431
+ orgId: decision.orgId,
1432
+ id: decision.workItemId
1433
+ });
1434
+ if (!item || !isTerminalFactoryRuleStage(item.stages)) continue;
1435
+ await this.supersedeDecisionsForWorkItem({
1436
+ orgId: decision.orgId,
1437
+ factoryProjectId: decision.factoryProjectId,
1438
+ workItemId: decision.workItemId,
1439
+ supersededAt: /* @__PURE__ */ new Date()
1440
+ });
1441
+ }
1442
+ const last = page.at(-1);
1443
+ if (rows.length <= ATTENTION_RECEIPT_QUERY_BATCH_SIZE || !last) return;
1444
+ if (!(last.created_at instanceof Date)) throw new Error("Deferred decision created_at is not a timestamp.");
1445
+ cursor = { values: [last.created_at, last.id] };
1446
+ }
1447
+ }
1052
1448
  /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
1053
1449
  async retryDeferredDecision(orgId, factoryProjectId, decisionId, now) {
1054
- let retried = false;
1055
- const row = await this.#db.updateAtomic("factory_deferred_decisions", {
1056
- id: decisionId,
1057
- org_id: orgId,
1058
- factory_project_id: factoryProjectId
1059
- }, (current) => {
1060
- if (current.status !== "failed") return null;
1061
- retried = true;
1062
- return {
1063
- status: "retry",
1064
- attempts: 0,
1065
- available_at: now,
1066
- lease_owner: null,
1067
- lease_expires_at: null,
1068
- last_error: null,
1069
- completed_at: null,
1070
- updated_at: now
1071
- };
1450
+ return this.storage.withTransaction(async (ops) => {
1451
+ let retried = false;
1452
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1453
+ id: decisionId,
1454
+ org_id: orgId,
1455
+ factory_project_id: factoryProjectId
1456
+ }, (current) => {
1457
+ if (current.status !== "failed") return null;
1458
+ retried = true;
1459
+ return {
1460
+ status: "retry",
1461
+ attempts: 0,
1462
+ available_at: now,
1463
+ lease_owner: null,
1464
+ lease_expires_at: null,
1465
+ last_error: null,
1466
+ failure_code: null,
1467
+ completed_at: null,
1468
+ updated_at: now
1469
+ };
1470
+ });
1471
+ if (!retried || !row) return null;
1472
+ await ops.deleteMany("factory_attention_receipts", {
1473
+ org_id: orgId,
1474
+ factory_project_id: factoryProjectId,
1475
+ kind: "automation-failed",
1476
+ source_id: decisionId,
1477
+ occurrence: Number(row.failure_occurrence ?? 0)
1478
+ });
1479
+ return toDeferredDecision(row);
1072
1480
  });
1073
- return retried && row ? toDeferredDecision(row) : null;
1074
1481
  }
1075
1482
  /** Resolve exact active agent authority; partial session matches never authorize. */
1076
1483
  async findActiveRunBinding(address) {
@@ -1518,6 +1925,13 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1518
1925
  const evaluation = await ops.findOne("factory_rule_evaluations", { id: evaluationId });
1519
1926
  if (evaluation) ingressIds.add(String(evaluation.ingress_id));
1520
1927
  }
1928
+ const decisionIds = decisions.map((decision) => decision.id);
1929
+ for (let index = 0; index < decisionIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) await ops.deleteMany("factory_attention_receipts", {
1930
+ org_id: orgId,
1931
+ factory_project_id: factoryProjectId,
1932
+ kind: "automation-failed",
1933
+ source_id: { in: decisionIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1934
+ });
1521
1935
  await ops.deleteMany("factory_deferred_decisions", {
1522
1936
  org_id: orgId,
1523
1937
  factory_project_id: factoryProjectId,
@@ -1559,6 +1973,6 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1559
1973
  }
1560
1974
  };
1561
1975
  //#endregion
1562
- export { FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1976
+ export { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryAttentionKey, factoryDecisionAttentionIdentity, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1563
1977
 
1564
1978
  //# sourceMappingURL=base.js.map