@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.
- package/CHANGELOG.md +212 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -1
- package/dist/auth.js.map +1 -1
- package/dist/factory.d.ts +8 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +25 -8
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/integrations/base.d.ts +3 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts +14 -0
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +48 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/provenance.d.ts +7 -2
- package/dist/integrations/github/provenance.d.ts.map +1 -1
- package/dist/integrations/github/provenance.js +16 -2
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +8 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +228 -7
- package/dist/integrations/github/routes.js.map +1 -1
- 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/integrations/github/session-subscriptions.d.ts +9 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +39 -1
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +50 -0
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +3 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +33 -0
- package/dist/integrations/platform/github/integration.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/intake.d.ts +5 -0
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +70 -19
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/surface.d.ts +6 -5
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +62 -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 +35 -14
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/defaults.js +5 -5
- package/dist/rules/defaults.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 +9 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +118 -45
- 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/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +9 -5
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +5 -2
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +15 -2
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +6 -1
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +25 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/secret-encryption.d.ts +25 -0
- package/dist/secret-encryption.d.ts.map +1 -0
- package/dist/secret-encryption.js +96 -0
- package/dist/secret-encryption.js.map +1 -0
- package/dist/session/factory-session.d.ts +13 -3
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +15 -6
- 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/session/session-title.d.ts +7 -0
- package/dist/session/session-title.d.ts.map +1 -0
- package/dist/session/session-title.js +14 -0
- package/dist/session/session-title.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +35 -0
- package/dist/session/thread-title-mirror.d.ts.map +1 -0
- package/dist/session/thread-title-mirror.js +51 -0
- package/dist/session/thread-title-mirror.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +3 -1
- package/dist/storage/domains/credentials/base.d.ts.map +1 -1
- package/dist/storage/domains/credentials/base.js +33 -15
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.d.ts +3 -1
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +45 -25
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +3 -1
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +28 -12
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +8 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +14 -2
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +7 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +120 -12
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +514 -44
- 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/factory-skills/factory-triage/SKILL.md +7 -17
- 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: "
|
|
127
|
+
name: "work_items_org_project_created_at_id_idx",
|
|
93
128
|
columns: [
|
|
94
129
|
"org_id",
|
|
95
130
|
"factory_project_id",
|
|
96
|
-
"
|
|
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: "
|
|
352
|
-
columns: [
|
|
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: [["
|
|
804
|
+
}, { orderBy: [["created_at", "desc"], ["id", "desc"]] })).map(toWorkItem);
|
|
626
805
|
}
|
|
627
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
1368
|
+
const superseded = [];
|
|
1032
1369
|
for (const row of rows) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|