Skip to main content

Online edition for BLOGE 0.9.9-RC1 · prepared 2026-11-02 · 中文

Chapter 26 — Durable and Temporal Verification

Chapter promise: You will verify suspension, signal, resume, and logical time on one retained execution with a finite action plan, while keeping final outcome, forbidden-before, and required-within facts separate.

Learning goals​

  1. Use EXECUTE, SIGNAL, ADVANCE_TIME, and RESUME instead of flattening a long-running flow into one method call.
  2. Separate a local DurableVerificationRunner report from DurableSourceBoundVerifier source-bound evidence.
  3. Express finite step, trace, and effect facts with DurableTemporalContract.
  4. Recognize streaming, timer, checkpoint, and non-cooperative timeout stop boundaries.

Final output cannot show the waiting path​

Two manual-review executions may both finish as approved. One sends the notification after the human decision; the other sends it while still waiting. A final-output-only test misses the second defect.

Figure 27: Durable actions, retained session, and temporal observations

DurableVerificationRunner creates a real DurableGraphEngine and retained session for one Case. Its default stores are isolated and in memory. Actions run serially against one execution identity; logical time is verifier-controlled and does not depend on elapsed wall-clock test time.

An action plan is a bounded script, not a workflow language​

This path starts the flow and receives approval within 24 hours:

schemaVersion: 1
actions:
- actionId: start
type: EXECUTE
- actionId: wait-before-decision
type: ADVANCE_TIME
duration: PT23H
- actionId: approve
type: SIGNAL
nodeId: approval
signalData: approved

The constraints matter more than the syntax:

  • A plan contains 1–64 actions.
  • Its first and only EXECUTE action is first.
  • ADVANCE_TIME requires a positive ISO-8601 duration.
  • SIGNAL must identify an exact currently suspended node.
  • RESUME is for a failed execution with a stored checkpoint. A suspended execution needs SIGNAL first.
  • CANCEL has no stable durable seam and returns DURABLE_ACTION_UNSUPPORTED from the sidecar loader.

Use a separate plan for the timeout path, advancing logical time to PT24H or the business-approved boundary. “Signal received” and “no signal received” are two business Cases, not branches to hide in one linear plan.

Check step state and event order together​

schemaVersion: 1
stepExpectations:
- expectationId: initially-suspended
afterActionId: start
outcome: SUSPENDED
suspendedNodeIds: [approval]
traceExpectations:
- expectationId: notify-after-approval
matcher: MUST_NOT_OCCUR_BEFORE
event: {type: NODE_COMPLETED, graphName: approval-flow, nodeId: notification}
afterActionId: approve
- expectationId: notify-within-five-minutes
matcher: MUST_OCCUR_WITHIN_LOGICAL_TIME
event: {type: NODE_COMPLETED, graphName: approval-flow, nodeId: notification}
within: PT5M

stepExpectations inspect the outcome and suspended set after an action. traceExpectations consume BLOGE listener facts: node identity, owning action, and logical-time offset. They do not evaluate scripts, exception text, or raw business payloads.

Use temporal v2 for duplicate business effects:

schemaVersion: 2
effectExpectations:
- expectationId: ledger-booked-once
matcher: EXACTLY_ONCE_EFFECT_KEY
effect: {portId: ledger.book, key: LA-42}

Every participating Operator needs a complete dependency manifest, and the target EffectPort must declare a stable idempotencyKey(input). Successful, failed, and denied calls all count because the external system may accept an effect before the caller sees a failure. The report stores a typed digest, not the raw key.

Run local durable verification​

DurableVerificationPlan plan = DurableVerificationPlan.load(actionPath);
DurableTemporalContract temporal = DurableTemporalContract.load(temporalPath);

DurableVerificationReport report = DurableVerificationRunner.builder()
.harnessTimeout(Duration.ofSeconds(30))
.build()
.verify(
new DurableVerificationInput(
new VerificationCaseContext("loan-review", "approved-in-time"),
compiledGraph,
initialContext,
compiledOperators),
plan,
temporal);

This report is local authoring evidence. Publishing source-bound durable evidence requires DurableSourceBoundVerifier to compile tracked graph and sidecar inputs from a clean Git worktree, a freezable registry and resolver, a DURABLE execution profile, and an ignored non-classpath output directory. Its reader does not call customer bootstrap to recompile the candidate.

Observe the same final output with a different order​

Run two versions of the same manual-review graph. Both end with decision=approved; only the notification position changes.

Figure 26-2: equal final output, different temporal verdict

control: approve → notification → final approved
mutant: notification → approve → final approved

DurableTemporalContractTest.detectsAnEventThatOccurredBeforeItsRequiredActionDespiteFinalSuccess executes the mutant. The final action still records COMPLETED, but the report is FAIL, its only reason code is TEMPORAL_EXPECTATION_MISMATCH, and notification-after-approval.matched=false.

This one test separates three facts that a final JSON object would collapse:

  • the durable execution completed;
  • the final business value was available;
  • the required event order was violated.

The focused RC1 run at cc38fbe5 executed 19 temporal-contract tests and 11 durable-scenario tests: 30 passed, with no failures, errors, or skips. The evidence is a finite declared relation, not a claim that every possible interleaving was explored.

A temporal mismatch is a business FAIL​

When a valid run completes but notification occurs before approve, the result is FAIL with TEMPORAL_EXPECTATION_MISMATCH. That differs from an invalid sidecar: an unknown action, bad node identity, incomplete dependency manifest, or unsupported capability composition is normally INVALID.

  • DURABLE_SIGNAL_TARGET_INVALID — the signal does not identify the suspended node. Inspect the suspended set; do not broaden the target.
  • DURABLE_RESUME_STATE_INVALID — RESUME was used on a suspended execution. Use SIGNAL; reserve resume for a failed checkpointed execution.
  • DURABLE_CHECKPOINT_MISSING — no checkpoint exists. Stop and inspect persistence; do not fabricate one.
  • DURABLE_TIMER_SET_AMBIGUOUS — one time advance expires multiple timers. Preview fails closed; split the plan or revise the model.
  • STREAM_DURABLE_UNSUPPORTED — a streaming graph entered the durable wrapper. Test the streaming boundary; do not claim undeclared support.
  • TEMPORAL_EXPECTATION_MISMATCH — a valid temporal contract did not match. Treat it as business FAIL and inspect the first divergent event.

After a harness timeout, the runner interrupts customer code and waits for settlement grace. A task that still does not exit produces DURABLE_HARNESS_ORPHANED; the runner is then poisoned and must not admit another plan in the same process.

Lab: signal path and timeout path​

  1. Create approved-in-time: EXECUTE → ADVANCE_TIME PT23H → SIGNAL approved.
  2. Require notification not to occur before approve, and to complete within the business-approved logical duration afterward.
  3. Create a separate review-timeout: EXECUTE → ADVANCE_TIME PT24H, expecting the timeout path.
  4. Move notification before the signal and confirm FAIL/TEMPORAL_EXPECTATION_MISMATCH; restore it, then verify both paths.
  5. Preserve the action trace, temporal expectation result, and first reason code. Do not put raw signal payloads in the report.

Stop before opening a session if streaming classification is incomplete, a custom nested provider cannot be statically inspected, or effect inventory is incomplete. Repair the capability declaration before discussing business results.

Transfer: an insurance-claim document wait​

An insurance claim may reach the same final APPROVED value whether missing documents arrived before the review deadline or after an escalation was sent. Model document arrival as a signal, the service-level deadline as logical time, and escalation-before-approval as a temporal relation. Preserve the event trace; the final claim object alone cannot tell those histories apart.

Chapter boundary​

  • ADVANCE_TIME advances verifier-owned logical time; it does not simulate real scheduling latency.
  • Step and trace contracts prove named finite relations, not every possible interleaving.
  • A persistence provider supplies per-Case stores and a unique mutable-resource namespace; the verifier still owns the engine, action order, and shutdown.
  • Source-bound manifests contain a canonical SHA-256 of initial context, not business payloads.

Experiment acceptance card​

  • Expected and observed: The same final output still fails when event order violates the temporal expectation.
  • Failure and recovery: Move notification before approval; restore order and replay.
  • Proof boundary: Proves timing for the fixed action plan, not every concurrency interleaving.
  • Exercise contract: Approval and notification events; swap order only; deliver matched results; stop when order alone explains the failure.

Coding practice​

BP-14 — Test independent answers and forbidden actions​

Use approved expected answers, not current outputs. Observe business results, node paths, and effect evidence independently; mock only the boundary whose behavior is intentionally controlled.

Try one change: Run the workshop blacklist-priority case and its reordered-rule mutant. Explain why preserving old behavior, rejecting one mutant, and proving all policies correct are different claims.

Reasoning, counterexamples, and limits

Summary​

Long-flow correctness combines outcomes, state boundaries, and event-time relations. A finite action plan makes those facts repeatable; a temporal sidecar makes them diagnosable. The next chapter asks whether passing Cases can actually detect a wrong rule.

Next: Chapter 27 — Finding Verification Blind Spots

Coding Agent: Open the versioned task guide.