@mastra/factory 0.10.0-alpha.1 → 0.10.0-alpha.13

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 (138) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +2 -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 +25 -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/base.d.ts +3 -1
  13. package/dist/integrations/base.d.ts.map +1 -1
  14. package/dist/integrations/github/integration.d.ts +14 -0
  15. package/dist/integrations/github/integration.d.ts.map +1 -1
  16. package/dist/integrations/github/integration.js +48 -1
  17. package/dist/integrations/github/integration.js.map +1 -1
  18. package/dist/integrations/github/provenance.d.ts +7 -2
  19. package/dist/integrations/github/provenance.d.ts.map +1 -1
  20. package/dist/integrations/github/provenance.js +16 -2
  21. package/dist/integrations/github/provenance.js.map +1 -1
  22. package/dist/integrations/github/routes.d.ts +8 -1
  23. package/dist/integrations/github/routes.d.ts.map +1 -1
  24. package/dist/integrations/github/routes.js +228 -7
  25. package/dist/integrations/github/routes.js.map +1 -1
  26. package/dist/integrations/github/rules.d.ts.map +1 -1
  27. package/dist/integrations/github/rules.js +193 -127
  28. package/dist/integrations/github/rules.js.map +1 -1
  29. package/dist/integrations/github/session-subscriptions.d.ts +9 -0
  30. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  31. package/dist/integrations/github/session-subscriptions.js +39 -1
  32. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  33. package/dist/integrations/linear/routes.d.ts.map +1 -1
  34. package/dist/integrations/linear/routes.js +50 -0
  35. package/dist/integrations/linear/routes.js.map +1 -1
  36. package/dist/integrations/platform/github/integration.d.ts +3 -1
  37. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  38. package/dist/integrations/platform/github/integration.js +33 -0
  39. package/dist/integrations/platform/github/integration.js.map +1 -1
  40. package/dist/routes/attention.d.ts +17 -0
  41. package/dist/routes/attention.d.ts.map +1 -0
  42. package/dist/routes/attention.js +305 -0
  43. package/dist/routes/attention.js.map +1 -0
  44. package/dist/routes/intake.d.ts +5 -0
  45. package/dist/routes/intake.d.ts.map +1 -1
  46. package/dist/routes/intake.js +70 -19
  47. package/dist/routes/intake.js.map +1 -1
  48. package/dist/routes/surface.d.ts +6 -5
  49. package/dist/routes/surface.d.ts.map +1 -1
  50. package/dist/routes/surface.js +62 -43
  51. package/dist/routes/surface.js.map +1 -1
  52. package/dist/routes/work-items.d.ts.map +1 -1
  53. package/dist/routes/work-items.js +35 -14
  54. package/dist/routes/work-items.js.map +1 -1
  55. package/dist/rules/defaults.js +5 -5
  56. package/dist/rules/defaults.js.map +1 -1
  57. package/dist/rules/dispatch-errors.d.ts +13 -0
  58. package/dist/rules/dispatch-errors.d.ts.map +1 -0
  59. package/dist/rules/dispatch-errors.js +77 -0
  60. package/dist/rules/dispatch-errors.js.map +1 -0
  61. package/dist/rules/dispatcher.d.ts +9 -0
  62. package/dist/rules/dispatcher.d.ts.map +1 -1
  63. package/dist/rules/dispatcher.js +118 -45
  64. package/dist/rules/dispatcher.js.map +1 -1
  65. package/dist/rules/processor.d.ts.map +1 -1
  66. package/dist/rules/processor.js +30 -14
  67. package/dist/rules/processor.js.map +1 -1
  68. package/dist/rules/terminal-cleanup.d.ts +1 -1
  69. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  70. package/dist/rules/terminal-cleanup.js +2 -2
  71. package/dist/rules/terminal-cleanup.js.map +1 -1
  72. package/dist/rules/tools.d.ts.map +1 -1
  73. package/dist/rules/tools.js +9 -5
  74. package/dist/rules/tools.js.map +1 -1
  75. package/dist/rules/transition-service.d.ts +5 -2
  76. package/dist/rules/transition-service.d.ts.map +1 -1
  77. package/dist/rules/transition-service.js +15 -2
  78. package/dist/rules/transition-service.js.map +1 -1
  79. package/dist/rules/types.d.ts +6 -1
  80. package/dist/rules/types.d.ts.map +1 -1
  81. package/dist/rules/types.js +25 -1
  82. package/dist/rules/types.js.map +1 -1
  83. package/dist/secret-encryption.d.ts +25 -0
  84. package/dist/secret-encryption.d.ts.map +1 -0
  85. package/dist/secret-encryption.js +96 -0
  86. package/dist/secret-encryption.js.map +1 -0
  87. package/dist/session/factory-session.d.ts +13 -3
  88. package/dist/session/factory-session.d.ts.map +1 -1
  89. package/dist/session/factory-session.js +15 -6
  90. package/dist/session/factory-session.js.map +1 -1
  91. package/dist/session/first-exec-capture.d.ts +16 -10
  92. package/dist/session/first-exec-capture.d.ts.map +1 -1
  93. package/dist/session/first-exec-capture.js +74 -14
  94. package/dist/session/first-exec-capture.js.map +1 -1
  95. package/dist/session/first-message-capture.d.ts +14 -10
  96. package/dist/session/first-message-capture.d.ts.map +1 -1
  97. package/dist/session/first-message-capture.js +17 -11
  98. package/dist/session/first-message-capture.js.map +1 -1
  99. package/dist/session/session-title.d.ts +7 -0
  100. package/dist/session/session-title.d.ts.map +1 -0
  101. package/dist/session/session-title.js +14 -0
  102. package/dist/session/session-title.js.map +1 -0
  103. package/dist/session/thread-title-mirror.d.ts +35 -0
  104. package/dist/session/thread-title-mirror.d.ts.map +1 -0
  105. package/dist/session/thread-title-mirror.js +51 -0
  106. package/dist/session/thread-title-mirror.js.map +1 -0
  107. package/dist/storage/domains/credentials/base.d.ts +3 -1
  108. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  109. package/dist/storage/domains/credentials/base.js +33 -15
  110. package/dist/storage/domains/credentials/base.js.map +1 -1
  111. package/dist/storage/domains/custom-providers/base.d.ts +3 -1
  112. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
  113. package/dist/storage/domains/custom-providers/base.js +45 -25
  114. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  115. package/dist/storage/domains/integrations/base.d.ts +3 -1
  116. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  117. package/dist/storage/domains/integrations/base.js +28 -12
  118. package/dist/storage/domains/integrations/base.js.map +1 -1
  119. package/dist/storage/domains/source-control/base.d.ts +8 -0
  120. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  121. package/dist/storage/domains/source-control/base.js +14 -2
  122. package/dist/storage/domains/source-control/base.js.map +1 -1
  123. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  124. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  125. package/dist/storage/domains/source-control/inmemory.js +7 -0
  126. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  127. package/dist/storage/domains/work-items/base.d.ts +120 -12
  128. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  129. package/dist/storage/domains/work-items/base.js +514 -44
  130. package/dist/storage/domains/work-items/base.js.map +1 -1
  131. package/dist/work-item-branch.d.ts +23 -0
  132. package/dist/work-item-branch.d.ts.map +1 -0
  133. package/dist/work-item-branch.js +39 -0
  134. package/dist/work-item-branch.js.map +1 -0
  135. package/factory-skills/factory-rereview/SKILL.md +2 -0
  136. package/factory-skills/factory-review/SKILL.md +2 -0
  137. package/factory-skills/factory-triage/SKILL.md +7 -17
  138. package/package.json +5 -5
@@ -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
@@ -72,6 +103,10 @@ const WORK_ITEMS_SCHEMA = {
72
103
  type: "json",
73
104
  nullable: true
74
105
  },
106
+ triage_type: {
107
+ type: "text",
108
+ nullable: true
109
+ },
75
110
  autonomy_armed_at: {
76
111
  type: "timestamp",
77
112
  nullable: true
@@ -89,11 +124,12 @@ const WORK_ITEMS_SCHEMA = {
89
124
  columns: ["factory_project_id", "source_key"]
90
125
  }],
91
126
  indexes: [{
92
- name: "work_items_org_project_updated_at_idx",
127
+ name: "work_items_org_project_created_at_id_idx",
93
128
  columns: [
94
129
  "org_id",
95
130
  "factory_project_id",
96
- "updated_at"
131
+ "created_at",
132
+ "id"
97
133
  ]
98
134
  }, {
99
135
  name: "work_items_project_parent_idx",
@@ -119,6 +155,7 @@ function toWorkItem(row) {
119
155
  stageHistory: row.stage_history,
120
156
  sessions: row.sessions,
121
157
  metadata: row.metadata,
158
+ triageType: row.triage_type ?? null,
122
159
  autonomyArmedAt: row.autonomy_armed_at ?? null,
123
160
  revision: row.revision,
124
161
  createdBy: row.created_by,
@@ -135,6 +172,7 @@ function patchColumns(changes) {
135
172
  ...changes.stageHistory !== void 0 ? { stage_history: changes.stageHistory } : {},
136
173
  ...changes.sessions !== void 0 ? { sessions: changes.sessions } : {},
137
174
  ...changes.metadata !== void 0 ? { metadata: changes.metadata } : {},
175
+ ...changes.triageType !== void 0 ? { triage_type: changes.triageType } : {},
138
176
  ...changes.autonomyArmedAt !== void 0 && changes.autonomyArmedAt !== null ? { autonomy_armed_at: changes.autonomyArmedAt } : {},
139
177
  ...changes.revision !== void 0 ? { revision: changes.revision } : {},
140
178
  ...changes.updatedAt !== void 0 ? { updated_at: changes.updatedAt } : {}
@@ -223,6 +261,8 @@ function applyUpdate({ current, userId, input }) {
223
261
  updated_at: now
224
262
  };
225
263
  }
264
+ const ATTENTION_RECEIPT_QUERY_BATCH_SIZE = 200;
265
+ const ATTENTION_RECEIPT_WRITE_BATCH_SIZE = 25;
226
266
  const projectRelationLocks = /* @__PURE__ */ new Map();
227
267
  function withInProcessProjectLock(key, fn) {
228
268
  const result = (projectRelationLocks.get(key) ?? Promise.resolve()).then(fn, fn);
@@ -240,6 +280,13 @@ function decisionSourceKey(decision) {
240
280
  if (record.type !== "upsertLinkedWorkItem" || typeof record.sourceKey !== "string") return null;
241
281
  return record.sourceKey;
242
282
  }
283
+ function deferredDecisionType(decision) {
284
+ return typeof decision.decision.type === "string" ? decision.decision.type : void 0;
285
+ }
286
+ function ingressWasAccepted(row) {
287
+ if (!row || typeof row.result !== "object" || row.result === null || Array.isArray(row.result)) return false;
288
+ return "status" in row.result && row.result.status === "accepted";
289
+ }
243
290
  const FACTORY_GOVERNANCE_SCHEMAS = [
244
291
  {
245
292
  name: "factory_rule_ingress",
@@ -315,6 +362,14 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
315
362
  decision: { type: "json" },
316
363
  status: { type: "text" },
317
364
  attempts: { type: "integer" },
365
+ delivery_generation: {
366
+ type: "integer",
367
+ default: 0
368
+ },
369
+ failure_occurrence: {
370
+ type: "integer",
371
+ default: 0
372
+ },
318
373
  available_at: { type: "timestamp" },
319
374
  lease_owner: {
320
375
  type: "text",
@@ -328,10 +383,18 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
328
383
  type: "text",
329
384
  nullable: true
330
385
  },
386
+ failure_code: {
387
+ type: "text",
388
+ nullable: true
389
+ },
331
390
  approved_at: {
332
391
  type: "timestamp",
333
392
  nullable: true
334
393
  },
394
+ approved_by: {
395
+ type: "text",
396
+ nullable: true
397
+ },
335
398
  completed_at: {
336
399
  type: "timestamp",
337
400
  nullable: true
@@ -347,9 +410,80 @@ const FACTORY_GOVERNANCE_SCHEMAS = [
347
410
  "idempotency_key"
348
411
  ]
349
412
  }],
413
+ indexes: [
414
+ {
415
+ name: "factory_deferred_decisions_claim_idx",
416
+ columns: ["status", "created_at"]
417
+ },
418
+ {
419
+ name: "factory_deferred_decisions_tenant_status_created_idx",
420
+ columns: [
421
+ "org_id",
422
+ "factory_project_id",
423
+ "status",
424
+ "created_at",
425
+ "id"
426
+ ]
427
+ },
428
+ {
429
+ name: "factory_deferred_decisions_tenant_status_failed_idx",
430
+ columns: [
431
+ "org_id",
432
+ "factory_project_id",
433
+ "status",
434
+ "updated_at",
435
+ "id"
436
+ ]
437
+ }
438
+ ]
439
+ },
440
+ {
441
+ name: "factory_attention_receipts",
442
+ columns: {
443
+ id: { type: "uuid-pk" },
444
+ org_id: { type: "text" },
445
+ factory_project_id: { type: "text" },
446
+ user_id: { type: "text" },
447
+ kind: { type: "text" },
448
+ source_id: { type: "text" },
449
+ occurrence: { type: "integer" },
450
+ state: { type: "text" },
451
+ read_at: { type: "timestamp" },
452
+ archived_at: {
453
+ type: "timestamp",
454
+ nullable: true
455
+ },
456
+ created_at: { type: "timestamp" },
457
+ updated_at: { type: "timestamp" }
458
+ },
459
+ uniqueIndexes: [{
460
+ name: "factory_attention_receipts_user_source_unique",
461
+ columns: [
462
+ "org_id",
463
+ "factory_project_id",
464
+ "user_id",
465
+ "kind",
466
+ "source_id",
467
+ "occurrence"
468
+ ]
469
+ }],
350
470
  indexes: [{
351
- name: "factory_deferred_decisions_claim_idx",
352
- columns: ["status", "created_at"]
471
+ name: "factory_attention_receipts_user_state_idx",
472
+ columns: [
473
+ "org_id",
474
+ "factory_project_id",
475
+ "user_id",
476
+ "state"
477
+ ]
478
+ }, {
479
+ name: "factory_attention_receipts_source_idx",
480
+ columns: [
481
+ "org_id",
482
+ "factory_project_id",
483
+ "kind",
484
+ "source_id",
485
+ "occurrence"
486
+ ]
353
487
  }]
354
488
  },
355
489
  {
@@ -477,16 +611,52 @@ function toDeferredDecision(row) {
477
611
  decision: row.decision,
478
612
  status: row.status,
479
613
  attempts: Number(row.attempts),
614
+ deliveryGeneration: Number(row.delivery_generation ?? 0),
615
+ failureOccurrence: Number(row.failure_occurrence ?? 0),
480
616
  availableAt: row.available_at,
481
617
  leaseOwner: row.lease_owner ?? null,
482
618
  leaseExpiresAt: row.lease_expires_at ?? null,
483
619
  lastError: row.last_error ?? null,
620
+ failureCode: isFactoryDispatchFailureCode(row.failure_code) ? row.failure_code : null,
484
621
  approvedAt: row.approved_at ?? null,
622
+ approvedBy: row.approved_by ?? null,
485
623
  completedAt: row.completed_at ?? null,
486
624
  createdAt: row.created_at,
487
625
  updatedAt: row.updated_at
488
626
  };
489
627
  }
628
+ function attentionReceiptKind(value) {
629
+ if (value === "automation-failed") return value;
630
+ throw new Error(`Unsupported attention receipt kind '${String(value)}'.`);
631
+ }
632
+ function attentionReceiptState(value) {
633
+ if (value === "read" || value === "archived") return value;
634
+ throw new Error(`Unsupported attention receipt state '${String(value)}'.`);
635
+ }
636
+ function toAttentionReceipt(row) {
637
+ if (typeof row.source_id !== "string" || !row.source_id) throw new Error("Attention receipt source_id must be a non-empty string.");
638
+ const occurrence = Number(row.occurrence);
639
+ if (!Number.isSafeInteger(occurrence) || occurrence < 0) throw new Error("Attention receipt occurrence must be a non-negative safe integer.");
640
+ if (!(row.read_at instanceof Date)) throw new Error("Attention receipt read_at must be a timestamp.");
641
+ const archivedAt = row.archived_at;
642
+ if (archivedAt !== null && archivedAt !== void 0 && !(archivedAt instanceof Date)) throw new Error("Attention receipt archived_at must be a timestamp or null.");
643
+ if (!(row.created_at instanceof Date)) throw new Error("Attention receipt created_at must be a timestamp.");
644
+ if (!(row.updated_at instanceof Date)) throw new Error("Attention receipt updated_at must be a timestamp.");
645
+ return {
646
+ id: row.id,
647
+ orgId: row.org_id,
648
+ factoryProjectId: String(row.factory_project_id),
649
+ userId: String(row.user_id),
650
+ kind: attentionReceiptKind(row.kind),
651
+ sourceId: row.source_id,
652
+ occurrence,
653
+ state: attentionReceiptState(row.state),
654
+ readAt: row.read_at,
655
+ archivedAt: archivedAt ?? null,
656
+ createdAt: row.created_at,
657
+ updatedAt: row.updated_at
658
+ };
659
+ }
490
660
  function toPendingStart(row) {
491
661
  return {
492
662
  id: row.id,
@@ -512,6 +682,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
512
682
  }
513
683
  async init() {
514
684
  await this.ensureCollections([WORK_ITEMS_SCHEMA, ...FACTORY_GOVERNANCE_SCHEMAS]);
685
+ await this.repairLegacyAttentionState();
515
686
  }
516
687
  async dangerouslyClearAll() {
517
688
  await this.ops.deleteMany("work_items", {});
@@ -519,6 +690,11 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
519
690
  get #db() {
520
691
  return this.ops;
521
692
  }
693
+ async #countRows(collection, where) {
694
+ const count = this.#db.count;
695
+ if (!count) throw new Error("[WorkItemsStorage] storage backend does not support collection counts.");
696
+ return count.call(this.#db, collection, where);
697
+ }
522
698
  async #withProjectRelationTransaction(orgId, factoryProjectId, fn) {
523
699
  return withInProcessProjectLock(`work-items:${orgId}:${factoryProjectId}`, () => this.storage.withTransaction(fn, { isolationLevel: "serializable" }));
524
700
  }
@@ -608,11 +784,14 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
608
784
  failed = true;
609
785
  return {
610
786
  status: input.terminal ? "failed" : "retry",
787
+ ...table === "factory_deferred_decisions" && !input.terminal && input.advanceDeliveryGeneration !== false ? { delivery_generation: Number(current.delivery_generation ?? 0) + 1 } : {},
611
788
  available_at: input.availableAt,
612
789
  lease_owner: null,
613
790
  lease_expires_at: null,
614
791
  last_error: input.lastError,
792
+ ...table === "factory_deferred_decisions" ? { failure_code: input.failureCode } : {},
615
793
  completed_at: input.terminal ? input.now : null,
794
+ ...table === "factory_deferred_decisions" && input.terminal ? { failure_occurrence: Number(current.failure_occurrence ?? 0) + 1 } : {},
616
795
  updated_at: input.now
617
796
  };
618
797
  });
@@ -622,12 +801,24 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
622
801
  return (await ops.findMany("work_items", {
623
802
  org_id: orgId,
624
803
  factory_project_id: factoryProjectId
625
- }, { orderBy: [["updated_at", "desc"]] })).map(toWorkItem);
804
+ }, { orderBy: [["created_at", "desc"], ["id", "desc"]] })).map(toWorkItem);
626
805
  }
627
- /** List the org's work items for a project, newest first. */
806
+ /**
807
+ * List the org's work items for a project, newest first. Ordered on `created_at` with an id
808
+ * tiebreak so the order is stable: `updated_at` moves under every write, which makes it useless
809
+ * as a cursor and non-deterministic for the callers that iterate this list.
810
+ */
628
811
  async list({ orgId, factoryProjectId }) {
629
812
  return this.#listWithOps(this.#db, orgId, factoryProjectId);
630
813
  }
814
+ async listByIds({ orgId, factoryProjectId, ids }) {
815
+ if (ids.length === 0) return [];
816
+ return (await this.#db.findMany("work_items", {
817
+ org_id: orgId,
818
+ factory_project_id: factoryProjectId,
819
+ id: { in: [...new Set(ids)] }
820
+ })).map(toWorkItem);
821
+ }
631
822
  async get({ orgId, id }) {
632
823
  const row = await this.#db.findOne("work_items", {
633
824
  org_id: orgId,
@@ -685,9 +876,19 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
685
876
  return null;
686
877
  }
687
878
  const arm = input.armAutonomy === true && !existing.autonomyArmedAt;
688
- if (existing.stages.length === 1 && existing.stages[0] === input.destinationStage) return arm ? patchColumns({ autonomyArmedAt: now }) : null;
879
+ const triageType = existing.triageType ?? input.triageType ?? null;
880
+ const classified = triageType !== existing.triageType;
881
+ if (existing.stages.length === 1 && existing.stages[0] === input.destinationStage) return arm || classified ? patchColumns({
882
+ ...arm ? { autonomyArmedAt: now } : {},
883
+ ...classified ? { triageType } : {},
884
+ ...classified ? {
885
+ revision: existing.revision + 1,
886
+ updatedAt: now
887
+ } : {}
888
+ }) : null;
689
889
  return patchColumns({
690
890
  ...arm ? { autonomyArmedAt: now } : {},
891
+ ...classified ? { triageType } : {},
691
892
  stages: [input.destinationStage],
692
893
  stageHistory: applyStageTransition(existing.stageHistory, existing.stages, [input.destinationStage], input.actorId, now),
693
894
  revision: existing.revision + 1,
@@ -749,6 +950,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
749
950
  decision,
750
951
  status: "pending",
751
952
  attempts: 0,
953
+ delivery_generation: 0,
752
954
  available_at: now,
753
955
  lease_owner: null,
754
956
  lease_expires_at: null,
@@ -865,6 +1067,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
865
1067
  decision,
866
1068
  status: "pending",
867
1069
  attempts: 0,
1070
+ delivery_generation: 0,
868
1071
  available_at: input.now,
869
1072
  lease_owner: null,
870
1073
  lease_expires_at: null,
@@ -934,6 +1137,143 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
934
1137
  hasMore: rows.length > input.limit
935
1138
  };
936
1139
  }
1140
+ async listFailedDecisionPage(input) {
1141
+ const rows = await this.#db.findMany("factory_deferred_decisions", {
1142
+ org_id: input.orgId,
1143
+ factory_project_id: input.factoryProjectId,
1144
+ status: "failed"
1145
+ }, {
1146
+ orderBy: [["updated_at", "desc"], ["id", "desc"]],
1147
+ limit: input.limit + 1,
1148
+ ...input.before ? { cursor: { values: [input.before.occurredAt, input.before.id] } } : {}
1149
+ });
1150
+ return {
1151
+ decisions: rows.slice(0, input.limit).map(toDeferredDecision),
1152
+ hasMore: rows.length > input.limit
1153
+ };
1154
+ }
1155
+ async countDeferredDecisionsByStatuses({ orgId, factoryProjectId, statuses }) {
1156
+ return this.#countRows("factory_deferred_decisions", {
1157
+ org_id: orgId,
1158
+ factory_project_id: factoryProjectId,
1159
+ status: { in: statuses }
1160
+ });
1161
+ }
1162
+ async getDeferredDecision(orgId, factoryProjectId, decisionId) {
1163
+ const row = await this.#db.findOne("factory_deferred_decisions", {
1164
+ id: decisionId,
1165
+ org_id: orgId,
1166
+ factory_project_id: factoryProjectId
1167
+ });
1168
+ return row ? toDeferredDecision(row) : null;
1169
+ }
1170
+ async listAttentionReceipts({ orgId, factoryProjectId, userId, identities }) {
1171
+ if (identities.length === 0) return [];
1172
+ const requested = new Set(identities.map((identity) => `${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`));
1173
+ const sourceIds = [...new Set(identities.map((identity) => identity.sourceId))];
1174
+ const kinds = [...new Set(identities.map((identity) => identity.kind))];
1175
+ const receipts = [];
1176
+ for (let index = 0; index < sourceIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) {
1177
+ const rows = await this.#db.findMany("factory_attention_receipts", {
1178
+ org_id: orgId,
1179
+ factory_project_id: factoryProjectId,
1180
+ user_id: userId,
1181
+ kind: { in: kinds },
1182
+ source_id: { in: sourceIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1183
+ });
1184
+ receipts.push(...rows.map(toAttentionReceipt).filter((receipt) => requested.has(`${receipt.kind}\0${receipt.sourceId}\0${receipt.occurrence}`)));
1185
+ }
1186
+ return receipts;
1187
+ }
1188
+ async countAttentionReceipts({ orgId, factoryProjectId, userId, state }) {
1189
+ return this.#countRows("factory_attention_receipts", {
1190
+ org_id: orgId,
1191
+ factory_project_id: factoryProjectId,
1192
+ user_id: userId,
1193
+ kind: "automation-failed",
1194
+ ...state ? { state } : {}
1195
+ });
1196
+ }
1197
+ async deleteAttentionReceipts({ orgId, factoryProjectId, identities }) {
1198
+ const unique = [...new Map(identities.map((identity) => [`${identity.kind}\0${identity.sourceId}\0${identity.occurrence}`, identity])).values()];
1199
+ 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", {
1200
+ org_id: orgId,
1201
+ factory_project_id: factoryProjectId,
1202
+ kind: identity.kind,
1203
+ source_id: identity.sourceId,
1204
+ occurrence: identity.occurrence
1205
+ })));
1206
+ }
1207
+ async setAttentionReceipt(input) {
1208
+ return this.#retryAttentionReceiptWrite(() => this.storage.withTransaction((ops) => this.#setAttentionReceiptWithOps(ops, input)));
1209
+ }
1210
+ async #retryAttentionReceiptWrite(write) {
1211
+ try {
1212
+ return await write();
1213
+ } catch (error) {
1214
+ if (!(error instanceof UniqueViolationError)) throw error;
1215
+ return write();
1216
+ }
1217
+ }
1218
+ async #setAttentionReceiptWithOps(ops, { orgId, factoryProjectId, userId, decisionId, failureOccurrence, action, now }) {
1219
+ const identity = factoryDecisionAttentionIdentity(decisionId, failureOccurrence);
1220
+ let currentOccurrence = false;
1221
+ if (!await ops.updateAtomic("factory_deferred_decisions", {
1222
+ id: decisionId,
1223
+ org_id: orgId,
1224
+ factory_project_id: factoryProjectId
1225
+ }, (current) => {
1226
+ currentOccurrence = current.status === "failed" && Number(current.failure_occurrence ?? 0) === failureOccurrence;
1227
+ return null;
1228
+ }) || !currentOccurrence) return null;
1229
+ const where = {
1230
+ org_id: orgId,
1231
+ factory_project_id: factoryProjectId,
1232
+ user_id: userId,
1233
+ kind: identity.kind,
1234
+ source_id: identity.sourceId,
1235
+ occurrence: identity.occurrence
1236
+ };
1237
+ const existing = await ops.findOne("factory_attention_receipts", where);
1238
+ if (!existing) return toAttentionReceipt(await ops.insertOne("factory_attention_receipts", {
1239
+ ...where,
1240
+ state: action === "archive" ? "archived" : "read",
1241
+ read_at: now,
1242
+ archived_at: action === "archive" ? now : null,
1243
+ created_at: now,
1244
+ updated_at: now
1245
+ }));
1246
+ const row = await ops.updateAtomic("factory_attention_receipts", {
1247
+ id: existing.id,
1248
+ ...where
1249
+ }, (current) => {
1250
+ const preserveArchive = action === "read" && current.state === "archived";
1251
+ const archived = action === "archive" || preserveArchive;
1252
+ return {
1253
+ state: archived ? "archived" : "read",
1254
+ read_at: current.read_at,
1255
+ archived_at: action === "archive" ? now : archived ? current.archived_at ?? now : null,
1256
+ updated_at: now
1257
+ };
1258
+ });
1259
+ return row ? toAttentionReceipt(row) : null;
1260
+ }
1261
+ async markAttentionReceiptsRead({ orgId, factoryProjectId, userId, occurrences, now }) {
1262
+ const uniqueOccurrences = [...new Map(occurrences.map((occurrence) => [`${occurrence.decisionId}:${occurrence.failureOccurrence}`, occurrence])).values()];
1263
+ for (let index = 0; index < uniqueOccurrences.length; index += ATTENTION_RECEIPT_WRITE_BATCH_SIZE) {
1264
+ const batch = uniqueOccurrences.slice(index, index + ATTENTION_RECEIPT_WRITE_BATCH_SIZE);
1265
+ await this.#retryAttentionReceiptWrite(() => this.storage.withTransaction(async (ops) => {
1266
+ for (const occurrence of batch) await this.#setAttentionReceiptWithOps(ops, {
1267
+ orgId,
1268
+ factoryProjectId,
1269
+ userId,
1270
+ ...occurrence,
1271
+ action: "read",
1272
+ now
1273
+ });
1274
+ }));
1275
+ }
1276
+ }
937
1277
  async claimDeferredDecisions(input) {
938
1278
  return this.#claimLeases("factory_deferred_decisions", input, toDeferredDecision);
939
1279
  }
@@ -974,7 +1314,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
974
1314
  * is armed in the same transaction — a crash cannot release the run while
975
1315
  * leaving its follow-up work parked for re-approval.
976
1316
  */
977
- async approveDeferredDecision(orgId, factoryProjectId, decisionId, now) {
1317
+ async approveDeferredDecision(orgId, factoryProjectId, decisionId, now, approvedBy) {
978
1318
  return this.storage.withTransaction(async (ops) => {
979
1319
  let settled = false;
980
1320
  const row = await ops.updateAtomic("factory_deferred_decisions", {
@@ -989,6 +1329,7 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
989
1329
  attempts: 0,
990
1330
  available_at: now,
991
1331
  approved_at: now,
1332
+ approved_by: approvedBy ?? null,
992
1333
  updated_at: now
993
1334
  };
994
1335
  });
@@ -1014,27 +1355,38 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1014
1355
  });
1015
1356
  }
1016
1357
  /**
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.
1358
+ * Automatically retire proposed or failed work that a newer run or terminal
1359
+ * work-item state has overtaken. Human dismissal remains `dismissed`.
1023
1360
  */
1024
- async dismissProposalsForWorkItem(input) {
1361
+ async supersedeDecisionsForWorkItem(input) {
1025
1362
  const rows = await this.#db.findMany("factory_deferred_decisions", {
1026
1363
  org_id: input.orgId,
1027
1364
  factory_project_id: input.factoryProjectId,
1028
1365
  work_item_id: input.workItemId,
1029
- status: "proposed"
1366
+ status: { in: ["proposed", "failed"] }
1030
1367
  });
1031
- const dismissed = [];
1368
+ const superseded = [];
1032
1369
  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);
1370
+ const decision = toDeferredDecision(row);
1371
+ if (input.role !== void 0 && decision.decision.role !== input.role) continue;
1372
+ const record = decision.status === "proposed" ? await this.#settleProposedDecision({
1373
+ orgId: input.orgId,
1374
+ factoryProjectId: input.factoryProjectId,
1375
+ decisionId: decision.id
1376
+ }, {
1377
+ status: "superseded",
1378
+ updated_at: input.supersededAt,
1379
+ completed_at: input.supersededAt
1380
+ }) : await this.#resolveFailedDecision({
1381
+ orgId: input.orgId,
1382
+ factoryProjectId: input.factoryProjectId,
1383
+ decisionId: decision.id,
1384
+ status: "superseded",
1385
+ now: input.supersededAt
1386
+ });
1387
+ if (record) superseded.push(record);
1036
1388
  }
1037
- return dismissed;
1389
+ return superseded;
1038
1390
  }
1039
1391
  async #settleProposedDecision({ orgId, factoryProjectId, decisionId }, patch) {
1040
1392
  let settled = false;
@@ -1049,28 +1401,119 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1049
1401
  });
1050
1402
  return settled && row ? toDeferredDecision(row) : null;
1051
1403
  }
1404
+ async #resolveFailedDecision({ orgId, factoryProjectId, decisionId, status, now }) {
1405
+ return this.storage.withTransaction(async (ops) => {
1406
+ let resolved = false;
1407
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1408
+ id: decisionId,
1409
+ org_id: orgId,
1410
+ factory_project_id: factoryProjectId
1411
+ }, (current) => {
1412
+ if (current.status !== "failed") return null;
1413
+ resolved = true;
1414
+ return {
1415
+ status,
1416
+ updated_at: now
1417
+ };
1418
+ });
1419
+ if (!resolved || !row) return null;
1420
+ await ops.deleteMany("factory_attention_receipts", {
1421
+ org_id: orgId,
1422
+ factory_project_id: factoryProjectId,
1423
+ kind: "automation-failed",
1424
+ source_id: decisionId,
1425
+ occurrence: Number(row.failure_occurrence ?? 0)
1426
+ });
1427
+ return toDeferredDecision(row);
1428
+ });
1429
+ }
1430
+ async supersedeTerminalDecisionsForWorkItem(input) {
1431
+ await this.supersedeDecisionsForWorkItem(input);
1432
+ }
1433
+ async repairLegacyAttentionState() {
1434
+ let cursor;
1435
+ const inspectedWorkItems = /* @__PURE__ */ new Set();
1436
+ while (true) {
1437
+ const rows = await this.#db.findMany("factory_deferred_decisions", { status: { in: ["failed", "proposed"] } }, {
1438
+ orderBy: [["created_at", "asc"], ["id", "asc"]],
1439
+ limit: 201,
1440
+ ...cursor ? { cursor } : {}
1441
+ });
1442
+ const page = rows.slice(0, ATTENTION_RECEIPT_QUERY_BATCH_SIZE);
1443
+ for (const row of page) {
1444
+ const decision = toDeferredDecision(row);
1445
+ if (decision.status === "failed" && decision.failureOccurrence === 0 && deferredDecisionType(decision) === "transition") {
1446
+ if (ingressWasAccepted(await this.#db.findOne("factory_rule_ingress", {
1447
+ org_id: decision.orgId,
1448
+ factory_project_id: decision.factoryProjectId,
1449
+ identity: `decision:${decision.idempotencyKey}`
1450
+ }))) {
1451
+ await this.#resolveFailedDecision({
1452
+ orgId: decision.orgId,
1453
+ factoryProjectId: decision.factoryProjectId,
1454
+ decisionId: decision.id,
1455
+ status: "succeeded",
1456
+ now: /* @__PURE__ */ new Date()
1457
+ });
1458
+ continue;
1459
+ }
1460
+ }
1461
+ if (!decision.workItemId) continue;
1462
+ const itemKey = `${decision.orgId}\0${decision.factoryProjectId}\0${decision.workItemId}`;
1463
+ if (inspectedWorkItems.has(itemKey)) continue;
1464
+ inspectedWorkItems.add(itemKey);
1465
+ const item = await this.get({
1466
+ orgId: decision.orgId,
1467
+ id: decision.workItemId
1468
+ });
1469
+ if (!item || !isTerminalFactoryRuleStage(item.stages)) continue;
1470
+ await this.supersedeDecisionsForWorkItem({
1471
+ orgId: decision.orgId,
1472
+ factoryProjectId: decision.factoryProjectId,
1473
+ workItemId: decision.workItemId,
1474
+ supersededAt: /* @__PURE__ */ new Date()
1475
+ });
1476
+ }
1477
+ const last = page.at(-1);
1478
+ if (rows.length <= ATTENTION_RECEIPT_QUERY_BATCH_SIZE || !last) return;
1479
+ if (!(last.created_at instanceof Date)) throw new Error("Deferred decision created_at is not a timestamp.");
1480
+ cursor = { values: [last.created_at, last.id] };
1481
+ }
1482
+ }
1052
1483
  /** Requeue the same idempotent terminal effect; non-failed decisions are never rerun. */
1053
1484
  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
- };
1485
+ return this.storage.withTransaction(async (ops) => {
1486
+ let retried = false;
1487
+ const row = await ops.updateAtomic("factory_deferred_decisions", {
1488
+ id: decisionId,
1489
+ org_id: orgId,
1490
+ factory_project_id: factoryProjectId
1491
+ }, (current) => {
1492
+ if (current.status !== "failed") return null;
1493
+ retried = true;
1494
+ return {
1495
+ status: "retry",
1496
+ attempts: 0,
1497
+ delivery_generation: Number(current.delivery_generation ?? 0) + 1,
1498
+ available_at: now,
1499
+ lease_owner: null,
1500
+ lease_expires_at: null,
1501
+ last_error: null,
1502
+ failure_code: null,
1503
+ completed_at: null,
1504
+ updated_at: now
1505
+ };
1506
+ });
1507
+ if (!retried || !row) return null;
1508
+ await ops.deleteMany("factory_attention_receipts", {
1509
+ org_id: orgId,
1510
+ factory_project_id: factoryProjectId,
1511
+ kind: "automation-failed",
1512
+ source_id: decisionId,
1513
+ occurrence: Number(row.failure_occurrence ?? 0)
1514
+ });
1515
+ return toDeferredDecision(row);
1072
1516
  });
1073
- return retried && row ? toDeferredDecision(row) : null;
1074
1517
  }
1075
1518
  /** Resolve exact active agent authority; partial session matches never authorize. */
1076
1519
  async findActiveRunBinding(address) {
@@ -1256,12 +1699,10 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1256
1699
  let item;
1257
1700
  if (row) {
1258
1701
  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
1702
  return applyUpdate({
1262
1703
  current,
1263
1704
  userId: input.userId,
1264
- input: { sessions }
1705
+ input: { sessions: { [input.role]: input.session } }
1265
1706
  });
1266
1707
  });
1267
1708
  item = toRow(row);
@@ -1461,6 +1902,28 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1461
1902
  previous: emptyPrior()
1462
1903
  };
1463
1904
  }
1905
+ async setParentWorkItemIfMissing({ orgId, id, userId, parentWorkItemId }) {
1906
+ const candidate = await this.#db.findOne("work_items", {
1907
+ org_id: orgId,
1908
+ id
1909
+ });
1910
+ if (!candidate) return null;
1911
+ return this.#withProjectRelationTransaction(orgId, candidate.factory_project_id, async (ops) => {
1912
+ const row = await ops.updateAtomic("work_items", {
1913
+ org_id: orgId,
1914
+ id
1915
+ }, async (current) => {
1916
+ if (current.parent_work_item_id !== null) return current;
1917
+ validateParentRelation(await this.#listWithOps(ops, orgId, current.factory_project_id), current.id, parentWorkItemId);
1918
+ return applyUpdate({
1919
+ current,
1920
+ userId,
1921
+ input: { parentWorkItemId }
1922
+ });
1923
+ });
1924
+ return row ? toWorkItem(row) : null;
1925
+ });
1926
+ }
1464
1927
  async update({ orgId, id, userId, patch }) {
1465
1928
  const run = async (ops) => {
1466
1929
  let previous = emptyPrior();
@@ -1518,6 +1981,13 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1518
1981
  const evaluation = await ops.findOne("factory_rule_evaluations", { id: evaluationId });
1519
1982
  if (evaluation) ingressIds.add(String(evaluation.ingress_id));
1520
1983
  }
1984
+ const decisionIds = decisions.map((decision) => decision.id);
1985
+ for (let index = 0; index < decisionIds.length; index += ATTENTION_RECEIPT_QUERY_BATCH_SIZE) await ops.deleteMany("factory_attention_receipts", {
1986
+ org_id: orgId,
1987
+ factory_project_id: factoryProjectId,
1988
+ kind: "automation-failed",
1989
+ source_id: { in: decisionIds.slice(index, index + ATTENTION_RECEIPT_QUERY_BATCH_SIZE) }
1990
+ });
1521
1991
  await ops.deleteMany("factory_deferred_decisions", {
1522
1992
  org_id: orgId,
1523
1993
  factory_project_id: factoryProjectId,
@@ -1559,6 +2029,6 @@ var WorkItemsStorage = class extends FactoryStorageDomain {
1559
2029
  }
1560
2030
  };
1561
2031
  //#endregion
1562
- export { FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
2032
+ export { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WORK_ITEMS_SCHEMA, WorkItemRelationError, WorkItemsStorage, applyStageTransition, factoryAttentionKey, factoryDecisionAttentionIdentity, factoryDecisionHash, isAgentActor, stampSessions, validateParentRelation };
1563
2033
 
1564
2034
  //# sourceMappingURL=base.js.map