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