@abelxiaoxing/cadence 1.4.0 → 1.4.1
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/README.md +58 -4
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/package.json +5 -3
- package/prompts/abel-design.md +93 -44
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +896 -0
- package/src/child-model.ts +129 -0
- package/src/child-session.ts +293 -326
- package/src/contracts.ts +184 -76
- package/src/delivery-compiler.ts +103 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +210 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/model-source.ts +22 -0
- package/src/package-candidate.ts +224 -0
- package/src/package-workflow.ts +253 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +146 -0
- package/src/plan-draft.ts +493 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +32 -0
- package/src/worker-broker.ts +37 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-worker.mjs +7 -0
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import type { WorkflowActivityUpdate } from "./activity-contracts.ts";
|
|
4
5
|
import {
|
|
5
6
|
assertPlanWithinChangeContract,
|
|
6
7
|
normalizeChangeContract,
|
|
@@ -18,11 +19,11 @@ import {
|
|
|
18
19
|
type ImplementPlan,
|
|
19
20
|
type PlanTaskDraft,
|
|
20
21
|
} from "./delivery-compiler.ts";
|
|
22
|
+
import { compareDeliveryRevision } from "./delivery-revision.ts";
|
|
21
23
|
import { snapshotFiles } from "./file-snapshot.ts";
|
|
22
24
|
import type { RoutePolicy } from "./route-policy.ts";
|
|
23
25
|
import {
|
|
24
26
|
canonicalJson,
|
|
25
|
-
legalControlCommands,
|
|
26
27
|
type RunProjection,
|
|
27
28
|
type RunState,
|
|
28
29
|
} from "./run-state.ts";
|
|
@@ -39,11 +40,9 @@ import {
|
|
|
39
40
|
RECOVERY_SCHEMA,
|
|
40
41
|
REJECTED_DELIVERY_SCHEMA,
|
|
41
42
|
} from "./storage-schema.ts";
|
|
42
|
-
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
43
43
|
import { permitsContextRead } from "./submit-tool.ts";
|
|
44
44
|
import {
|
|
45
45
|
type ActiveOperation,
|
|
46
|
-
approvalRequirement,
|
|
47
46
|
assertDelivery,
|
|
48
47
|
type BootstrapRow,
|
|
49
48
|
bootstrapAcceptanceHash,
|
|
@@ -51,7 +50,6 @@ import {
|
|
|
51
50
|
cancellableRead,
|
|
52
51
|
classifyPersistedContextRequest,
|
|
53
52
|
decideRecoveryAction,
|
|
54
|
-
deliveryBoundPaths,
|
|
55
53
|
type EngineOperationRow,
|
|
56
54
|
type EngineRunRow,
|
|
57
55
|
type EngineTaskRow,
|
|
@@ -66,16 +64,12 @@ import {
|
|
|
66
64
|
normalizeBootstrapAcceptanceFacts,
|
|
67
65
|
normalizeWorkflowContextRequest,
|
|
68
66
|
normalizeWorkflowVerificationStatus,
|
|
69
|
-
parseDeliveryDiagnostics,
|
|
70
67
|
parseJsonRecord,
|
|
71
68
|
parsePlan,
|
|
72
|
-
permitsPlanAmendment,
|
|
73
69
|
phases,
|
|
74
70
|
recoveryKey,
|
|
75
71
|
type SafeAttemptDiagnostic,
|
|
76
72
|
SHA256,
|
|
77
|
-
taskApprovalBoundary,
|
|
78
|
-
taskConflicts,
|
|
79
73
|
type WorkflowApplication,
|
|
80
74
|
type WorkflowApplicationContext,
|
|
81
75
|
type WorkflowAttemptOutcome,
|
|
@@ -90,6 +84,16 @@ import {
|
|
|
90
84
|
type WorkflowVerificationStatus,
|
|
91
85
|
type WorkflowWorker,
|
|
92
86
|
} from "./workflow-policy.ts";
|
|
87
|
+
import {
|
|
88
|
+
assessRecoveryGrant,
|
|
89
|
+
recoveryExhausted,
|
|
90
|
+
} from "./workflow-recovery-policy.ts";
|
|
91
|
+
import { selectRunnableTasks } from "./workflow-scheduling.ts";
|
|
92
|
+
import {
|
|
93
|
+
firstPausedTask,
|
|
94
|
+
projectWorkflowStatus,
|
|
95
|
+
type WorkflowResourceBudget,
|
|
96
|
+
} from "./workflow-status.ts";
|
|
93
97
|
export class WorkflowEngine {
|
|
94
98
|
readonly #consumerRoot: string;
|
|
95
99
|
readonly #stateRoot: ResolvedStateRoot;
|
|
@@ -948,36 +952,13 @@ export class WorkflowEngine {
|
|
|
948
952
|
engineRun.current_revision === null
|
|
949
953
|
? undefined
|
|
950
954
|
: this.#planForRevision(runId, engineRun.current_revision);
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
delivery.plan.tasks.map((task) => [task.taskId, task]),
|
|
956
|
-
);
|
|
957
|
-
const priorBoundPaths = new Set(
|
|
958
|
-
priorPlan ? deliveryBoundPaths(priorPlan) : [],
|
|
959
|
-
);
|
|
960
|
-
const boundaryExpanded = deliveryBoundPaths(delivery.plan).some(
|
|
961
|
-
(relative) => !priorBoundPaths.has(relative),
|
|
962
|
-
);
|
|
963
|
-
const compatible = new Set<string>();
|
|
964
|
-
if (priorPlan) {
|
|
965
|
-
for (const [taskId, priorTask] of priorTasks) {
|
|
966
|
-
const nextTask = nextTasks.get(taskId);
|
|
967
|
-
if (
|
|
968
|
-
nextTask &&
|
|
969
|
-
taskApprovalBoundary(priorPlan, priorTask) ===
|
|
970
|
-
taskApprovalBoundary(delivery.plan, nextTask)
|
|
971
|
-
) {
|
|
972
|
-
compatible.add(taskId);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
let invalidated = new Set(
|
|
977
|
-
priorRows
|
|
978
|
-
.filter((row) => !compatible.has(row.task_id))
|
|
979
|
-
.map((row) => row.task_id),
|
|
955
|
+
const comparison = compareDeliveryRevision(
|
|
956
|
+
priorPlan,
|
|
957
|
+
delivery.plan,
|
|
958
|
+
priorRows,
|
|
980
959
|
);
|
|
960
|
+
const { compatible, boundaryExpanded } = comparison;
|
|
961
|
+
let invalidated = comparison.invalidated;
|
|
981
962
|
let revalidatedWorkspace:
|
|
982
963
|
| {
|
|
983
964
|
baselineRevisionId: string;
|
|
@@ -1416,62 +1397,6 @@ export class WorkflowEngine {
|
|
|
1416
1397
|
});
|
|
1417
1398
|
}
|
|
1418
1399
|
|
|
1419
|
-
#hasConflict(
|
|
1420
|
-
row: EngineTaskRow,
|
|
1421
|
-
rows: EngineTaskRow[],
|
|
1422
|
-
selectedTaskIds: ReadonlySet<string>,
|
|
1423
|
-
): boolean {
|
|
1424
|
-
const candidate = this.#taskPlan(row);
|
|
1425
|
-
return rows.some((other) => {
|
|
1426
|
-
if (other.task_id === row.task_id || other.state === "verified") {
|
|
1427
|
-
return false;
|
|
1428
|
-
}
|
|
1429
|
-
if (other.state === "queued") {
|
|
1430
|
-
if (row.state !== "queued") return false;
|
|
1431
|
-
const candidatePosition = row.queue_position ?? Number.MAX_SAFE_INTEGER;
|
|
1432
|
-
const otherPosition = other.queue_position ?? Number.MAX_SAFE_INTEGER;
|
|
1433
|
-
if (
|
|
1434
|
-
otherPosition > candidatePosition ||
|
|
1435
|
-
(otherPosition === candidatePosition &&
|
|
1436
|
-
other.task_order > row.task_order)
|
|
1437
|
-
) {
|
|
1438
|
-
return false;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
if (other.task_order > row.task_order && other.state === "pending") {
|
|
1442
|
-
return false;
|
|
1443
|
-
}
|
|
1444
|
-
if (
|
|
1445
|
-
other.state === "pending" &&
|
|
1446
|
-
!this.#dependenciesVerified(other, rows)
|
|
1447
|
-
) {
|
|
1448
|
-
return false;
|
|
1449
|
-
}
|
|
1450
|
-
const conflict = taskConflicts(candidate, this.#taskPlan(other));
|
|
1451
|
-
return (
|
|
1452
|
-
conflict.taskLifetime ||
|
|
1453
|
-
(conflict.verification &&
|
|
1454
|
-
([
|
|
1455
|
-
"pending",
|
|
1456
|
-
"phase-ready",
|
|
1457
|
-
"phase-running",
|
|
1458
|
-
"validating",
|
|
1459
|
-
"queued",
|
|
1460
|
-
].includes(other.state) ||
|
|
1461
|
-
selectedTaskIds.has(other.task_id)))
|
|
1462
|
-
);
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
#dependenciesVerified(row: EngineTaskRow, rows: EngineTaskRow[]): boolean {
|
|
1467
|
-
const byId = new Map(
|
|
1468
|
-
rows.map((candidate) => [candidate.task_id, candidate]),
|
|
1469
|
-
);
|
|
1470
|
-
return this.#taskPlan(row).dependsOn.every(
|
|
1471
|
-
(dependency) => byId.get(dependency)?.state === "verified",
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
1400
|
#recordWorkspaceFacts(
|
|
1476
1401
|
runId: string,
|
|
1477
1402
|
outcome: WorkflowAttemptOutcome,
|
|
@@ -1662,9 +1587,7 @@ export class WorkflowEngine {
|
|
|
1662
1587
|
recoveryKey(task, phase, row, current),
|
|
1663
1588
|
);
|
|
1664
1589
|
const verificationOnly =
|
|
1665
|
-
|
|
1666
|
-
activeRecovery &&
|
|
1667
|
-
activeRecovery.failures >= maxRecoveryAttempts &&
|
|
1590
|
+
recoveryExhausted(activeRecovery, maxRecoveryAttempts, granted) &&
|
|
1668
1591
|
(await this.#worker.hasPendingVerification?.({
|
|
1669
1592
|
runId,
|
|
1670
1593
|
operationId,
|
|
@@ -1682,10 +1605,9 @@ export class WorkflowEngine {
|
|
|
1682
1605
|
: {}),
|
|
1683
1606
|
})) === true;
|
|
1684
1607
|
if (
|
|
1685
|
-
!granted &&
|
|
1686
1608
|
!verificationOnly &&
|
|
1687
1609
|
activeRecovery &&
|
|
1688
|
-
activeRecovery
|
|
1610
|
+
recoveryExhausted(activeRecovery, maxRecoveryAttempts, granted)
|
|
1689
1611
|
) {
|
|
1690
1612
|
this.#setTask(
|
|
1691
1613
|
runId,
|
|
@@ -2141,10 +2063,7 @@ export class WorkflowEngine {
|
|
|
2141
2063
|
}
|
|
2142
2064
|
|
|
2143
2065
|
#firstPaused(rows: EngineTaskRow[]): EngineTaskRow | undefined {
|
|
2144
|
-
return (
|
|
2145
|
-
rows.find((row) => row.state === "approval-needed") ??
|
|
2146
|
-
rows.find((row) => ["paused", "retryable"].includes(row.state))
|
|
2147
|
-
);
|
|
2066
|
+
return firstPausedTask(rows);
|
|
2148
2067
|
}
|
|
2149
2068
|
|
|
2150
2069
|
#recoverLegacyContextApproval(runId: string): boolean {
|
|
@@ -2791,46 +2710,34 @@ export class WorkflowEngine {
|
|
|
2791
2710
|
while (madeProgress && !signal.aborted) {
|
|
2792
2711
|
madeProgress = false;
|
|
2793
2712
|
const rows = this.#tasks(runId);
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
}
|
|
2821
|
-
if (
|
|
2822
|
-
this.#activeTasks + runnable.length >=
|
|
2823
|
-
LIMITS.maxActiveChildSessions
|
|
2824
|
-
) {
|
|
2825
|
-
this.#queueTask(runId, row, lease, "capacity");
|
|
2826
|
-
capacityBlocked = true;
|
|
2827
|
-
continue;
|
|
2828
|
-
}
|
|
2829
|
-
const before = `${row.state}:${row.phase}`;
|
|
2830
|
-
attempted.add(row.task_id);
|
|
2831
|
-
selectedTaskIds.add(row.task_id);
|
|
2832
|
-
runnable.push({ row, before });
|
|
2713
|
+
const selected = selectRunnableTasks({
|
|
2714
|
+
rows,
|
|
2715
|
+
attempted,
|
|
2716
|
+
activeTasks: this.#activeTasks,
|
|
2717
|
+
capacity: LIMITS.maxActiveChildSessions,
|
|
2718
|
+
nextQueuePosition: this.#engineRun(runId).next_queue_position,
|
|
2719
|
+
});
|
|
2720
|
+
const runnable = selected.runnable.map((taskId) => {
|
|
2721
|
+
const row = rows.find((candidate) => candidate.task_id === taskId);
|
|
2722
|
+
if (!row) throw new Error("scheduler-task-unavailable");
|
|
2723
|
+
attempted.add(taskId);
|
|
2724
|
+
return { row, before: `${row.state}:${row.phase}` };
|
|
2725
|
+
});
|
|
2726
|
+
for (const update of selected.updates) {
|
|
2727
|
+
const row = rows.find(
|
|
2728
|
+
(candidate) => candidate.task_id === update.taskId,
|
|
2729
|
+
);
|
|
2730
|
+
if (!row) throw new Error("scheduler-task-unavailable");
|
|
2731
|
+
if (update.kind === "pending")
|
|
2732
|
+
this.#setTask(
|
|
2733
|
+
runId,
|
|
2734
|
+
row.task_id,
|
|
2735
|
+
{ state: "pending", pauseCode: null, queuePosition: null },
|
|
2736
|
+
lease,
|
|
2737
|
+
);
|
|
2738
|
+
else this.#queueTask(runId, row, lease, update.reason);
|
|
2833
2739
|
}
|
|
2740
|
+
const capacityBlocked = selected.capacityBlocked;
|
|
2834
2741
|
if (runnable.length === 0 && capacityBlocked) {
|
|
2835
2742
|
await this.#waitForCapacity(signal);
|
|
2836
2743
|
madeProgress = true;
|
|
@@ -3019,20 +2926,22 @@ export class WorkflowEngine {
|
|
|
3019
2926
|
.get(runId, request.incidentKey) as { conditions_json: string };
|
|
3020
2927
|
const previous = JSON.parse(conditions.conditions_json);
|
|
3021
2928
|
const observed = this.#recoveryConditions(runId, request.incidentKey);
|
|
3022
|
-
if (
|
|
3023
|
-
(request.reason === "route-changed" &&
|
|
3024
|
-
(!previous.route || previous.route === observed.route)) ||
|
|
3025
|
-
(request.reason === "context-extended" &&
|
|
3026
|
-
(!previous.context || previous.context === observed.context))
|
|
3027
|
-
)
|
|
3028
|
-
throw new Error("recovery-evidence-unavailable");
|
|
3029
2929
|
const budget = this.#database
|
|
3030
2930
|
.prepare(
|
|
3031
2931
|
"SELECT used, max_work FROM workflow_work_budget WHERE run_id = ?",
|
|
3032
2932
|
)
|
|
3033
2933
|
.get(runId) as { used: number; max_work: number };
|
|
3034
|
-
|
|
3035
|
-
|
|
2934
|
+
const rejected = assessRecoveryGrant({
|
|
2935
|
+
current,
|
|
2936
|
+
incident,
|
|
2937
|
+
failureSequence: this.#failureSequence(runId, request.incidentKey),
|
|
2938
|
+
requestedSequence: request.failureSequence,
|
|
2939
|
+
reason: request.reason,
|
|
2940
|
+
previous,
|
|
2941
|
+
observed,
|
|
2942
|
+
budget,
|
|
2943
|
+
});
|
|
2944
|
+
if (rejected) throw new Error(rejected);
|
|
3036
2945
|
this.#database
|
|
3037
2946
|
.prepare(
|
|
3038
2947
|
"INSERT INTO workflow_recovery_grants(run_id, operation_id, incident_key, failure_sequence, reason) VALUES (?, ?, ?, ?, ?)",
|
|
@@ -3147,100 +3056,22 @@ export class WorkflowEngine {
|
|
|
3147
3056
|
#statusByRun(runId: string): Record<string, unknown> {
|
|
3148
3057
|
const projection = this.#runStore.status(runId);
|
|
3149
3058
|
const rows = this.#tasks(runId);
|
|
3150
|
-
const queued = rows
|
|
3151
|
-
.filter((row) => row.state === "queued")
|
|
3152
|
-
.sort(
|
|
3153
|
-
(left, right) =>
|
|
3154
|
-
(left.queue_position ?? Number.MAX_SAFE_INTEGER) -
|
|
3155
|
-
(right.queue_position ?? Number.MAX_SAFE_INTEGER) ||
|
|
3156
|
-
left.task_order - right.task_order,
|
|
3157
|
-
);
|
|
3158
|
-
const firstPaused = this.#firstPaused(rows);
|
|
3159
3059
|
let engineRun: EngineRunRow | undefined;
|
|
3160
3060
|
try {
|
|
3161
3061
|
engineRun = this.#engineRun(runId);
|
|
3162
3062
|
} catch {
|
|
3163
3063
|
engineRun = undefined;
|
|
3164
3064
|
}
|
|
3165
|
-
const
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
const approval =
|
|
3176
|
-
projection.state === "approval-needed"
|
|
3177
|
-
? approvalRequirement(pauseCode, contextRequest)
|
|
3178
|
-
: undefined;
|
|
3179
|
-
const attemptDiagnostic = this.#safeAttemptDiagnostic(runId, firstPaused);
|
|
3180
|
-
const recovery =
|
|
3181
|
-
rows
|
|
3182
|
-
.map((row) => this.#safeAttemptDiagnostic(runId, row)?.recovery)
|
|
3183
|
-
.find((fact) => fact && fact.failures >= fact.feedback.maxAttempts) ??
|
|
3184
|
-
attemptDiagnostic?.recovery;
|
|
3185
|
-
const recoveryExhausted =
|
|
3186
|
-
recovery && recovery.failures >= recovery.feedback.maxAttempts;
|
|
3187
|
-
const blockers = rows
|
|
3188
|
-
.filter((row) =>
|
|
3189
|
-
["approval-needed", "paused", "retryable"].includes(row.state),
|
|
3190
|
-
)
|
|
3191
|
-
.map((row) => {
|
|
3192
|
-
const context = row.context_request_json
|
|
3193
|
-
? normalizeWorkflowContextRequest(
|
|
3194
|
-
JSON.parse(row.context_request_json),
|
|
3195
|
-
)
|
|
3196
|
-
: undefined;
|
|
3197
|
-
const code = row.pause_code ?? "task-paused";
|
|
3198
|
-
const requirement =
|
|
3199
|
-
row.state === "approval-needed"
|
|
3200
|
-
? approvalRequirement(code, context)
|
|
3201
|
-
: undefined;
|
|
3202
|
-
return {
|
|
3203
|
-
id: hash(runId, row.task_id, code, canonicalJson(context ?? {})),
|
|
3204
|
-
taskId: row.task_id,
|
|
3205
|
-
phase: row.phase,
|
|
3206
|
-
code,
|
|
3207
|
-
kind: requirement ? "decision" : "execution",
|
|
3208
|
-
...(requirement ?? {}),
|
|
3209
|
-
...(context ? { contextRequest: context } : {}),
|
|
3210
|
-
};
|
|
3211
|
-
});
|
|
3212
|
-
if (
|
|
3213
|
-
projection.state === "approval-needed" &&
|
|
3214
|
-
!blockers.some((item) => item.kind === "decision") &&
|
|
3215
|
-
approval
|
|
3216
|
-
) {
|
|
3217
|
-
blockers.push({
|
|
3218
|
-
id: hash(runId, pauseCode),
|
|
3219
|
-
taskId: "change",
|
|
3220
|
-
phase: "green",
|
|
3221
|
-
code: pauseCode,
|
|
3222
|
-
kind: "decision",
|
|
3223
|
-
...approval,
|
|
3224
|
-
});
|
|
3225
|
-
}
|
|
3226
|
-
const decisions = blockers.filter((item) => item.kind === "decision");
|
|
3227
|
-
const amendments = blockers.filter(
|
|
3228
|
-
(item) => item.kind === "decision" || permitsPlanAmendment(item.code),
|
|
3229
|
-
);
|
|
3230
|
-
if (
|
|
3231
|
-
projection.state === "paused" &&
|
|
3232
|
-
permitsPlanAmendment(pauseCode) &&
|
|
3233
|
-
!amendments.some((item) => item.code === pauseCode)
|
|
3234
|
-
) {
|
|
3235
|
-
const item = {
|
|
3236
|
-
id: hash(runId, pauseCode, engineRun?.delivery_diagnostics_json ?? ""),
|
|
3237
|
-
taskId: "change",
|
|
3238
|
-
phase: "red" as const,
|
|
3239
|
-
code: pauseCode,
|
|
3240
|
-
kind: "execution",
|
|
3241
|
-
};
|
|
3242
|
-
blockers.push(item);
|
|
3243
|
-
amendments.push(item);
|
|
3065
|
+
const attemptDiagnostics = new Map<string, SafeAttemptDiagnostic>();
|
|
3066
|
+
const failureSequences = new Map<string, number>();
|
|
3067
|
+
for (const row of rows) {
|
|
3068
|
+
const diagnostic = this.#safeAttemptDiagnostic(runId, row);
|
|
3069
|
+
if (!diagnostic) continue;
|
|
3070
|
+
attemptDiagnostics.set(row.task_id, diagnostic);
|
|
3071
|
+
if (diagnostic.recovery) {
|
|
3072
|
+
const key = diagnostic.recovery.key;
|
|
3073
|
+
failureSequences.set(key, this.#failureSequence(runId, key));
|
|
3074
|
+
}
|
|
3244
3075
|
}
|
|
3245
3076
|
const amendmentUsed =
|
|
3246
3077
|
(
|
|
@@ -3250,254 +3081,20 @@ export class WorkflowEngine {
|
|
|
3250
3081
|
)
|
|
3251
3082
|
.get(runId) as { used: number } | undefined
|
|
3252
3083
|
)?.used ?? 0;
|
|
3253
|
-
const amendmentBudget = {
|
|
3254
|
-
used: amendmentUsed,
|
|
3255
|
-
maximum: 64,
|
|
3256
|
-
remaining: 64 - amendmentUsed,
|
|
3257
|
-
exhausted: amendmentUsed >= 64,
|
|
3258
|
-
};
|
|
3259
|
-
const decisionBatch =
|
|
3260
|
-
amendments.length &&
|
|
3261
|
-
pauseCode !== "operation-cancelled" &&
|
|
3262
|
-
["approval-needed", "paused"].includes(projection.state)
|
|
3263
|
-
? {
|
|
3264
|
-
id: hash(
|
|
3265
|
-
runId,
|
|
3266
|
-
String(projection.deliveryRevision ?? 0),
|
|
3267
|
-
canonicalJson(amendments),
|
|
3268
|
-
),
|
|
3269
|
-
continuation: {
|
|
3270
|
-
action: "amend",
|
|
3271
|
-
change: projection.change,
|
|
3272
|
-
batchId: hash(
|
|
3273
|
-
runId,
|
|
3274
|
-
String(projection.deliveryRevision ?? 0),
|
|
3275
|
-
canonicalJson(amendments),
|
|
3276
|
-
),
|
|
3277
|
-
},
|
|
3278
|
-
resolution: {
|
|
3279
|
-
owner: "parent",
|
|
3280
|
-
strategy: "recommended",
|
|
3281
|
-
requiresUserInput: false,
|
|
3282
|
-
},
|
|
3283
|
-
items: amendments,
|
|
3284
|
-
requiredGates: [
|
|
3285
|
-
...new Set(
|
|
3286
|
-
amendments.flatMap((item) => item.requiredGates ?? ["gate-b"]),
|
|
3287
|
-
),
|
|
3288
|
-
].sort(),
|
|
3289
|
-
}
|
|
3290
|
-
: undefined;
|
|
3291
3084
|
const resourceBudget = this.#database
|
|
3292
3085
|
.prepare(
|
|
3293
3086
|
"SELECT used, max_work AS maximum, phase_high_water AS phaseHighWater, hard_limit AS hardLimit FROM workflow_work_budget WHERE run_id = ?",
|
|
3294
3087
|
)
|
|
3295
|
-
.get(runId) as
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
stage: projection.stage,
|
|
3306
|
-
...(projection.change ? { change: projection.change } : {}),
|
|
3307
|
-
state: projection.state,
|
|
3308
|
-
...(projection.deliveryRevision === undefined
|
|
3309
|
-
? {}
|
|
3310
|
-
: { deliveryRevision: projection.deliveryRevision }),
|
|
3311
|
-
...(projection.deliveryBindings.length > 0
|
|
3312
|
-
? { deliveryBindings: structuredClone(projection.deliveryBindings) }
|
|
3313
|
-
: {}),
|
|
3314
|
-
completed: projection.state === "completed",
|
|
3315
|
-
blockers,
|
|
3316
|
-
...(resourceBudget
|
|
3317
|
-
? {
|
|
3318
|
-
resourceBudget: {
|
|
3319
|
-
...resourceBudget,
|
|
3320
|
-
remaining: resourceBudget.maximum - resourceBudget.used,
|
|
3321
|
-
},
|
|
3322
|
-
}
|
|
3323
|
-
: {}),
|
|
3324
|
-
...(budgetExhausted
|
|
3325
|
-
? {
|
|
3326
|
-
recovery: {
|
|
3327
|
-
automatic: false,
|
|
3328
|
-
exhausted: true,
|
|
3329
|
-
code: budgetCode,
|
|
3330
|
-
},
|
|
3331
|
-
}
|
|
3332
|
-
: {}),
|
|
3333
|
-
...(decisionBatch
|
|
3334
|
-
? {
|
|
3335
|
-
decisionBatch,
|
|
3336
|
-
amendmentBudget,
|
|
3337
|
-
...(!amendmentBudget.exhausted &&
|
|
3338
|
-
!budgetExhausted &&
|
|
3339
|
-
(!resourceBudget || resourceBudget.used < resourceBudget.maximum)
|
|
3340
|
-
? {
|
|
3341
|
-
continuation: {
|
|
3342
|
-
owner: "parent",
|
|
3343
|
-
automatic: true,
|
|
3344
|
-
...decisionBatch.continuation,
|
|
3345
|
-
},
|
|
3346
|
-
}
|
|
3347
|
-
: {}),
|
|
3348
|
-
}
|
|
3349
|
-
: {}),
|
|
3350
|
-
...(recoveryExhausted
|
|
3351
|
-
? {
|
|
3352
|
-
recovery: {
|
|
3353
|
-
automatic: false,
|
|
3354
|
-
exhausted: true,
|
|
3355
|
-
code: recovery.feedback.code,
|
|
3356
|
-
attempts: recovery.failures,
|
|
3357
|
-
maxAttempts: recovery.feedback.maxAttempts,
|
|
3358
|
-
automaticRetryExhausted: true,
|
|
3359
|
-
...(!budgetExhausted &&
|
|
3360
|
-
resourceBudget &&
|
|
3361
|
-
resourceBudget.used < resourceBudget.maximum
|
|
3362
|
-
? {
|
|
3363
|
-
additionalAttempt: {
|
|
3364
|
-
incidentKey: recovery.key,
|
|
3365
|
-
failureSequence: this.#failureSequence(
|
|
3366
|
-
runId,
|
|
3367
|
-
recovery.key,
|
|
3368
|
-
),
|
|
3369
|
-
reason: "parent-directed-retry",
|
|
3370
|
-
},
|
|
3371
|
-
}
|
|
3372
|
-
: {}),
|
|
3373
|
-
},
|
|
3374
|
-
}
|
|
3375
|
-
: {}),
|
|
3376
|
-
...(projection.terminal ? { terminal: projection.terminal } : {}),
|
|
3377
|
-
...(projection.pauseCode || firstPaused?.pause_code
|
|
3378
|
-
? {
|
|
3379
|
-
pause: {
|
|
3380
|
-
code: pauseCode,
|
|
3381
|
-
...(attemptDiagnostic ? { diagnostic: attemptDiagnostic } : {}),
|
|
3382
|
-
},
|
|
3383
|
-
}
|
|
3384
|
-
: {}),
|
|
3385
|
-
legalCommands:
|
|
3386
|
-
projection.state === "approval-needed"
|
|
3387
|
-
? ["status", "discard"]
|
|
3388
|
-
: legalControlCommands(projection.state).filter(
|
|
3389
|
-
(command) =>
|
|
3390
|
-
(command !== "rebind" || firstPaused !== undefined) &&
|
|
3391
|
-
(command !== "resume" ||
|
|
3392
|
-
(!recoveryExhausted && !budgetExhausted)),
|
|
3393
|
-
),
|
|
3394
|
-
...(approval && projection.change
|
|
3395
|
-
? {
|
|
3396
|
-
approval: {
|
|
3397
|
-
category: approval.category,
|
|
3398
|
-
requiredGates:
|
|
3399
|
-
decisionBatch?.requiredGates ?? approval.requiredGates,
|
|
3400
|
-
categories: [
|
|
3401
|
-
...new Set(
|
|
3402
|
-
decisions.flatMap((item) =>
|
|
3403
|
-
item.category ? [item.category] : [],
|
|
3404
|
-
),
|
|
3405
|
-
),
|
|
3406
|
-
],
|
|
3407
|
-
refs: [...new Set(decisions.flatMap((item) => item.refs ?? []))],
|
|
3408
|
-
continuation: decisionBatch?.continuation,
|
|
3409
|
-
retainedRun: {
|
|
3410
|
-
runId: projection.runId,
|
|
3411
|
-
deliveryRevision: currentRevision,
|
|
3412
|
-
},
|
|
3413
|
-
receiptPrecondition: {
|
|
3414
|
-
deliveryRevision: { greaterThan: currentRevision },
|
|
3415
|
-
receiptHash: "matching-ready-receipt",
|
|
3416
|
-
},
|
|
3417
|
-
},
|
|
3418
|
-
conditionalCommands: [
|
|
3419
|
-
{
|
|
3420
|
-
command: "resume",
|
|
3421
|
-
stage: "abel-implement",
|
|
3422
|
-
change: projection.change,
|
|
3423
|
-
requires: {
|
|
3424
|
-
deliveryRevision: { greaterThan: currentRevision },
|
|
3425
|
-
receiptHash: "matching-ready-receipt",
|
|
3426
|
-
},
|
|
3427
|
-
},
|
|
3428
|
-
],
|
|
3429
|
-
}
|
|
3430
|
-
: {}),
|
|
3431
|
-
tasks: rows.map((row) => ({
|
|
3432
|
-
taskId: row.task_id,
|
|
3433
|
-
state: row.state,
|
|
3434
|
-
...(row.state === "verified" ? {} : { phase: row.phase }),
|
|
3435
|
-
...(row.context_request_json
|
|
3436
|
-
? {
|
|
3437
|
-
contextRequest: normalizeWorkflowContextRequest(
|
|
3438
|
-
JSON.parse(row.context_request_json) as unknown,
|
|
3439
|
-
),
|
|
3440
|
-
}
|
|
3441
|
-
: {}),
|
|
3442
|
-
})),
|
|
3443
|
-
queue: queued.map((row, index) => ({
|
|
3444
|
-
taskId: row.task_id,
|
|
3445
|
-
position: index + 1,
|
|
3446
|
-
reason: row.pause_code === "worker-capacity" ? "capacity" : "conflict",
|
|
3447
|
-
})),
|
|
3448
|
-
...(engineRun?.verification_json
|
|
3449
|
-
? {
|
|
3450
|
-
verification: normalizeWorkflowVerificationStatus(
|
|
3451
|
-
JSON.parse(engineRun.verification_json) as unknown,
|
|
3452
|
-
),
|
|
3453
|
-
}
|
|
3454
|
-
: {}),
|
|
3455
|
-
...(engineRun?.delivery_diagnostics_json
|
|
3456
|
-
? {
|
|
3457
|
-
delivery: {
|
|
3458
|
-
code: "delivery-invalid",
|
|
3459
|
-
diagnostics: parseDeliveryDiagnostics(
|
|
3460
|
-
engineRun.delivery_diagnostics_json,
|
|
3461
|
-
),
|
|
3462
|
-
},
|
|
3463
|
-
}
|
|
3464
|
-
: {}),
|
|
3465
|
-
...(engineRun?.route_id
|
|
3466
|
-
? {
|
|
3467
|
-
routeBinding: {
|
|
3468
|
-
routeId: engineRun.route_id,
|
|
3469
|
-
...(engineRun.route_fingerprint
|
|
3470
|
-
? { routeFingerprint: engineRun.route_fingerprint }
|
|
3471
|
-
: {}),
|
|
3472
|
-
},
|
|
3473
|
-
}
|
|
3474
|
-
: {}),
|
|
3475
|
-
...(engineRun?.cleanup_state === "retained"
|
|
3476
|
-
? {
|
|
3477
|
-
privateData: {
|
|
3478
|
-
retained: true,
|
|
3479
|
-
cleanup: "retained",
|
|
3480
|
-
...(engineRun.baseline_workspace_revision
|
|
3481
|
-
? {
|
|
3482
|
-
baselineRevisionId: engineRun.baseline_workspace_revision,
|
|
3483
|
-
}
|
|
3484
|
-
: {}),
|
|
3485
|
-
...(engineRun.current_workspace_revision
|
|
3486
|
-
? {
|
|
3487
|
-
currentRevisionId: engineRun.current_workspace_revision,
|
|
3488
|
-
}
|
|
3489
|
-
: {}),
|
|
3490
|
-
},
|
|
3491
|
-
}
|
|
3492
|
-
: engineRun?.cleanup_state === "complete"
|
|
3493
|
-
? {
|
|
3494
|
-
privateData: {
|
|
3495
|
-
retained: false,
|
|
3496
|
-
cleanup: "complete",
|
|
3497
|
-
},
|
|
3498
|
-
}
|
|
3499
|
-
: {}),
|
|
3500
|
-
};
|
|
3088
|
+
.get(runId) as WorkflowResourceBudget | undefined;
|
|
3089
|
+
return projectWorkflowStatus({
|
|
3090
|
+
projection,
|
|
3091
|
+
rows,
|
|
3092
|
+
engineRun,
|
|
3093
|
+
attemptDiagnostics,
|
|
3094
|
+
failureSequences,
|
|
3095
|
+
amendmentUsed,
|
|
3096
|
+
resourceBudget,
|
|
3097
|
+
});
|
|
3501
3098
|
}
|
|
3502
3099
|
|
|
3503
3100
|
async #withAvailableDelivery(
|
|
@@ -3896,21 +3493,6 @@ export class WorkflowEngine {
|
|
|
3896
3493
|
signal,
|
|
3897
3494
|
);
|
|
3898
3495
|
this.#admitDelivery(runId, delivery, lease);
|
|
3899
|
-
this.#transition(
|
|
3900
|
-
runId,
|
|
3901
|
-
"ready",
|
|
3902
|
-
`${command.operationId}:delivery-ready`,
|
|
3903
|
-
undefined,
|
|
3904
|
-
lease,
|
|
3905
|
-
);
|
|
3906
|
-
} else if (current.state !== "ready") {
|
|
3907
|
-
this.#transition(
|
|
3908
|
-
runId,
|
|
3909
|
-
"ready",
|
|
3910
|
-
`${command.operationId}:resume-ready`,
|
|
3911
|
-
undefined,
|
|
3912
|
-
lease,
|
|
3913
|
-
);
|
|
3914
3496
|
}
|
|
3915
3497
|
if (command.recovery)
|
|
3916
3498
|
this.#grantRecovery(
|
|
@@ -3919,6 +3501,13 @@ export class WorkflowEngine {
|
|
|
3919
3501
|
command.recovery,
|
|
3920
3502
|
lease,
|
|
3921
3503
|
);
|
|
3504
|
+
this.#transition(
|
|
3505
|
+
runId,
|
|
3506
|
+
"ready",
|
|
3507
|
+
`${command.operationId}:resume-ready`,
|
|
3508
|
+
undefined,
|
|
3509
|
+
lease,
|
|
3510
|
+
);
|
|
3922
3511
|
return this.#launchAdvance(
|
|
3923
3512
|
runId,
|
|
3924
3513
|
command.operationId,
|
|
@@ -3943,6 +3532,25 @@ export class WorkflowEngine {
|
|
|
3943
3532
|
if (failure) {
|
|
3944
3533
|
return this.#commitOperation(runId, command.operationId, failure);
|
|
3945
3534
|
}
|
|
3535
|
+
// A revised delivery may invalidate a grant. Retain its admitted facts,
|
|
3536
|
+
// but never leave a rejected resume looking ready or still validating.
|
|
3537
|
+
if (
|
|
3538
|
+
error instanceof Error &&
|
|
3539
|
+
[
|
|
3540
|
+
"recovery-request-stale",
|
|
3541
|
+
"recovery-evidence-unavailable",
|
|
3542
|
+
"change-work-budget-exhausted",
|
|
3543
|
+
].includes(error.message) &&
|
|
3544
|
+
this.#runStore.status(runId).state === "validating-delivery"
|
|
3545
|
+
) {
|
|
3546
|
+
this.#transition(
|
|
3547
|
+
runId,
|
|
3548
|
+
"paused",
|
|
3549
|
+
`${command.operationId}:recovery-rejected`,
|
|
3550
|
+
error.message,
|
|
3551
|
+
lease,
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3946
3554
|
this.#interruptOperation(runId, command.operationId);
|
|
3947
3555
|
throw error;
|
|
3948
3556
|
}
|