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

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 (65) hide show
  1. package/CHANGELOG.md +80 -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 -3
  11. package/dist/routes/surface.d.ts.map +1 -1
  12. package/dist/routes/surface.js +60 -43
  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 +60 -35
  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 +30 -14
  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/transition-service.d.ts +2 -1
  32. package/dist/rules/transition-service.d.ts.map +1 -1
  33. package/dist/rules/transition-service.js +1 -1
  34. package/dist/rules/transition-service.js.map +1 -1
  35. package/dist/rules/types.d.ts +2 -0
  36. package/dist/rules/types.d.ts.map +1 -1
  37. package/dist/rules/types.js +9 -1
  38. package/dist/rules/types.js.map +1 -1
  39. package/dist/session/factory-session.d.ts +4 -0
  40. package/dist/session/factory-session.d.ts.map +1 -1
  41. package/dist/session/factory-session.js +10 -2
  42. package/dist/session/factory-session.js.map +1 -1
  43. package/dist/session/first-exec-capture.d.ts +16 -10
  44. package/dist/session/first-exec-capture.d.ts.map +1 -1
  45. package/dist/session/first-exec-capture.js +74 -14
  46. package/dist/session/first-exec-capture.js.map +1 -1
  47. package/dist/session/first-message-capture.d.ts +14 -10
  48. package/dist/session/first-message-capture.d.ts.map +1 -1
  49. package/dist/session/first-message-capture.js +17 -11
  50. package/dist/session/first-message-capture.js.map +1 -1
  51. package/dist/storage/domains/source-control/base.d.ts +3 -0
  52. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  53. package/dist/storage/domains/source-control/base.js +8 -2
  54. package/dist/storage/domains/source-control/base.js.map +1 -1
  55. package/dist/storage/domains/work-items/base.d.ts +101 -10
  56. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  57. package/dist/storage/domains/work-items/base.js +458 -35
  58. package/dist/storage/domains/work-items/base.js.map +1 -1
  59. package/dist/work-item-branch.d.ts +23 -0
  60. package/dist/work-item-branch.d.ts.map +1 -0
  61. package/dist/work-item-branch.js +39 -0
  62. package/dist/work-item-branch.js.map +1 -0
  63. package/factory-skills/factory-rereview/SKILL.md +2 -0
  64. package/factory-skills/factory-review/SKILL.md +2 -0
  65. package/package.json +7 -7
@@ -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,14 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
315
355
  decision: { type: "json" },
316
356
  status: { type: "text" },
317
357
  attempts: { type: "integer" },
358
+ delivery_generation: {
359
+ type: "integer",
360
+ default: 0
361
+ },
362
+ failure_occurrence: {
363
+ type: "integer",
364
+ default: 0
365
+ },
318
366
  available_at: { type: "timestamp" },
319
367
  lease_owner: {
320
368
  type: "text",
@@ -328,6 +376,10 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
328
376
  type: "text",
329
377
  nullable: true
330
378
  },
379
+ failure_code: {
380
+ type: "text",
381
+ nullable: true
382
+ },
331
383
  approved_at: {
332
384
  type: "timestamp",
333
385
  nullable: true
@@ -347,9 +399,80 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
347
399
  "idempotency_key"
348
400
  ]
349
401
  }],
402
+ indexes: [
403
+ {
404
+ name: "factory_deferred_decisions_claim_idx",
405
+ columns: ["status", "created_at"]
406
+ },
407
+ {
408
+ name: "factory_deferred_decisions_tenant_status_created_idx",
409
+ columns: [
410
+ "org_id",
411
+ "factory_project_id",
412
+ "status",
413
+ "created_at",
414
+ "id"
415
+ ]
416
+ },
417
+ {
418
+ name: "factory_deferred_decisions_tenant_status_failed_idx",
419
+ columns: [
420
+ "org_id",
421
+ "factory_project_id",
422
+ "status",
423
+ "updated_at",
424
+ "id"
425
+ ]
426
+ }
427
+ ]
428
+ },
429
+ {
430
+ name: "factory_attention_receipts",
431
+ columns: {
432
+ id: { type: "uuid-pk" },
433
+ org_id: { type: "text" },
434
+ factory_project_id: { type: "text" },
435
+ user_id: { type: "text" },
436
+ kind: { type: "text" },
437
+ source_id: { type: "text" },
438
+ occurrence: { type: "integer" },
439
+ state: { type: "text" },
440
+ read_at: { type: "timestamp" },
441
+ archived_at: {
442
+ type: "timestamp",
443
+ nullable: true
444
+ },
445
+ created_at: { type: "timestamp" },
446
+ updated_at: { type: "timestamp" }
447
+ },
448
+ uniqueIndexes: [{
449
+ name: "factory_attention_receipts_user_source_unique",
450
+ columns: [
451
+ "org_id",
452
+ "factory_project_id",
453
+ "user_id",
454
+ "kind",
455
+ "source_id",
456
+ "occurrence"
457
+ ]
458
+ }],
350
459
  indexes: [{
351
- name: "factory_deferred_decisions_claim_idx",
352
- columns: ["status", "created_at"]
460
+ name: "factory_attention_receipts_user_state_idx",
461
+ columns: [
462
+ "org_id",
463
+ "factory_project_id",
464
+ "user_id",
465
+ "state"
466
+ ]
467
+ }, {
468
+ name: "factory_attention_receipts_source_idx",
469
+ columns: [
470
+ "org_id",
471
+ "factory_project_id",
472
+ "kind",
473
+ "source_id",
474
+ "occurrence"
475
+ ]
353
476
  }]
354
477
  },
355
478
  {
@@ -477,16 +600,51 @@ function toDeferredDecision(row) {
477
600
  decision: row.decision,
478
601
  status: row.status,
479
602
  attempts: Number(row.attempts),
603
+ deliveryGeneration: Number(row.delivery_generation ?? 0),
604
+ failureOccurrence: Number(row.failure_occurrence ?? 0),
480
605
  availableAt: row.available_at,
481
606
  leaseOwner: row.lease_owner ?? null,
482
607
  leaseExpiresAt: row.lease_expires_at ?? null,
483
608
  lastError: row.last_error ?? null,
609
+ failureCode: isFactoryDispatchFailureCode(row.failure_code) ? row.failure_code : null,
484
610
  approvedAt: row.approved_at ?? null,
485
611
  completedAt: row.completed_at ?? null,
486
612
  createdAt: row.created_at,
487
613
  updatedAt: row.updated_at
488
614
  };
489
615
  }
616
+ function attentionReceiptKind(value) {
617
+ if (value === "automation-failed") return value;
618
+ throw new Error(`Unsupported attention receipt kind '${String(value)}'.`);
619
+ }
620
+ function attentionReceiptState(value) {
621
+ if (value === "read" || value === "archived") return value;
622
+ throw new Error(`Unsupported attention receipt state '${String(value)}'.`);
623
+ }
624
+ function toAttentionReceipt(row) {
625
+ if (typeof row.source_id !== "string" || !row.source_id) throw new Error("Attention receipt source_id must be a non-empty string.");
626
+ const occurrence = Number(row.occurrence);
627
+ if (!Number.isSafeInteger(occurrence) || occurrence < 0) throw new Error("Attention receipt occurrence must be a non-negative safe integer.");
628
+ if (!(row.read_at instanceof Date)) throw new Error("Attention receipt read_at must be a timestamp.");
629
+ const archivedAt = row.archived_at;
630
+ if (archivedAt !== null && archivedAt !== void 0 && !(archivedAt instanceof Date)) throw new Error("Attention receipt archived_at must be a timestamp or null.");
631
+ if (!(row.created_at instanceof Date)) throw new Error("Attention receipt created_at must be a timestamp.");
632
+ if (!(row.updated_at instanceof Date)) throw new Error("Attention receipt updated_at must be a timestamp.");
633
+ return {
634
+ id: row.id,
635
+ orgId: row.org_id,
636
+ factoryProjectId: String(row.factory_project_id),
637
+ userId: String(row.user_id),
638
+ kind: attentionReceiptKind(row.kind),
639
+ sourceId: row.source_id,
640
+ occurrence,
641
+ state: attentionReceiptState(row.state),
642
+ readAt: row.read_at,
643
+ archivedAt: archivedAt ?? null,
644
+ createdAt: row.created_at,
645
+ updatedAt: row.updated_at
646
+ };
647
+ }
490
648
  function toPendingStart(row) {
491
649
  return {
492
650
  id: row.id,
@@ -512,6 +670,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
512
670
  }
513
671
  async init() {
514
672
  await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
673
+ await this.repairLegacyAttentionState();
515
674
  }
516
675
  async dangerouslyClearAll() {
517
676
  await this.ops.deleteMany("work_items", {});
@@ -519,6 +678,11 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
519
678
  get #db() {
520
679
  return this.ops;
521
680
  }
681
+ async #countRows(collection, where) {
682
+ const count = this.#db.count;
683
+ if (!count) throw new Error("[WorkItemsStorage] storage backend does not support collection counts.");
684
+ return count.call(this.#db, collection, where);
685
+ }
522
686
  async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
523
687
  return withInProcessProjectLock(`work-items:${orgId}:${factoryProjectId}`, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
524
688
  }
@@ -608,11 +772,14 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
608
772
  failed = true;
609
773
  return {
610
774
  status: input.terminal ? "failed" : "retry",
775
+ ...table === "factory_deferred_decisions" && !input.terminal && input.advanceDeliveryGeneration !== false ? { delivery_generation: Number(current.delivery_generation ?? 0) + 1 } : {},
611
776
  available_at: input.availableAt,
612
777
  lease_owner: null,
613
778
  lease_expires_at: null,
614
779
  last_error: input.lastError,
780
+ ...table === "factory_deferred_decisions" ? { failure_code: input.failureCode } : {},
615
781
  completed_at: input.terminal ? input.now : null,
782
+ ...table === "factory_deferred_decisions" && input.terminal ? { failure_occurrence: Number(current.failure_occurrence ?? 0) + 1 } : {},
616
783
  updated_at: input.now
617
784
  };
618
785
  });
@@ -628,6 +795,14 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
628
795
  async list({ orgId, factoryProjectId }) {
629
796
  return this.#listWithOps(this.#db, orgId, factoryProjectId);
630
797
  }
798
+ async listByIds({ orgId, factoryProjectId, ids }) {
799
+ if (ids.length === 0) return [];
800
+ return (await this.#db.findMany("work_items", {
801
+ org_id: orgId,
802
+ factory_project_id: factoryProjectId,
803
+ id: { in: [...new Set(ids)] }
804
+ })).map(toWorkItem);
805
+ }
631
806
  async get({ orgId, id }) {
632
807
  const row = await this.#db.findOne("work_items", {
633
808
  org_id: orgId,
@@ -749,6 +924,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
749
924
  decision,
750
925
  status: "pending",
751
926
  attempts: 0,
927
+ delivery_generation: 0,
752
928
  available_at: now,
753
929
  lease_owner: null,
754
930
  lease_expires_at: null,
@@ -865,6 +1041,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
865
1041
  decision,
866
1042
  status: "pending",
867
1043
  attempts: 0,
1044
+ delivery_generation: 0,
868
1045
  available_at: input.now,
869
1046
  lease_owner: null,
870
1047
  lease_expires_at: null,
@@ -934,6 +1111,143 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
934
1111
  hasMore: rows.length > input.limit
935
1112
  };
936
1113
  }
1114
+ async listFailedDecisionPage(input) {
1115
+ const rows = await this.#db.findMany("factory_deferred_decisions", {
1116
+ org_id: input.orgId,
1117
+ factory_project_id: input.factoryProjectId,
1118
+ status: "failed"
1119
+ }, {
1120
+ orderBy: [["updated_at", "desc"], ["id", "desc"]],
1121
+ limit: input.limit + 1,
1122
+ ...input.before ? { cursor: { values: [input.before.occurredAt, input.before.id] } } : {}
1123
+ });
1124
+ return {
1125
+ decisions: rows.slice(0, input.limit).map(toDeferredDecision),
1126
+ hasMore: rows.length > input.limit
1127
+ };
1128
+ }
1129
+ async countDeferredDecisionsByStatuses({ orgId, factoryProjectId, statuses }) {
1130
+ return this.#countRows("factory_deferred_decisions", {
1131
+ org_id: orgId,
1132
+ factory_project_id: factoryProjectId,
1133
+ status: { in: statuses }
1134
+ });
1135
+ }
1136
+ async getDeferredDecision(orgId, factoryProjectId, decisionId) {
1137
+ const row = await this.#db.findOne("factory_deferred_decisions", {
1138
+ id: decisionId,
1139
+ org_id: orgId,
1140
+ factory_project_id: factoryProjectId
1141
+ });
1142
+ return row ? toDeferredDecision(row) : null;
1143
+ }
1144
+ async listAttentionReceipts({ orgId, factoryProjectId, userId, identities }) {
1145
+ if (identities.length === 0) return [];
1146
+ const requested = new Set(identities.map((identity) => `${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`));
1147
+ const sourceIds = [...new Set(identities.map((identity) => identity.sourceId))];
1148
+ const kinds = [...new Set(identities.map((identity) => identity.kind))];
1149
+ const receipts = [];
1150
+ for (let index = 0; index < sourceIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) {
1151
+ const rows = await this.#db.findMany("factory_attention_receipts", {
1152
+ org_id: orgId,
1153
+ factory_project_id: factoryProjectId,
1154
+ user_id: userId,
1155
+ kind: { in: kinds },
1156
+ source_id: { in: sourceIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1157
+ });
1158
+ receipts.push(...rows.map(toAttentionReceipt).filter((receipt) => requested.has(`${receipt.kind}\0${receipt.sourceId}\0${receipt.occurrence}`)));
1159
+ }
1160
+ return receipts;
1161
+ }
1162
+ async countAttentionReceipts({ orgId, factoryProjectId, userId, state }) {
1163
+ return this.#countRows("factory_attention_receipts", {
1164
+ org_id: orgId,
1165
+ factory_project_id: factoryProjectId,
1166
+ user_id: userId,
1167
+ kind: "automation-failed",
1168
+ ...state ? { state } : {}
1169
+ });
1170
+ }
1171
+ async deleteAttentionReceipts({ orgId, factoryProjectId, identities }) {
1172
+ const unique = [...new Map(identities.map((identity) => [`${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`, identity])).values()];
1173
+ 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", {
1174
+ org_id: orgId,
1175
+ factory_project_id: factoryProjectId,
1176
+ kind: identity.kind,
1177
+ source_id: identity.sourceId,
1178
+ occurrence: identity.occurrence
1179
+ })));
1180
+ }
1181
+ async setAttentionReceipt(input) {
1182
+ return this.#retryAttentionReceiptWrite(() => this.storage.withTransaction((ops) => this.#setAttentionReceiptWithOps(ops, input)));
1183
+ }
1184
+ async #retryAttentionReceiptWrite(write) {
1185
+ try {
1186
+ return await write();
1187
+ } catch (error) {
1188
+ if (!(error instanceof UniqueViolationError)) throw error;
1189
+ return write();
1190
+ }
1191
+ }
1192
+ async #setAttentionReceiptWithOps(ops, { orgId, factoryProjectId, userId, decisionId, failureOccurrence, action, now }) {
1193
+ const identity = factoryDecisionAttentionIdentity(decisionId, failureOccurrence);
1194
+ let currentOccurrence = false;
1195
+ if (!await ops.updateAtomic("factory_deferred_decisions", {
1196
+ id: decisionId,
1197
+ org_id: orgId,
1198
+ factory_project_id: factoryProjectId
1199
+ }, (current) => {
1200
+ currentOccurrence = current.status === "failed" && Number(current.failure_occurrence ?? 0) === failureOccurrence;
1201
+ return null;
1202
+ }) || !currentOccurrence) return null;
1203
+ const where = {
1204
+ org_id: orgId,
1205
+ factory_project_id: factoryProjectId,
1206
+ user_id: userId,
1207
+ kind: identity.kind,
1208
+ source_id: identity.sourceId,
1209
+ occurrence: identity.occurrence
1210
+ };
1211
+ const existing = await ops.findOne("factory_attention_receipts", where);
1212
+ if (!existing) return toAttentionReceipt(await ops.insertOne("factory_attention_receipts", {
1213
+ ...where,
1214
+ state: action === "archive" ? "archived" : "read",
1215
+ read_at: now,
1216
+ archived_at: action === "archive" ? now : null,
1217
+ created_at: now,
1218
+ updated_at: now
1219
+ }));
1220
+ const row = await ops.updateAtomic("factory_attention_receipts", {
1221
+ id: existing.id,
1222
+ ...where
1223
+ }, (current) => {
1224
+ const preserveArchive = action === "read" && current.state === "archived";
1225
+ const archived = action === "archive" || preserveArchive;
1226
+ return {
1227
+ state: archived ? "archived" : "read",
1228
+ read_at: current.read_at,
1229
+ archived_at: action === "archive" ? now : archived ? current.archived_at ?? now : null,
1230
+ updated_at: now
1231
+ };
1232
+ });
1233
+ return row ? toAttentionReceipt(row) : null;
1234
+ }
1235
+ async markAttentionReceiptsRead({ orgId, factoryProjectId, userId, occurrences, now }) {
1236
+ const uniqueOccurrences = [...new Map(occurrences.map((occurrence) => [`${occurrence.decisionId}:${occurrence.failureOccurrence}`, occurrence])).values()];
1237
+ for (let index = 0; index < uniqueOccurrences.length; index += ATTENTION_RECEIPT_WRITE_BATCH_SIZE) {
1238
+ const batch = uniqueOccurrences.slice(index, index + ATTENTION_RECEIPT_WRITE_BATCH_SIZE);
1239
+ await this.#retryAttentionReceiptWrite(() => this.storage.withTransaction(async (ops) => {
1240
+ for (const occurrence of batch) await this.#setAttentionReceiptWithOps(ops, {
1241
+ orgId,
1242
+ factoryProjectId,
1243
+ userId,
1244
+ ...occurrence,
1245
+ action: "read",
1246
+ now
1247
+ });
1248
+ }));
1249
+ }
1250
+ }
937
1251
  async claimDeferredDecisions(input) {
938
1252
  return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
939
1253
  }
@@ -1014,27 +1328,38 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1014
1328
  });
1015
1329
  }
1016
1330
  /**
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.
1331
+ * Automatically retire proposed or failed work that a newer run or terminal
1332
+ * work-item state has overtaken. Human dismissal remains `dismissed`.
1023
1333
  */
1024
- async dismissProposalsForWorkItem(input) {
1334
+ async supersedeDecisionsForWorkItem(input) {
1025
1335
  const rows = await this.#db.findMany("factory_deferred_decisions", {
1026
1336
  org_id: input.orgId,
1027
1337
  factory_project_id: input.factoryProjectId,
1028
1338
  work_item_id: input.workItemId,
1029
- status: "proposed"
1339
+ status: { in: ["proposed", "failed"] }
1030
1340
  });
1031
- const dismissed = [];
1341
+ const superseded = [];
1032
1342
  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);
1343
+ const decision = toDeferredDecision(row);
1344
+ if (input.role !== void 0 && decision.decision.role !== input.role) continue;
1345
+ const record = decision.status === "proposed" ? await this.#settleProposedDecision({
1346
+ orgId: input.orgId,
1347
+ factoryProjectId: input.factoryProjectId,
1348
+ decisionId: decision.id
1349
+ }, {
1350
+ status: "superseded",
1351
+ updated_at: input.supersededAt,
1352
+ completed_at: input.supersededAt
1353
+ }) : await this.#resolveFailedDecision({
1354
+ orgId: input.orgId,
1355
+ factoryProjectId: input.factoryProjectId,
1356
+ decisionId: decision.id,
1357
+ status: "superseded",
1358
+ now: input.supersededAt
1359
+ });
1360
+ if (record) superseded.push(record);
1036
1361
  }
1037
- return dismissed;
1362
+ return superseded;
1038
1363
  }
1039
1364
  async #settleProposedDecision({ orgId, factoryProjectId, decisionId }, patch) {
1040
1365
  let settled = false;
@@ -1049,28 +1374,119 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1049
1374
  });
1050
1375
  return settled && row ? toDeferredDecision(row) : null;
1051
1376
  }
1377
+ async #resolveFailedDecision({ orgId, factoryProjectId, decisionId, status, now }) {
1378
+ return this.storage.withTransaction(async (ops) => {
1379
+ let resolved = false;
1380
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1381
+ id: decisionId,
1382
+ org_id: orgId,
1383
+ factory_project_id: factoryProjectId
1384
+ }, (current) => {
1385
+ if (current.status !== "failed") return null;
1386
+ resolved = true;
1387
+ return {
1388
+ status,
1389
+ updated_at: now
1390
+ };
1391
+ });
1392
+ if (!resolved || !row) return null;
1393
+ await ops.deleteMany("factory_attention_receipts", {
1394
+ org_id: orgId,
1395
+ factory_project_id: factoryProjectId,
1396
+ kind: "automation-failed",
1397
+ source_id: decisionId,
1398
+ occurrence: Number(row.failure_occurrence ?? 0)
1399
+ });
1400
+ return toDeferredDecision(row);
1401
+ });
1402
+ }
1403
+ async supersedeTerminalDecisionsForWorkItem(input) {
1404
+ await this.supersedeDecisionsForWorkItem(input);
1405
+ }
1406
+ async repairLegacyAttentionState() {
1407
+ let cursor;
1408
+ const inspectedWorkItems = /* @__PURE__ */ new Set();
1409
+ while (true) {
1410
+ const rows = await this.#db.findMany("factory_deferred_decisions", { status: { in: ["failed", "proposed"] } }, {
1411
+ orderBy: [["created_at", "asc"], ["id", "asc"]],
1412
+ limit: 201,
1413
+ ...cursor ? { cursor } : {}
1414
+ });
1415
+ const page = rows.slice(0, ATTENTION_RECEIPT_QUERY_BATCH_SIZE);
1416
+ for (const row of page) {
1417
+ const decision = toDeferredDecision(row);
1418
+ if (decision.status === "failed" && decision.failureOccurrence === 0 && deferredDecisionType(decision) === "transition") {
1419
+ if (ingressWasAccepted(await this.#db.findOne("factory_rule_ingress", {
1420
+ org_id: decision.orgId,
1421
+ factory_project_id: decision.factoryProjectId,
1422
+ identity: `decision:${decision.idempotencyKey}`
1423
+ }))) {
1424
+ await this.#resolveFailedDecision({
1425
+ orgId: decision.orgId,
1426
+ factoryProjectId: decision.factoryProjectId,
1427
+ decisionId: decision.id,
1428
+ status: "succeeded",
1429
+ now: /* @__PURE__ */ new Date()
1430
+ });
1431
+ continue;
1432
+ }
1433
+ }
1434
+ if (!decision.workItemId) continue;
1435
+ const itemKey = `${decision.orgId}\0${decision.factoryProjectId}\0${decision.workItemId}`;
1436
+ if (inspectedWorkItems.has(itemKey)) continue;
1437
+ inspectedWorkItems.add(itemKey);
1438
+ const item = await this.get({
1439
+ orgId: decision.orgId,
1440
+ id: decision.workItemId
1441
+ });
1442
+ if (!item || !isTerminalFactoryRuleStage(item.stages)) continue;
1443
+ await this.supersedeDecisionsForWorkItem({
1444
+ orgId: decision.orgId,
1445
+ factoryProjectId: decision.factoryProjectId,
1446
+ workItemId: decision.workItemId,
1447
+ supersededAt: /* @__PURE__ */ new Date()
1448
+ });
1449
+ }
1450
+ const last = page.at(-1);
1451
+ if (rows.length <= ATTENTION_RECEIPT_QUERY_BATCH_SIZE || !last) return;
1452
+ if (!(last.created_at instanceof Date)) throw new Error("Deferred decision created_at is not a timestamp.");
1453
+ cursor = { values: [last.created_at, last.id] };
1454
+ }
1455
+ }
1052
1456
  /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
1053
1457
  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
- };
1458
+ return this.storage.withTransaction(async (ops) => {
1459
+ let retried = false;
1460
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1461
+ id: decisionId,
1462
+ org_id: orgId,
1463
+ factory_project_id: factoryProjectId
1464
+ }, (current) => {
1465
+ if (current.status !== "failed") return null;
1466
+ retried = true;
1467
+ return {
1468
+ status: "retry",
1469
+ attempts: 0,
1470
+ delivery_generation: Number(current.delivery_generation ?? 0) + 1,
1471
+ available_at: now,
1472
+ lease_owner: null,
1473
+ lease_expires_at: null,
1474
+ last_error: null,
1475
+ failure_code: null,
1476
+ completed_at: null,
1477
+ updated_at: now
1478
+ };
1479
+ });
1480
+ if (!retried || !row) return null;
1481
+ await ops.deleteMany("factory_attention_receipts", {
1482
+ org_id: orgId,
1483
+ factory_project_id: factoryProjectId,
1484
+ kind: "automation-failed",
1485
+ source_id: decisionId,
1486
+ occurrence: Number(row.failure_occurrence ?? 0)
1487
+ });
1488
+ return toDeferredDecision(row);
1072
1489
  });
1073
- return retried && row ? toDeferredDecision(row) : null;
1074
1490
  }
1075
1491
  /** Resolve exact active agent authority; partial session matches never authorize. */
1076
1492
  async findActiveRunBinding(address) {
@@ -1518,6 +1934,13 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1518
1934
  const evaluation = await ops.findOne("factory_rule_evaluations", { id: evaluationId });
1519
1935
  if (evaluation) ingressIds.add(String(evaluation.ingress_id));
1520
1936
  }
1937
+ const decisionIds = decisions.map((decision) => decision.id);
1938
+ for (let index = 0; index < decisionIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) await ops.deleteMany("factory_attention_receipts", {
1939
+ org_id: orgId,
1940
+ factory_project_id: factoryProjectId,
1941
+ kind: "automation-failed",
1942
+ source_id: { in: decisionIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1943
+ });
1521
1944
  await ops.deleteMany("factory_deferred_decisions", {
1522
1945
  org_id: orgId,
1523
1946
  factory_project_id: factoryProjectId,
@@ -1559,6 +1982,6 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1559
1982
  }
1560
1983
  };
1561
1984
  //#endregion
1562
- export { FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1985
+ export { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryAttentionKey, factoryDecisionAttentionIdentity, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1563
1986
 
1564
1987
  //# sourceMappingURL=base.js.map