Skip to main content

Online edition for BLOGE 0.9.8-RC1 · facts verified 2026-09-15 · 中文

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_INVALIDRESUME 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.

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.