Skip directly to article

Current guidance · concepts baseline 0.2.0-preview.1

Lost response and UNKNOWN

What if a response is lost?

Architecture view

Lost response and UNKNOWN

UNKNOWN preserves uncertainty about the external world when dispatch may have occurred but authoritative evidence cannot yet decide the effect.

Not dispatchedEffect unresolvedDispatch boundary
Lost response and UNKNOWNThe dispatch boundary divides two mutually exclusive outcomes. Before it, a preparation issue is NOT_DISPATCHED. If it may have been crossed, a lost response leaves the effect unresolved as UNKNOWN.Boundary not crossedPreparation / integrationboundary not crossedNOT_DISPATCHEDDispatch boundaryBoundary may have crossedResponse lost / timeouteffect unresolvedEffect unresolvedUNKNOWNpreserve resultDo not blind-redispatch

Boundary not crossed

Dispatch boundary

These sibling branches are mutually exclusive; the boundary is the decision point.

Boundary may have crossed

Response lost / timeouteffect unresolved
UNKNOWN
UNKNOWNpreserve result

Do not blind-redispatch.

Read diagram as text
  1. IF BOUNDARY NOT CROSSEDpreparation or integration issue; result is NOT_DISPATCHED
  2. IF BOUNDARY MAY HAVE CROSSEDresponse lost, timed out, or unusable; effect is unresolved
  3. UNKNOWNpreserve the result and seek authoritative evidence later

Protected limits: Do not blind-redispatch an UNKNOWN attempt.

UNKNOWN means dispatch may have crossed the boundary while the effect remains unproven. More precisely, the provider boundary may have been crossed, but available evidence cannot determine whether the intended external effect exists. This is unresolved world state, not merely a software exception. It is not success, automatic recovery, or permission to redispatch. Verifier-only recovery performs zero provider redispatches.

Why the dispatch location matters

Treating UNKNOWN as failure and repeating the action may duplicate an effect. Treating it as success may mislead the user. The location of a transport failure helps explain which conclusion is safe:

  • A timeout before dispatch may be a pre-dispatch outcome.
  • A lost response after dispatch leaves the external effect uncertain.
  • An unavailable verifier after dispatch also leaves effect truth unresolved.

The dispatch boundary records what Core can know about the lifecycle; it does not itself establish the state of the external system.

Worked verifier binding from the selected HTTP example

The supplied Generic HTTP reference 0.2.0-preview.3 uses Core 1.0.0-rc.1, Runtime 0.2.0-preview.5, and Actions 0.2.0-preview.8. In its src/action.mjs, the host saves the original tenant, business input, operation, execution, effect, and provider dispatch attempt identities before the synthetic /reserve POST. Its separate verifier reads /effects/{effectIdentity} and compares the returned record with that saved dispatch binding. The essential callback shape is:

verifier: {
  verifierIdentity: 'initial-exact-reservation-reader-v1',
  async verify({ effectIdentity }) {
    const sent = state.effects[effectIdentity]; // host-owned original dispatch binding
    if (!sent) return { kind: 'INDETERMINATE', observationRef: 'effect-read:unbound' };
    let response;
    try { response = await client(`${providerURL}/effects/${encodeURIComponent(effectIdentity)}`); }
    catch { return { kind: 'INDETERMINATE', observationRef: 'effect-read:unavailable' }; }
    const record = response.data;
    const fields = ['tenant', 'equipmentId', 'callerName', 'operationIdentity',
      'executionIdentity', 'effectIdentity', 'attemptIdentity'];
    const matches = value => fields.every(field => value?.[field] === sent[field]);
    if (response.status !== 200 || !matches(record))
      return { kind: 'INDETERMINATE', observationRef: 'effect-read:unmatched' };
    if (matches(record.reservation) && record.reservation.status === 'reserved'
        && record.reservation.quantity === 1)
      return { kind: 'PRESENT', observationRef: 'effect-read:exact' };
    if (!record.reservation && record.rejection?.permanent === true
        && typeof record.finalityRef === 'string'
        && record.finalityRef === record.rejection.reference)
      return { kind: 'ABSENT_FINAL', observationRef: 'effect-read:final', finalityRef: record.finalityRef };
    return { kind: 'INDETERMINATE', observationRef: 'effect-read:nonfinal' };
  },
}

This is a shortened view of the existing customer example, not an additional package interface. state.effects, client, and the synthetic finality rule belong to that host. In Actions preview.8, the initial verifier callback also receives attemptIdentity: "verification"; that read marker is not the provider dispatch attempt saved in sent.attemptIdentity. Comparing the two would turn a real matching effect into an unsupported UNKNOWN, as the refund DX exercise initially did. A real integration must use its own authoritative system and finality rule; provider acknowledgment, a missing row, or a transport failure alone cannot produce PRESENT or ABSENT_FINAL.

Safe application response

Do not blind-redispatch. Preserve the original immutable result and its proposal, grounded referent, authorization context, identities, and observations. The application can show a bounded pending or unable-to-confirm state while it retains approved correlation references. UNKNOWN is not permission to poll forever, and Core does not automatically poll, schedule checks, run a queue, operate monitoring, or choose a support workflow.

Core records and preserves UNKNOWN, protects the exact reconciliation authority, enforces a verifier-only follow-up, prevents provider redispatch through reconciliation, and returns bounded classification. The application or integrator chooses the wording, retention, access, escalation, support workflow, later checking, and the point at which the case leaves a pending state.

Exact live reconciliation authority

Actions live reconciliation accepts the exact UNKNOWN result object returned by the protected operation. Retain that object in memory and pass that same object to the same ProtectedActionRuntime. The authority is process-local, identity-bound, and one-shot. Clones, spreads, reconstructed objects, serialized values, results from another runtime instance, terminal results, and already-consumed results carry no live authority.

import type { ProtectedActionResult, ProtectedActionRuntime } from '@answerup/actions';

// Pass the exact returned UNKNOWN object, not a property or reconstructed value.
export function reconcileInitial(runtime: ProtectedActionRuntime, initial: ProtectedActionResult) {
  if (initial.kind !== 'UNKNOWN') throw new Error('not live-reconcilable');
  return runtime.reconcile(initial, { claimPlan: {
    propositions: {
      COMMITTED: { propositionRef: 'effect:committed' },
      DISPROVED: { propositionRef: 'effect:did-not-occur' },
      UNKNOWN: { propositionRef: 'live-observation:unresolved' },
    },
  } });
}

A reconciliation performs verifier-only authoritative read-back and zero provider redispatch. With the optional LiveReconciliationClaimPlan, the verifier establishes truth before Runtime mechanically selects the matching one of three mutually exclusive propositions for Core evaluation. Unused branches never become authority. It creates a related result without overwriting the original UNKNOWN. A matching PRESENT observation can produce COMMITTED; supported ABSENT_FINAL can produce DISPROVED; unavailable, stale, contradictory, or non-final evidence may return a related UNKNOWN. That new exact UNKNOWN may carry the next one-shot live association; the consumed input does not. The one-argument path remains proposition-specific and fail-closed.

Durable verifier-only recovery after process loss

Runtime durable recovery can rehydrate verifier-only continuation after process loss from a closed DurableRecoveryRecordV2 through accepted recovery authority and stable verifier binding. Its preferred RecoveryClaimPlan supplies mutually exclusive COMMITTED, DISPROVED, and UNKNOWN alternatives; recovered truth is established before the matching proposition is selected and Core-evaluated. The legacy single-proposal overload remains compatibility behavior. This separate host-operated path validates the recovery record and provenance, preserves the accepted preview.3 version-2 record compatibility, recovery identities, and source-record digest/CAS behavior, and performs authoritative read-back of the original effect. It performs zero provider redispatch. It does not reconstruct Actions live authority and cannot authorize or redispatch mutation. Live reconciliation and durable recovery are not interchangeable.

The host must durably retain the required record, source record, verifier registration, routing, authority controls, and any required leases, fences, and CAS state. It remains responsible for retry scheduling, monitoring, and bounded delivery. This is not automatic recovery, exactly-once execution, hosted AnswerUp recovery, universal or production-grade durability, or a claim that every host persists those prerequisites.

Three non-interchangeable paths

  • Initial execution dispatches the provider once, verifies initial effect truth, and may use registration-owned InitialExecutionClaimPlan.
  • Live reconciliation stays in the same process, consumes the exact Actions-owned UNKNOWN object once, calls only the verifier, performs no redispatch, and may use LiveReconciliationClaimPlan.
  • Durable recovery can continue after process loss only from host-retained durable material and accepted authority; it is verifier-only, uses lease/fencing/CAS ownership, performs no redispatch, and uses RecoveryClaimPlan.

An authorized initial UNKNOWN plan branch appears only as RELATED_INITIAL_VERIFICATION_OBSERVATION_ONLY; live uses RELATED_LIVE_RECONCILIATION_OBSERVATION_ONLY; recovery uses RELATED_RECOVERY_OBSERVATION_ONLY. Each is historical authority about one exact observation. None establishes that the effect is still unresolved now, success, failure, retry, redispatch, compensation, or future-state truth.

Example: submitting a supplier payment

An accounts-payable application sends an authenticated backend an approved payment instruction. A payment-provider adapter dispatches it, the boundary is crossed, and the response is lost. The initial result is UNKNOWN. Resubmitting may create a duplicate payment, so the application does not send the instruction again or tell the supplier that payment is complete.

If the exact in-memory UNKNOWN result remains available, verifier-only reconciliation reads the authoritative payment ledger. A matching ledger entry supports COMMITTED; supported final absence can support DISPROVED; unavailable, stale, contradictory, or non-final ledger evidence may remain UNKNOWN. After process loss, a host can rehydrate verifier-only recovery only from its closed durable recovery record through the accepted recovery authority and stable verifier binding; it cannot reconstruct live authority or recovery authority from the payment request, a log, or a provider request ID.

What later evidence can establish

Later authoritative evidence may support a new narrow statement tied to the original operation. It does not retroactively change the original immutable result, prove that duplication was impossible, or authorize a different action. A later retry or compensation is a new proposal requiring fresh grounding and authorization.

The integrator defines bounded retention, access, escalation, and user-facing transitions. Core does not operate the monitoring or support process. Durable verifier-only recovery is host-operated and is not supplied by copying or serializing a live result.