@abelxiaoxing/cadence 1.6.0 → 1.8.0

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.
Files changed (45) hide show
  1. package/README.md +25 -4
  2. package/config/.env.example +6 -0
  3. package/config/plan-draft.example.json +3 -0
  4. package/config/plan-draft.multiple-tasks.example.json +30 -0
  5. package/package.json +1 -1
  6. package/prompts/abel-design.md +33 -3
  7. package/prompts/abel-implement.md +36 -4
  8. package/src/apply-transaction.ts +28 -8
  9. package/src/build-windows-job.mjs +95 -0
  10. package/src/candidate-workspace.ts +6 -0
  11. package/src/change-verification.ts +764 -67
  12. package/src/contracts.ts +8 -0
  13. package/src/delivery-compiler.ts +452 -9
  14. package/src/design-control.ts +2 -2
  15. package/src/design-diagnostics.ts +6 -4
  16. package/src/durable-contracts.ts +21 -3
  17. package/src/durable-workflow.ts +55 -40
  18. package/src/execution-profile.ts +28 -2
  19. package/src/execution-retention.ts +77 -0
  20. package/src/implement-continuation.ts +559 -0
  21. package/src/implement-graph.ts +114 -0
  22. package/src/implement-plan.ts +2 -0
  23. package/src/index.ts +151 -1
  24. package/src/isolation-backend.ts +2 -1
  25. package/src/operator-cli.mjs +664 -104
  26. package/src/operator-tools.ts +7 -1
  27. package/src/package-state.ts +11 -0
  28. package/src/package-verification.ts +66 -18
  29. package/src/package-workflow.ts +6 -0
  30. package/src/phase-execution.ts +18 -7
  31. package/src/plan-draft-summary.ts +385 -7
  32. package/src/plan-draft.ts +9 -0
  33. package/src/remove-path.ts +32 -0
  34. package/src/subagent-activity.ts +3 -1
  35. package/src/verification-capability.ts +18 -6
  36. package/src/verification-environment-io.ts +123 -14
  37. package/src/verification-environment.ts +50 -3
  38. package/src/windows-job-backend.ts +419 -0
  39. package/src/windows-job.c +149 -0
  40. package/src/workflow-policy.ts +296 -5
  41. package/src/workflow-recovery-policy.ts +2 -0
  42. package/src/workflow-state-machine.ts +314 -80
  43. package/src/workflow-status.ts +251 -59
  44. package/src/workspace-io-entry.mjs +2 -2
  45. package/src/workspace-io-worker.mjs +207 -110
@@ -7,7 +7,7 @@ import {
7
7
  normalizeChangeContract,
8
8
  retainedChangeContract,
9
9
  } from "./change-contract.ts";
10
- import { LIMITS } from "./contracts.ts";
10
+ import { LIMITS, verificationInputPaths } from "./contracts.ts";
11
11
  import {
12
12
  assertControlCommand,
13
13
  type ControlCommand,
@@ -45,6 +45,7 @@ import {
45
45
  type ActiveOperation,
46
46
  assertDelivery,
47
47
  type BootstrapRow,
48
+ baselineRecoveryKey,
48
49
  bootstrapAcceptanceHash,
49
50
  CHANGE_NAME,
50
51
  cancellableRead,
@@ -62,6 +63,7 @@ import {
62
63
  isRecord,
63
64
  isWorkflowApprovalCode,
64
65
  normalizeBootstrapAcceptanceFacts,
66
+ normalizeVerificationPrerequisite,
65
67
  normalizeWorkflowContextRequest,
66
68
  normalizeWorkflowVerificationStatus,
67
69
  parseJsonRecord,
@@ -1153,12 +1155,7 @@ export class WorkflowEngine {
1153
1155
  serializedPlan,
1154
1156
  );
1155
1157
 
1156
- if (
1157
- database
1158
- .prepare("SELECT 1 FROM workflow_work_budget WHERE run_id = ?")
1159
- .get(runId)
1160
- )
1161
- this.#growWorkBudget(database, runId, delivery.plan);
1158
+ this.#growWorkBudget(database, runId, delivery.plan);
1162
1159
  const admitted = new Set<string>();
1163
1160
  delivery.plan.tasks.forEach((task, taskOrder) => {
1164
1161
  admitted.add(task.taskId);
@@ -1595,8 +1592,12 @@ export class WorkflowEngine {
1595
1592
  : undefined;
1596
1593
  const priorDiagnostic = this.#safeAttemptDiagnostic(runId, row);
1597
1594
  const priorRecovery = priorDiagnostic?.recovery;
1595
+ const preparationKey = baselineRecoveryKey(task, phase);
1598
1596
  const activeRecovery =
1599
- priorRecovery?.key === recoveryKey(task, phase, row, current)
1597
+ priorRecovery &&
1598
+ [recoveryKey(task, phase, row, current), preparationKey].includes(
1599
+ priorRecovery.key,
1600
+ )
1600
1601
  ? priorRecovery
1601
1602
  : undefined;
1602
1603
  const maxRecoveryAttempts = Math.min(
@@ -1619,9 +1620,10 @@ export class WorkflowEngine {
1619
1620
  const granted = this.#hasRecoveryGrant(
1620
1621
  runId,
1621
1622
  operationId,
1622
- recoveryKey(task, phase, row, current),
1623
+ activeRecovery?.key ?? recoveryKey(task, phase, row, current),
1623
1624
  );
1624
1625
  const verificationOnly =
1626
+ activeRecovery?.key !== preparationKey &&
1625
1627
  recoveryExhausted(activeRecovery, maxRecoveryAttempts, granted) &&
1626
1628
  (await this.#worker.hasPendingVerification?.({
1627
1629
  runId,
@@ -1658,7 +1660,7 @@ export class WorkflowEngine {
1658
1660
  );
1659
1661
  return;
1660
1662
  }
1661
- if (activeRecovery)
1663
+ if (activeRecovery && activeRecovery.key !== preparationKey)
1662
1664
  artifactAttempts = Math.max(artifactAttempts, activeRecovery.failures);
1663
1665
  const repair =
1664
1666
  verificationStatus?.scope === "change-task-affected" &&
@@ -1684,6 +1686,187 @@ export class WorkflowEngine {
1684
1686
  : {}),
1685
1687
  }
1686
1688
  : undefined;
1689
+ if (this.#worker.prepareTask) {
1690
+ // Reserve this selected task's conflicts while asynchronous baseline
1691
+ // preparation runs, without reserving a Worker attempt.
1692
+ this.#setTask(
1693
+ runId,
1694
+ row.task_id,
1695
+ { state: "phase-ready", phase, queuePosition: null },
1696
+ lease,
1697
+ );
1698
+ if (granted && activeRecovery?.key === preparationKey) {
1699
+ this.#leasedTransaction(lease, () => {
1700
+ const consumed = this.#database
1701
+ .prepare(
1702
+ "UPDATE workflow_recovery_grants SET consumed = 1 WHERE run_id = ? AND operation_id = ? AND incident_key = ? AND consumed = 0",
1703
+ )
1704
+ .run(runId, operationId, preparationKey);
1705
+ if (consumed.changes !== 1)
1706
+ throw new Error("recovery-request-stale");
1707
+ this.#database
1708
+ .prepare(`INSERT INTO workflow_recovery_events(run_id, sequence, incident_key, kind)
1709
+ SELECT ?, COALESCE(MAX(sequence), 0) + 1, ?, 'launch' FROM workflow_recovery_events WHERE run_id = ?`)
1710
+ .run(runId, preparationKey, runId);
1711
+ });
1712
+ }
1713
+ const prepared = await this.#worker.prepareTask({
1714
+ runId,
1715
+ operationId,
1716
+ deliveryRevision: row.delivery_revision,
1717
+ taskId: row.task_id,
1718
+ phase,
1719
+ task: structuredClone(task),
1720
+ plan: structuredClone(plan),
1721
+ ...(current.baseline_workspace_revision
1722
+ ? { baselineRevisionId: current.baseline_workspace_revision }
1723
+ : {}),
1724
+ ...(current.current_workspace_revision
1725
+ ? { currentWorkspaceRevisionId: current.current_workspace_revision }
1726
+ : {}),
1727
+ signal,
1728
+ });
1729
+ this.#runStore.assertLease(lease);
1730
+ if (prepared.kind === "phase-committed")
1731
+ throw new Error("workflow-preparation-outcome-invalid");
1732
+ this.#recordWorkspaceFacts(
1733
+ runId,
1734
+ {
1735
+ ...prepared,
1736
+ kind: "paused",
1737
+ code: "task-prepared",
1738
+ // Preparation has no merge authority over a sibling's newer commit.
1739
+ ...(prepared.currentWorkspaceRevisionId
1740
+ ? {
1741
+ currentWorkspaceRevisionId:
1742
+ this.#engineRun(runId).current_workspace_revision ??
1743
+ prepared.currentWorkspaceRevisionId,
1744
+ }
1745
+ : {}),
1746
+ },
1747
+ lease,
1748
+ );
1749
+ // Preparation may finish successfully while close is settling it.
1750
+ // Re-enter the cancellation guard before granting any Worker work.
1751
+ if (signal.aborted) continue;
1752
+ if (prepared.kind !== "prepared") {
1753
+ const prerequisite =
1754
+ prepared.kind !== "operation-cancelled"
1755
+ ? normalizeVerificationPrerequisite(prepared.prerequisite)
1756
+ : undefined;
1757
+ if (prerequisite) {
1758
+ const verification =
1759
+ prerequisite.scope === "baseline-full-suite"
1760
+ ? plan.verification.baseline?.fullSuite
1761
+ : (task.baselineVerification ?? task.affectedVerification);
1762
+ if (
1763
+ !verification ||
1764
+ verification.id !== prerequisite.verificationId ||
1765
+ (prerequisite.scope === "baseline-task-affected" &&
1766
+ prerequisite.taskId !== row.task_id) ||
1767
+ (prerequisite.input &&
1768
+ !verificationInputPaths(verification).includes(
1769
+ prerequisite.input.path,
1770
+ )) ||
1771
+ (prerequisite.producer &&
1772
+ !plan.outputs.some(
1773
+ (output) =>
1774
+ output.path === prerequisite.input?.path &&
1775
+ output.producer.taskId === prerequisite.producer?.taskId &&
1776
+ output.producer.phase === prerequisite.producer?.phase,
1777
+ ))
1778
+ )
1779
+ throw new Error("workflow-prerequisite-evidence-invalid");
1780
+ }
1781
+ if (prerequisite && prepared.kind === "paused") {
1782
+ const identity = hash(
1783
+ canonicalJson({
1784
+ ...prerequisite,
1785
+ verificationId: undefined,
1786
+ taskId: undefined,
1787
+ producer: prerequisite.producer
1788
+ ? { phase: prerequisite.producer.phase }
1789
+ : undefined,
1790
+ }),
1791
+ );
1792
+ const priorPreparation = this.#recovery(runId, preparationKey);
1793
+ if (priorPreparation?.feedback.prerequisiteIdentity !== identity) {
1794
+ const failures = (priorPreparation?.failures ?? 0) + 1;
1795
+ this.#recordRecovery(
1796
+ runId,
1797
+ {
1798
+ key: preparationKey,
1799
+ failures,
1800
+ feedback: {
1801
+ code: prepared.code,
1802
+ attempt: failures + 1,
1803
+ maxAttempts: maxRecoveryAttempts,
1804
+ strategy: "repair-verification",
1805
+ prerequisiteIdentity: identity,
1806
+ },
1807
+ },
1808
+ lease,
1809
+ );
1810
+ }
1811
+ }
1812
+ if (prepared.kind !== "operation-cancelled" && prepared.verification)
1813
+ this.#setVerificationStatus(runId, prepared.verification, lease);
1814
+ this.#setTask(
1815
+ runId,
1816
+ row.task_id,
1817
+ {
1818
+ state:
1819
+ prepared.kind === "approval-needed"
1820
+ ? "approval-needed"
1821
+ : "paused",
1822
+ phase,
1823
+ pauseCode:
1824
+ prepared.kind === "operation-cancelled"
1825
+ ? "operation-cancelled"
1826
+ : prepared.code,
1827
+ attemptDiagnostic: {
1828
+ ...priorDiagnostic,
1829
+ prerequisite,
1830
+ },
1831
+ queuePosition: null,
1832
+ },
1833
+ lease,
1834
+ );
1835
+ return;
1836
+ }
1837
+ if (priorDiagnostic) delete priorDiagnostic.prerequisite;
1838
+ if (activeRecovery?.key === preparationKey) {
1839
+ // A successful original-baseline observation resolves this incident.
1840
+ // Keep its events and immutable failure evidence, then re-enter the
1841
+ // ordinary phase recovery checks before reserving Worker work.
1842
+ this.#leasedTransaction(lease, () => {
1843
+ this.#database
1844
+ .prepare(`INSERT INTO workflow_recovery_events(run_id, sequence, incident_key, kind)
1845
+ SELECT ?, COALESCE(MAX(sequence), 0) + 1, ?, 'resolved' FROM workflow_recovery_events WHERE run_id = ?`)
1846
+ .run(runId, preparationKey, runId);
1847
+ this.#database
1848
+ .prepare(
1849
+ "DELETE FROM workflow_recovery_incidents WHERE run_id = ? AND incident_key = ?",
1850
+ )
1851
+ .run(runId, preparationKey);
1852
+ });
1853
+ if (priorDiagnostic) delete priorDiagnostic.recovery;
1854
+ const retainedDiagnostic =
1855
+ priorDiagnostic && Object.keys(priorDiagnostic).length
1856
+ ? priorDiagnostic
1857
+ : null;
1858
+ this.#setTask(
1859
+ runId,
1860
+ row.task_id,
1861
+ { attemptDiagnostic: retainedDiagnostic },
1862
+ lease,
1863
+ );
1864
+ row.attempt_diagnostic_json = retainedDiagnostic
1865
+ ? JSON.stringify(retainedDiagnostic)
1866
+ : null;
1867
+ continue;
1868
+ }
1869
+ }
1687
1870
  this.#setTask(
1688
1871
  runId,
1689
1872
  row.task_id,
@@ -1857,6 +2040,8 @@ export class WorkflowEngine {
1857
2040
  ? { action: "rebind-or-revise-delivery" as const }
1858
2041
  : {}),
1859
2042
  };
2043
+ // Only parent preparation can contribute original-baseline authority.
2044
+ delete attemptDiagnostic.prerequisite;
1860
2045
  this.#setTask(runId, row.task_id, { attemptDiagnostic }, lease);
1861
2046
  row.attempt_diagnostic_json = JSON.stringify(attemptDiagnostic);
1862
2047
  }
@@ -2741,73 +2926,90 @@ export class WorkflowEngine {
2741
2926
  this.#setVerificationStatus(runId, null, lease);
2742
2927
  }
2743
2928
  const attempted = new Set<string>();
2744
- let madeProgress = true;
2745
- while (madeProgress && !signal.aborted) {
2746
- madeProgress = false;
2747
- const rows = this.#tasks(runId);
2748
- const selected = selectRunnableTasks({
2749
- rows,
2750
- attempted,
2751
- activeTasks: this.#activeTasks,
2752
- capacity: LIMITS.maxActiveChildSessions,
2753
- nextQueuePosition: this.#engineRun(runId).next_queue_position,
2754
- });
2755
- const runnable = selected.runnable.map((taskId) => {
2756
- const row = rows.find((candidate) => candidate.task_id === taskId);
2757
- if (!row) throw new Error("scheduler-task-unavailable");
2758
- attempted.add(taskId);
2759
- return { row, before: `${row.state}:${row.phase}` };
2760
- });
2761
- for (const update of selected.updates) {
2762
- const row = rows.find(
2763
- (candidate) => candidate.task_id === update.taskId,
2764
- );
2765
- if (!row) throw new Error("scheduler-task-unavailable");
2766
- if (update.kind === "pending")
2767
- this.#setTask(
2768
- runId,
2769
- row.task_id,
2770
- { state: "pending", pauseCode: null, queuePosition: null },
2771
- lease,
2929
+ // Keep every launched operation owned until it settles, including failures.
2930
+ // A fulfilled envelope avoids unhandled rejections while another task wins
2931
+ // the race; the finally barrier also protects cancellation and storage close.
2932
+ type Settlement =
2933
+ | { taskId: string; status: "fulfilled" }
2934
+ | { taskId: string; status: "rejected"; reason: unknown };
2935
+ const pending = new Map<string, Promise<Settlement>>();
2936
+ let firstFailure: { reason: unknown } | undefined;
2937
+ try {
2938
+ while (!signal.aborted) {
2939
+ if (firstFailure) throw firstFailure.reason;
2940
+ const rows = this.#tasks(runId);
2941
+ const selected = selectRunnableTasks({
2942
+ rows,
2943
+ attempted,
2944
+ activeTasks: this.#activeTasks,
2945
+ capacity: LIMITS.maxActiveChildSessions,
2946
+ nextQueuePosition: this.#engineRun(runId).next_queue_position,
2947
+ });
2948
+ const runnable = selected.runnable.map((taskId) => {
2949
+ const row = rows.find((candidate) => candidate.task_id === taskId);
2950
+ if (!row) throw new Error("scheduler-task-unavailable");
2951
+ attempted.add(taskId);
2952
+ return row;
2953
+ });
2954
+ for (const update of selected.updates) {
2955
+ const row = rows.find(
2956
+ (candidate) => candidate.task_id === update.taskId,
2772
2957
  );
2773
- else this.#queueTask(runId, row, lease, update.reason);
2774
- }
2775
- const capacityBlocked = selected.capacityBlocked;
2776
- if (runnable.length === 0 && capacityBlocked) {
2777
- await this.#waitForCapacity(signal);
2778
- madeProgress = true;
2779
- continue;
2780
- }
2781
- this.#activeTasks += runnable.length;
2782
- const settled = await Promise.allSettled(
2783
- runnable.map(({ row }) =>
2784
- this.#runTask(
2958
+ if (!row) throw new Error("scheduler-task-unavailable");
2959
+ if (update.kind === "pending")
2960
+ this.#setTask(
2961
+ runId,
2962
+ row.task_id,
2963
+ { state: "pending", pauseCode: null, queuePosition: null },
2964
+ lease,
2965
+ );
2966
+ else this.#queueTask(runId, row, lease, update.reason);
2967
+ }
2968
+ this.#activeTasks += runnable.length;
2969
+ for (const row of runnable) {
2970
+ const operation = this.#runTask(
2785
2971
  runId,
2786
2972
  row,
2787
2973
  operationId,
2788
2974
  signal,
2789
2975
  lease,
2790
2976
  onActivity,
2791
- ).finally(() => {
2792
- this.#activeTasks--;
2793
- for (const wake of this.#capacityWaiters) wake();
2794
- }),
2795
- ),
2796
- );
2797
- const rejected = settled.find(
2798
- (outcome): outcome is PromiseRejectedResult =>
2799
- outcome.status === "rejected",
2800
- );
2801
- if (rejected) throw rejected.reason;
2802
- for (const { row, before } of runnable) {
2803
- const after = this.#tasks(runId).find(
2804
- (candidate) => candidate.task_id === row.task_id,
2805
- );
2806
- if (after && `${after.state}:${after.phase}` !== before) {
2807
- madeProgress = true;
2977
+ )
2978
+ .then<Settlement, Settlement>(
2979
+ () => ({ taskId: row.task_id, status: "fulfilled" }),
2980
+ (reason: unknown) => {
2981
+ firstFailure ??= { reason };
2982
+ return { taskId: row.task_id, status: "rejected", reason };
2983
+ },
2984
+ )
2985
+ .finally(() => {
2986
+ this.#activeTasks--;
2987
+ for (const wake of this.#capacityWaiters) wake();
2988
+ });
2989
+ pending.set(row.task_id, operation);
2990
+ }
2991
+ if (pending.size > 0) {
2992
+ const settled = await Promise.race([
2993
+ ...pending.values(),
2994
+ ...(selected.capacityBlocked
2995
+ ? [this.#waitForCapacity(signal).then(() => null)]
2996
+ : []),
2997
+ ]);
2998
+ if (settled === null) continue;
2999
+ pending.delete(settled.taskId);
3000
+ if (settled.status === "rejected") throw settled.reason;
3001
+ continue;
3002
+ }
3003
+ if (selected.capacityBlocked) {
3004
+ await this.#waitForCapacity(signal);
3005
+ continue;
2808
3006
  }
3007
+ break;
2809
3008
  }
3009
+ } finally {
3010
+ await Promise.all(pending.values());
2810
3011
  }
3012
+ if (firstFailure) throw firstFailure.reason;
2811
3013
 
2812
3014
  if (signal.aborted) {
2813
3015
  const current = this.#runStore.status(runId);
@@ -2898,8 +3100,11 @@ export class WorkflowEngine {
2898
3100
 
2899
3101
  #recoveryConditions(runId: string, key: string) {
2900
3102
  const run = this.#engineRun(runId);
2901
- const task = this.#tasks(runId).find(
2902
- (row) => recoveryKey(this.#taskPlan(row), row.phase, row, run) === key,
3103
+ const task = this.#tasks(runId).find((row) =>
3104
+ [
3105
+ recoveryKey(this.#taskPlan(row), row.phase, row, run),
3106
+ baselineRecoveryKey(this.#taskPlan(row), row.phase),
3107
+ ].includes(key),
2903
3108
  );
2904
3109
  return {
2905
3110
  route: task?.route_fingerprint ?? run.route_fingerprint,
@@ -2939,12 +3144,15 @@ export class WorkflowEngine {
2939
3144
  const current = this.#tasks(runId).some(
2940
3145
  (row) =>
2941
3146
  ["paused", "retryable"].includes(row.state) &&
2942
- recoveryKey(
2943
- this.#taskPlan(row),
2944
- row.phase,
2945
- row,
2946
- this.#engineRun(runId),
2947
- ) === request.incidentKey,
3147
+ [
3148
+ recoveryKey(
3149
+ this.#taskPlan(row),
3150
+ row.phase,
3151
+ row,
3152
+ this.#engineRun(runId),
3153
+ ),
3154
+ baselineRecoveryKey(this.#taskPlan(row), row.phase),
3155
+ ].includes(request.incidentKey),
2948
3156
  );
2949
3157
  if (
2950
3158
  !current ||
@@ -3080,10 +3288,20 @@ export class WorkflowEngine {
3080
3288
  ? (JSON.parse(row.attempt_diagnostic_json) as SafeAttemptDiagnostic)
3081
3289
  : {};
3082
3290
  delete diagnostic.recovery;
3083
- const recovery = this.#recovery(
3084
- runId,
3085
- recoveryKey(this.#taskPlan(row), row.phase, row, this.#engineRun(runId)),
3086
- );
3291
+ const recovery =
3292
+ this.#recovery(
3293
+ runId,
3294
+ baselineRecoveryKey(this.#taskPlan(row), row.phase),
3295
+ ) ??
3296
+ this.#recovery(
3297
+ runId,
3298
+ recoveryKey(
3299
+ this.#taskPlan(row),
3300
+ row.phase,
3301
+ row,
3302
+ this.#engineRun(runId),
3303
+ ),
3304
+ );
3087
3305
  if (recovery) diagnostic.recovery = recovery;
3088
3306
  return Object.keys(diagnostic).length ? diagnostic : undefined;
3089
3307
  }
@@ -3794,8 +4012,24 @@ export class WorkflowEngine {
3794
4012
  lease,
3795
4013
  );
3796
4014
  }
3797
- outcome = this.#statusByRun(runId);
3798
4015
  await activeSettlement;
4016
+ for (const task of this.#tasks(runId)) {
4017
+ if (
4018
+ ["phase-ready", "phase-running", "validating"].includes(task.state)
4019
+ ) {
4020
+ this.#setTask(
4021
+ runId,
4022
+ task.task_id,
4023
+ {
4024
+ state: "paused",
4025
+ pauseCode: "operation-cancelled",
4026
+ queuePosition: null,
4027
+ },
4028
+ lease,
4029
+ );
4030
+ }
4031
+ }
4032
+ outcome = this.#statusByRun(runId);
3799
4033
  }
3800
4034
  outcome = {
3801
4035
  ...outcome,