@mastra/factory 0.9.0 → 0.10.0-alpha.10

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 (129) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +1 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/factory.d.ts +8 -0
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +24 -8
  8. package/dist/factory.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/integrations/github/integration.d.ts.map +1 -1
  13. package/dist/integrations/github/integration.js +1 -0
  14. package/dist/integrations/github/integration.js.map +1 -1
  15. package/dist/integrations/github/provenance.d.ts +7 -2
  16. package/dist/integrations/github/provenance.d.ts.map +1 -1
  17. package/dist/integrations/github/provenance.js +16 -2
  18. package/dist/integrations/github/provenance.js.map +1 -1
  19. package/dist/integrations/github/routes.d.ts +3 -0
  20. package/dist/integrations/github/routes.d.ts.map +1 -1
  21. package/dist/integrations/github/routes.js +165 -6
  22. package/dist/integrations/github/routes.js.map +1 -1
  23. package/dist/integrations/github/rules.d.ts.map +1 -1
  24. package/dist/integrations/github/rules.js +193 -127
  25. package/dist/integrations/github/rules.js.map +1 -1
  26. package/dist/integrations/linear/routes.d.ts.map +1 -1
  27. package/dist/integrations/linear/routes.js +50 -0
  28. package/dist/integrations/linear/routes.js.map +1 -1
  29. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  30. package/dist/integrations/platform/github/integration.js +1 -0
  31. package/dist/integrations/platform/github/integration.js.map +1 -1
  32. package/dist/routes/attention.d.ts +17 -0
  33. package/dist/routes/attention.d.ts.map +1 -0
  34. package/dist/routes/attention.js +305 -0
  35. package/dist/routes/attention.js.map +1 -0
  36. package/dist/routes/intake.d.ts.map +1 -1
  37. package/dist/routes/intake.js +11 -2
  38. package/dist/routes/intake.js.map +1 -1
  39. package/dist/routes/surface.d.ts +2 -3
  40. package/dist/routes/surface.d.ts.map +1 -1
  41. package/dist/routes/surface.js +61 -43
  42. package/dist/routes/surface.js.map +1 -1
  43. package/dist/routes/work-items.d.ts.map +1 -1
  44. package/dist/routes/work-items.js +35 -14
  45. package/dist/routes/work-items.js.map +1 -1
  46. package/dist/rules/defaults.js +5 -5
  47. package/dist/rules/defaults.js.map +1 -1
  48. package/dist/rules/dispatch-errors.d.ts +13 -0
  49. package/dist/rules/dispatch-errors.d.ts.map +1 -0
  50. package/dist/rules/dispatch-errors.js +77 -0
  51. package/dist/rules/dispatch-errors.js.map +1 -0
  52. package/dist/rules/dispatcher.d.ts +9 -0
  53. package/dist/rules/dispatcher.d.ts.map +1 -1
  54. package/dist/rules/dispatcher.js +118 -45
  55. package/dist/rules/dispatcher.js.map +1 -1
  56. package/dist/rules/processor.d.ts.map +1 -1
  57. package/dist/rules/processor.js +30 -14
  58. package/dist/rules/processor.js.map +1 -1
  59. package/dist/rules/terminal-cleanup.d.ts +1 -1
  60. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  61. package/dist/rules/terminal-cleanup.js +2 -2
  62. package/dist/rules/terminal-cleanup.js.map +1 -1
  63. package/dist/rules/transition-service.d.ts +2 -1
  64. package/dist/rules/transition-service.d.ts.map +1 -1
  65. package/dist/rules/transition-service.js +1 -1
  66. package/dist/rules/transition-service.js.map +1 -1
  67. package/dist/rules/types.d.ts +2 -0
  68. package/dist/rules/types.d.ts.map +1 -1
  69. package/dist/rules/types.js +9 -1
  70. package/dist/rules/types.js.map +1 -1
  71. package/dist/secret-encryption.d.ts +25 -0
  72. package/dist/secret-encryption.d.ts.map +1 -0
  73. package/dist/secret-encryption.js +96 -0
  74. package/dist/secret-encryption.js.map +1 -0
  75. package/dist/session/factory-session.d.ts +13 -3
  76. package/dist/session/factory-session.d.ts.map +1 -1
  77. package/dist/session/factory-session.js +15 -6
  78. package/dist/session/factory-session.js.map +1 -1
  79. package/dist/session/first-exec-capture.d.ts +16 -10
  80. package/dist/session/first-exec-capture.d.ts.map +1 -1
  81. package/dist/session/first-exec-capture.js +74 -14
  82. package/dist/session/first-exec-capture.js.map +1 -1
  83. package/dist/session/first-message-capture.d.ts +14 -10
  84. package/dist/session/first-message-capture.d.ts.map +1 -1
  85. package/dist/session/first-message-capture.js +17 -11
  86. package/dist/session/first-message-capture.js.map +1 -1
  87. package/dist/session/session-title.d.ts +7 -0
  88. package/dist/session/session-title.d.ts.map +1 -0
  89. package/dist/session/session-title.js +14 -0
  90. package/dist/session/session-title.js.map +1 -0
  91. package/dist/session/thread-title-mirror.d.ts +35 -0
  92. package/dist/session/thread-title-mirror.d.ts.map +1 -0
  93. package/dist/session/thread-title-mirror.js +51 -0
  94. package/dist/session/thread-title-mirror.js.map +1 -0
  95. package/dist/storage/domains/credentials/base.d.ts +3 -1
  96. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  97. package/dist/storage/domains/credentials/base.js +33 -15
  98. package/dist/storage/domains/credentials/base.js.map +1 -1
  99. package/dist/storage/domains/custom-providers/base.d.ts +3 -1
  100. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
  101. package/dist/storage/domains/custom-providers/base.js +45 -25
  102. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  103. package/dist/storage/domains/intake/base.d.ts +6 -5
  104. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  105. package/dist/storage/domains/intake/base.js +21 -13
  106. package/dist/storage/domains/intake/base.js.map +1 -1
  107. package/dist/storage/domains/integrations/base.d.ts +3 -1
  108. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  109. package/dist/storage/domains/integrations/base.js +28 -12
  110. package/dist/storage/domains/integrations/base.js.map +1 -1
  111. package/dist/storage/domains/source-control/base.d.ts +8 -0
  112. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  113. package/dist/storage/domains/source-control/base.js +14 -2
  114. package/dist/storage/domains/source-control/base.js.map +1 -1
  115. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  116. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  117. package/dist/storage/domains/source-control/inmemory.js +7 -0
  118. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  119. package/dist/storage/domains/work-items/base.d.ts +115 -12
  120. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  121. package/dist/storage/domains/work-items/base.js +497 -43
  122. package/dist/storage/domains/work-items/base.js.map +1 -1
  123. package/dist/work-item-branch.d.ts +23 -0
  124. package/dist/work-item-branch.d.ts.map +1 -0
  125. package/dist/work-item-branch.js +39 -0
  126. package/dist/work-item-branch.js.map +1 -0
  127. package/factory-skills/factory-rereview/SKILL.md +2 -0
  128. package/factory-skills/factory-review/SKILL.md +2 -0
  129. 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
@@ -89,11 +120,12 @@ const WORK_ITEMS_SCHEMA = {
89
120
  columns: ["factory_project_id", "source_key"]
90
121
  }],
91
122
  indexes: [{
92
- name: "work_items_org_project_updated_at_idx",
123
+ name: "work_items_org_project_created_at_id_idx",
93
124
  columns: [
94
125
  "org_id",
95
126
  "factory_project_id",
96
- "updated_at"
127
+ "created_at",
128
+ "id"
97
129
  ]
98
130
  }, {
99
131
  name: "work_items_project_parent_idx",
@@ -223,6 +255,8 @@ function applyUpdate({ current, userId, input }) {
223
255
  updated_at: now
224
256
  };
225
257
  }
258
+ const ATTENTION_RECEIPT_QUERY_BATCH_SIZE = 200;
259
+ const ATTENTION_RECEIPT_WRITE_BATCH_SIZE = 25;
226
260
  const projectRelationLocks = /* @__PURE__ */ new Map();
227
261
  function withInProcessProjectLock(key, fn) {
228
262
  const result = (projectRelationLocks.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -240,6 +274,13 @@ function decisionSourceKey(decision) {
240
274
  if (record.type !== "upsertLinkedWorkItem" || typeof record.sourceKey !== "string") return null;
241
275
  return record.sourceKey;
242
276
  }
277
+ function deferredDecisionType(decision) {
278
+ return typeof decision.decision.type === "string" ? decision.decision.type : void 0;
279
+ }
280
+ function ingressWasAccepted(row) {
281
+ if (!row || typeof row.result !== "object" || row.result === null || Array.isArray(row.result)) return false;
282
+ return "status" in row.result && row.result.status === "accepted";
283
+ }
243
284
  const FACTORY_GOVERNANCE_SCHEMAS = [
244
285
  {
245
286
  name: "factory_rule_ingress",
@@ -315,6 +356,14 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
315
356
  decision: { type: "json" },
316
357
  status: { type: "text" },
317
358
  attempts: { type: "integer" },
359
+ delivery_generation: {
360
+ type: "integer",
361
+ default: 0
362
+ },
363
+ failure_occurrence: {
364
+ type: "integer",
365
+ default: 0
366
+ },
318
367
  available_at: { type: "timestamp" },
319
368
  lease_owner: {
320
369
  type: "text",
@@ -328,10 +377,18 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
328
377
  type: "text",
329
378
  nullable: true
330
379
  },
380
+ failure_code: {
381
+ type: "text",
382
+ nullable: true
383
+ },
331
384
  approved_at: {
332
385
  type: "timestamp",
333
386
  nullable: true
334
387
  },
388
+ approved_by: {
389
+ type: "text",
390
+ nullable: true
391
+ },
335
392
  completed_at: {
336
393
  type: "timestamp",
337
394
  nullable: true
@@ -347,9 +404,80 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
347
404
  "idempotency_key"
348
405
  ]
349
406
  }],
407
+ indexes: [
408
+ {
409
+ name: "factory_deferred_decisions_claim_idx",
410
+ columns: ["status", "created_at"]
411
+ },
412
+ {
413
+ name: "factory_deferred_decisions_tenant_status_created_idx",
414
+ columns: [
415
+ "org_id",
416
+ "factory_project_id",
417
+ "status",
418
+ "created_at",
419
+ "id"
420
+ ]
421
+ },
422
+ {
423
+ name: "factory_deferred_decisions_tenant_status_failed_idx",
424
+ columns: [
425
+ "org_id",
426
+ "factory_project_id",
427
+ "status",
428
+ "updated_at",
429
+ "id"
430
+ ]
431
+ }
432
+ ]
433
+ },
434
+ {
435
+ name: "factory_attention_receipts",
436
+ columns: {
437
+ id: { type: "uuid-pk" },
438
+ org_id: { type: "text" },
439
+ factory_project_id: { type: "text" },
440
+ user_id: { type: "text" },
441
+ kind: { type: "text" },
442
+ source_id: { type: "text" },
443
+ occurrence: { type: "integer" },
444
+ state: { type: "text" },
445
+ read_at: { type: "timestamp" },
446
+ archived_at: {
447
+ type: "timestamp",
448
+ nullable: true
449
+ },
450
+ created_at: { type: "timestamp" },
451
+ updated_at: { type: "timestamp" }
452
+ },
453
+ uniqueIndexes: [{
454
+ name: "factory_attention_receipts_user_source_unique",
455
+ columns: [
456
+ "org_id",
457
+ "factory_project_id",
458
+ "user_id",
459
+ "kind",
460
+ "source_id",
461
+ "occurrence"
462
+ ]
463
+ }],
350
464
  indexes: [{
351
- name: "factory_deferred_decisions_claim_idx",
352
- columns: ["status", "created_at"]
465
+ name: "factory_attention_receipts_user_state_idx",
466
+ columns: [
467
+ "org_id",
468
+ "factory_project_id",
469
+ "user_id",
470
+ "state"
471
+ ]
472
+ }, {
473
+ name: "factory_attention_receipts_source_idx",
474
+ columns: [
475
+ "org_id",
476
+ "factory_project_id",
477
+ "kind",
478
+ "source_id",
479
+ "occurrence"
480
+ ]
353
481
  }]
354
482
  },
355
483
  {
@@ -477,16 +605,52 @@ function toDeferredDecision(row) {
477
605
  decision: row.decision,
478
606
  status: row.status,
479
607
  attempts: Number(row.attempts),
608
+ deliveryGeneration: Number(row.delivery_generation ?? 0),
609
+ failureOccurrence: Number(row.failure_occurrence ?? 0),
480
610
  availableAt: row.available_at,
481
611
  leaseOwner: row.lease_owner ?? null,
482
612
  leaseExpiresAt: row.lease_expires_at ?? null,
483
613
  lastError: row.last_error ?? null,
614
+ failureCode: isFactoryDispatchFailureCode(row.failure_code) ? row.failure_code : null,
484
615
  approvedAt: row.approved_at ?? null,
616
+ approvedBy: row.approved_by ?? null,
485
617
  completedAt: row.completed_at ?? null,
486
618
  createdAt: row.created_at,
487
619
  updatedAt: row.updated_at
488
620
  };
489
621
  }
622
+ function attentionReceiptKind(value) {
623
+ if (value === "automation-failed") return value;
624
+ throw new Error(`Unsupported attention receipt kind '${String(value)}'.`);
625
+ }
626
+ function attentionReceiptState(value) {
627
+ if (value === "read" || value === "archived") return value;
628
+ throw new Error(`Unsupported attention receipt state '${String(value)}'.`);
629
+ }
630
+ function toAttentionReceipt(row) {
631
+ if (typeof row.source_id !== "string" || !row.source_id) throw new Error("Attention receipt source_id must be a non-empty string.");
632
+ const occurrence = Number(row.occurrence);
633
+ if (!Number.isSafeInteger(occurrence) || occurrence < 0) throw new Error("Attention receipt occurrence must be a non-negative safe integer.");
634
+ if (!(row.read_at instanceof Date)) throw new Error("Attention receipt read_at must be a timestamp.");
635
+ const archivedAt = row.archived_at;
636
+ if (archivedAt !== null && archivedAt !== void 0 && !(archivedAt instanceof Date)) throw new Error("Attention receipt archived_at must be a timestamp or null.");
637
+ if (!(row.created_at instanceof Date)) throw new Error("Attention receipt created_at must be a timestamp.");
638
+ if (!(row.updated_at instanceof Date)) throw new Error("Attention receipt updated_at must be a timestamp.");
639
+ return {
640
+ id: row.id,
641
+ orgId: row.org_id,
642
+ factoryProjectId: String(row.factory_project_id),
643
+ userId: String(row.user_id),
644
+ kind: attentionReceiptKind(row.kind),
645
+ sourceId: row.source_id,
646
+ occurrence,
647
+ state: attentionReceiptState(row.state),
648
+ readAt: row.read_at,
649
+ archivedAt: archivedAt ?? null,
650
+ createdAt: row.created_at,
651
+ updatedAt: row.updated_at
652
+ };
653
+ }
490
654
  function toPendingStart(row) {
491
655
  return {
492
656
  id: row.id,
@@ -512,6 +676,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
512
676
  }
513
677
  async init() {
514
678
  await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
679
+ await this.repairLegacyAttentionState();
515
680
  }
516
681
  async dangerouslyClearAll() {
517
682
  await this.ops.deleteMany("work_items", {});
@@ -519,6 +684,11 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
519
684
  get #db() {
520
685
  return this.ops;
521
686
  }
687
+ async #countRows(collection, where) {
688
+ const count = this.#db.count;
689
+ if (!count) throw new Error("[WorkItemsStorage] storage backend does not support collection counts.");
690
+ return count.call(this.#db, collection, where);
691
+ }
522
692
  async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
523
693
  return withInProcessProjectLock(`work-items:${orgId}:${factoryProjectId}`, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
524
694
  }
@@ -608,11 +778,14 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
608
778
  failed = true;
609
779
  return {
610
780
  status: input.terminal ? "failed" : "retry",
781
+ ...table === "factory_deferred_decisions" && !input.terminal && input.advanceDeliveryGeneration !== false ? { delivery_generation: Number(current.delivery_generation ?? 0) + 1 } : {},
611
782
  available_at: input.availableAt,
612
783
  lease_owner: null,
613
784
  lease_expires_at: null,
614
785
  last_error: input.lastError,
786
+ ...table === "factory_deferred_decisions" ? { failure_code: input.failureCode } : {},
615
787
  completed_at: input.terminal ? input.now : null,
788
+ ...table === "factory_deferred_decisions" && input.terminal ? { failure_occurrence: Number(current.failure_occurrence ?? 0) + 1 } : {},
616
789
  updated_at: input.now
617
790
  };
618
791
  });
@@ -622,12 +795,24 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
622
795
  return (await ops.findMany("work_items", {
623
796
  org_id: orgId,
624
797
  factory_project_id: factoryProjectId
625
- }, { orderBy: [["updated_at", "desc"]] })).map(toWorkItem);
798
+ }, { orderBy: [["created_at", "desc"], ["id", "desc"]] })).map(toWorkItem);
626
799
  }
627
- /** List the org's work items for a project, newest first. */
800
+ /**
801
+ * List the org's work items for a project, newest first. Ordered on `created_at` with an id
802
+ * tiebreak so the order is stable: `updated_at` moves under every write, which makes it useless
803
+ * as a cursor and non-deterministic for the callers that iterate this list.
804
+ */
628
805
  async list({ orgId, factoryProjectId }) {
629
806
  return this.#listWithOps(this.#db, orgId, factoryProjectId);
630
807
  }
808
+ async listByIds({ orgId, factoryProjectId, ids }) {
809
+ if (ids.length === 0) return [];
810
+ return (await this.#db.findMany("work_items", {
811
+ org_id: orgId,
812
+ factory_project_id: factoryProjectId,
813
+ id: { in: [...new Set(ids)] }
814
+ })).map(toWorkItem);
815
+ }
631
816
  async get({ orgId, id }) {
632
817
  const row = await this.#db.findOne("work_items", {
633
818
  org_id: orgId,
@@ -749,6 +934,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
749
934
  decision,
750
935
  status: "pending",
751
936
  attempts: 0,
937
+ delivery_generation: 0,
752
938
  available_at: now,
753
939
  lease_owner: null,
754
940
  lease_expires_at: null,
@@ -865,6 +1051,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
865
1051
  decision,
866
1052
  status: "pending",
867
1053
  attempts: 0,
1054
+ delivery_generation: 0,
868
1055
  available_at: input.now,
869
1056
  lease_owner: null,
870
1057
  lease_expires_at: null,
@@ -934,6 +1121,143 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
934
1121
  hasMore: rows.length > input.limit
935
1122
  };
936
1123
  }
1124
+ async listFailedDecisionPage(input) {
1125
+ const rows = await this.#db.findMany("factory_deferred_decisions", {
1126
+ org_id: input.orgId,
1127
+ factory_project_id: input.factoryProjectId,
1128
+ status: "failed"
1129
+ }, {
1130
+ orderBy: [["updated_at", "desc"], ["id", "desc"]],
1131
+ limit: input.limit + 1,
1132
+ ...input.before ? { cursor: { values: [input.before.occurredAt, input.before.id] } } : {}
1133
+ });
1134
+ return {
1135
+ decisions: rows.slice(0, input.limit).map(toDeferredDecision),
1136
+ hasMore: rows.length > input.limit
1137
+ };
1138
+ }
1139
+ async countDeferredDecisionsByStatuses({ orgId, factoryProjectId, statuses }) {
1140
+ return this.#countRows("factory_deferred_decisions", {
1141
+ org_id: orgId,
1142
+ factory_project_id: factoryProjectId,
1143
+ status: { in: statuses }
1144
+ });
1145
+ }
1146
+ async getDeferredDecision(orgId, factoryProjectId, decisionId) {
1147
+ const row = await this.#db.findOne("factory_deferred_decisions", {
1148
+ id: decisionId,
1149
+ org_id: orgId,
1150
+ factory_project_id: factoryProjectId
1151
+ });
1152
+ return row ? toDeferredDecision(row) : null;
1153
+ }
1154
+ async listAttentionReceipts({ orgId, factoryProjectId, userId, identities }) {
1155
+ if (identities.length === 0) return [];
1156
+ const requested = new Set(identities.map((identity) => `${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`));
1157
+ const sourceIds = [...new Set(identities.map((identity) => identity.sourceId))];
1158
+ const kinds = [...new Set(identities.map((identity) => identity.kind))];
1159
+ const receipts = [];
1160
+ for (let index = 0; index < sourceIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) {
1161
+ const rows = await this.#db.findMany("factory_attention_receipts", {
1162
+ org_id: orgId,
1163
+ factory_project_id: factoryProjectId,
1164
+ user_id: userId,
1165
+ kind: { in: kinds },
1166
+ source_id: { in: sourceIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1167
+ });
1168
+ receipts.push(...rows.map(toAttentionReceipt).filter((receipt) => requested.has(`${receipt.kind}\0${receipt.sourceId}\0${receipt.occurrence}`)));
1169
+ }
1170
+ return receipts;
1171
+ }
1172
+ async countAttentionReceipts({ orgId, factoryProjectId, userId, state }) {
1173
+ return this.#countRows("factory_attention_receipts", {
1174
+ org_id: orgId,
1175
+ factory_project_id: factoryProjectId,
1176
+ user_id: userId,
1177
+ kind: "automation-failed",
1178
+ ...state ? { state } : {}
1179
+ });
1180
+ }
1181
+ async deleteAttentionReceipts({ orgId, factoryProjectId, identities }) {
1182
+ const unique = [...new Map(identities.map((identity) => [`${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`, identity])).values()];
1183
+ 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", {
1184
+ org_id: orgId,
1185
+ factory_project_id: factoryProjectId,
1186
+ kind: identity.kind,
1187
+ source_id: identity.sourceId,
1188
+ occurrence: identity.occurrence
1189
+ })));
1190
+ }
1191
+ async setAttentionReceipt(input) {
1192
+ return this.#retryAttentionReceiptWrite(() => this.storage.withTransaction((ops) => this.#setAttentionReceiptWithOps(ops, input)));
1193
+ }
1194
+ async #retryAttentionReceiptWrite(write) {
1195
+ try {
1196
+ return await write();
1197
+ } catch (error) {
1198
+ if (!(error instanceof UniqueViolationError)) throw error;
1199
+ return write();
1200
+ }
1201
+ }
1202
+ async #setAttentionReceiptWithOps(ops, { orgId, factoryProjectId, userId, decisionId, failureOccurrence, action, now }) {
1203
+ const identity = factoryDecisionAttentionIdentity(decisionId, failureOccurrence);
1204
+ let currentOccurrence = false;
1205
+ if (!await ops.updateAtomic("factory_deferred_decisions", {
1206
+ id: decisionId,
1207
+ org_id: orgId,
1208
+ factory_project_id: factoryProjectId
1209
+ }, (current) => {
1210
+ currentOccurrence = current.status === "failed" && Number(current.failure_occurrence ?? 0) === failureOccurrence;
1211
+ return null;
1212
+ }) || !currentOccurrence) return null;
1213
+ const where = {
1214
+ org_id: orgId,
1215
+ factory_project_id: factoryProjectId,
1216
+ user_id: userId,
1217
+ kind: identity.kind,
1218
+ source_id: identity.sourceId,
1219
+ occurrence: identity.occurrence
1220
+ };
1221
+ const existing = await ops.findOne("factory_attention_receipts", where);
1222
+ if (!existing) return toAttentionReceipt(await ops.insertOne("factory_attention_receipts", {
1223
+ ...where,
1224
+ state: action === "archive" ? "archived" : "read",
1225
+ read_at: now,
1226
+ archived_at: action === "archive" ? now : null,
1227
+ created_at: now,
1228
+ updated_at: now
1229
+ }));
1230
+ const row = await ops.updateAtomic("factory_attention_receipts", {
1231
+ id: existing.id,
1232
+ ...where
1233
+ }, (current) => {
1234
+ const preserveArchive = action === "read" && current.state === "archived";
1235
+ const archived = action === "archive" || preserveArchive;
1236
+ return {
1237
+ state: archived ? "archived" : "read",
1238
+ read_at: current.read_at,
1239
+ archived_at: action === "archive" ? now : archived ? current.archived_at ?? now : null,
1240
+ updated_at: now
1241
+ };
1242
+ });
1243
+ return row ? toAttentionReceipt(row) : null;
1244
+ }
1245
+ async markAttentionReceiptsRead({ orgId, factoryProjectId, userId, occurrences, now }) {
1246
+ const uniqueOccurrences = [...new Map(occurrences.map((occurrence) => [`${occurrence.decisionId}:${occurrence.failureOccurrence}`, occurrence])).values()];
1247
+ for (let index = 0; index < uniqueOccurrences.length; index += ATTENTION_RECEIPT_WRITE_BATCH_SIZE) {
1248
+ const batch = uniqueOccurrences.slice(index, index + ATTENTION_RECEIPT_WRITE_BATCH_SIZE);
1249
+ await this.#retryAttentionReceiptWrite(() => this.storage.withTransaction(async (ops) => {
1250
+ for (const occurrence of batch) await this.#setAttentionReceiptWithOps(ops, {
1251
+ orgId,
1252
+ factoryProjectId,
1253
+ userId,
1254
+ ...occurrence,
1255
+ action: "read",
1256
+ now
1257
+ });
1258
+ }));
1259
+ }
1260
+ }
937
1261
  async claimDeferredDecisions(input) {
938
1262
  return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
939
1263
  }
@@ -974,7 +1298,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
974
1298
  * is armed in the same transaction — a crash cannot release the run while
975
1299
  * leaving its follow-up work parked for re-approval.
976
1300
  */
977
- async approveDeferredDecision(orgId, factoryProjectId, decisionId, now) {
1301
+ async approveDeferredDecision(orgId, factoryProjectId, decisionId, now, approvedBy) {
978
1302
  return this.storage.withTransaction(async (ops) => {
979
1303
  let settled = false;
980
1304
  const row = await ops.updateAtomic("factory_deferred_decisions", {
@@ -989,6 +1313,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
989
1313
  attempts: 0,
990
1314
  available_at: now,
991
1315
  approved_at: now,
1316
+ approved_by: approvedBy ?? null,
992
1317
  updated_at: now
993
1318
  };
994
1319
  });
@@ -1014,27 +1339,38 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1014
1339
  });
1015
1340
  }
1016
1341
  /**
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.
1342
+ * Automatically retire proposed or failed work that a newer run or terminal
1343
+ * work-item state has overtaken. Human dismissal remains `dismissed`.
1023
1344
  */
1024
- async dismissProposalsForWorkItem(input) {
1345
+ async supersedeDecisionsForWorkItem(input) {
1025
1346
  const rows = await this.#db.findMany("factory_deferred_decisions", {
1026
1347
  org_id: input.orgId,
1027
1348
  factory_project_id: input.factoryProjectId,
1028
1349
  work_item_id: input.workItemId,
1029
- status: "proposed"
1350
+ status: { in: ["proposed", "failed"] }
1030
1351
  });
1031
- const dismissed = [];
1352
+ const superseded = [];
1032
1353
  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);
1354
+ const decision = toDeferredDecision(row);
1355
+ if (input.role !== void 0 && decision.decision.role !== input.role) continue;
1356
+ const record = decision.status === "proposed" ? await this.#settleProposedDecision({
1357
+ orgId: input.orgId,
1358
+ factoryProjectId: input.factoryProjectId,
1359
+ decisionId: decision.id
1360
+ }, {
1361
+ status: "superseded",
1362
+ updated_at: input.supersededAt,
1363
+ completed_at: input.supersededAt
1364
+ }) : await this.#resolveFailedDecision({
1365
+ orgId: input.orgId,
1366
+ factoryProjectId: input.factoryProjectId,
1367
+ decisionId: decision.id,
1368
+ status: "superseded",
1369
+ now: input.supersededAt
1370
+ });
1371
+ if (record) superseded.push(record);
1036
1372
  }
1037
- return dismissed;
1373
+ return superseded;
1038
1374
  }
1039
1375
  async #settleProposedDecision({ orgId, factoryProjectId, decisionId }, patch) {
1040
1376
  let settled = false;
@@ -1049,28 +1385,119 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1049
1385
  });
1050
1386
  return settled && row ? toDeferredDecision(row) : null;
1051
1387
  }
1388
+ async #resolveFailedDecision({ orgId, factoryProjectId, decisionId, status, now }) {
1389
+ return this.storage.withTransaction(async (ops) => {
1390
+ let resolved = false;
1391
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1392
+ id: decisionId,
1393
+ org_id: orgId,
1394
+ factory_project_id: factoryProjectId
1395
+ }, (current) => {
1396
+ if (current.status !== "failed") return null;
1397
+ resolved = true;
1398
+ return {
1399
+ status,
1400
+ updated_at: now
1401
+ };
1402
+ });
1403
+ if (!resolved || !row) return null;
1404
+ await ops.deleteMany("factory_attention_receipts", {
1405
+ org_id: orgId,
1406
+ factory_project_id: factoryProjectId,
1407
+ kind: "automation-failed",
1408
+ source_id: decisionId,
1409
+ occurrence: Number(row.failure_occurrence ?? 0)
1410
+ });
1411
+ return toDeferredDecision(row);
1412
+ });
1413
+ }
1414
+ async supersedeTerminalDecisionsForWorkItem(input) {
1415
+ await this.supersedeDecisionsForWorkItem(input);
1416
+ }
1417
+ async repairLegacyAttentionState() {
1418
+ let cursor;
1419
+ const inspectedWorkItems = /* @__PURE__ */ new Set();
1420
+ while (true) {
1421
+ const rows = await this.#db.findMany("factory_deferred_decisions", { status: { in: ["failed", "proposed"] } }, {
1422
+ orderBy: [["created_at", "asc"], ["id", "asc"]],
1423
+ limit: 201,
1424
+ ...cursor ? { cursor } : {}
1425
+ });
1426
+ const page = rows.slice(0, ATTENTION_RECEIPT_QUERY_BATCH_SIZE);
1427
+ for (const row of page) {
1428
+ const decision = toDeferredDecision(row);
1429
+ if (decision.status === "failed" && decision.failureOccurrence === 0 && deferredDecisionType(decision) === "transition") {
1430
+ if (ingressWasAccepted(await this.#db.findOne("factory_rule_ingress", {
1431
+ org_id: decision.orgId,
1432
+ factory_project_id: decision.factoryProjectId,
1433
+ identity: `decision:${decision.idempotencyKey}`
1434
+ }))) {
1435
+ await this.#resolveFailedDecision({
1436
+ orgId: decision.orgId,
1437
+ factoryProjectId: decision.factoryProjectId,
1438
+ decisionId: decision.id,
1439
+ status: "succeeded",
1440
+ now: /* @__PURE__ */ new Date()
1441
+ });
1442
+ continue;
1443
+ }
1444
+ }
1445
+ if (!decision.workItemId) continue;
1446
+ const itemKey = `${decision.orgId}\0${decision.factoryProjectId}\0${decision.workItemId}`;
1447
+ if (inspectedWorkItems.has(itemKey)) continue;
1448
+ inspectedWorkItems.add(itemKey);
1449
+ const item = await this.get({
1450
+ orgId: decision.orgId,
1451
+ id: decision.workItemId
1452
+ });
1453
+ if (!item || !isTerminalFactoryRuleStage(item.stages)) continue;
1454
+ await this.supersedeDecisionsForWorkItem({
1455
+ orgId: decision.orgId,
1456
+ factoryProjectId: decision.factoryProjectId,
1457
+ workItemId: decision.workItemId,
1458
+ supersededAt: /* @__PURE__ */ new Date()
1459
+ });
1460
+ }
1461
+ const last = page.at(-1);
1462
+ if (rows.length <= ATTENTION_RECEIPT_QUERY_BATCH_SIZE || !last) return;
1463
+ if (!(last.created_at instanceof Date)) throw new Error("Deferred decision created_at is not a timestamp.");
1464
+ cursor = { values: [last.created_at, last.id] };
1465
+ }
1466
+ }
1052
1467
  /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
1053
1468
  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
- };
1469
+ return this.storage.withTransaction(async (ops) => {
1470
+ let retried = false;
1471
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1472
+ id: decisionId,
1473
+ org_id: orgId,
1474
+ factory_project_id: factoryProjectId
1475
+ }, (current) => {
1476
+ if (current.status !== "failed") return null;
1477
+ retried = true;
1478
+ return {
1479
+ status: "retry",
1480
+ attempts: 0,
1481
+ delivery_generation: Number(current.delivery_generation ?? 0) + 1,
1482
+ available_at: now,
1483
+ lease_owner: null,
1484
+ lease_expires_at: null,
1485
+ last_error: null,
1486
+ failure_code: null,
1487
+ completed_at: null,
1488
+ updated_at: now
1489
+ };
1490
+ });
1491
+ if (!retried || !row) return null;
1492
+ await ops.deleteMany("factory_attention_receipts", {
1493
+ org_id: orgId,
1494
+ factory_project_id: factoryProjectId,
1495
+ kind: "automation-failed",
1496
+ source_id: decisionId,
1497
+ occurrence: Number(row.failure_occurrence ?? 0)
1498
+ });
1499
+ return toDeferredDecision(row);
1072
1500
  });
1073
- return retried && row ? toDeferredDecision(row) : null;
1074
1501
  }
1075
1502
  /** Resolve exact active agent authority; partial session matches never authorize. */
1076
1503
  async findActiveRunBinding(address) {
@@ -1256,12 +1683,10 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1256
1683
  let item;
1257
1684
  if (row) {
1258
1685
  row = await ops.updateAtomic("work_items", { id: row.id }, (current) => {
1259
- const roles = /* @__PURE__ */ new Set([...Object.keys(current.sessions), input.role]);
1260
- const sessions = Object.fromEntries([...roles].map((role) => [role, input.session]));
1261
1686
  return applyUpdate({
1262
1687
  current,
1263
1688
  userId: input.userId,
1264
- input: { sessions }
1689
+ input: { sessions: { [input.role]: input.session } }
1265
1690
  });
1266
1691
  });
1267
1692
  item = toRow(row);
@@ -1461,6 +1886,28 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1461
1886
  previous: emptyPrior()
1462
1887
  };
1463
1888
  }
1889
+ async setParentWorkItemIfMissing({ orgId, id, userId, parentWorkItemId }) {
1890
+ const candidate = await this.#db.findOne("work_items", {
1891
+ org_id: orgId,
1892
+ id
1893
+ });
1894
+ if (!candidate) return null;
1895
+ return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, async (ops) => {
1896
+ const row = await ops.updateAtomic("work_items", {
1897
+ org_id: orgId,
1898
+ id
1899
+ }, async (current) => {
1900
+ if (current.parent_work_item_id !== null) return current;
1901
+ validateParentRelation(await this.#listWithOps(ops, orgId, current.factory_project_id), current.id, parentWorkItemId);
1902
+ return applyUpdate({
1903
+ current,
1904
+ userId,
1905
+ input: { parentWorkItemId }
1906
+ });
1907
+ });
1908
+ return row ? toWorkItem(row) : null;
1909
+ });
1910
+ }
1464
1911
  async update({ orgId, id, userId, patch }) {
1465
1912
  const run = async (ops) => {
1466
1913
  let previous = emptyPrior();
@@ -1518,6 +1965,13 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1518
1965
  const evaluation = await ops.findOne("factory_rule_evaluations", { id: evaluationId });
1519
1966
  if (evaluation) ingressIds.add(String(evaluation.ingress_id));
1520
1967
  }
1968
+ const decisionIds = decisions.map((decision) => decision.id);
1969
+ for (let index = 0; index < decisionIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) await ops.deleteMany("factory_attention_receipts", {
1970
+ org_id: orgId,
1971
+ factory_project_id: factoryProjectId,
1972
+ kind: "automation-failed",
1973
+ source_id: { in: decisionIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1974
+ });
1521
1975
  await ops.deleteMany("factory_deferred_decisions", {
1522
1976
  org_id: orgId,
1523
1977
  factory_project_id: factoryProjectId,
@@ -1559,6 +2013,6 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1559
2013
  }
1560
2014
  };
1561
2015
  //#endregion
1562
- export { FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
2016
+ export { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryAttentionKey, factoryDecisionAttentionIdentity, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1563
2017
 
1564
2018
  //# sourceMappingURL=base.js.map