@abelxiaoxing/cadence 1.1.0 → 1.2.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.
@@ -16,6 +16,7 @@ import {
16
16
  } from "./apply-transaction.ts";
17
17
  import { ArtifactStore } from "./artifact-store.ts";
18
18
  import {
19
+ type ApprovalBoundaryCode,
19
20
  diffWritePaths,
20
21
  isValidRelativePath,
21
22
  type StructuredVerificationContract,
@@ -26,6 +27,7 @@ import {
26
27
  type ControlCommand,
27
28
  type ControlStage,
28
29
  } from "./control-contracts.ts";
30
+ import type { GateApprovalProof } from "./delivery-compiler.ts";
29
31
  import {
30
32
  DeliveryValidationError,
31
33
  IMPLEMENT_PLAN_SCHEMA_VERSION,
@@ -172,6 +174,88 @@ interface WorkflowContextRequest {
172
174
  refs: string[];
173
175
  }
174
176
 
177
+ export const APPROVAL_AUTHORITY_CATEGORIES = [
178
+ "observable-behavior",
179
+ "architecture-policy",
180
+ "dependency",
181
+ "path-boundary",
182
+ "conflict-resource",
183
+ "verification-contract",
184
+ "agents-contract",
185
+ "irreversible-scope",
186
+ ] as const;
187
+
188
+ export type ApprovalAuthorityCategory =
189
+ (typeof APPROVAL_AUTHORITY_CATEGORIES)[number];
190
+
191
+ interface ApprovalRequirement {
192
+ category: ApprovalAuthorityCategory;
193
+ requiredGates: Array<"gate-a" | "gate-b">;
194
+ refs: string[];
195
+ }
196
+
197
+ const INTERNAL_APPROVAL_CODES = [
198
+ "boundary-review-needed",
199
+ "repair-boundary-expansion",
200
+ "task-write-set-empty",
201
+ ] as const;
202
+
203
+ type InternalApprovalCode = (typeof INTERNAL_APPROVAL_CODES)[number];
204
+ export type WorkflowApprovalCode = ApprovalBoundaryCode | InternalApprovalCode;
205
+
206
+ const APPROVAL_REQUIREMENT_BY_CODE = {
207
+ "agents-contract-insufficient": {
208
+ category: "agents-contract",
209
+ requiredGates: ["gate-b"],
210
+ },
211
+ "architecture-contract-insufficient": {
212
+ category: "architecture-policy",
213
+ requiredGates: ["gate-b"],
214
+ },
215
+ "behavior-contract-insufficient": {
216
+ category: "observable-behavior",
217
+ requiredGates: ["gate-a", "gate-b"],
218
+ },
219
+ "boundary-review-needed": {
220
+ category: "path-boundary",
221
+ requiredGates: ["gate-b"],
222
+ },
223
+ "conflict-resource-authority-insufficient": {
224
+ category: "conflict-resource",
225
+ requiredGates: ["gate-b"],
226
+ },
227
+ "irreversible-scope-insufficient": {
228
+ category: "irreversible-scope",
229
+ requiredGates: ["gate-a", "gate-b"],
230
+ },
231
+ "repair-boundary-expansion": {
232
+ category: "path-boundary",
233
+ requiredGates: ["gate-b"],
234
+ },
235
+ "task-scope-insufficient": {
236
+ category: "path-boundary",
237
+ requiredGates: ["gate-b"],
238
+ },
239
+ "task-write-set-empty": {
240
+ category: "path-boundary",
241
+ requiredGates: ["gate-b"],
242
+ },
243
+ "unapproved-dependency-change": {
244
+ category: "dependency",
245
+ requiredGates: ["gate-b"],
246
+ },
247
+ "verification-contract-insufficient": {
248
+ category: "verification-contract",
249
+ requiredGates: ["gate-b"],
250
+ },
251
+ } as const satisfies Record<
252
+ WorkflowApprovalCode,
253
+ {
254
+ category: ApprovalAuthorityCategory;
255
+ requiredGates: readonly ("gate-a" | "gate-b")[];
256
+ }
257
+ >;
258
+
175
259
  type WorkflowRetryPolicy = "artifact" | "stale" | "verification" | "checkpoint";
176
260
 
177
261
  export type WorkflowAttemptOutcome = (
@@ -261,6 +345,15 @@ export interface WorkflowDelivery {
261
345
  revision: number;
262
346
  receiptHash: string;
263
347
  plan: ImplementPlan;
348
+ approvalProofs?: {
349
+ gateA: GateApprovalProof;
350
+ gateB: GateApprovalProof;
351
+ };
352
+ }
353
+
354
+ export interface WorkflowAvailableDelivery {
355
+ deliveryRevision: number;
356
+ receiptHash: string;
264
357
  }
265
358
 
266
359
  export interface WorkflowDeliverySource {
@@ -270,6 +363,10 @@ export interface WorkflowDeliverySource {
270
363
  deliveryRevision?: number;
271
364
  receiptHash?: string;
272
365
  }): Promise<WorkflowDelivery>;
366
+ discoverLatest?(input: {
367
+ stage: ControlStage;
368
+ change: string;
369
+ }): Promise<WorkflowAvailableDelivery | undefined>;
273
370
  }
274
371
 
275
372
  export interface WorkflowChangeVerifier {
@@ -489,6 +586,27 @@ function normalizeWorkflowContextRequest(
489
586
  };
490
587
  }
491
588
 
589
+ function approvalRequirement(
590
+ code: string,
591
+ contextRequest?: WorkflowContextRequest,
592
+ ): ApprovalRequirement {
593
+ const requirement = Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code)
594
+ ? APPROVAL_REQUIREMENT_BY_CODE[
595
+ code as keyof typeof APPROVAL_REQUIREMENT_BY_CODE
596
+ ]
597
+ : undefined;
598
+ if (!requirement) throw new Error("approval-code-invalid");
599
+ return {
600
+ category: requirement.category,
601
+ requiredGates: [...requirement.requiredGates],
602
+ refs: contextRequest ? [...contextRequest.refs] : [],
603
+ };
604
+ }
605
+
606
+ function isWorkflowApprovalCode(code: string): code is WorkflowApprovalCode {
607
+ return Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code);
608
+ }
609
+
492
610
  function hash(...values: string[]): string {
493
611
  const digest = createHash("sha256");
494
612
  for (const value of values) {
@@ -846,6 +964,23 @@ function assertDelivery(
846
964
  throw new Error("delivery-invalid");
847
965
  }
848
966
  assertPlan(value.plan);
967
+ if (value.approvalProofs !== undefined) {
968
+ if (!isRecord(value.approvalProofs)) throw new Error("delivery-invalid");
969
+ for (const gate of ["gateA", "gateB"] as const) {
970
+ const proof = value.approvalProofs[gate];
971
+ if (
972
+ !isRecord(proof) ||
973
+ !Number.isSafeInteger(proof.revision) ||
974
+ (proof.revision as number) < 1 ||
975
+ typeof proof.contractHash !== "string" ||
976
+ !SHA256.test(proof.contractHash) ||
977
+ typeof proof.recordHash !== "string" ||
978
+ !SHA256.test(proof.recordHash)
979
+ ) {
980
+ throw new Error("delivery-invalid");
981
+ }
982
+ }
983
+ }
849
984
  if (stage === "abel-implement" && value.gate !== "gate-b") {
850
985
  throw new Error("delivery-gate-b-required");
851
986
  }
@@ -2698,13 +2833,14 @@ class DurableWorkflowComposition
2698
2833
  const phase = request.task.phases[request.phase];
2699
2834
  if (!phase) throw new Error("workflow-task-phase-invalid");
2700
2835
  const allPhases = Object.values(request.task.phases);
2836
+ const approvedWritePaths = [
2837
+ ...new Set(allPhases.flatMap((boundary) => boundary.write)),
2838
+ ].sort();
2839
+ const approvedDeletePaths = [
2840
+ ...new Set(allPhases.flatMap((boundary) => boundary.delete)),
2841
+ ].sort();
2701
2842
  const approvedPaths = [
2702
- ...new Set(
2703
- allPhases.flatMap((boundary) => [
2704
- ...boundary.write,
2705
- ...boundary.delete,
2706
- ]),
2707
- ),
2843
+ ...new Set([...approvedWritePaths, ...approvedDeletePaths]),
2708
2844
  ].sort();
2709
2845
  const boundPaths = [
2710
2846
  ...new Set(
@@ -2780,7 +2916,13 @@ class DurableWorkflowComposition
2780
2916
  task: structuredClone(request.task),
2781
2917
  workspaceRoot: proposalRoot,
2782
2918
  ledgerProjection,
2783
- candidateArtifact: { ledger, identity },
2919
+ candidateArtifact: {
2920
+ ledger,
2921
+ identity,
2922
+ workspaceRoot: proposalRoot,
2923
+ writePaths: approvedWritePaths,
2924
+ deletePaths: approvedDeletePaths,
2925
+ },
2784
2926
  route: attempt.route,
2785
2927
  repair,
2786
2928
  signal: attempt.signal,
@@ -3549,7 +3691,13 @@ class DurableWorkflowComposition
3549
3691
  task: structuredClone(input.task),
3550
3692
  workspaceRoot: proposalRoot,
3551
3693
  ledgerProjection,
3552
- candidateArtifact: { ledger, identity },
3694
+ candidateArtifact: {
3695
+ ledger,
3696
+ identity,
3697
+ workspaceRoot: proposalRoot,
3698
+ writePaths: phase.write,
3699
+ deletePaths: phase.delete,
3700
+ },
3553
3701
  route: attempt.route,
3554
3702
  signal: attempt.signal,
3555
3703
  onHeaders: attempt.onHeaders,
@@ -4676,21 +4824,6 @@ export class WorkflowEngine {
4676
4824
  )?.run_id;
4677
4825
  }
4678
4826
 
4679
- #lookupProvisionalRun(provisionalKey: string): string | undefined {
4680
- return (
4681
- this.#database
4682
- .prepare(
4683
- `SELECT run_id FROM runs
4684
- WHERE root_hash = ? AND stage = 'abel-design'
4685
- AND lookup_key = ?`,
4686
- )
4687
- .get(
4688
- this.#stateRoot.consumerRootHash,
4689
- `provisional:${provisionalKey}`,
4690
- ) as { run_id: string } | undefined
4691
- )?.run_id;
4692
- }
4693
-
4694
4827
  #ensureEngineRun(runId: string): void {
4695
4828
  this.#database
4696
4829
  .prepare(
@@ -5187,35 +5320,69 @@ export class WorkflowEngine {
5187
5320
  };
5188
5321
  }
5189
5322
  }
5190
- this.#runStore.bindDeliveryAtomically(
5191
- {
5192
- runId,
5193
- gate: delivery.gate,
5194
- revision: delivery.revision,
5195
- receiptHash: delivery.receiptHash,
5196
- operationId: `bind-${hash(runId, String(delivery.revision), delivery.receiptHash).slice(0, 40)}`,
5197
- lease,
5198
- },
5199
- (database) => {
5200
- const engineLease = database
5201
- .prepare(
5202
- `SELECT state, lease_expires_at
5323
+ const deliveryBindings = delivery.approvalProofs
5324
+ ? ([
5325
+ {
5326
+ runId,
5327
+ gate: "gate-a" as const,
5328
+ revision: delivery.revision,
5329
+ receiptHash: delivery.receiptHash,
5330
+ approvalProof: structuredClone(delivery.approvalProofs.gateA),
5331
+ operationId: `bind-a-${hash(
5332
+ runId,
5333
+ String(delivery.revision),
5334
+ delivery.approvalProofs.gateA.recordHash,
5335
+ ).slice(0, 40)}`,
5336
+ lease,
5337
+ },
5338
+ {
5339
+ runId,
5340
+ gate: "gate-b" as const,
5341
+ revision: delivery.revision,
5342
+ receiptHash: delivery.receiptHash,
5343
+ approvalProof: structuredClone(delivery.approvalProofs.gateB),
5344
+ operationId: `bind-b-${hash(
5345
+ runId,
5346
+ String(delivery.revision),
5347
+ delivery.approvalProofs.gateB.recordHash,
5348
+ ).slice(0, 40)}`,
5349
+ lease,
5350
+ },
5351
+ ] as const)
5352
+ : ([
5353
+ {
5354
+ runId,
5355
+ gate: delivery.gate,
5356
+ revision: delivery.revision,
5357
+ receiptHash: delivery.receiptHash,
5358
+ operationId: `bind-${hash(
5359
+ runId,
5360
+ String(delivery.revision),
5361
+ delivery.receiptHash,
5362
+ ).slice(0, 40)}`,
5363
+ lease,
5364
+ },
5365
+ ] as const);
5366
+ this.#runStore.bindDeliveriesAtomically(deliveryBindings, (database) => {
5367
+ const engineLease = database
5368
+ .prepare(
5369
+ `SELECT state, lease_expires_at
5203
5370
  FROM workflow_engine_operations
5204
5371
  WHERE run_id = ? AND lease_token = ?`,
5205
- )
5206
- .get(runId, lease.token) as
5207
- | { state: string; lease_expires_at: number | null }
5208
- | undefined;
5209
- if (
5210
- engineLease?.state !== "running" ||
5211
- engineLease.lease_expires_at === null ||
5212
- engineLease.lease_expires_at < this.#now()
5213
- ) {
5214
- throw new Error("lease-fenced");
5215
- }
5216
- database
5217
- .prepare(
5218
- `INSERT INTO workflow_engine_runs(
5372
+ )
5373
+ .get(runId, lease.token) as
5374
+ | { state: string; lease_expires_at: number | null }
5375
+ | undefined;
5376
+ if (
5377
+ engineLease?.state !== "running" ||
5378
+ engineLease.lease_expires_at === null ||
5379
+ engineLease.lease_expires_at < this.#now()
5380
+ ) {
5381
+ throw new Error("lease-fenced");
5382
+ }
5383
+ database
5384
+ .prepare(
5385
+ `INSERT INTO workflow_engine_runs(
5219
5386
  run_id, current_revision, delivery_diagnostics_json,
5220
5387
  next_queue_position
5221
5388
  ) VALUES (?, ?, NULL, 1)
@@ -5223,85 +5390,84 @@ export class WorkflowEngine {
5223
5390
  current_revision = excluded.current_revision,
5224
5391
  verification_json = NULL,
5225
5392
  delivery_diagnostics_json = NULL`,
5226
- )
5227
- .run(runId, delivery.revision);
5228
- if (revalidatedWorkspace) {
5229
- database
5230
- .prepare(
5231
- `UPDATE workflow_engine_runs
5393
+ )
5394
+ .run(runId, delivery.revision);
5395
+ if (revalidatedWorkspace) {
5396
+ database
5397
+ .prepare(
5398
+ `UPDATE workflow_engine_runs
5232
5399
  SET baseline_workspace_revision = ?,
5233
5400
  current_workspace_revision = ?, cleanup_state = 'retained'
5234
5401
  WHERE run_id = ?`,
5235
- )
5236
- .run(
5237
- revalidatedWorkspace.baselineRevisionId,
5238
- revalidatedWorkspace.currentWorkspaceRevisionId,
5239
- runId,
5240
- );
5241
- }
5242
- database
5243
- .prepare(
5244
- `INSERT INTO workflow_engine_deliveries(
5245
- run_id, revision, gate, receipt_hash, plan_json
5246
- ) VALUES (?, ?, ?, ?, ?)
5247
- ON CONFLICT(run_id, revision) DO NOTHING`,
5248
5402
  )
5249
5403
  .run(
5404
+ revalidatedWorkspace.baselineRevisionId,
5405
+ revalidatedWorkspace.currentWorkspaceRevisionId,
5250
5406
  runId,
5251
- delivery.revision,
5252
- delivery.gate,
5253
- delivery.receiptHash,
5254
- serializedPlan,
5255
5407
  );
5408
+ }
5409
+ database
5410
+ .prepare(
5411
+ `INSERT INTO workflow_engine_deliveries(
5412
+ run_id, revision, gate, receipt_hash, plan_json
5413
+ ) VALUES (?, ?, ?, ?, ?)
5414
+ ON CONFLICT(run_id, revision) DO NOTHING`,
5415
+ )
5416
+ .run(
5417
+ runId,
5418
+ delivery.revision,
5419
+ delivery.gate,
5420
+ delivery.receiptHash,
5421
+ serializedPlan,
5422
+ );
5256
5423
 
5257
- const admitted = new Set<string>();
5258
- delivery.plan.tasks.forEach((task, taskOrder) => {
5259
- admitted.add(task.taskId);
5260
- const taskJson = JSON.stringify(task);
5261
- const prior = priorById.get(task.taskId);
5262
- if (!prior) {
5263
- database
5264
- .prepare(
5265
- `INSERT INTO workflow_engine_tasks(
5424
+ const admitted = new Set<string>();
5425
+ delivery.plan.tasks.forEach((task, taskOrder) => {
5426
+ admitted.add(task.taskId);
5427
+ const taskJson = JSON.stringify(task);
5428
+ const prior = priorById.get(task.taskId);
5429
+ if (!prior) {
5430
+ database
5431
+ .prepare(
5432
+ `INSERT INTO workflow_engine_tasks(
5266
5433
  run_id, task_id, task_order, delivery_revision, plan_json,
5267
5434
  state, phase
5268
5435
  ) VALUES (?, ?, ?, ?, ?, 'pending', 'red')`,
5269
- )
5270
- .run(runId, task.taskId, taskOrder, delivery.revision, taskJson);
5271
- return;
5272
- }
5273
- if (compatible.has(task.taskId) && !invalidated.has(task.taskId)) {
5274
- database
5275
- .prepare(
5276
- `UPDATE workflow_engine_tasks
5277
- SET task_order = ?, plan_json = ?
5278
- WHERE run_id = ? AND task_id = ?`,
5279
- )
5280
- .run(taskOrder, taskJson, runId, task.taskId);
5281
- return;
5282
- }
5436
+ )
5437
+ .run(runId, task.taskId, taskOrder, delivery.revision, taskJson);
5438
+ return;
5439
+ }
5440
+ if (compatible.has(task.taskId) && !invalidated.has(task.taskId)) {
5283
5441
  database
5284
5442
  .prepare(
5285
5443
  `UPDATE workflow_engine_tasks
5444
+ SET task_order = ?, plan_json = ?
5445
+ WHERE run_id = ? AND task_id = ?`,
5446
+ )
5447
+ .run(taskOrder, taskJson, runId, task.taskId);
5448
+ return;
5449
+ }
5450
+ database
5451
+ .prepare(
5452
+ `UPDATE workflow_engine_tasks
5286
5453
  SET task_order = ?, delivery_revision = ?, plan_json = ?,
5287
5454
  state = 'pending', phase = 'red', pause_code = NULL,
5288
5455
  context_request_json = NULL, queue_position = NULL
5289
5456
  WHERE run_id = ? AND task_id = ?`,
5290
- )
5291
- .run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
5292
- });
5293
- for (const row of priorRows) {
5294
- if (!admitted.has(row.task_id)) {
5295
- database
5296
- .prepare(
5297
- `DELETE FROM workflow_engine_tasks
5457
+ )
5458
+ .run(taskOrder, delivery.revision, taskJson, runId, task.taskId);
5459
+ });
5460
+ for (const row of priorRows) {
5461
+ if (!admitted.has(row.task_id)) {
5462
+ database
5463
+ .prepare(
5464
+ `DELETE FROM workflow_engine_tasks
5298
5465
  WHERE run_id = ? AND task_id = ?`,
5299
- )
5300
- .run(runId, row.task_id);
5301
- }
5466
+ )
5467
+ .run(runId, row.task_id);
5302
5468
  }
5303
- },
5304
- );
5469
+ }
5470
+ });
5305
5471
  }
5306
5472
 
5307
5473
  #deliveryFailure(
@@ -5797,16 +5963,22 @@ export class WorkflowEngine {
5797
5963
  if (typeof outcome.code !== "string" || outcome.code.length === 0) {
5798
5964
  throw new Error("workflow-attempt-outcome-invalid");
5799
5965
  }
5966
+ const approvalCodeInvalid =
5967
+ outcome.kind === "approval-needed" &&
5968
+ !isWorkflowApprovalCode(outcome.code);
5800
5969
  this.#setTask(
5801
5970
  runId,
5802
5971
  row.task_id,
5803
5972
  {
5804
- state: outcome.kind,
5973
+ state: approvalCodeInvalid ? "paused" : outcome.kind,
5805
5974
  phase,
5806
- pauseCode: outcome.code,
5807
- contextRequest: outcome.contextRequest
5808
- ? normalizeWorkflowContextRequest(outcome.contextRequest)
5809
- : null,
5975
+ pauseCode: approvalCodeInvalid
5976
+ ? "approval-code-invalid"
5977
+ : outcome.code,
5978
+ contextRequest:
5979
+ !approvalCodeInvalid && outcome.contextRequest
5980
+ ? normalizeWorkflowContextRequest(outcome.contextRequest)
5981
+ : null,
5810
5982
  queuePosition: null,
5811
5983
  },
5812
5984
  lease,
@@ -5936,13 +6108,21 @@ export class WorkflowEngine {
5936
6108
  );
5937
6109
  }
5938
6110
  }
6111
+ const approvalCodeInvalid =
6112
+ verification.kind === "approval-needed" &&
6113
+ !isWorkflowApprovalCode(verification.code);
5939
6114
  const state =
5940
- verification.kind === "approval-needed" ? "approval-needed" : "paused";
6115
+ verification.kind === "approval-needed" && !approvalCodeInvalid
6116
+ ? "approval-needed"
6117
+ : "paused";
6118
+ const code = approvalCodeInvalid
6119
+ ? "approval-code-invalid"
6120
+ : verification.code;
5941
6121
  this.#transition(
5942
6122
  runId,
5943
6123
  state,
5944
6124
  `${operationId}:change-verification-${state}`,
5945
- verification.code,
6125
+ code,
5946
6126
  lease,
5947
6127
  );
5948
6128
  return this.#statusByRun(runId);
@@ -6406,6 +6586,18 @@ export class WorkflowEngine {
6406
6586
  } catch {
6407
6587
  engineRun = undefined;
6408
6588
  }
6589
+ const contextRequest = firstPaused?.context_request_json
6590
+ ? normalizeWorkflowContextRequest(
6591
+ JSON.parse(firstPaused.context_request_json) as unknown,
6592
+ )
6593
+ : undefined;
6594
+ const pauseCode =
6595
+ projection.pauseCode ?? firstPaused?.pause_code ?? "task-paused";
6596
+ const approval =
6597
+ projection.state === "approval-needed"
6598
+ ? approvalRequirement(pauseCode, contextRequest)
6599
+ : undefined;
6600
+ const currentRevision = projection.deliveryRevision ?? 0;
6409
6601
  return {
6410
6602
  version: 2,
6411
6603
  runId: projection.runId,
@@ -6415,21 +6607,53 @@ export class WorkflowEngine {
6415
6607
  ...(projection.deliveryRevision === undefined
6416
6608
  ? {}
6417
6609
  : { deliveryRevision: projection.deliveryRevision }),
6610
+ ...(projection.deliveryBindings.length > 0
6611
+ ? { deliveryBindings: structuredClone(projection.deliveryBindings) }
6612
+ : {}),
6418
6613
  completed: projection.state === "completed",
6419
6614
  ...(projection.terminal ? { terminal: projection.terminal } : {}),
6420
6615
  ...(projection.pauseCode || firstPaused?.pause_code
6421
6616
  ? {
6422
6617
  pause: {
6423
- code:
6424
- projection.pauseCode ??
6425
- firstPaused?.pause_code ??
6426
- "task-paused",
6618
+ code: pauseCode,
6427
6619
  },
6428
6620
  }
6429
6621
  : {}),
6430
- legalCommands: legalControlCommands(projection.state).filter(
6431
- (command) => command !== "rebind" || firstPaused !== undefined,
6432
- ),
6622
+ legalCommands:
6623
+ projection.state === "approval-needed"
6624
+ ? ["status", "discard"]
6625
+ : legalControlCommands(projection.state).filter(
6626
+ (command) => command !== "rebind" || firstPaused !== undefined,
6627
+ ),
6628
+ ...(approval && projection.change
6629
+ ? {
6630
+ approval: {
6631
+ category: approval.category,
6632
+ requiredGates: approval.requiredGates,
6633
+ refs: approval.refs,
6634
+ designRequest: `/abel-design --change ${projection.change}`,
6635
+ retainedRun: {
6636
+ runId: projection.runId,
6637
+ deliveryRevision: currentRevision,
6638
+ },
6639
+ receiptPrecondition: {
6640
+ deliveryRevision: { greaterThan: currentRevision },
6641
+ receiptHash: "matching-ready-receipt",
6642
+ },
6643
+ },
6644
+ conditionalCommands: [
6645
+ {
6646
+ command: "resume",
6647
+ stage: "abel-implement",
6648
+ change: projection.change,
6649
+ requires: {
6650
+ deliveryRevision: { greaterThan: currentRevision },
6651
+ receiptHash: "matching-ready-receipt",
6652
+ },
6653
+ },
6654
+ ],
6655
+ }
6656
+ : {}),
6433
6657
  tasks: rows.map((row) => ({
6434
6658
  taskId: row.task_id,
6435
6659
  state: row.state,
@@ -6502,6 +6726,59 @@ export class WorkflowEngine {
6502
6726
  };
6503
6727
  }
6504
6728
 
6729
+ async #withAvailableDelivery(
6730
+ outcome: Record<string, unknown>,
6731
+ ): Promise<Record<string, unknown>> {
6732
+ if (
6733
+ outcome.stage !== "abel-implement" ||
6734
+ outcome.state !== "approval-needed" ||
6735
+ typeof outcome.change !== "string" ||
6736
+ !this.#deliverySource.discoverLatest
6737
+ ) {
6738
+ return outcome;
6739
+ }
6740
+ let available: WorkflowAvailableDelivery | undefined;
6741
+ try {
6742
+ available = await this.#deliverySource.discoverLatest({
6743
+ stage: "abel-implement",
6744
+ change: outcome.change,
6745
+ });
6746
+ } catch {
6747
+ return outcome;
6748
+ }
6749
+ const currentRevision =
6750
+ typeof outcome.deliveryRevision === "number"
6751
+ ? outcome.deliveryRevision
6752
+ : 0;
6753
+ if (
6754
+ !available ||
6755
+ !Number.isSafeInteger(available.deliveryRevision) ||
6756
+ available.deliveryRevision <= currentRevision ||
6757
+ !SHA256.test(available.receiptHash)
6758
+ ) {
6759
+ return outcome;
6760
+ }
6761
+ const conditionalCommands = Array.isArray(outcome.conditionalCommands)
6762
+ ? outcome.conditionalCommands.map((entry) =>
6763
+ isRecord(entry) && entry.command === "resume"
6764
+ ? {
6765
+ ...entry,
6766
+ satisfiedBy: {
6767
+ deliveryRevision: available.deliveryRevision,
6768
+ receiptHash: available.receiptHash,
6769
+ },
6770
+ }
6771
+ : entry,
6772
+ )
6773
+ : outcome.conditionalCommands;
6774
+ return {
6775
+ ...outcome,
6776
+ legalCommands: ["status", "resume", "discard"],
6777
+ availableDelivery: structuredClone(available),
6778
+ ...(conditionalCommands === undefined ? {} : { conditionalCommands }),
6779
+ };
6780
+ }
6781
+
6505
6782
  #launchAdvance(
6506
6783
  runId: string,
6507
6784
  operationId: string,
@@ -6567,84 +6844,7 @@ export class WorkflowEngine {
6567
6844
  signal?: AbortSignal,
6568
6845
  onActivity?: (event: WorkflowActivityUpdate) => void,
6569
6846
  ) {
6570
- if (!("change" in command)) {
6571
- const existingRun = this.#lookupProvisionalRun(command.provisionalKey);
6572
- if (existingRun) {
6573
- this.#ensureEngineRun(existingRun);
6574
- const replay = this.#operationReplay(
6575
- existingRun,
6576
- command.operationId,
6577
- command.command,
6578
- );
6579
- if (replay) return replay;
6580
- const restarted = this.#runStore.startRun({
6581
- stage: "abel-design",
6582
- provisionalKey: command.provisionalKey,
6583
- operationId: command.operationId,
6584
- });
6585
- const begun = this.#beginOperation(
6586
- existingRun,
6587
- command.operationId,
6588
- command.command,
6589
- );
6590
- if (begun) return begun;
6591
- if (restarted.state === "created") {
6592
- const lease = this.#operationLease(existingRun, command.operationId);
6593
- this.#transition(
6594
- existingRun,
6595
- "paused",
6596
- `${command.operationId}:awaiting-gate-a`,
6597
- "design-awaiting-gate-a",
6598
- lease,
6599
- );
6600
- }
6601
- return this.#commitOperation(
6602
- existingRun,
6603
- command.operationId,
6604
- this.#statusByRun(existingRun),
6605
- );
6606
- }
6607
- const created = this.#runStore.startRun({
6608
- stage: "abel-design",
6609
- provisionalKey: command.provisionalKey,
6610
- operationId: command.operationId,
6611
- });
6612
- this.#ensureEngineRun(created.runId);
6613
- const begun = this.#beginOperation(
6614
- created.runId,
6615
- command.operationId,
6616
- command.command,
6617
- );
6618
- if (begun) return begun;
6619
- const lease = this.#operationLease(created.runId, command.operationId);
6620
- this.#transition(
6621
- created.runId,
6622
- "paused",
6623
- `${command.operationId}:awaiting-gate-a`,
6624
- "design-awaiting-gate-a",
6625
- lease,
6626
- );
6627
- return this.#commitOperation(
6628
- created.runId,
6629
- command.operationId,
6630
- this.#statusByRun(created.runId),
6631
- );
6632
- }
6633
- let existingRun = this.#lookupRun(command.stage, command.change);
6634
- const requestedProvisionalRun =
6635
- command.stage === "abel-design" && command.provisionalKey
6636
- ? this.#lookupProvisionalRun(command.provisionalKey)
6637
- : undefined;
6638
- if (command.provisionalKey && !requestedProvisionalRun) {
6639
- throw new Error("provisional-design-run-not-found");
6640
- }
6641
- if (
6642
- existingRun &&
6643
- requestedProvisionalRun &&
6644
- existingRun !== requestedProvisionalRun
6645
- ) {
6646
- throw new Error("provisional-design-run-conflict");
6647
- }
6847
+ const existingRun = this.#lookupRun(command.stage, command.change);
6648
6848
  if (existingRun) {
6649
6849
  this.#ensureEngineRun(existingRun);
6650
6850
  const replay = this.#operationReplay(
@@ -6673,17 +6873,6 @@ export class WorkflowEngine {
6673
6873
  }
6674
6874
  }
6675
6875
 
6676
- if (requestedProvisionalRun) {
6677
- this.#runStore.bindChange({
6678
- runId: requestedProvisionalRun,
6679
- change: command.change,
6680
- operationId: `bind-change-${hash(
6681
- requestedProvisionalRun,
6682
- command.change,
6683
- ).slice(0, 40)}`,
6684
- });
6685
- existingRun = requestedProvisionalRun;
6686
- }
6687
6876
  const runId =
6688
6877
  existingRun ??
6689
6878
  this.#runStore.startRun({
@@ -6699,20 +6888,6 @@ export class WorkflowEngine {
6699
6888
  );
6700
6889
  if (begun) return begun;
6701
6890
  const lease = this.#operationLease(runId, command.operationId);
6702
- if (command.stage === "abel-design") {
6703
- this.#transition(
6704
- runId,
6705
- "paused",
6706
- `${command.operationId}:awaiting-design-evidence`,
6707
- "design-awaiting-evidence",
6708
- lease,
6709
- );
6710
- return this.#commitOperation(
6711
- runId,
6712
- command.operationId,
6713
- this.#statusByRun(runId),
6714
- );
6715
- }
6716
6891
  this.#transition(
6717
6892
  runId,
6718
6893
  "validating-delivery",
@@ -6774,17 +6949,6 @@ export class WorkflowEngine {
6774
6949
  if (replay) return replay;
6775
6950
  const lease = this.#operationLease(runId, command.operationId);
6776
6951
  const current = this.#runStore.status(runId);
6777
- if (current.stage === "abel-design") {
6778
- if (current.state !== "paused") {
6779
- this.#interruptOperation(runId, command.operationId);
6780
- throw new Error("resume-not-allowed");
6781
- }
6782
- return this.#commitOperation(
6783
- runId,
6784
- command.operationId,
6785
- this.#statusByRun(runId),
6786
- );
6787
- }
6788
6952
  if (current.state === "recovering") {
6789
6953
  emitWorkflowActivity(onActivity, {
6790
6954
  state: "recovering",
@@ -6830,15 +6994,13 @@ export class WorkflowEngine {
6830
6994
  projectedRevision !== undefined && projectedRevision !== engineRevision;
6831
6995
  if (
6832
6996
  current.state === "approval-needed" &&
6833
- command.deliveryRevision === undefined &&
6834
- engineRevision !== null &&
6835
- !deliveryOutOfSync
6997
+ (command.deliveryRevision === undefined ||
6998
+ command.receiptHash === undefined ||
6999
+ command.deliveryRevision <=
7000
+ (engineRevision ?? projectedRevision ?? 0))
6836
7001
  ) {
6837
- return this.#commitOperation(
6838
- runId,
6839
- command.operationId,
6840
- this.#statusByRun(runId),
6841
- );
7002
+ this.#interruptOperation(runId, command.operationId);
7003
+ throw new Error("approval-receipt-required");
6842
7004
  }
6843
7005
  if (
6844
7006
  command.deliveryRevision !== undefined ||
@@ -6922,12 +7084,12 @@ export class WorkflowEngine {
6922
7084
  if (replay) return replay;
6923
7085
  const lease = this.#operationLease(runId, command.operationId);
6924
7086
  const state = this.#runStore.status(runId).state;
6925
- if (!["paused", "retryable", "approval-needed"].includes(state)) {
7087
+ if (!["paused", "retryable"].includes(state)) {
6926
7088
  this.#interruptOperation(runId, command.operationId);
6927
7089
  throw new Error("rebind-not-allowed");
6928
7090
  }
6929
7091
  const task = this.#tasks(runId).find((row) =>
6930
- ["paused", "retryable", "approval-needed"].includes(row.state),
7092
+ ["paused", "retryable"].includes(row.state),
6931
7093
  );
6932
7094
  if (!task) {
6933
7095
  this.#interruptOperation(runId, command.operationId);
@@ -7169,14 +7331,18 @@ export class WorkflowEngine {
7169
7331
  const command = assertControlCommand(value);
7170
7332
  switch (command.command) {
7171
7333
  case "start":
7172
- return this.#start(command, signal, onActivity);
7334
+ return this.#withAvailableDelivery(
7335
+ await this.#start(command, signal, onActivity),
7336
+ );
7173
7337
  case "status": {
7174
7338
  const runId = this.#lookupRun(command.stage, command.change);
7175
7339
  if (!runId) throw new Error("run-not-found");
7176
- return this.#statusByRun(runId);
7340
+ return this.#withAvailableDelivery(this.#statusByRun(runId));
7177
7341
  }
7178
7342
  case "resume":
7179
- return this.#resume(command, signal, onActivity);
7343
+ return this.#withAvailableDelivery(
7344
+ await this.#resume(command, signal, onActivity),
7345
+ );
7180
7346
  case "rebind":
7181
7347
  return this.#rebind(command);
7182
7348
  case "cancel":