@abelxiaoxing/cadence 1.2.1 → 1.2.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/README.md +7 -1
- package/agents/diagnosis-worker.md +6 -2
- package/agents/implementation-worker.md +9 -2
- package/config/routes.example.json +0 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +15 -4
- package/skills/abel-workflow/SKILL.md +5 -3
- package/src/apply-transaction.ts +1 -5
- package/src/child-session.ts +37 -46
- package/src/contracts.ts +278 -32
- package/src/control-contracts.ts +62 -0
- package/src/delivery-compiler.ts +239 -124
- package/src/design-control.ts +72 -27
- package/src/design-journal.ts +2 -2
- package/src/file-snapshot.ts +1 -1
- package/src/index.ts +295 -58
- package/src/packet-runtime.ts +79 -21
- package/src/parent-provider.ts +63 -30
- package/src/route-policy.ts +58 -14
- package/src/run-store.ts +115 -23
- package/src/subagent-activity.ts +82 -6
- package/src/submit-tool.ts +431 -39
- package/src/task-ledger.ts +1 -2
- package/src/worker-broker.ts +72 -13
- package/src/workflow-engine.ts +782 -54
- package/src/workspace-store.ts +2 -10
package/src/workflow-engine.ts
CHANGED
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
import type { GateApprovalProof } from "./delivery-compiler.ts";
|
|
31
31
|
import {
|
|
32
32
|
DeliveryValidationError,
|
|
33
|
-
IMPLEMENT_PLAN_SCHEMA_VERSION,
|
|
34
33
|
type ImplementPlan,
|
|
35
34
|
type PlanTaskDraft,
|
|
36
35
|
} from "./delivery-compiler.ts";
|
|
@@ -45,7 +44,13 @@ import { type OperationLease, RunStore } from "./run-store.ts";
|
|
|
45
44
|
import { observeSafePath } from "./safe-path.ts";
|
|
46
45
|
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
47
46
|
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
48
|
-
import
|
|
47
|
+
import {
|
|
48
|
+
type CandidateArtifactSubmission,
|
|
49
|
+
type CandidateContextBoundary,
|
|
50
|
+
type CandidateContextRef,
|
|
51
|
+
classifyCandidateContextRequest,
|
|
52
|
+
normalizeCandidateContextRefs,
|
|
53
|
+
} from "./submit-tool.ts";
|
|
49
54
|
import {
|
|
50
55
|
type BeginCandidateInput,
|
|
51
56
|
TASK_LEDGER_LIMITS,
|
|
@@ -70,11 +75,72 @@ const LOCKFILES = new Set([
|
|
|
70
75
|
"pnpm-lock.yaml",
|
|
71
76
|
"yarn.lock",
|
|
72
77
|
]);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
const IMPLEMENTATION_ROUTE_LIMITS = Object.freeze({
|
|
79
|
+
preferredContextWindow: 128_000,
|
|
80
|
+
preferredOutputTokens: 64_000,
|
|
81
|
+
minimumContextWindow: 16_000,
|
|
82
|
+
minimumOutputTokens: 8_000,
|
|
76
83
|
});
|
|
77
84
|
|
|
85
|
+
function implementationRouteRequirements(
|
|
86
|
+
input: {
|
|
87
|
+
task?: PlanTaskDraft;
|
|
88
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
89
|
+
repair?: { failureIdentities: string[] };
|
|
90
|
+
} = {},
|
|
91
|
+
) {
|
|
92
|
+
if (!input.task) {
|
|
93
|
+
return {
|
|
94
|
+
minContextWindow: IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
|
|
95
|
+
minOutputTokens: IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const serializedBytes = Buffer.byteLength(canonicalJson(input.task), "utf8");
|
|
99
|
+
const pathCount = Object.values(input.task.phases).reduce(
|
|
100
|
+
(total, phase) =>
|
|
101
|
+
total + phase.read.length + phase.write.length + phase.delete.length,
|
|
102
|
+
0,
|
|
103
|
+
);
|
|
104
|
+
const complexity =
|
|
105
|
+
serializedBytes +
|
|
106
|
+
pathCount * 512 +
|
|
107
|
+
(input.artifactCorrection ? 16_384 : 0) +
|
|
108
|
+
(input.repair ? input.repair.failureIdentities.length * 256 + 16_384 : 0);
|
|
109
|
+
return {
|
|
110
|
+
minContextWindow: Math.min(
|
|
111
|
+
IMPLEMENTATION_ROUTE_LIMITS.preferredContextWindow,
|
|
112
|
+
Math.max(
|
|
113
|
+
IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
|
|
114
|
+
16_384 + complexity * 2,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
minOutputTokens: Math.min(
|
|
118
|
+
IMPLEMENTATION_ROUTE_LIMITS.preferredOutputTokens,
|
|
119
|
+
Math.max(
|
|
120
|
+
IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
|
|
121
|
+
8_192 + Math.ceil(complexity / 2),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SEMANTIC_ROUTE_FAILURE_CODES = new Set([
|
|
128
|
+
"child-no-structural-submit",
|
|
129
|
+
"invalid-structural-result",
|
|
130
|
+
"structural-identity-mismatch",
|
|
131
|
+
"invalid-diff",
|
|
132
|
+
"candidate-diff-invalid",
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
function semanticRouteFailure(
|
|
136
|
+
value: DurableCandidateProposal,
|
|
137
|
+
): string | undefined {
|
|
138
|
+
return (value.kind === "retryable" || value.kind === "paused") &&
|
|
139
|
+
SEMANTIC_ROUTE_FAILURE_CODES.has(value.code)
|
|
140
|
+
? value.code
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
78
144
|
function isCancellationException(error: unknown, signal: AbortSignal): boolean {
|
|
79
145
|
if (!signal.aborted) return false;
|
|
80
146
|
if (error === signal.reason) return true;
|
|
@@ -131,6 +197,7 @@ const ENGINE_SCHEMA = `
|
|
|
131
197
|
phase TEXT NOT NULL,
|
|
132
198
|
pause_code TEXT,
|
|
133
199
|
context_request_json TEXT,
|
|
200
|
+
attempt_diagnostic_json TEXT,
|
|
134
201
|
route_id TEXT,
|
|
135
202
|
route_fingerprint TEXT,
|
|
136
203
|
queue_position INTEGER,
|
|
@@ -171,7 +238,14 @@ interface WorkflowContextRequest {
|
|
|
171
238
|
| "approved-context-needed"
|
|
172
239
|
| "task-split-needed"
|
|
173
240
|
| "boundary-review-needed";
|
|
174
|
-
refs:
|
|
241
|
+
refs: CandidateContextRef[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface RedArtifactCorrection {
|
|
245
|
+
code: "red-artifact-constraint";
|
|
246
|
+
attempt: number;
|
|
247
|
+
maxAttempts: number;
|
|
248
|
+
contextRequest?: WorkflowContextRequest;
|
|
175
249
|
}
|
|
176
250
|
|
|
177
251
|
export const APPROVAL_AUTHORITY_CATEGORIES = [
|
|
@@ -277,7 +351,9 @@ export type WorkflowAttemptOutcome = (
|
|
|
277
351
|
| { kind: "operation-cancelled"; code: "cancelled" }
|
|
278
352
|
) &
|
|
279
353
|
WorkflowWorkspaceFacts &
|
|
280
|
-
WorkflowRouteFacts
|
|
354
|
+
WorkflowRouteFacts & {
|
|
355
|
+
attemptDiagnostic?: SafeAttemptDiagnostic;
|
|
356
|
+
};
|
|
281
357
|
|
|
282
358
|
export interface WorkflowWorker {
|
|
283
359
|
runAttempt(input: {
|
|
@@ -296,6 +372,8 @@ export interface WorkflowWorker {
|
|
|
296
372
|
attribution: "introduced";
|
|
297
373
|
failureIdentities: string[];
|
|
298
374
|
};
|
|
375
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
376
|
+
contextRequest?: WorkflowContextRequest;
|
|
299
377
|
signal: AbortSignal;
|
|
300
378
|
onActivity?: (
|
|
301
379
|
event: Pick<
|
|
@@ -307,6 +385,7 @@ export interface WorkflowWorker {
|
|
|
307
385
|
rebind(input: {
|
|
308
386
|
runId: string;
|
|
309
387
|
taskId: string;
|
|
388
|
+
task: PlanTaskDraft;
|
|
310
389
|
role: "implementation-worker";
|
|
311
390
|
routeId: string;
|
|
312
391
|
}):
|
|
@@ -340,7 +419,6 @@ export interface WorkflowWorker {
|
|
|
340
419
|
}
|
|
341
420
|
|
|
342
421
|
export interface WorkflowDelivery {
|
|
343
|
-
version: 2;
|
|
344
422
|
gate: "gate-a" | "gate-b";
|
|
345
423
|
revision: number;
|
|
346
424
|
receiptHash: string;
|
|
@@ -480,11 +558,22 @@ interface EngineTaskRow {
|
|
|
480
558
|
phase: "red" | "green" | "refactor";
|
|
481
559
|
pause_code: string | null;
|
|
482
560
|
context_request_json: string | null;
|
|
561
|
+
attempt_diagnostic_json: string | null;
|
|
483
562
|
route_id: string | null;
|
|
484
563
|
route_fingerprint: string | null;
|
|
485
564
|
queue_position: number | null;
|
|
486
565
|
}
|
|
487
566
|
|
|
567
|
+
interface SafeAttemptDiagnostic {
|
|
568
|
+
fingerprint?: string;
|
|
569
|
+
finalCategory?: string;
|
|
570
|
+
submitAttempts?: number;
|
|
571
|
+
schema?: string;
|
|
572
|
+
identityMismatch?: string[];
|
|
573
|
+
sameFailureCount?: number;
|
|
574
|
+
action?: "rebind-or-revise-delivery";
|
|
575
|
+
}
|
|
576
|
+
|
|
488
577
|
interface EngineOperationRow {
|
|
489
578
|
command: string;
|
|
490
579
|
state: string;
|
|
@@ -568,24 +657,72 @@ function normalizeWorkflowContextRequest(
|
|
|
568
657
|
"approved-context-needed",
|
|
569
658
|
"task-split-needed",
|
|
570
659
|
"boundary-review-needed",
|
|
571
|
-
].includes(String(value.code))
|
|
572
|
-
!Array.isArray(value.refs) ||
|
|
573
|
-
value.refs.length === 0 ||
|
|
574
|
-
value.refs.length > 32 ||
|
|
575
|
-
value.refs.some(
|
|
576
|
-
(relative) =>
|
|
577
|
-
typeof relative !== "string" || !isValidRelativePath(relative),
|
|
578
|
-
) ||
|
|
579
|
-
new Set(value.refs).size !== value.refs.length
|
|
660
|
+
].includes(String(value.code))
|
|
580
661
|
) {
|
|
581
662
|
throw new Error("workflow-context-request-invalid");
|
|
582
663
|
}
|
|
664
|
+
let refs: CandidateContextRef[];
|
|
665
|
+
try {
|
|
666
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
667
|
+
} catch {
|
|
668
|
+
throw new Error("workflow-context-request-invalid");
|
|
669
|
+
}
|
|
583
670
|
return {
|
|
584
671
|
code: value.code as WorkflowContextRequest["code"],
|
|
585
|
-
refs
|
|
672
|
+
refs,
|
|
586
673
|
};
|
|
587
674
|
}
|
|
588
675
|
|
|
676
|
+
function contextApprovalRefs(request: WorkflowContextRequest): string[] {
|
|
677
|
+
return request.refs.flatMap((ref) =>
|
|
678
|
+
ref.kind === "requested-path" ? [ref.path] : [],
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function contextBoundaryForTask(
|
|
683
|
+
task: PlanTaskDraft,
|
|
684
|
+
phase: "red" | "green" | "refactor",
|
|
685
|
+
): CandidateContextBoundary {
|
|
686
|
+
const boundary = task.phases[phase];
|
|
687
|
+
if (!boundary) throw new Error("workflow-task-phase-invalid");
|
|
688
|
+
return {
|
|
689
|
+
phase,
|
|
690
|
+
readPaths: boundary.read,
|
|
691
|
+
writePaths: boundary.write,
|
|
692
|
+
deletePaths: boundary.delete,
|
|
693
|
+
taskPaths: [
|
|
694
|
+
...new Set(
|
|
695
|
+
Object.values(task.phases).flatMap((candidate) => [
|
|
696
|
+
...candidate.read,
|
|
697
|
+
...candidate.write,
|
|
698
|
+
...candidate.delete,
|
|
699
|
+
]),
|
|
700
|
+
),
|
|
701
|
+
],
|
|
702
|
+
redWritePaths: task.phases.red.write,
|
|
703
|
+
agents: {
|
|
704
|
+
impact: task.agents.impact,
|
|
705
|
+
...(task.agents.target ? { target: task.agents.target } : {}),
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function classifyPersistedContextRequest(
|
|
711
|
+
request: WorkflowContextRequest,
|
|
712
|
+
task: PlanTaskDraft,
|
|
713
|
+
phase: "red" | "green" | "refactor",
|
|
714
|
+
) {
|
|
715
|
+
return classifyCandidateContextRequest(
|
|
716
|
+
{
|
|
717
|
+
kind: "context-request",
|
|
718
|
+
candidateId: "candidate-durable-reclassification",
|
|
719
|
+
code: request.code,
|
|
720
|
+
refs: request.refs,
|
|
721
|
+
},
|
|
722
|
+
contextBoundaryForTask(task, phase),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
589
726
|
function approvalRequirement(
|
|
590
727
|
code: string,
|
|
591
728
|
contextRequest?: WorkflowContextRequest,
|
|
@@ -599,7 +736,7 @@ function approvalRequirement(
|
|
|
599
736
|
return {
|
|
600
737
|
category: requirement.category,
|
|
601
738
|
requiredGates: [...requirement.requiredGates],
|
|
602
|
-
refs: contextRequest ?
|
|
739
|
+
refs: contextRequest ? contextApprovalRefs(contextRequest) : [],
|
|
603
740
|
};
|
|
604
741
|
}
|
|
605
742
|
|
|
@@ -908,7 +1045,6 @@ function parsePlan(value: string): ImplementPlan {
|
|
|
908
1045
|
function assertPlan(value: unknown): asserts value is ImplementPlan {
|
|
909
1046
|
if (
|
|
910
1047
|
!isRecord(value) ||
|
|
911
|
-
value.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
|
|
912
1048
|
typeof value.changeId !== "string" ||
|
|
913
1049
|
!Array.isArray(value.tasks) ||
|
|
914
1050
|
!Array.isArray(value.outputs) ||
|
|
@@ -954,7 +1090,6 @@ function assertDelivery(
|
|
|
954
1090
|
): asserts value is WorkflowDelivery {
|
|
955
1091
|
if (
|
|
956
1092
|
!isRecord(value) ||
|
|
957
|
-
value.version !== 2 ||
|
|
958
1093
|
(value.gate !== "gate-a" && value.gate !== "gate-b") ||
|
|
959
1094
|
!Number.isSafeInteger(value.revision) ||
|
|
960
1095
|
(value.revision as number) < 1 ||
|
|
@@ -1155,6 +1290,7 @@ function ensureEngineColumns(database: DatabaseSync): void {
|
|
|
1155
1290
|
ensure("workflow_engine_tasks", [
|
|
1156
1291
|
["route_fingerprint", "TEXT"],
|
|
1157
1292
|
["context_request_json", "TEXT"],
|
|
1293
|
+
["attempt_diagnostic_json", "TEXT"],
|
|
1158
1294
|
]);
|
|
1159
1295
|
}
|
|
1160
1296
|
|
|
@@ -1171,6 +1307,7 @@ export type DurableCandidateProposal =
|
|
|
1171
1307
|
kind: "paused" | "retryable" | "approval-needed";
|
|
1172
1308
|
code: string;
|
|
1173
1309
|
contextRequest?: WorkflowContextRequest;
|
|
1310
|
+
attemptDiagnostic?: SafeAttemptDiagnostic;
|
|
1174
1311
|
}
|
|
1175
1312
|
| { kind: "operation-cancelled"; code: "cancelled" };
|
|
1176
1313
|
|
|
@@ -1224,7 +1361,6 @@ interface DurableVerificationObservation {
|
|
|
1224
1361
|
}
|
|
1225
1362
|
|
|
1226
1363
|
interface DurableVerificationBaseline {
|
|
1227
|
-
version: 1;
|
|
1228
1364
|
revisionId: string;
|
|
1229
1365
|
targetContracts: Array<{
|
|
1230
1366
|
taskId: string;
|
|
@@ -1282,6 +1418,8 @@ export interface DurableWorkflowEngineOptions {
|
|
|
1282
1418
|
attribution: "introduced";
|
|
1283
1419
|
failureIdentities: string[];
|
|
1284
1420
|
};
|
|
1421
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
1422
|
+
contextRequest?: WorkflowContextRequest;
|
|
1285
1423
|
signal: AbortSignal;
|
|
1286
1424
|
onHeaders(): void;
|
|
1287
1425
|
onProgress(): void;
|
|
@@ -1571,7 +1709,7 @@ function verificationIdentities(
|
|
|
1571
1709
|
return [...new Set(supplied)].sort();
|
|
1572
1710
|
}
|
|
1573
1711
|
if (result.ok) return [];
|
|
1574
|
-
return [hash("verification-failure
|
|
1712
|
+
return [hash("verification-failure", scope, verification.id, result.code)];
|
|
1575
1713
|
}
|
|
1576
1714
|
|
|
1577
1715
|
function parseVerificationBaseline(
|
|
@@ -1579,7 +1717,6 @@ function parseVerificationBaseline(
|
|
|
1579
1717
|
): DurableVerificationBaseline {
|
|
1580
1718
|
if (
|
|
1581
1719
|
!isRecord(value) ||
|
|
1582
|
-
value.version !== 1 ||
|
|
1583
1720
|
typeof value.revisionId !== "string" ||
|
|
1584
1721
|
!SHA256.test(value.revisionId) ||
|
|
1585
1722
|
!Array.isArray(value.targetContracts) ||
|
|
@@ -2038,7 +2175,6 @@ class DurableWorkflowComposition
|
|
|
2038
2175
|
});
|
|
2039
2176
|
if (!fullSuite.ok) return fullSuite;
|
|
2040
2177
|
const baseline: DurableVerificationBaseline = {
|
|
2041
|
-
version: 1,
|
|
2042
2178
|
revisionId: input.resources.baselineRevisionId,
|
|
2043
2179
|
targetContracts: plan.tasks.map((task) => ({
|
|
2044
2180
|
taskId: task.taskId,
|
|
@@ -2319,6 +2455,9 @@ class DurableWorkflowComposition
|
|
|
2319
2455
|
ledger: TaskLedger,
|
|
2320
2456
|
input: Parameters<WorkflowWorker["runAttempt"]>[0],
|
|
2321
2457
|
): WorkflowAttemptOutcome | undefined {
|
|
2458
|
+
if (input.artifactCorrection?.code === "red-artifact-constraint") {
|
|
2459
|
+
return undefined;
|
|
2460
|
+
}
|
|
2322
2461
|
const projection = ledger.projection({
|
|
2323
2462
|
runId: input.runId,
|
|
2324
2463
|
taskId: input.taskId,
|
|
@@ -2866,7 +3005,13 @@ class DurableWorkflowComposition
|
|
|
2866
3005
|
...(request.routeFingerprint
|
|
2867
3006
|
? { expectedFingerprint: request.routeFingerprint }
|
|
2868
3007
|
: {}),
|
|
2869
|
-
requirements:
|
|
3008
|
+
requirements: implementationRouteRequirements({
|
|
3009
|
+
task: request.task,
|
|
3010
|
+
...(request.artifactCorrection
|
|
3011
|
+
? { artifactCorrection: request.artifactCorrection }
|
|
3012
|
+
: {}),
|
|
3013
|
+
repair: { failureIdentities: input.failureIdentities },
|
|
3014
|
+
}),
|
|
2870
3015
|
});
|
|
2871
3016
|
if (!rebound.ok) return { kind: "paused", code: rebound.code };
|
|
2872
3017
|
}
|
|
@@ -2885,9 +3030,16 @@ class DurableWorkflowComposition
|
|
|
2885
3030
|
runId: request.runId,
|
|
2886
3031
|
operationId: `${request.operationId}:${request.taskId}:repair:${input.repairAttempt}`,
|
|
2887
3032
|
role: "implementation-worker",
|
|
2888
|
-
requirements:
|
|
3033
|
+
requirements: implementationRouteRequirements({
|
|
3034
|
+
task: request.task,
|
|
3035
|
+
...(request.artifactCorrection
|
|
3036
|
+
? { artifactCorrection: request.artifactCorrection }
|
|
3037
|
+
: {}),
|
|
3038
|
+
repair: { failureIdentities: input.failureIdentities },
|
|
3039
|
+
}),
|
|
2889
3040
|
signal: request.signal,
|
|
2890
3041
|
...(request.onActivity ? { onActivity: request.onActivity } : {}),
|
|
3042
|
+
classifyResult: semanticRouteFailure,
|
|
2891
3043
|
execute: (attempt) => {
|
|
2892
3044
|
identity = {
|
|
2893
3045
|
candidateId: `candidate-${randomUUID()}`,
|
|
@@ -2925,6 +3077,12 @@ class DurableWorkflowComposition
|
|
|
2925
3077
|
},
|
|
2926
3078
|
route: attempt.route,
|
|
2927
3079
|
repair,
|
|
3080
|
+
...(request.artifactCorrection
|
|
3081
|
+
? { artifactCorrection: request.artifactCorrection }
|
|
3082
|
+
: {}),
|
|
3083
|
+
...(request.contextRequest
|
|
3084
|
+
? { contextRequest: request.contextRequest }
|
|
3085
|
+
: {}),
|
|
2928
3086
|
signal: attempt.signal,
|
|
2929
3087
|
onHeaders: attempt.onHeaders,
|
|
2930
3088
|
onProgress: attempt.onProgress,
|
|
@@ -2972,7 +3130,8 @@ class DurableWorkflowComposition
|
|
|
2972
3130
|
proposal.kind !== "sealed-candidate"
|
|
2973
3131
|
) {
|
|
2974
3132
|
return proposal.kind === "operation-cancelled" ||
|
|
2975
|
-
proposal.kind === "paused"
|
|
3133
|
+
proposal.kind === "paused" ||
|
|
3134
|
+
proposal.kind === "approval-needed"
|
|
2976
3135
|
? withRoute(proposal)
|
|
2977
3136
|
: reject(proposal, {
|
|
2978
3137
|
category: "artifact",
|
|
@@ -3213,7 +3372,9 @@ class DurableWorkflowComposition
|
|
|
3213
3372
|
changes: revisionChanges(proposalRoot, approvedPaths),
|
|
3214
3373
|
});
|
|
3215
3374
|
const requiredOutputs = request.plan.outputs.filter(
|
|
3216
|
-
(output) =>
|
|
3375
|
+
(output) =>
|
|
3376
|
+
output.producer.taskId === request.taskId &&
|
|
3377
|
+
(!request.artifactCorrection || output.producer.phase !== "refactor"),
|
|
3217
3378
|
);
|
|
3218
3379
|
if (
|
|
3219
3380
|
requiredOutputs.some(
|
|
@@ -3352,7 +3513,7 @@ class DurableWorkflowComposition
|
|
|
3352
3513
|
#verifiedPhaseReplay(
|
|
3353
3514
|
ledger: TaskLedger,
|
|
3354
3515
|
input: Parameters<WorkflowWorker["runAttempt"]>[0],
|
|
3355
|
-
): { artifactHash: string; exitCode: number } {
|
|
3516
|
+
): { artifactHash: string; exitCode: number; isolatedRevisionId: string } {
|
|
3356
3517
|
const projection = ledger.projection({
|
|
3357
3518
|
runId: input.runId,
|
|
3358
3519
|
taskId: input.taskId,
|
|
@@ -3366,6 +3527,8 @@ class DurableWorkflowComposition
|
|
|
3366
3527
|
!event ||
|
|
3367
3528
|
typeof event.artifactHash !== "string" ||
|
|
3368
3529
|
!SHA256.test(event.artifactHash) ||
|
|
3530
|
+
typeof event.isolatedRevisionId !== "string" ||
|
|
3531
|
+
!SHA256.test(event.isolatedRevisionId) ||
|
|
3369
3532
|
typeof event.exitCode !== "number" ||
|
|
3370
3533
|
event.actualClassification !== expectedClassification(input.phase)
|
|
3371
3534
|
) {
|
|
@@ -3374,6 +3537,285 @@ class DurableWorkflowComposition
|
|
|
3374
3537
|
return {
|
|
3375
3538
|
artifactHash: event.artifactHash,
|
|
3376
3539
|
exitCode: event.exitCode,
|
|
3540
|
+
isolatedRevisionId: event.isolatedRevisionId,
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
async #runRedArtifactCorrection(input: {
|
|
3545
|
+
resources: DurableRunResources;
|
|
3546
|
+
ledger: TaskLedger;
|
|
3547
|
+
baseline?: DurableVerificationBaseline;
|
|
3548
|
+
attempt: Parameters<WorkflowWorker["runAttempt"]>[0];
|
|
3549
|
+
}): Promise<WorkflowAttemptOutcome> {
|
|
3550
|
+
const { resources, ledger } = input;
|
|
3551
|
+
const request = input.attempt;
|
|
3552
|
+
if (
|
|
3553
|
+
request.phase !== "green" ||
|
|
3554
|
+
request.artifactCorrection?.code !== "red-artifact-constraint"
|
|
3555
|
+
) {
|
|
3556
|
+
throw new Error("workflow-red-artifact-correction-invalid");
|
|
3557
|
+
}
|
|
3558
|
+
const priorRed = this.#verifiedPhaseReplay(ledger, {
|
|
3559
|
+
...request,
|
|
3560
|
+
phase: "red",
|
|
3561
|
+
});
|
|
3562
|
+
if (!hasVerificationLifecycle(request.plan)) {
|
|
3563
|
+
return { kind: "paused", code: "red-artifact-constraint" };
|
|
3564
|
+
}
|
|
3565
|
+
const stableRevisionId = resources.currentRevisionId;
|
|
3566
|
+
const correctionPaths = [
|
|
3567
|
+
...new Set(
|
|
3568
|
+
Object.values(request.task.phases).flatMap((phase) => [
|
|
3569
|
+
...phase.write,
|
|
3570
|
+
...phase.delete,
|
|
3571
|
+
]),
|
|
3572
|
+
),
|
|
3573
|
+
];
|
|
3574
|
+
const rejectCorrection = async <T extends WorkflowAttemptOutcome>(
|
|
3575
|
+
outcome: T,
|
|
3576
|
+
): Promise<T> => {
|
|
3577
|
+
if (resources.currentRevisionId !== stableRevisionId) {
|
|
3578
|
+
await this.#rollbackWorkspacePaths({
|
|
3579
|
+
resources,
|
|
3580
|
+
baseRevisionId: stableRevisionId,
|
|
3581
|
+
rejectedRevisionId: resources.currentRevisionId,
|
|
3582
|
+
paths: correctionPaths,
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
return outcome;
|
|
3586
|
+
};
|
|
3587
|
+
const failureIdentities = [
|
|
3588
|
+
hash(
|
|
3589
|
+
"red-artifact-constraint",
|
|
3590
|
+
request.taskId,
|
|
3591
|
+
request.task.phases.red.verification.id,
|
|
3592
|
+
),
|
|
3593
|
+
];
|
|
3594
|
+
const correctionAttemptUsed = request.artifactCorrection.attempt;
|
|
3595
|
+
if (
|
|
3596
|
+
correctionAttemptUsed < 1 ||
|
|
3597
|
+
correctionAttemptUsed > request.artifactCorrection.maxAttempts
|
|
3598
|
+
) {
|
|
3599
|
+
throw new Error("workflow-red-artifact-correction-invalid");
|
|
3600
|
+
}
|
|
3601
|
+
const repaired = await this.#runRepairCandidate({
|
|
3602
|
+
resources,
|
|
3603
|
+
ledger,
|
|
3604
|
+
attempt: request,
|
|
3605
|
+
repairAttempt: correctionAttemptUsed,
|
|
3606
|
+
failureIdentities,
|
|
3607
|
+
});
|
|
3608
|
+
if (repaired.kind !== "repair-committed") {
|
|
3609
|
+
return rejectCorrection(repaired);
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
const correctedRevision = resources.workspaces.getRevision(
|
|
3613
|
+
repaired.commit.isolatedRevisionId,
|
|
3614
|
+
);
|
|
3615
|
+
const acceptedRedRevision = resources.workspaces.getRevision(
|
|
3616
|
+
priorRed.isolatedRevisionId,
|
|
3617
|
+
);
|
|
3618
|
+
if (!acceptedRedRevision.parentRevisionId) {
|
|
3619
|
+
throw new Error("workflow-ledger-revision-base-invalid");
|
|
3620
|
+
}
|
|
3621
|
+
const correctedRedChanges: Record<
|
|
3622
|
+
string,
|
|
3623
|
+
{ kind: "absent" } | { kind: "file"; bytes: Uint8Array; mode: number }
|
|
3624
|
+
> = {};
|
|
3625
|
+
for (const relative of [
|
|
3626
|
+
...new Set([
|
|
3627
|
+
...request.task.phases.red.write,
|
|
3628
|
+
...request.task.phases.red.delete,
|
|
3629
|
+
]),
|
|
3630
|
+
]) {
|
|
3631
|
+
const entry = correctedRevision.entries[relative];
|
|
3632
|
+
correctedRedChanges[relative] =
|
|
3633
|
+
entry?.kind === "file"
|
|
3634
|
+
? {
|
|
3635
|
+
kind: "file",
|
|
3636
|
+
bytes: resources.artifacts.read(entry.hash),
|
|
3637
|
+
mode: entry.mode,
|
|
3638
|
+
}
|
|
3639
|
+
: { kind: "absent" };
|
|
3640
|
+
}
|
|
3641
|
+
const correctedRedRevision = resources.workspaces.createRevision({
|
|
3642
|
+
parentRevisionId: acceptedRedRevision.parentRevisionId,
|
|
3643
|
+
changes: correctedRedChanges,
|
|
3644
|
+
});
|
|
3645
|
+
const verifyCorrectedContract = async (input: {
|
|
3646
|
+
phase: "red" | "green";
|
|
3647
|
+
revisionId: string;
|
|
3648
|
+
verification: StructuredVerificationContract;
|
|
3649
|
+
}): Promise<DurablePhaseVerificationResult> => {
|
|
3650
|
+
const root = mkdtempSync(path.join(resources.root, "correction-check-"));
|
|
3651
|
+
try {
|
|
3652
|
+
resources.workspaces.materialize(input.revisionId, root);
|
|
3653
|
+
return await this.#options.verifyPhase({
|
|
3654
|
+
runId: request.runId,
|
|
3655
|
+
deliveryRevision: request.deliveryRevision,
|
|
3656
|
+
taskId: request.taskId,
|
|
3657
|
+
phase: input.phase,
|
|
3658
|
+
root,
|
|
3659
|
+
verification: structuredClone(input.verification),
|
|
3660
|
+
signal: request.signal,
|
|
3661
|
+
});
|
|
3662
|
+
} finally {
|
|
3663
|
+
rmSync(root, { recursive: true, force: true });
|
|
3664
|
+
}
|
|
3665
|
+
};
|
|
3666
|
+
emitWorkflowActivity(request.onActivity, { state: "verifying" });
|
|
3667
|
+
const redVerification = await verifyCorrectedContract({
|
|
3668
|
+
phase: "red",
|
|
3669
|
+
revisionId: correctedRedRevision.revisionId,
|
|
3670
|
+
verification: request.task.phases.red.verification,
|
|
3671
|
+
});
|
|
3672
|
+
if (!redVerification.ok) {
|
|
3673
|
+
return rejectCorrection({
|
|
3674
|
+
...redVerification,
|
|
3675
|
+
...(redVerification.kind === "retryable"
|
|
3676
|
+
? { retryPolicy: "verification" as const }
|
|
3677
|
+
: {}),
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
if (
|
|
3681
|
+
redVerification.classification !== "expected-red" ||
|
|
3682
|
+
redVerification.exitCode === 0
|
|
3683
|
+
) {
|
|
3684
|
+
return rejectCorrection({
|
|
3685
|
+
kind: "retryable",
|
|
3686
|
+
code: "verification-rejected",
|
|
3687
|
+
retryPolicy: "verification",
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
const greenVerification = await verifyCorrectedContract({
|
|
3691
|
+
phase: "green",
|
|
3692
|
+
revisionId: repaired.commit.isolatedRevisionId,
|
|
3693
|
+
verification: request.task.phases.green.verification,
|
|
3694
|
+
});
|
|
3695
|
+
if (!greenVerification.ok) {
|
|
3696
|
+
return rejectCorrection({
|
|
3697
|
+
...greenVerification,
|
|
3698
|
+
...(greenVerification.kind === "retryable"
|
|
3699
|
+
? { retryPolicy: "verification" as const }
|
|
3700
|
+
: {}),
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
if (
|
|
3704
|
+
greenVerification.classification !== "expected-green" ||
|
|
3705
|
+
greenVerification.exitCode !== 0
|
|
3706
|
+
) {
|
|
3707
|
+
return rejectCorrection({
|
|
3708
|
+
kind: "retryable",
|
|
3709
|
+
code: "verification-rejected",
|
|
3710
|
+
retryPolicy: "verification",
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
const repairEvent: VerifiedTaskEvent = {
|
|
3715
|
+
runId: request.runId,
|
|
3716
|
+
taskId: request.taskId,
|
|
3717
|
+
eventId: `red-artifact-correction-${hash(
|
|
3718
|
+
request.operationId,
|
|
3719
|
+
request.taskId,
|
|
3720
|
+
repaired.commit.artifactHash,
|
|
3721
|
+
).slice(0, 40)}`,
|
|
3722
|
+
kind: "repair-verified",
|
|
3723
|
+
phase: request.phase,
|
|
3724
|
+
attempt: correctionAttemptUsed,
|
|
3725
|
+
commandId: request.task.repairVerification.id,
|
|
3726
|
+
exitCode: 0,
|
|
3727
|
+
diagnostic: repaired.commit.diagnostic,
|
|
3728
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3729
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3730
|
+
outputFacts: repaired.commit.outputFacts,
|
|
3731
|
+
attribution: "introduced",
|
|
3732
|
+
failureIdentities: [
|
|
3733
|
+
hash(
|
|
3734
|
+
"red-artifact-constraint",
|
|
3735
|
+
request.taskId,
|
|
3736
|
+
request.task.phases.red.verification.id,
|
|
3737
|
+
),
|
|
3738
|
+
],
|
|
3739
|
+
routeId: repaired.commit.routeId,
|
|
3740
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3741
|
+
};
|
|
3742
|
+
const greenPaths = new Set([
|
|
3743
|
+
...request.task.phases.green.write,
|
|
3744
|
+
...request.task.phases.green.delete,
|
|
3745
|
+
]);
|
|
3746
|
+
const greenEvent: VerifiedTaskEvent = {
|
|
3747
|
+
runId: request.runId,
|
|
3748
|
+
taskId: request.taskId,
|
|
3749
|
+
eventId: `green-corrected-${hash(
|
|
3750
|
+
request.operationId,
|
|
3751
|
+
request.taskId,
|
|
3752
|
+
repaired.commit.artifactHash,
|
|
3753
|
+
).slice(0, 40)}`,
|
|
3754
|
+
kind: "phase-verified",
|
|
3755
|
+
phase: "green",
|
|
3756
|
+
commandId: request.task.phases.green.verification.id,
|
|
3757
|
+
exitCode: greenVerification.exitCode,
|
|
3758
|
+
expectedClassification: "expected-green",
|
|
3759
|
+
actualClassification: greenVerification.classification,
|
|
3760
|
+
diagnostic: greenVerification.diagnostic,
|
|
3761
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3762
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3763
|
+
outputFacts: repaired.commit.outputFacts.filter((fact) =>
|
|
3764
|
+
greenPaths.has(fact.path),
|
|
3765
|
+
),
|
|
3766
|
+
routeId: repaired.commit.routeId,
|
|
3767
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3768
|
+
};
|
|
3769
|
+
const events: VerifiedTaskEvent[] = [repairEvent, greenEvent];
|
|
3770
|
+
if (input.baseline) {
|
|
3771
|
+
const affected = await this.#verifyTaskAffected({
|
|
3772
|
+
resources,
|
|
3773
|
+
baseline: input.baseline,
|
|
3774
|
+
task: request.task,
|
|
3775
|
+
revisionId: repaired.commit.isolatedRevisionId,
|
|
3776
|
+
signal: request.signal,
|
|
3777
|
+
});
|
|
3778
|
+
if (affected.kind === "repairable") {
|
|
3779
|
+
return rejectCorrection({
|
|
3780
|
+
kind: "retryable",
|
|
3781
|
+
code: "red-artifact-constraint",
|
|
3782
|
+
retryPolicy: "artifact",
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
if (affected.kind !== "verified") return rejectCorrection(affected);
|
|
3786
|
+
}
|
|
3787
|
+
if (request.task.phases.refactor) {
|
|
3788
|
+
ledger.commitVerifiedEvents(events);
|
|
3789
|
+
return {
|
|
3790
|
+
kind: "phase-committed",
|
|
3791
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3792
|
+
isolatedRevisionId: repaired.commit.isolatedRevisionId,
|
|
3793
|
+
exitCode: greenVerification.exitCode,
|
|
3794
|
+
classification: greenVerification.classification,
|
|
3795
|
+
routeId: repaired.commit.routeId,
|
|
3796
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
const completedRevision = this.#completeTracking(resources, request.taskId);
|
|
3800
|
+
ledger.commitTaskCompletion({
|
|
3801
|
+
events,
|
|
3802
|
+
factKey: `task-final-${hash(request.taskId).slice(0, 40)}`,
|
|
3803
|
+
fact: {
|
|
3804
|
+
taskId: request.taskId,
|
|
3805
|
+
phase: request.phase,
|
|
3806
|
+
isolatedRevisionId: completedRevision.revisionId,
|
|
3807
|
+
attribution: "introduced",
|
|
3808
|
+
repairAttempts: correctionAttemptUsed,
|
|
3809
|
+
},
|
|
3810
|
+
});
|
|
3811
|
+
return {
|
|
3812
|
+
kind: "phase-committed",
|
|
3813
|
+
artifactHash: repaired.commit.artifactHash,
|
|
3814
|
+
isolatedRevisionId: completedRevision.revisionId,
|
|
3815
|
+
exitCode: greenVerification.exitCode,
|
|
3816
|
+
classification: greenVerification.classification,
|
|
3817
|
+
routeId: repaired.commit.routeId,
|
|
3818
|
+
routeFingerprint: repaired.commit.routeFingerprint,
|
|
3377
3819
|
};
|
|
3378
3820
|
}
|
|
3379
3821
|
|
|
@@ -3598,6 +4040,16 @@ class DurableWorkflowComposition
|
|
|
3598
4040
|
verificationBaseline = captured.baseline;
|
|
3599
4041
|
}
|
|
3600
4042
|
this.#openTask(resources, ledger, input);
|
|
4043
|
+
if (input.artifactCorrection?.code === "red-artifact-constraint") {
|
|
4044
|
+
return retained(
|
|
4045
|
+
await this.#runRedArtifactCorrection({
|
|
4046
|
+
resources,
|
|
4047
|
+
ledger,
|
|
4048
|
+
...(verificationBaseline ? { baseline: verificationBaseline } : {}),
|
|
4049
|
+
attempt: input,
|
|
4050
|
+
}),
|
|
4051
|
+
);
|
|
4052
|
+
}
|
|
3601
4053
|
if (input.repair) {
|
|
3602
4054
|
if (!verificationBaseline) {
|
|
3603
4055
|
throw new Error("workflow-verification-baseline-unavailable");
|
|
@@ -3644,7 +4096,13 @@ class DurableWorkflowComposition
|
|
|
3644
4096
|
...(input.routeFingerprint
|
|
3645
4097
|
? { expectedFingerprint: input.routeFingerprint }
|
|
3646
4098
|
: {}),
|
|
3647
|
-
requirements:
|
|
4099
|
+
requirements: implementationRouteRequirements({
|
|
4100
|
+
task: input.task,
|
|
4101
|
+
...(input.artifactCorrection
|
|
4102
|
+
? { artifactCorrection: input.artifactCorrection }
|
|
4103
|
+
: {}),
|
|
4104
|
+
...(input.repair ? { repair: input.repair } : {}),
|
|
4105
|
+
}),
|
|
3648
4106
|
});
|
|
3649
4107
|
if (!rebound.ok) {
|
|
3650
4108
|
return retained({ kind: "paused", code: rebound.code });
|
|
@@ -3660,9 +4118,16 @@ class DurableWorkflowComposition
|
|
|
3660
4118
|
runId: input.runId,
|
|
3661
4119
|
operationId: `${input.operationId}:${input.taskId}:${input.phase}`,
|
|
3662
4120
|
role: "implementation-worker",
|
|
3663
|
-
requirements:
|
|
4121
|
+
requirements: implementationRouteRequirements({
|
|
4122
|
+
task: input.task,
|
|
4123
|
+
...(input.artifactCorrection
|
|
4124
|
+
? { artifactCorrection: input.artifactCorrection }
|
|
4125
|
+
: {}),
|
|
4126
|
+
...(input.repair ? { repair: input.repair } : {}),
|
|
4127
|
+
}),
|
|
3664
4128
|
signal: input.signal,
|
|
3665
4129
|
...(input.onActivity ? { onActivity: input.onActivity } : {}),
|
|
4130
|
+
classifyResult: semanticRouteFailure,
|
|
3666
4131
|
execute: (attempt) => {
|
|
3667
4132
|
identity = {
|
|
3668
4133
|
candidateId: `candidate-${randomUUID()}`,
|
|
@@ -3699,6 +4164,9 @@ class DurableWorkflowComposition
|
|
|
3699
4164
|
deletePaths: phase.delete,
|
|
3700
4165
|
},
|
|
3701
4166
|
route: attempt.route,
|
|
4167
|
+
...(input.contextRequest
|
|
4168
|
+
? { contextRequest: input.contextRequest }
|
|
4169
|
+
: {}),
|
|
3702
4170
|
signal: attempt.signal,
|
|
3703
4171
|
onHeaders: attempt.onHeaders,
|
|
3704
4172
|
onProgress: attempt.onProgress,
|
|
@@ -3747,7 +4215,8 @@ class DurableWorkflowComposition
|
|
|
3747
4215
|
proposal.kind !== "sealed-candidate"
|
|
3748
4216
|
) {
|
|
3749
4217
|
return proposal.kind === "operation-cancelled" ||
|
|
3750
|
-
proposal.kind === "paused"
|
|
4218
|
+
proposal.kind === "paused" ||
|
|
4219
|
+
proposal.kind === "approval-needed"
|
|
3751
4220
|
? retained(proposal)
|
|
3752
4221
|
: reject(proposal, {
|
|
3753
4222
|
category: "artifact",
|
|
@@ -4277,7 +4746,7 @@ class DurableWorkflowComposition
|
|
|
4277
4746
|
runId: input.runId,
|
|
4278
4747
|
role: input.role,
|
|
4279
4748
|
routeId: input.routeId,
|
|
4280
|
-
requirements:
|
|
4749
|
+
requirements: implementationRouteRequirements({ task: input.task }),
|
|
4281
4750
|
});
|
|
4282
4751
|
return rebound.ok
|
|
4283
4752
|
? {
|
|
@@ -5080,8 +5549,18 @@ export class WorkflowEngine {
|
|
|
5080
5549
|
this.#database
|
|
5081
5550
|
.prepare(
|
|
5082
5551
|
`UPDATE workflow_engine_tasks
|
|
5083
|
-
SET state = 'paused',
|
|
5084
|
-
|
|
5552
|
+
SET state = 'paused',
|
|
5553
|
+
pause_code = CASE
|
|
5554
|
+
WHEN pause_code = 'red-artifact-constraint'
|
|
5555
|
+
THEN pause_code
|
|
5556
|
+
ELSE 'operation-interrupted'
|
|
5557
|
+
END,
|
|
5558
|
+
context_request_json = CASE
|
|
5559
|
+
WHEN pause_code = 'red-artifact-constraint'
|
|
5560
|
+
THEN context_request_json
|
|
5561
|
+
ELSE NULL
|
|
5562
|
+
END,
|
|
5563
|
+
queue_position = NULL
|
|
5085
5564
|
WHERE run_id = ? AND state IN ('phase-running', 'validating')`,
|
|
5086
5565
|
)
|
|
5087
5566
|
.run(row.run_id);
|
|
@@ -5154,8 +5633,8 @@ export class WorkflowEngine {
|
|
|
5154
5633
|
return this.#database
|
|
5155
5634
|
.prepare(
|
|
5156
5635
|
`SELECT task_id, task_order, delivery_revision, plan_json, state,
|
|
5157
|
-
phase, pause_code, context_request_json,
|
|
5158
|
-
route_fingerprint, queue_position
|
|
5636
|
+
phase, pause_code, context_request_json, attempt_diagnostic_json,
|
|
5637
|
+
route_id, route_fingerprint, queue_position
|
|
5159
5638
|
FROM workflow_engine_tasks WHERE run_id = ? ORDER BY task_order`,
|
|
5160
5639
|
)
|
|
5161
5640
|
.all(runId) as unknown as EngineTaskRow[];
|
|
@@ -5452,7 +5931,8 @@ export class WorkflowEngine {
|
|
|
5452
5931
|
`UPDATE workflow_engine_tasks
|
|
5453
5932
|
SET task_order = ?, delivery_revision = ?, plan_json = ?,
|
|
5454
5933
|
state = 'pending', phase = 'red', pause_code = NULL,
|
|
5455
|
-
context_request_json = NULL,
|
|
5934
|
+
context_request_json = NULL, attempt_diagnostic_json = NULL,
|
|
5935
|
+
queue_position = NULL
|
|
5456
5936
|
WHERE run_id = ? AND task_id = ?`,
|
|
5457
5937
|
)
|
|
5458
5938
|
.run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
|
|
@@ -5509,6 +5989,7 @@ export class WorkflowEngine {
|
|
|
5509
5989
|
phase?: "red" | "green" | "refactor";
|
|
5510
5990
|
pauseCode?: string | null;
|
|
5511
5991
|
contextRequest?: WorkflowContextRequest | null;
|
|
5992
|
+
attemptDiagnostic?: SafeAttemptDiagnostic | null;
|
|
5512
5993
|
routeId?: string | null;
|
|
5513
5994
|
routeFingerprint?: string | null;
|
|
5514
5995
|
queuePosition?: number | null;
|
|
@@ -5539,6 +6020,14 @@ export class WorkflowEngine {
|
|
|
5539
6020
|
),
|
|
5540
6021
|
);
|
|
5541
6022
|
}
|
|
6023
|
+
if (values.attemptDiagnostic !== undefined) {
|
|
6024
|
+
assignments.push("attempt_diagnostic_json = ?");
|
|
6025
|
+
parameters.push(
|
|
6026
|
+
values.attemptDiagnostic === null
|
|
6027
|
+
? null
|
|
6028
|
+
: JSON.stringify(values.attemptDiagnostic),
|
|
6029
|
+
);
|
|
6030
|
+
}
|
|
5542
6031
|
if (values.routeId !== undefined) {
|
|
5543
6032
|
assignments.push("route_id = ?");
|
|
5544
6033
|
parameters.push(values.routeId);
|
|
@@ -5745,6 +6234,24 @@ export class WorkflowEngine {
|
|
|
5745
6234
|
const orderedPhases = phases(task);
|
|
5746
6235
|
let phaseIndex = orderedPhases.indexOf(row.phase);
|
|
5747
6236
|
let artifactAttempts = 0;
|
|
6237
|
+
const persistedContextRequest = row.context_request_json
|
|
6238
|
+
? normalizeWorkflowContextRequest(
|
|
6239
|
+
JSON.parse(row.context_request_json) as unknown,
|
|
6240
|
+
)
|
|
6241
|
+
: undefined;
|
|
6242
|
+
const persistedRedArtifactCorrection =
|
|
6243
|
+
row.phase === "green" &&
|
|
6244
|
+
persistedContextRequest !== undefined &&
|
|
6245
|
+
classifyPersistedContextRequest(persistedContextRequest, task, "green")
|
|
6246
|
+
.code === "red-artifact-constraint";
|
|
6247
|
+
let redArtifactCorrection =
|
|
6248
|
+
row.phase === "green" &&
|
|
6249
|
+
(row.pause_code === "red-artifact-constraint" ||
|
|
6250
|
+
persistedRedArtifactCorrection);
|
|
6251
|
+
let requestedContext = persistedContextRequest;
|
|
6252
|
+
let correctionContextRequest = redArtifactCorrection
|
|
6253
|
+
? persistedContextRequest
|
|
6254
|
+
: undefined;
|
|
5748
6255
|
if (phaseIndex < 0) throw new Error("workflow-task-phase-invalid");
|
|
5749
6256
|
while (phaseIndex < orderedPhases.length) {
|
|
5750
6257
|
const phase = orderedPhases[phaseIndex];
|
|
@@ -5771,6 +6278,7 @@ export class WorkflowEngine {
|
|
|
5771
6278
|
JSON.parse(current.verification_json) as unknown,
|
|
5772
6279
|
)
|
|
5773
6280
|
: undefined;
|
|
6281
|
+
const plan = this.#planForRevision(runId, row.delivery_revision);
|
|
5774
6282
|
const repair =
|
|
5775
6283
|
verificationStatus?.scope === "change-task-affected" &&
|
|
5776
6284
|
verificationStatus.attribution === "introduced" &&
|
|
@@ -5782,19 +6290,32 @@ export class WorkflowEngine {
|
|
|
5782
6290
|
failureIdentities: [...verificationStatus.failureIdentities],
|
|
5783
6291
|
}
|
|
5784
6292
|
: undefined;
|
|
6293
|
+
const artifactCorrection =
|
|
6294
|
+
redArtifactCorrection && phase === "green"
|
|
6295
|
+
? {
|
|
6296
|
+
code: "red-artifact-constraint" as const,
|
|
6297
|
+
attempt: artifactAttempts + 1,
|
|
6298
|
+
maxAttempts: plan.verification.artifactCorrection.maxAttempts,
|
|
6299
|
+
...(correctionContextRequest
|
|
6300
|
+
? {
|
|
6301
|
+
contextRequest: structuredClone(correctionContextRequest),
|
|
6302
|
+
}
|
|
6303
|
+
: {}),
|
|
6304
|
+
}
|
|
6305
|
+
: undefined;
|
|
5785
6306
|
this.#setTask(
|
|
5786
6307
|
runId,
|
|
5787
6308
|
row.task_id,
|
|
5788
6309
|
{
|
|
5789
6310
|
state: "phase-running",
|
|
5790
6311
|
phase,
|
|
5791
|
-
pauseCode: null,
|
|
5792
|
-
contextRequest: null,
|
|
6312
|
+
pauseCode: artifactCorrection?.code ?? null,
|
|
6313
|
+
contextRequest: requestedContext ?? null,
|
|
6314
|
+
attemptDiagnostic: null,
|
|
5793
6315
|
queuePosition: null,
|
|
5794
6316
|
},
|
|
5795
6317
|
lease,
|
|
5796
6318
|
);
|
|
5797
|
-
const plan = this.#planForRevision(runId, row.delivery_revision);
|
|
5798
6319
|
let outcome: WorkflowAttemptOutcome;
|
|
5799
6320
|
try {
|
|
5800
6321
|
outcome = await this.#worker.runAttempt({
|
|
@@ -5825,6 +6346,8 @@ export class WorkflowEngine {
|
|
|
5825
6346
|
}
|
|
5826
6347
|
: {}),
|
|
5827
6348
|
...(repair ? { repair } : {}),
|
|
6349
|
+
...(artifactCorrection ? { artifactCorrection } : {}),
|
|
6350
|
+
...(requestedContext ? { contextRequest: requestedContext } : {}),
|
|
5828
6351
|
signal,
|
|
5829
6352
|
onActivity: (event) => {
|
|
5830
6353
|
const projection = this.#runStore.status(runId);
|
|
@@ -5863,10 +6386,69 @@ export class WorkflowEngine {
|
|
|
5863
6386
|
) {
|
|
5864
6387
|
this.#setVerificationStatus(runId, outcome.verification, lease);
|
|
5865
6388
|
}
|
|
6389
|
+
if (
|
|
6390
|
+
outcome.kind !== "phase-committed" &&
|
|
6391
|
+
outcome.kind !== "operation-cancelled" &&
|
|
6392
|
+
outcome.contextRequest
|
|
6393
|
+
) {
|
|
6394
|
+
requestedContext = normalizeWorkflowContextRequest(
|
|
6395
|
+
outcome.contextRequest,
|
|
6396
|
+
);
|
|
6397
|
+
}
|
|
6398
|
+
if (
|
|
6399
|
+
outcome.kind !== "phase-committed" &&
|
|
6400
|
+
outcome.kind !== "operation-cancelled" &&
|
|
6401
|
+
outcome.attemptDiagnostic
|
|
6402
|
+
) {
|
|
6403
|
+
const previous = row.attempt_diagnostic_json
|
|
6404
|
+
? (JSON.parse(row.attempt_diagnostic_json) as SafeAttemptDiagnostic)
|
|
6405
|
+
: undefined;
|
|
6406
|
+
const fingerprint = hash(
|
|
6407
|
+
outcome.code,
|
|
6408
|
+
outcome.attemptDiagnostic.finalCategory ?? "unknown",
|
|
6409
|
+
outcome.attemptDiagnostic.schema ?? "unknown",
|
|
6410
|
+
JSON.stringify(outcome.attemptDiagnostic.identityMismatch ?? []),
|
|
6411
|
+
).slice(0, 16);
|
|
6412
|
+
const sameFailure = previous?.fingerprint === fingerprint;
|
|
6413
|
+
const sameFailureCount = sameFailure
|
|
6414
|
+
? (previous?.sameFailureCount ?? 1) + 1
|
|
6415
|
+
: 1;
|
|
6416
|
+
const attemptDiagnostic = {
|
|
6417
|
+
...outcome.attemptDiagnostic,
|
|
6418
|
+
fingerprint,
|
|
6419
|
+
sameFailureCount,
|
|
6420
|
+
...(sameFailureCount > 1
|
|
6421
|
+
? { action: "rebind-or-revise-delivery" as const }
|
|
6422
|
+
: {}),
|
|
6423
|
+
};
|
|
6424
|
+
this.#setTask(runId, row.task_id, { attemptDiagnostic }, lease);
|
|
6425
|
+
row.attempt_diagnostic_json = JSON.stringify(attemptDiagnostic);
|
|
6426
|
+
}
|
|
5866
6427
|
if (outcome.kind === "retryable" && outcome.retryPolicy === "artifact") {
|
|
6428
|
+
if (outcome.code === "red-artifact-constraint") {
|
|
6429
|
+
redArtifactCorrection = true;
|
|
6430
|
+
correctionContextRequest = outcome.contextRequest
|
|
6431
|
+
? normalizeWorkflowContextRequest(outcome.contextRequest)
|
|
6432
|
+
: correctionContextRequest;
|
|
6433
|
+
requestedContext = correctionContextRequest ?? requestedContext;
|
|
6434
|
+
}
|
|
5867
6435
|
artifactAttempts += 1;
|
|
5868
6436
|
const maxAttempts = plan.verification.artifactCorrection.maxAttempts;
|
|
5869
6437
|
if (artifactAttempts < maxAttempts) {
|
|
6438
|
+
this.#setTask(
|
|
6439
|
+
runId,
|
|
6440
|
+
row.task_id,
|
|
6441
|
+
{
|
|
6442
|
+
state: "retryable",
|
|
6443
|
+
phase,
|
|
6444
|
+
pauseCode: redArtifactCorrection
|
|
6445
|
+
? "red-artifact-constraint"
|
|
6446
|
+
: outcome.code,
|
|
6447
|
+
contextRequest: requestedContext ?? null,
|
|
6448
|
+
queuePosition: null,
|
|
6449
|
+
},
|
|
6450
|
+
lease,
|
|
6451
|
+
);
|
|
5870
6452
|
emitWorkflowActivity(onActivity, {
|
|
5871
6453
|
state: "retrying",
|
|
5872
6454
|
stage: this.#runStore.status(runId).stage,
|
|
@@ -5903,6 +6485,7 @@ export class WorkflowEngine {
|
|
|
5903
6485
|
phase,
|
|
5904
6486
|
pauseCode: null,
|
|
5905
6487
|
contextRequest: null,
|
|
6488
|
+
attemptDiagnostic: null,
|
|
5906
6489
|
queuePosition: null,
|
|
5907
6490
|
},
|
|
5908
6491
|
lease,
|
|
@@ -5910,6 +6493,9 @@ export class WorkflowEngine {
|
|
|
5910
6493
|
return;
|
|
5911
6494
|
}
|
|
5912
6495
|
artifactAttempts = 0;
|
|
6496
|
+
redArtifactCorrection = false;
|
|
6497
|
+
requestedContext = undefined;
|
|
6498
|
+
correctionContextRequest = undefined;
|
|
5913
6499
|
phaseIndex += 1;
|
|
5914
6500
|
if (phaseIndex >= orderedPhases.length) {
|
|
5915
6501
|
this.#setTask(
|
|
@@ -5920,6 +6506,7 @@ export class WorkflowEngine {
|
|
|
5920
6506
|
phase,
|
|
5921
6507
|
pauseCode: null,
|
|
5922
6508
|
contextRequest: null,
|
|
6509
|
+
attemptDiagnostic: null,
|
|
5923
6510
|
queuePosition: null,
|
|
5924
6511
|
},
|
|
5925
6512
|
lease,
|
|
@@ -5934,6 +6521,7 @@ export class WorkflowEngine {
|
|
|
5934
6521
|
phase: orderedPhases[phaseIndex],
|
|
5935
6522
|
pauseCode: null,
|
|
5936
6523
|
contextRequest: null,
|
|
6524
|
+
attemptDiagnostic: null,
|
|
5937
6525
|
queuePosition: null,
|
|
5938
6526
|
},
|
|
5939
6527
|
lease,
|
|
@@ -5975,10 +6563,9 @@ export class WorkflowEngine {
|
|
|
5975
6563
|
pauseCode: approvalCodeInvalid
|
|
5976
6564
|
? "approval-code-invalid"
|
|
5977
6565
|
: outcome.code,
|
|
5978
|
-
contextRequest:
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
: null,
|
|
6566
|
+
contextRequest: approvalCodeInvalid
|
|
6567
|
+
? null
|
|
6568
|
+
: (requestedContext ?? null),
|
|
5982
6569
|
queuePosition: null,
|
|
5983
6570
|
},
|
|
5984
6571
|
lease,
|
|
@@ -5990,11 +6577,119 @@ export class WorkflowEngine {
|
|
|
5990
6577
|
}
|
|
5991
6578
|
|
|
5992
6579
|
#firstPaused(rows: EngineTaskRow[]): EngineTaskRow | undefined {
|
|
5993
|
-
return
|
|
5994
|
-
|
|
6580
|
+
return (
|
|
6581
|
+
rows.find((row) => row.state === "approval-needed") ??
|
|
6582
|
+
rows.find((row) => ["paused", "retryable"].includes(row.state))
|
|
5995
6583
|
);
|
|
5996
6584
|
}
|
|
5997
6585
|
|
|
6586
|
+
#recoverLegacyContextApproval(runId: string): boolean {
|
|
6587
|
+
const projection = this.#runStore.status(runId);
|
|
6588
|
+
if (!["approval-needed", "paused"].includes(projection.state)) return false;
|
|
6589
|
+
const rows = this.#tasks(runId);
|
|
6590
|
+
const approvalRows = rows.filter((row) => row.state === "approval-needed");
|
|
6591
|
+
if (approvalRows.length === 0) return false;
|
|
6592
|
+
const reclassified = rows.flatMap((row) => {
|
|
6593
|
+
if (
|
|
6594
|
+
row.state !== "approval-needed" ||
|
|
6595
|
+
row.pause_code !== "boundary-review-needed" ||
|
|
6596
|
+
row.context_request_json === null
|
|
6597
|
+
) {
|
|
6598
|
+
return [];
|
|
6599
|
+
}
|
|
6600
|
+
try {
|
|
6601
|
+
const request = normalizeWorkflowContextRequest(
|
|
6602
|
+
JSON.parse(row.context_request_json) as unknown,
|
|
6603
|
+
);
|
|
6604
|
+
const classified = classifyPersistedContextRequest(
|
|
6605
|
+
request,
|
|
6606
|
+
this.#taskPlan(row),
|
|
6607
|
+
row.phase,
|
|
6608
|
+
);
|
|
6609
|
+
if (classified.kind === "approval-needed") return [];
|
|
6610
|
+
return [
|
|
6611
|
+
{
|
|
6612
|
+
row,
|
|
6613
|
+
state: classified.kind === "retryable" ? "retryable" : "paused",
|
|
6614
|
+
code: classified.code,
|
|
6615
|
+
contextRequest: JSON.stringify(
|
|
6616
|
+
normalizeWorkflowContextRequest(classified.contextRequest),
|
|
6617
|
+
),
|
|
6618
|
+
},
|
|
6619
|
+
];
|
|
6620
|
+
} catch {
|
|
6621
|
+
return [];
|
|
6622
|
+
}
|
|
6623
|
+
});
|
|
6624
|
+
const reclassifiedTaskIds = new Set(
|
|
6625
|
+
reclassified.map((entry) => entry.row.task_id),
|
|
6626
|
+
);
|
|
6627
|
+
const retainedApprovals = approvalRows.filter(
|
|
6628
|
+
(row) => !reclassifiedTaskIds.has(row.task_id),
|
|
6629
|
+
);
|
|
6630
|
+
const updateTasks = (database: DatabaseSync) => {
|
|
6631
|
+
for (const entry of reclassified) {
|
|
6632
|
+
const updated = database
|
|
6633
|
+
.prepare(
|
|
6634
|
+
`UPDATE workflow_engine_tasks
|
|
6635
|
+
SET state = ?, phase = ?, pause_code = ?, context_request_json = ?,
|
|
6636
|
+
queue_position = NULL
|
|
6637
|
+
WHERE run_id = ? AND task_id = ?
|
|
6638
|
+
AND state = 'approval-needed'
|
|
6639
|
+
AND pause_code = 'boundary-review-needed'`,
|
|
6640
|
+
)
|
|
6641
|
+
.run(
|
|
6642
|
+
entry.state,
|
|
6643
|
+
entry.row.phase,
|
|
6644
|
+
entry.code,
|
|
6645
|
+
entry.contextRequest,
|
|
6646
|
+
runId,
|
|
6647
|
+
entry.row.task_id,
|
|
6648
|
+
);
|
|
6649
|
+
if (Number(updated.changes) !== 1) {
|
|
6650
|
+
throw new Error("workflow-legacy-context-race");
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
};
|
|
6654
|
+
const transitionWithUpdates = (
|
|
6655
|
+
to: "paused" | "approval-needed",
|
|
6656
|
+
code: string,
|
|
6657
|
+
) =>
|
|
6658
|
+
this.#runStore.transitionAtomically(
|
|
6659
|
+
{
|
|
6660
|
+
runId,
|
|
6661
|
+
to,
|
|
6662
|
+
operationId: `legacy-context-${hash(
|
|
6663
|
+
runId,
|
|
6664
|
+
String(projection.sequence),
|
|
6665
|
+
to,
|
|
6666
|
+
code,
|
|
6667
|
+
JSON.stringify(
|
|
6668
|
+
reclassified.map((entry) => [entry.row.task_id, entry.code]),
|
|
6669
|
+
),
|
|
6670
|
+
).slice(0, 40)}`,
|
|
6671
|
+
code,
|
|
6672
|
+
},
|
|
6673
|
+
updateTasks,
|
|
6674
|
+
);
|
|
6675
|
+
if (retainedApprovals.length > 0) {
|
|
6676
|
+
if (projection.state === "approval-needed") {
|
|
6677
|
+
if (reclassified.length === 0) return false;
|
|
6678
|
+
this.#transaction(() => updateTasks(this.#database));
|
|
6679
|
+
} else {
|
|
6680
|
+
transitionWithUpdates(
|
|
6681
|
+
"approval-needed",
|
|
6682
|
+
retainedApprovals[0].pause_code ?? "approval-code-invalid",
|
|
6683
|
+
);
|
|
6684
|
+
}
|
|
6685
|
+
return true;
|
|
6686
|
+
}
|
|
6687
|
+
const first = reclassified[0];
|
|
6688
|
+
if (!first) return false;
|
|
6689
|
+
transitionWithUpdates("paused", first.code);
|
|
6690
|
+
return true;
|
|
6691
|
+
}
|
|
6692
|
+
|
|
5998
6693
|
async #finishChange(
|
|
5999
6694
|
runId: string,
|
|
6000
6695
|
operationId: string,
|
|
@@ -6568,6 +7263,21 @@ export class WorkflowEngine {
|
|
|
6568
7263
|
return this.#statusByRun(runId);
|
|
6569
7264
|
}
|
|
6570
7265
|
|
|
7266
|
+
#safeAttemptDiagnostic(
|
|
7267
|
+
_runId: string,
|
|
7268
|
+
row: EngineTaskRow | undefined,
|
|
7269
|
+
): SafeAttemptDiagnostic | undefined {
|
|
7270
|
+
if (!row) return undefined;
|
|
7271
|
+
if (row.attempt_diagnostic_json) {
|
|
7272
|
+
try {
|
|
7273
|
+
return JSON.parse(row.attempt_diagnostic_json) as SafeAttemptDiagnostic;
|
|
7274
|
+
} catch {
|
|
7275
|
+
return undefined;
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
return undefined;
|
|
7279
|
+
}
|
|
7280
|
+
|
|
6571
7281
|
#statusByRun(runId: string): Record<string, unknown> {
|
|
6572
7282
|
const projection = this.#runStore.status(runId);
|
|
6573
7283
|
const rows = this.#tasks(runId);
|
|
@@ -6592,14 +7302,17 @@ export class WorkflowEngine {
|
|
|
6592
7302
|
)
|
|
6593
7303
|
: undefined;
|
|
6594
7304
|
const pauseCode =
|
|
6595
|
-
projection.
|
|
7305
|
+
projection.state === "approval-needed" &&
|
|
7306
|
+
firstPaused?.state === "approval-needed"
|
|
7307
|
+
? (firstPaused.pause_code ?? projection.pauseCode ?? "task-paused")
|
|
7308
|
+
: (projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused");
|
|
6596
7309
|
const approval =
|
|
6597
7310
|
projection.state === "approval-needed"
|
|
6598
7311
|
? approvalRequirement(pauseCode, contextRequest)
|
|
6599
7312
|
: undefined;
|
|
7313
|
+
const attemptDiagnostic = this.#safeAttemptDiagnostic(runId, firstPaused);
|
|
6600
7314
|
const currentRevision = projection.deliveryRevision ?? 0;
|
|
6601
7315
|
return {
|
|
6602
|
-
version: 2,
|
|
6603
7316
|
runId: projection.runId,
|
|
6604
7317
|
stage: projection.stage,
|
|
6605
7318
|
...(projection.change ? { change: projection.change } : {}),
|
|
@@ -6616,6 +7329,7 @@ export class WorkflowEngine {
|
|
|
6616
7329
|
? {
|
|
6617
7330
|
pause: {
|
|
6618
7331
|
code: pauseCode,
|
|
7332
|
+
...(attemptDiagnostic ? { diagnostic: attemptDiagnostic } : {}),
|
|
6619
7333
|
},
|
|
6620
7334
|
}
|
|
6621
7335
|
: {}),
|
|
@@ -6941,6 +7655,7 @@ export class WorkflowEngine {
|
|
|
6941
7655
|
) {
|
|
6942
7656
|
const runId = this.#lookupRun(command.stage, command.change);
|
|
6943
7657
|
if (!runId) throw new Error("run-not-found");
|
|
7658
|
+
this.#recoverLegacyContextApproval(runId);
|
|
6944
7659
|
const replay = this.#beginOperation(
|
|
6945
7660
|
runId,
|
|
6946
7661
|
command.operationId,
|
|
@@ -7098,6 +7813,7 @@ export class WorkflowEngine {
|
|
|
7098
7813
|
const rebound = this.#worker.rebind({
|
|
7099
7814
|
runId,
|
|
7100
7815
|
taskId: task.task_id,
|
|
7816
|
+
task: structuredClone(this.#taskPlan(task)),
|
|
7101
7817
|
role: "implementation-worker",
|
|
7102
7818
|
routeId: command.routeId,
|
|
7103
7819
|
});
|
|
@@ -7336,7 +8052,19 @@ export class WorkflowEngine {
|
|
|
7336
8052
|
);
|
|
7337
8053
|
case "status": {
|
|
7338
8054
|
const runId = this.#lookupRun(command.stage, command.change);
|
|
7339
|
-
if (!runId)
|
|
8055
|
+
if (!runId) {
|
|
8056
|
+
return {
|
|
8057
|
+
stage: command.stage,
|
|
8058
|
+
change: command.change,
|
|
8059
|
+
state: "not-started",
|
|
8060
|
+
durable: true,
|
|
8061
|
+
completed: false,
|
|
8062
|
+
legalCommands: ["start"],
|
|
8063
|
+
tasks: [],
|
|
8064
|
+
queue: [],
|
|
8065
|
+
};
|
|
8066
|
+
}
|
|
8067
|
+
this.#recoverLegacyContextApproval(runId);
|
|
7340
8068
|
return this.#withAvailableDelivery(this.#statusByRun(runId));
|
|
7341
8069
|
}
|
|
7342
8070
|
case "resume":
|
|
@@ -7401,7 +8129,7 @@ export class WorkflowEngine {
|
|
|
7401
8129
|
outcome.handoffId === existing.handoff_id &&
|
|
7402
8130
|
outcome.runId === existing.owner_run_id &&
|
|
7403
8131
|
outcome.receiptHash === existing.receipt_hash &&
|
|
7404
|
-
outcome.selectorBeforeCutover === "
|
|
8132
|
+
outcome.selectorBeforeCutover === "bootstrap" &&
|
|
7405
8133
|
existing.state === "prepared" &&
|
|
7406
8134
|
outcome.state === "prepared" &&
|
|
7407
8135
|
outcome.selectorCasPending === true;
|
|
@@ -7422,7 +8150,7 @@ export class WorkflowEngine {
|
|
|
7422
8150
|
if (
|
|
7423
8151
|
existing.state !== "prepared" ||
|
|
7424
8152
|
outcome.state !== "prepared" ||
|
|
7425
|
-
outcome.selectorBeforeCutover !== "
|
|
8153
|
+
outcome.selectorBeforeCutover !== "bootstrap" ||
|
|
7426
8154
|
outcome.selectorCasPending !== true ||
|
|
7427
8155
|
acceptanceFacts === undefined ||
|
|
7428
8156
|
workspaceManifestHash === undefined
|
|
@@ -7479,7 +8207,7 @@ export class WorkflowEngine {
|
|
|
7479
8207
|
acceptanceHash: input.acceptanceHash,
|
|
7480
8208
|
...(acceptanceFacts ? { acceptanceFacts } : {}),
|
|
7481
8209
|
...(workspaceManifestHash ? { workspaceManifestHash } : {}),
|
|
7482
|
-
selectorBeforeCutover: "
|
|
8210
|
+
selectorBeforeCutover: "bootstrap",
|
|
7483
8211
|
selectorCasPending: true,
|
|
7484
8212
|
};
|
|
7485
8213
|
const preparedRows = this.#database
|