-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workflow): add atomic durable task claim and checkpoint CAS #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
160
commits into
main
Choose a base branch
from
feat/workflow-state-store-atomic-claim
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 75 commits
Commits
Show all changes
160 commits
Select commit
Hold shift + click to select a range
efe524d
test(workflow): add RED atomic state-store contract
seonghobae df17d1e
feat(workflow): add atomic durable state-store boundary
seonghobae 30edea3
test(workflow): cover state-store failure contracts
seonghobae ca9857b
test(workflow): add RED blocked-recovery contract
seonghobae 5c73335
feat(workflow): add bounded crash recovery and blocked descendants
seonghobae 38a2b94
test(workflow): add RED stored-state identity regressions
seonghobae f4862b0
fix(workflow): bind durable state to execution recovery invariants
seonghobae 731e3da
test(workflow): add RED cancellation and policy contracts
seonghobae 723a04d
feat(workflow): make cancellation and scheduling policy durable
seonghobae 64ef7dc
docs(gap): align runtime scheduler and protected source truth
seonghobae 16bd1bf
test(workflow): prove restart retains active effect authority
seonghobae acf1f1f
test(workflow): define restart claim reconstruction boundary
seonghobae 19c27d3
test(workflow): define durable transition provenance boundary
seonghobae 78bb2a5
test(workflow): distinguish durable claim from effect start
seonghobae 162bf5c
feat(workflow): retain bounded transition provenance
seonghobae c9ca661
docs(adr): define durable workflow execution authority
seonghobae f9e8063
docs(adr): index workflow state authority decision
seonghobae 3dcedbd
docs(gap): align state-store provenance maturity
seonghobae 664c603
test(workflow): harden transition ledger integrity
seonghobae 823741f
test(workflow): make ledger mutation cases type-safe
seonghobae 8860504
test(workflow): define claim-to-effect execution ordering
seonghobae d1e567d
feat(workflow): compose durable claim and effect start
seonghobae d12d811
test(workflow): reject malformed effect outcomes
seonghobae 4b9f871
test(workflow): require effect-start before completion
seonghobae a0a266c
fix(workflow): require durable effect-start before completion
seonghobae 351bcf7
test(workflow): mark effects before terminal completion
seonghobae 75ab1f5
test(workflow): preserve effect-start completion invariant
seonghobae 890e097
test(workflow): reconcile only effect-started claims
seonghobae 9015113
test(workflow): distinguish pre-effect side-effect recovery
seonghobae 430c1fe
test: allow recovery before side effect starts
seonghobae 4d1dbbf
fix(workflow): recover unstarted side-effect claims
seonghobae 18a323b
test(workflow): prove pre-effect failure is recoverable
seonghobae 2a19eb2
docs(adr): align recovery with effect-start authority
seonghobae 056bd53
test(recovery): reject unknown side-effect start evidence
seonghobae b04b9b9
fix(recovery): fail closed on unknown side-effect start
seonghobae e6e0f21
test(workflow): reject effect start after cancellation
seonghobae c25e032
fix(workflow): honor cancellation before effect start
seonghobae ae0ac5a
test(workflow): reject pending state with crossed effect boundary
seonghobae 9360761
test(workflow): reject legacy unknown replay authority
seonghobae 17beb35
fix(workflow): fail closed on ambiguous effect replay state
seonghobae 2715409
test(workflow): align recovery contract with effect-start evidence
seonghobae 8e16612
test(workflow): align exhausted-attempt failure contract
seonghobae 6b774c2
test(workflow): preserve legacy pure recovery authority
seonghobae 4f44657
fix(workflow): scope replay evidence gate to side effects
seonghobae b976df6
test(workflow): prove bounded admission-order starvation
seonghobae 8061cca
test(workflow): exercise atomic claim on side effects
seonghobae e83d7f0
test: retain started idempotent claim under cancellation
seonghobae e2e49d1
fix: preserve started idempotent claim on cancellation
seonghobae 16c896e
docs: record idempotent cancellation reconciliation
seonghobae a2bdbd4
Merge protected main into workflow state-store candidate
seonghobae 6a0f5c0
fix(workflow): close CAS/validation gaps and coverage found in PR review
claude b9549e4
test(workflow): close remaining 100%-coverage gaps in the state store
claude 9025364
test(workflow): require deployed single-authority Durable Object routing
seonghobae 8f269fb
feat(workflow): bind durable state authority to one execution object
seonghobae c9bbeff
feat(workflow): export durable workflow state runtime class
seonghobae 7dd4806
feat(workflow): declare workflow state durable object binding
seonghobae fe2b928
test(workflow): cover durable routing failure contracts
seonghobae 6c727ac
docs(workflow): bind ADR-0013 to deployed durable-object composition
seonghobae b42206a
docs(workflow): record durable-object routing repair in product baseline
seonghobae 6292cc4
fix(ci): skip docs-only changes for ci, reviewer-ci, patch-validator-…
seonghobae 81e2e14
test(workflow): require authenticated durable-state commands
seonghobae f9124e3
revert test: preserve binding-scoped Durable Object authority
seonghobae bd25823
docs(adr): correct Durable Object RPC and binding authority claims
seonghobae 88e2758
test(workflow): classify malformed durable claims as invalid requests
seonghobae 87a35fc
fix(workflow): reject malformed durable claims as invalid input
seonghobae f57eb83
test(workflow): forbid parallel plans for one execution
seonghobae cb19601
fix(workflow): bind one durable plan authority per execution
seonghobae c3d4d05
test(workflow): assert conflicting execution plan authority
seonghobae 138161b
test(workflow): cover missing execution plan authority
seonghobae 47b2365
test(workflow): reject malformed execution plan authority
seonghobae d24976d
fix(workflow): fail closed on malformed plan authority
seonghobae ce7520d
test(workflow): enforce routed single-plan execution authority
seonghobae 3cb93a7
docs(adr): bind one plan authority to each workflow execution
seonghobae fa4bb2b
test(ci): reject docs-only verification suppression
seonghobae def52aa
fix(ci): keep application verification mandatory for docs-only changes
seonghobae 6bc6e59
fix(ci): keep reviewer verification mandatory for docs-only changes
seonghobae 60b8333
fix(ci): keep image verification mandatory for docs-only changes
seonghobae 284ab47
test(workflow): reject legacy cross-plan authority takeover
seonghobae f8025d2
fix(workflow): reject legacy cross-plan authority takeover
seonghobae e138061
test(workflow): model transactional legacy-state listing
seonghobae d7e9dda
test(workflow): model legacy-state listing in atomicity fake
seonghobae fe4e5a2
test(workflow): model legacy-state listing in cancellation fake
seonghobae 967b714
test(workflow): model legacy-state listing in integrity fake
seonghobae 7066a84
test(workflow): model legacy-state listing in provenance fake
seonghobae 21a509f
test(workflow): model legacy-state listing in recovery fake
seonghobae a0daaf6
test(workflow): model legacy-state listing in runner fake
seonghobae 1daa3c0
test(workflow): model legacy-state listing in routed authority fake
seonghobae d8920e8
test(workflow): model legacy-state listing in routing fake
seonghobae 625088f
test(workflow): model legacy-state listing in recovery-claim fake
seonghobae 20170eb
test(workflow): model legacy-state listing in failure-contract fake
seonghobae fc17c70
test(workflow): reject malformed durable claims at command boundary
seonghobae 997ba83
fix(workflow): validate durable claim command shape
seonghobae 646fcf1
test(workflow): reject mismatched transition result receipts
seonghobae 467e575
fix(workflow): bind transition receipts to valid result states
seonghobae 97cecea
test(workflow): reject misrouted durable state commands
seonghobae 4bf919f
fix(workflow): bind durable object to execution identity
seonghobae 1332755
test(workflow): retain routed object identity in plan fixture
seonghobae c3781b3
test(workflow): reject malformed durable record shapes
seonghobae eeb7272
fix(workflow): classify malformed durable state as conflict
seonghobae 7d5ac41
docs(adr): bind workflow authority to retained state integrity
seonghobae f9738c5
test(workflow): reject malformed command scalar fields
seonghobae 3f60c86
fix(workflow): admit command scalar fields before arbitration
seonghobae a1058ae
test(workflow): reject noncanonical command task identities
seonghobae 6b27f08
fix(workflow): validate command task identity before arbitration
seonghobae 52c5724
test(workflow): reject state resurrection after partial durable loss
seonghobae 359e609
fix(workflow): fail closed on missing durable state with retained aut…
seonghobae bff157c
test(workflow): reject impossible claim attempt at command boundary
seonghobae cfb9a02
fix(workflow): reject impossible claim attempts before arbitration
seonghobae 0c0c681
test(workflow): require exact effect-start authority before invocation
seonghobae 637da32
fix(workflow): fail closed on unproven effect-start authority
seonghobae 891fa74
test(workflow): require durable terminal authority
seonghobae 3685ce5
fix(workflow): verify durable terminal authority
seonghobae 4f4625d
test(workflow): reject substituted task-effect claim authority
seonghobae da225d1
fix(workflow): bind runner claims to admitted task authority
seonghobae c744f9d
test(workflow): reject impossible runner claim attempts
seonghobae f3a0226
fix(workflow): reject impossible runner claim attempts
seonghobae 3f42df7
test(workflow): reject non-canonical runner claim authority
seonghobae da3c53c
fix(workflow): enforce canonical runner claim identity
seonghobae fb54bab
test(workflow): reject malformed claim before state port
seonghobae eca1e63
fix(workflow): admit claim id before state mutation
seonghobae 7188da7
Merge remote-tracking branch 'origin/main' into workflow-state-store-…
seonghobae 83c319b
Merge protected main into docs-gate regression lane
seonghobae 0663476
fix(runtime): narrow canonical execution identity after admission
seonghobae 1b26024
fix(runtime): document durable routing exports
seonghobae f0685c2
test(workflow): cover failure authority boundaries
seonghobae 7e5ff9c
test(workflow): cover both foreign snapshot identities
seonghobae 19c6fa2
test(workflow): fail on extra durable command payload
seonghobae 10708af
fix(workflow): minimize durable command transport payload
seonghobae 00e871e
test(workflow): pin command operation during transport
seonghobae 1f7f5b9
fix(workflow): snapshot command operation for transport
seonghobae e88a379
test(workflow): reject nested claim payload leakage
seonghobae 81abbab
test(workflow): cover nested checkpoint transport minimization
seonghobae f915d13
test(workflow): preserve fail-closed nested payload validation
seonghobae 037ec4e
fix(workflow): minimize nested durable command payloads
seonghobae 74a9493
test(workflow): cover missing durable state authority paths
seonghobae 6793a32
test(workflow): cover unexpected durable state errors
seonghobae d4e3f27
merge: restack #542 on protected reviewer truth
seonghobae f6c6af5
merge: restack #543 on protected reviewer truth
seonghobae 6f09d16
merge(context-fabric): restack required-gate regression on protected …
seonghobae 6953eaa
merge(context-fabric): restack durable workflow authority on protecte…
seonghobae c9c2d86
merge: converge exact-head gate regression onto protected #533 truth
seonghobae a6970ca
merge: converge exact-head gate regression onto protected #552 truth
seonghobae 93bfa5e
merge: converge durable workflow authority onto protected #552 truth
seonghobae f07679d
merge: restack exact-head gate regression after #527 trust integration
seonghobae 9236775
merge: restack durable workflow state after #527 trust integration
seonghobae 8e7e0bb
test(workflow): reject deleted retained transition receipts
seonghobae 35f80f2
fix(workflow): fail closed on deleted retained transition receipts
seonghobae 8afef54
test(workflow): reject explicit empty transition ledger
seonghobae 55194e1
fix(workflow): reject explicit empty transition ledger
seonghobae 4185df2
test(workflow): reject missing initialized provenance root
seonghobae 1909f23
fix(workflow): require initialized provenance root
seonghobae 84a2cd0
fix(workflow): preserve legacy first-claim provenance
seonghobae 2bb6076
test(workflow): cover malformed retained receipt rejection
seonghobae 7f743f4
merge(main): restack durable workflow state after trusted source roll…
seonghobae e255bf1
merge(main): restack exact-head gate regression after protected trust…
seonghobae 4616b5e
test(workflow): isolate malformed retained receipt invariant
seonghobae b14b37c
merge(restack): rebase #543 onto protected main d9b2a95
seonghobae 46439b1
merge(main): non-force restack durable workflow lane after #526
seonghobae 5cd6341
Merge pull request #543 from ContextualWisdomLab/chore/paths-ignore-fix
seonghobae 6cb43c1
merge(main): non-force restack durable workflow lane after #543
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import type { AdmittedWorkflowTaskPlan } from "./task-plan"; | ||
| import { | ||
| WorkflowStateConflictError, | ||
| type WorkflowExecutionStateSnapshot, | ||
| type WorkflowTaskClaim, | ||
| } from "./workflow-state-store"; | ||
|
|
||
| /** | ||
| * Reconstructs the exact durable claim authority for one actively running task from an admitted | ||
| * plan and a freshly read state snapshot alone, without minting a replacement claim identity. | ||
| * | ||
| * A `WorkflowTaskClaim` returned by `claimRunnableTask`/`claimNextRunnableTask` is an in-memory | ||
| * capability, not durable state on its own; it does not survive a crash or restart of the process | ||
| * that received it. This is the restart recovery seam ADR-0013 requires: a restarted process reads | ||
| * the durable state snapshot, then reconstructs the identical claim identity, attempt, and effect | ||
| * classification the prior process already recorded, so it can call `completeTask` or | ||
| * `recoverInterruptedTask` for a possibly-started side effect using real durable evidence instead | ||
| * of fabricating new claim authority for work it never itself claimed. | ||
| * | ||
| * @param plan Admitted workflow task plan that defines the task's effect classification. | ||
| * @param snapshot Current durable state snapshot obtained from `DurableWorkflowStateRepository.readState`. | ||
| * @param taskId Task to reconstruct durable claim authority for. | ||
| * @returns The exact `WorkflowTaskClaim` already retained as durable authority for this task. | ||
| * @throws {WorkflowStateConflictError} When the snapshot belongs to another execution or plan, the | ||
| * task is unknown to the admitted plan, or the task has no durable active claim to reconstruct. | ||
| */ | ||
| export function reconstructActiveTaskClaim( | ||
| plan: AdmittedWorkflowTaskPlan, | ||
| snapshot: WorkflowExecutionStateSnapshot, | ||
| taskId: string, | ||
| ): WorkflowTaskClaim { | ||
| if (snapshot.executionId !== plan.executionId || snapshot.planId !== plan.planId) { | ||
| throw new WorkflowStateConflictError("state snapshot belongs to another execution or plan"); | ||
| } | ||
| const definition = plan.tasks.find((task) => task.taskId === taskId); | ||
| if (!definition) { | ||
| throw new WorkflowStateConflictError("task does not belong to the admitted plan"); | ||
| } | ||
| const stored = snapshot.tasks.find((task) => task.taskId === taskId); | ||
| if (!stored || stored.state !== "running" || stored.activeClaimId === null) { | ||
| throw new WorkflowStateConflictError("task has no durable active claim authority to reconstruct"); | ||
| } | ||
| return Object.freeze({ | ||
| executionId: snapshot.executionId, | ||
| planId: snapshot.planId, | ||
| taskId: stored.taskId, | ||
| claimId: stored.activeClaimId, | ||
| attempt: stored.attempt, | ||
| effect: definition.effect, | ||
| }); | ||
| } |
264 changes: 264 additions & 0 deletions
264
src/workflow-task-execution/workflow-state-durable-object.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| import { | ||
| CheckpointAdmissionError, | ||
| admitExecutionCheckpoint, | ||
| type ExecutionCheckpoint, | ||
| } from "../state-checkpoint/checkpoint-admission"; | ||
| import { isCanonicalExecutionId } from "../runtime-shared/execution-identity"; | ||
| import { | ||
| WorkflowTaskPlanError, | ||
| admitWorkflowTaskPlan, | ||
| type WorkflowTaskPlan, | ||
| } from "./task-plan"; | ||
| import { | ||
| DurableWorkflowStateRepository, | ||
| WorkflowStateConflictError, | ||
| WorkflowStateStoreUnavailableError, | ||
| type WorkflowExecutionStateSnapshot, | ||
| type WorkflowTaskClaim, | ||
| type WorkflowTaskTerminalOutcome, | ||
| } from "./workflow-state-store"; | ||
|
|
||
| const WORKFLOW_STATE_INTERNAL_ENDPOINT = "https://noema-workflow-state.internal/command"; | ||
| const workflowStateOperations = new Set<WorkflowStateCommand["operation"]>([ | ||
| "initialize", | ||
| "read", | ||
| "claim_next", | ||
| "claim_runnable", | ||
| "mark_effect_started", | ||
| "request_cancellation", | ||
| "complete", | ||
| "recover_interrupted", | ||
| "resolve_blocked", | ||
| "commit_checkpoint", | ||
| ]); | ||
|
|
||
| /** Cloudflare binding required to route one execution to its single durable workflow-state authority. */ | ||
| export interface WorkflowStateDurableObjectEnv { | ||
| NOEMA_WORKFLOW_STATE: DurableObjectNamespace; | ||
| } | ||
|
|
||
| /** Serializable command surface used only between Noema's scheduler adapter and its private Durable Object. */ | ||
| export type WorkflowStateCommand = | ||
| | { readonly operation: "initialize"; readonly plan: WorkflowTaskPlan; readonly checkpoint: ExecutionCheckpoint } | ||
| | { readonly operation: "read"; readonly plan: WorkflowTaskPlan } | ||
| | { readonly operation: "claim_next"; readonly plan: WorkflowTaskPlan; readonly claimId: string } | ||
| | { | ||
| readonly operation: "claim_runnable"; | ||
| readonly plan: WorkflowTaskPlan; | ||
| readonly taskId: string; | ||
| readonly claimId: string; | ||
| } | ||
| | { readonly operation: "mark_effect_started"; readonly plan: WorkflowTaskPlan; readonly claim: WorkflowTaskClaim } | ||
| | { readonly operation: "request_cancellation"; readonly plan: WorkflowTaskPlan; readonly cancellationId: string } | ||
| | { | ||
| readonly operation: "complete"; | ||
| readonly plan: WorkflowTaskPlan; | ||
| readonly claim: WorkflowTaskClaim; | ||
| readonly outcome: WorkflowTaskTerminalOutcome; | ||
| } | ||
| | { readonly operation: "recover_interrupted"; readonly plan: WorkflowTaskPlan; readonly claim: WorkflowTaskClaim } | ||
| | { readonly operation: "resolve_blocked"; readonly plan: WorkflowTaskPlan } | ||
| | { | ||
| readonly operation: "commit_checkpoint"; | ||
| readonly plan: WorkflowTaskPlan; | ||
| readonly expected: ExecutionCheckpoint; | ||
| readonly candidate: ExecutionCheckpoint; | ||
| }; | ||
|
|
||
| type WorkflowStateCommandSuccess = { | ||
| readonly ok: true; | ||
| readonly data: WorkflowExecutionStateSnapshot | WorkflowTaskClaim; | ||
| }; | ||
|
|
||
| type WorkflowStateCommandFailure = { | ||
| readonly ok: false; | ||
| readonly error: "invalid_request" | "conflict" | "storage_unavailable" | "internal_error"; | ||
| }; | ||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| function isJsonMediaType(value: string | null): boolean { | ||
| return /^[ \t]*application\/json[ \t]*(?:;[ \t]*charset[ \t]*=[ \t]*utf-8[ \t]*)?$/iu.test(value ?? ""); | ||
| } | ||
|
|
||
| function jsonResponse( | ||
| body: WorkflowStateCommandSuccess | WorkflowStateCommandFailure, | ||
| status: number, | ||
| ): Response { | ||
| return new Response(JSON.stringify(body), { | ||
| status, | ||
| headers: { | ||
| "content-type": "application/json; charset=utf-8", | ||
| "cache-control": "no-store", | ||
| pragma: "no-cache", | ||
| "x-content-type-options": "nosniff", | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| function workflowTaskClaim(value: unknown): WorkflowTaskClaim { | ||
| if (!isRecord(value)) { | ||
| throw new WorkflowTaskPlanError("task claim must be an object"); | ||
| } | ||
| return { | ||
| executionId: value.executionId as string, | ||
| planId: value.planId as string, | ||
| taskId: value.taskId as string, | ||
| claimId: value.claimId as string, | ||
| attempt: value.attempt as number, | ||
| effect: value.effect as WorkflowTaskClaim["effect"], | ||
| }; | ||
| } | ||
|
|
||
| function validatedCheckpoint(value: unknown): ExecutionCheckpoint { | ||
| return admitExecutionCheckpoint(value as ExecutionCheckpoint, value as ExecutionCheckpoint).checkpoint; | ||
| } | ||
|
|
||
| function validatedInitialCheckpoint(value: unknown): ExecutionCheckpoint { | ||
| return admitExecutionCheckpoint(null, value as ExecutionCheckpoint).checkpoint; | ||
| } | ||
|
|
||
| async function sha256Hex(value: string): Promise<string> { | ||
| const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); | ||
| return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); | ||
| } | ||
|
|
||
| /** | ||
| * Derives the privacy-preserving deterministic Durable Object name for one canonical execution. | ||
| * Every plan revision and scheduler caller for the same execution therefore reaches one Cloudflare | ||
| * single-authority object, while the raw execution identity is not exposed in the object name. | ||
| */ | ||
| export async function workflowStateObjectName(executionId: unknown): Promise<string> { | ||
| if (!isCanonicalExecutionId(executionId)) { | ||
| throw new WorkflowTaskPlanError("workflow state routing execution identity is not canonical"); | ||
| } | ||
| return `workflow:${await sha256Hex(executionId)}`; | ||
| } | ||
|
|
||
| /** | ||
| * Routes a validated workflow-state command to the one Durable Object selected by execution identity. | ||
| * The Durable Object independently re-admits the plan and checkpoint/claim evidence before granting | ||
| * any mutation authority, so caller-side validation cannot replace the state owner's checks. | ||
| */ | ||
| export async function routeWorkflowStateCommand( | ||
| env: WorkflowStateDurableObjectEnv, | ||
| command: WorkflowStateCommand, | ||
| ): Promise<Response> { | ||
| const admittedPlan = admitWorkflowTaskPlan(command.plan); | ||
| const objectName = await workflowStateObjectName(admittedPlan.executionId); | ||
| const objectId = env.NOEMA_WORKFLOW_STATE.idFromName(objectName); | ||
| const stub = env.NOEMA_WORKFLOW_STATE.get(objectId); | ||
| return stub.fetch(WORKFLOW_STATE_INTERNAL_ENDPOINT, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ ...command, plan: admittedPlan }), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Cloudflare Durable Object adapter that owns one execution's deployed workflow-state serialization point. | ||
| * Domain scheduling remains in the admitted plan and repository; this adapter only binds that authority to | ||
| * Durable Object storage and a private Noema-to-Noema command boundary. | ||
| */ | ||
| export class NoemaWorkflowState { | ||
| private readonly repository: DurableWorkflowStateRepository; | ||
|
|
||
| constructor(state: DurableObjectState) { | ||
| this.repository = new DurableWorkflowStateRepository(state.storage); | ||
| } | ||
|
|
||
| /** | ||
| * Executes one private scheduler command against the durable repository for this object. | ||
| * Wrong endpoints, non-JSON input, malformed plans/checkpoints, stale claims, and storage failures | ||
| * fail closed without exposing secrets or foreign domain payloads. | ||
| */ | ||
| async fetch(request: Request): Promise<Response> { | ||
| if (request.method !== "POST" || request.url !== WORKFLOW_STATE_INTERNAL_ENDPOINT) { | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 404); | ||
| } | ||
| if (!isJsonMediaType(request.headers.get("content-type"))) { | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 415); | ||
| } | ||
|
|
||
| let rawCommand: unknown; | ||
| try { | ||
| rawCommand = await request.json(); | ||
| } catch { | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 400); | ||
| } | ||
| if ( | ||
| !isRecord(rawCommand) | ||
| || typeof rawCommand.operation !== "string" | ||
| || !workflowStateOperations.has(rawCommand.operation as WorkflowStateCommand["operation"]) | ||
| ) { | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 400); | ||
| } | ||
|
|
||
| try { | ||
| const plan = admitWorkflowTaskPlan(rawCommand.plan as WorkflowTaskPlan); | ||
| let data: WorkflowExecutionStateSnapshot | WorkflowTaskClaim; | ||
| switch (rawCommand.operation as WorkflowStateCommand["operation"]) { | ||
| case "initialize": | ||
| data = await this.repository.initialize(plan, validatedInitialCheckpoint(rawCommand.checkpoint)); | ||
| break; | ||
| case "read": | ||
| data = await this.repository.readState(plan); | ||
| break; | ||
| case "claim_next": | ||
| data = await this.repository.claimNextRunnableTask(plan, rawCommand.claimId as string); | ||
| break; | ||
| case "claim_runnable": | ||
| data = await this.repository.claimRunnableTask( | ||
| plan, | ||
| rawCommand.taskId as string, | ||
| rawCommand.claimId as string, | ||
| ); | ||
| break; | ||
| case "mark_effect_started": | ||
| data = await this.repository.markEffectStarted(plan, workflowTaskClaim(rawCommand.claim)); | ||
| break; | ||
| case "request_cancellation": | ||
| data = await this.repository.requestCancellation(plan, rawCommand.cancellationId as string); | ||
| break; | ||
| case "complete": | ||
| data = await this.repository.completeTask( | ||
| plan, | ||
| workflowTaskClaim(rawCommand.claim), | ||
| rawCommand.outcome as WorkflowTaskTerminalOutcome, | ||
| ); | ||
| break; | ||
| case "recover_interrupted": | ||
| data = await this.repository.recoverInterruptedTask(plan, workflowTaskClaim(rawCommand.claim)); | ||
| break; | ||
| case "resolve_blocked": | ||
| data = await this.repository.resolveBlockedDescendants(plan); | ||
| break; | ||
| case "commit_checkpoint": | ||
| data = await this.repository.commitCheckpoint( | ||
| plan, | ||
| validatedCheckpoint(rawCommand.expected), | ||
| validatedCheckpoint(rawCommand.candidate), | ||
| ); | ||
| break; | ||
| /* v8 ignore next -- operation membership is checked immediately before this exhaustive switch. */ | ||
| default: | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 400); | ||
| } | ||
| return jsonResponse({ ok: true, data }, 200); | ||
| } catch (error) { | ||
| if (error instanceof WorkflowTaskPlanError || error instanceof CheckpointAdmissionError) { | ||
| return jsonResponse({ ok: false, error: "invalid_request" }, 400); | ||
| } | ||
| if (error instanceof WorkflowStateConflictError) { | ||
| return jsonResponse({ ok: false, error: "conflict" }, 409); | ||
| } | ||
| if (error instanceof WorkflowStateStoreUnavailableError) { | ||
| return jsonResponse({ ok: false, error: "storage_unavailable" }, 503); | ||
| } | ||
| /* v8 ignore next -- repository/admission boundaries normalize their documented failures above. */ | ||
| return jsonResponse({ ok: false, error: "internal_error" }, 500); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.