@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.
- package/CHANGELOG.md +80 -0
- package/dist/factory.js +2 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +193 -127
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/routes/attention.d.ts +17 -0
- package/dist/routes/attention.d.ts.map +1 -0
- package/dist/routes/attention.js +305 -0
- package/dist/routes/attention.js.map +1 -0
- package/dist/routes/surface.d.ts +2 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +60 -43
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +34 -13
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts +13 -0
- package/dist/rules/dispatch-errors.d.ts.map +1 -0
- package/dist/rules/dispatch-errors.js +77 -0
- package/dist/rules/dispatch-errors.js.map +1 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +60 -35
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +30 -14
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +1 -1
- package/dist/rules/terminal-cleanup.d.ts.map +1 -1
- package/dist/rules/terminal-cleanup.js +2 -2
- package/dist/rules/terminal-cleanup.js.map +1 -1
- package/dist/rules/transition-service.d.ts +2 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +2 -0
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +9 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/session/factory-session.d.ts +4 -0
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +10 -2
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +16 -10
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +74 -14
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +14 -10
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +17 -11
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +3 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +8 -2
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +101 -10
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +458 -35
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/work-item-branch.d.ts +23 -0
- package/dist/work-item-branch.d.ts.map +1 -0
- package/dist/work-item-branch.js +39 -0
- package/dist/work-item-branch.js.map +1 -0
- package/factory-skills/factory-rereview/SKILL.md +2 -0
- package/factory-skills/factory-review/SKILL.md +2 -0
- 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: "
|
|
352
|
-
columns: [
|
|
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
|
-
*
|
|
1018
|
-
*
|
|
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
|
|
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
|
|
1341
|
+
const superseded = [];
|
|
1032
1342
|
for (const row of rows) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|