Skip to content
Draft
Show file tree
Hide file tree
Changes from 72 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 Sep 2, 2026
df17d1e
feat(workflow): add atomic durable state-store boundary
seonghobae Sep 2, 2026
30edea3
test(workflow): cover state-store failure contracts
seonghobae Sep 2, 2026
ca9857b
test(workflow): add RED blocked-recovery contract
seonghobae Sep 2, 2026
5c73335
feat(workflow): add bounded crash recovery and blocked descendants
seonghobae Sep 2, 2026
38a2b94
test(workflow): add RED stored-state identity regressions
seonghobae Sep 2, 2026
f4862b0
fix(workflow): bind durable state to execution recovery invariants
seonghobae Sep 2, 2026
731e3da
test(workflow): add RED cancellation and policy contracts
seonghobae Sep 2, 2026
723a04d
feat(workflow): make cancellation and scheduling policy durable
seonghobae Sep 2, 2026
64ef7dc
docs(gap): align runtime scheduler and protected source truth
seonghobae Sep 2, 2026
16bd1bf
test(workflow): prove restart retains active effect authority
seonghobae Sep 2, 2026
acf1f1f
test(workflow): define restart claim reconstruction boundary
seonghobae Sep 2, 2026
19c27d3
test(workflow): define durable transition provenance boundary
seonghobae Sep 2, 2026
78bb2a5
test(workflow): distinguish durable claim from effect start
seonghobae Sep 2, 2026
162bf5c
feat(workflow): retain bounded transition provenance
seonghobae Sep 2, 2026
c9ca661
docs(adr): define durable workflow execution authority
seonghobae Sep 2, 2026
f9e8063
docs(adr): index workflow state authority decision
seonghobae Sep 2, 2026
3dcedbd
docs(gap): align state-store provenance maturity
seonghobae Sep 2, 2026
664c603
test(workflow): harden transition ledger integrity
seonghobae Sep 2, 2026
823741f
test(workflow): make ledger mutation cases type-safe
seonghobae Sep 2, 2026
8860504
test(workflow): define claim-to-effect execution ordering
seonghobae Sep 2, 2026
d1e567d
feat(workflow): compose durable claim and effect start
seonghobae Sep 2, 2026
d12d811
test(workflow): reject malformed effect outcomes
seonghobae Sep 2, 2026
4b9f871
test(workflow): require effect-start before completion
seonghobae Sep 2, 2026
a0a266c
fix(workflow): require durable effect-start before completion
seonghobae Sep 2, 2026
351bcf7
test(workflow): mark effects before terminal completion
seonghobae Sep 2, 2026
75ab1f5
test(workflow): preserve effect-start completion invariant
seonghobae Sep 2, 2026
890e097
test(workflow): reconcile only effect-started claims
seonghobae Sep 2, 2026
9015113
test(workflow): distinguish pre-effect side-effect recovery
seonghobae Sep 2, 2026
430c1fe
test: allow recovery before side effect starts
seonghobae Sep 2, 2026
4d1dbbf
fix(workflow): recover unstarted side-effect claims
seonghobae Sep 2, 2026
18a323b
test(workflow): prove pre-effect failure is recoverable
seonghobae Sep 2, 2026
2a19eb2
docs(adr): align recovery with effect-start authority
seonghobae Sep 2, 2026
056bd53
test(recovery): reject unknown side-effect start evidence
seonghobae Sep 2, 2026
b04b9b9
fix(recovery): fail closed on unknown side-effect start
seonghobae Sep 2, 2026
e6e0f21
test(workflow): reject effect start after cancellation
seonghobae Sep 2, 2026
c25e032
fix(workflow): honor cancellation before effect start
seonghobae Sep 2, 2026
ae0ac5a
test(workflow): reject pending state with crossed effect boundary
seonghobae Sep 2, 2026
9360761
test(workflow): reject legacy unknown replay authority
seonghobae Sep 2, 2026
17beb35
fix(workflow): fail closed on ambiguous effect replay state
seonghobae Sep 2, 2026
2715409
test(workflow): align recovery contract with effect-start evidence
seonghobae Sep 2, 2026
8e16612
test(workflow): align exhausted-attempt failure contract
seonghobae Sep 2, 2026
6b774c2
test(workflow): preserve legacy pure recovery authority
seonghobae Sep 2, 2026
4f44657
fix(workflow): scope replay evidence gate to side effects
seonghobae Sep 2, 2026
b976df6
test(workflow): prove bounded admission-order starvation
seonghobae Sep 2, 2026
8061cca
test(workflow): exercise atomic claim on side effects
seonghobae Sep 2, 2026
e83d7f0
test: retain started idempotent claim under cancellation
seonghobae Sep 2, 2026
e2e49d1
fix: preserve started idempotent claim on cancellation
seonghobae Sep 2, 2026
16c896e
docs: record idempotent cancellation reconciliation
seonghobae Sep 2, 2026
a2bdbd4
Merge protected main into workflow state-store candidate
seonghobae Sep 3, 2026
6a0f5c0
fix(workflow): close CAS/validation gaps and coverage found in PR review
claude Sep 3, 2026
b9549e4
test(workflow): close remaining 100%-coverage gaps in the state store
claude Sep 3, 2026
9025364
test(workflow): require deployed single-authority Durable Object routing
seonghobae Sep 3, 2026
8f269fb
feat(workflow): bind durable state authority to one execution object
seonghobae Sep 3, 2026
c9bbeff
feat(workflow): export durable workflow state runtime class
seonghobae Sep 3, 2026
7dd4806
feat(workflow): declare workflow state durable object binding
seonghobae Sep 3, 2026
fe2b928
test(workflow): cover durable routing failure contracts
seonghobae Sep 3, 2026
6c727ac
docs(workflow): bind ADR-0013 to deployed durable-object composition
seonghobae Sep 3, 2026
b42206a
docs(workflow): record durable-object routing repair in product baseline
seonghobae Sep 3, 2026
6292cc4
fix(ci): skip docs-only changes for ci, reviewer-ci, patch-validator-…
seonghobae Sep 3, 2026
81e2e14
test(workflow): require authenticated durable-state commands
seonghobae Sep 3, 2026
f9124e3
revert test: preserve binding-scoped Durable Object authority
seonghobae Sep 3, 2026
bd25823
docs(adr): correct Durable Object RPC and binding authority claims
seonghobae Sep 3, 2026
88e2758
test(workflow): classify malformed durable claims as invalid requests
seonghobae Sep 3, 2026
87a35fc
fix(workflow): reject malformed durable claims as invalid input
seonghobae Sep 3, 2026
f57eb83
test(workflow): forbid parallel plans for one execution
seonghobae Sep 3, 2026
cb19601
fix(workflow): bind one durable plan authority per execution
seonghobae Sep 3, 2026
c3d4d05
test(workflow): assert conflicting execution plan authority
seonghobae Sep 3, 2026
138161b
test(workflow): cover missing execution plan authority
seonghobae Sep 3, 2026
47b2365
test(workflow): reject malformed execution plan authority
seonghobae Sep 3, 2026
d24976d
fix(workflow): fail closed on malformed plan authority
seonghobae Sep 3, 2026
ce7520d
test(workflow): enforce routed single-plan execution authority
seonghobae Sep 3, 2026
3cb93a7
docs(adr): bind one plan authority to each workflow execution
seonghobae Sep 3, 2026
fa4bb2b
test(ci): reject docs-only verification suppression
seonghobae Sep 3, 2026
def52aa
fix(ci): keep application verification mandatory for docs-only changes
seonghobae Sep 3, 2026
6bc6e59
fix(ci): keep reviewer verification mandatory for docs-only changes
seonghobae Sep 3, 2026
60b8333
fix(ci): keep image verification mandatory for docs-only changes
seonghobae Sep 3, 2026
284ab47
test(workflow): reject legacy cross-plan authority takeover
seonghobae Sep 3, 2026
f8025d2
fix(workflow): reject legacy cross-plan authority takeover
seonghobae Sep 3, 2026
e138061
test(workflow): model transactional legacy-state listing
seonghobae Sep 3, 2026
d7e9dda
test(workflow): model legacy-state listing in atomicity fake
seonghobae Sep 3, 2026
fe4e5a2
test(workflow): model legacy-state listing in cancellation fake
seonghobae Sep 3, 2026
967b714
test(workflow): model legacy-state listing in integrity fake
seonghobae Sep 3, 2026
7066a84
test(workflow): model legacy-state listing in provenance fake
seonghobae Sep 3, 2026
21a509f
test(workflow): model legacy-state listing in recovery fake
seonghobae Sep 3, 2026
a0daaf6
test(workflow): model legacy-state listing in runner fake
seonghobae Sep 3, 2026
1daa3c0
test(workflow): model legacy-state listing in routed authority fake
seonghobae Sep 3, 2026
d8920e8
test(workflow): model legacy-state listing in routing fake
seonghobae Sep 3, 2026
625088f
test(workflow): model legacy-state listing in recovery-claim fake
seonghobae Sep 3, 2026
20170eb
test(workflow): model legacy-state listing in failure-contract fake
seonghobae Sep 3, 2026
fc17c70
test(workflow): reject malformed durable claims at command boundary
seonghobae Sep 3, 2026
997ba83
fix(workflow): validate durable claim command shape
seonghobae Sep 3, 2026
646fcf1
test(workflow): reject mismatched transition result receipts
seonghobae Sep 3, 2026
467e575
fix(workflow): bind transition receipts to valid result states
seonghobae Sep 3, 2026
97cecea
test(workflow): reject misrouted durable state commands
seonghobae Sep 3, 2026
4bf919f
fix(workflow): bind durable object to execution identity
seonghobae Sep 3, 2026
1332755
test(workflow): retain routed object identity in plan fixture
seonghobae Sep 3, 2026
c3781b3
test(workflow): reject malformed durable record shapes
seonghobae Sep 3, 2026
eeb7272
fix(workflow): classify malformed durable state as conflict
seonghobae Sep 3, 2026
7d5ac41
docs(adr): bind workflow authority to retained state integrity
seonghobae Sep 3, 2026
f9738c5
test(workflow): reject malformed command scalar fields
seonghobae Sep 3, 2026
3f60c86
fix(workflow): admit command scalar fields before arbitration
seonghobae Sep 3, 2026
a1058ae
test(workflow): reject noncanonical command task identities
seonghobae Sep 3, 2026
6b27f08
fix(workflow): validate command task identity before arbitration
seonghobae Sep 3, 2026
52c5724
test(workflow): reject state resurrection after partial durable loss
seonghobae Sep 3, 2026
359e609
fix(workflow): fail closed on missing durable state with retained aut…
seonghobae Sep 3, 2026
bff157c
test(workflow): reject impossible claim attempt at command boundary
seonghobae Sep 3, 2026
cfb9a02
fix(workflow): reject impossible claim attempts before arbitration
seonghobae Sep 3, 2026
0c0c681
test(workflow): require exact effect-start authority before invocation
seonghobae Sep 3, 2026
637da32
fix(workflow): fail closed on unproven effect-start authority
seonghobae Sep 3, 2026
891fa74
test(workflow): require durable terminal authority
seonghobae Sep 3, 2026
3685ce5
fix(workflow): verify durable terminal authority
seonghobae Sep 4, 2026
4f4625d
test(workflow): reject substituted task-effect claim authority
seonghobae Sep 4, 2026
da225d1
fix(workflow): bind runner claims to admitted task authority
seonghobae Sep 4, 2026
c744f9d
test(workflow): reject impossible runner claim attempts
seonghobae Sep 4, 2026
f3a0226
fix(workflow): reject impossible runner claim attempts
seonghobae Sep 4, 2026
3f42df7
test(workflow): reject non-canonical runner claim authority
seonghobae Sep 4, 2026
da3c53c
fix(workflow): enforce canonical runner claim identity
seonghobae Sep 4, 2026
fb54bab
test(workflow): reject malformed claim before state port
seonghobae Sep 4, 2026
eca1e63
fix(workflow): admit claim id before state mutation
seonghobae Sep 4, 2026
7188da7
Merge remote-tracking branch 'origin/main' into workflow-state-store-…
seonghobae Sep 4, 2026
83c319b
Merge protected main into docs-gate regression lane
seonghobae Sep 4, 2026
0663476
fix(runtime): narrow canonical execution identity after admission
seonghobae Sep 4, 2026
1b26024
fix(runtime): document durable routing exports
seonghobae Sep 5, 2026
f0685c2
test(workflow): cover failure authority boundaries
seonghobae Sep 5, 2026
7e5ff9c
test(workflow): cover both foreign snapshot identities
seonghobae Sep 5, 2026
19c6fa2
test(workflow): fail on extra durable command payload
seonghobae Sep 5, 2026
10708af
fix(workflow): minimize durable command transport payload
seonghobae Sep 5, 2026
00e871e
test(workflow): pin command operation during transport
seonghobae Sep 5, 2026
1f7f5b9
fix(workflow): snapshot command operation for transport
seonghobae Sep 5, 2026
e88a379
test(workflow): reject nested claim payload leakage
seonghobae Sep 5, 2026
81abbab
test(workflow): cover nested checkpoint transport minimization
seonghobae Sep 5, 2026
f915d13
test(workflow): preserve fail-closed nested payload validation
seonghobae Sep 5, 2026
037ec4e
fix(workflow): minimize nested durable command payloads
seonghobae Sep 5, 2026
74a9493
test(workflow): cover missing durable state authority paths
seonghobae Sep 5, 2026
6793a32
test(workflow): cover unexpected durable state errors
seonghobae Sep 5, 2026
d4e3f27
merge: restack #542 on protected reviewer truth
seonghobae Sep 6, 2026
f6c6af5
merge: restack #543 on protected reviewer truth
seonghobae Sep 6, 2026
6f09d16
merge(context-fabric): restack required-gate regression on protected …
seonghobae Sep 6, 2026
6953eaa
merge(context-fabric): restack durable workflow authority on protecte…
seonghobae Sep 6, 2026
c9c2d86
merge: converge exact-head gate regression onto protected #533 truth
seonghobae Sep 6, 2026
a6970ca
merge: converge exact-head gate regression onto protected #552 truth
seonghobae Sep 6, 2026
93bfa5e
merge: converge durable workflow authority onto protected #552 truth
seonghobae Sep 6, 2026
f07679d
merge: restack exact-head gate regression after #527 trust integration
seonghobae Sep 6, 2026
9236775
merge: restack durable workflow state after #527 trust integration
seonghobae Sep 6, 2026
8e7e0bb
test(workflow): reject deleted retained transition receipts
seonghobae Sep 6, 2026
35f80f2
fix(workflow): fail closed on deleted retained transition receipts
seonghobae Sep 6, 2026
8afef54
test(workflow): reject explicit empty transition ledger
seonghobae Sep 6, 2026
55194e1
fix(workflow): reject explicit empty transition ledger
seonghobae Sep 6, 2026
4185df2
test(workflow): reject missing initialized provenance root
seonghobae Sep 6, 2026
1909f23
fix(workflow): require initialized provenance root
seonghobae Sep 6, 2026
84a2cd0
fix(workflow): preserve legacy first-claim provenance
seonghobae Sep 6, 2026
2bb6076
test(workflow): cover malformed retained receipt rejection
seonghobae Sep 6, 2026
7f743f4
merge(main): restack durable workflow state after trusted source roll…
seonghobae Sep 6, 2026
e255bf1
merge(main): restack exact-head gate regression after protected trust…
seonghobae Sep 6, 2026
4616b5e
test(workflow): isolate malformed retained receipt invariant
seonghobae Sep 6, 2026
b14b37c
merge(restack): rebase #543 onto protected main d9b2a95
seonghobae Sep 7, 2026
46439b1
merge(main): non-force restack durable workflow lane after #526
seonghobae Sep 7, 2026
5cd6341
Merge pull request #543 from ContextualWisdomLab/chore/paths-ignore-fix
seonghobae Sep 7, 2026
6cb43c1
merge(main): non-force restack durable workflow lane after #543
seonghobae Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions docs/adr/0013-durable-workflow-execution-authority.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ADR은 **왜 이 구조를 선택했는지**를 기록합니다. 구현 상태
| [0010](./0010-private-target-review-auth.md) | Proposed | private review target의 첫 live PR lookup부터 single-repository Noema App token을 사용하고 workflow `GITHUB_TOKEN` cross-repository fallback을 금지한다. |
| [0011](./0011-independent-reviewer-governance.md) | Proposed | qualifying formal approval의 eligibility·exact-head·staleness를 검증하고 check/status/scanner/model evidence가 approval을 대체하지 못하게 한다. |
| [0012](./0012-runtime-orchestration-bounded-contexts.md) | Proposed | Agent Runtime, Workflow / Task Execution, Tool / Capability, State / Checkpoint, isolation, policy, observability, recovery의 소유권을 분리하고 provider routing·foreign truth·cross-service SQL을 Noema 경계 밖에 둔다. |
| [0013](./0013-durable-workflow-execution-authority.md) | Proposed | runnable candidate와 durable claim/effect start/terminal recovery/checkpoint commit을 분리하고 bounded transition provenance를 Noema state-store 경계에 둔다. |

## ADR lifecycle

Expand Down
70 changes: 46 additions & 24 deletions docs/product-technical-gap-baseline.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/runtime-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { normalizeGitHubAppPrivateKeyPem } from "./github-app-private-key";
import { evaluateRuntimeReadiness } from "./runtime-readiness";

export { NoemaOidcReplayGuard, NoemaRateLimiter };
export { NoemaWorkflowState } from "./workflow-task-execution/workflow-state-durable-object";

/**
* Runtime bindings required by Noema's production worker entrypoint.
Expand Down
51 changes: 51 additions & 0 deletions src/workflow-task-execution/workflow-recovery-claim.ts
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 src/workflow-task-execution/workflow-state-durable-object.ts
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);
}
Comment thread
seonghobae marked this conversation as resolved.

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);
}
}
}
Loading
Loading