@fabricorg/platform-host 7.6.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,201 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 8d87a36: Release note: failure semantics (issue #79)
8
+
9
+ Once any adapter step has a durable succeeded row or accepted handoff, subsequent Host store
10
+ read/write errors raise RecoverableFinalizationError instead of failing the invocation. This
11
+ includes later checkpoints, recovery-time store operations, obligation reads and final completion
12
+ in either event phase. Idempotent recovery repairs recorded evidence without repeating that
13
+ provider call. A later step's definite failure or pre-call refusal can still fail the invocation.
14
+ Authorization/handler failures, validation/state rejection and unsatisfied obligations on recovery
15
+ retain their existing outcomes. The handler still re-runs and saga compensation is unchanged.
16
+
17
+ Recovery reads checkpoints before input generation or adapter lookup, replays accepted/success
18
+ events by stable ID (`recovered: true` on newly repaired success events), reuses existing stable-ID
19
+ attestations, and reapplies their obligation updates. Missing attestations can be extracted again
20
+ from stored safe input/output and checkpoint timestamps. Extractors must be deterministic for the
21
+ same input. No stored output means no extraction, including lost-marker handoffs and withheld output.
22
+
23
+ With neither success marker nor handoff durable, Host retries the failed checkpoint write once.
24
+ A second failure selects existing degraded atomic finalization or a verdict-first fallback to
25
+ reconciliation_required. Fallback checkpoint/events are best effort; terminal verdicts can retain
26
+ evidence gaps and automatic recovery excludes them. If even the verdict cannot be written, Host
27
+ raises RecoverableFinalizationError and a running row remains eligible for idempotent replay under
28
+ its stable adapterInvocationId within the worker budget. Lease loss retains its fencing error.
29
+
30
+ The optional recoveryMaxAttempts is a positive integer. Its effective default is
31
+ max(5, retryableExecutionMaxAttempts); Infinity is rejected. An explicit recovery budget below the
32
+ governance retry cap is rejected. Both caps count the same worker claims. With default settings,
33
+ pre-policy deferrals exhaust on claim 5; recovery exhaustion settles on claim 6 before mutation work.
34
+ Adopters who raised the governance cap get a matching implicit recovery budget.
35
+
36
+ The plain budget verdict commits before evidence reads. Running rows without a matching handoff
37
+ settle ambiguous; older ambiguous rows enrich the verdict from recorded evidence. A durable handoff
38
+ takes precedence over its running row and is recorded as an applied effect with its external
39
+ reference in the message and supported reconciliation record. Failed resolution/reads leave the
40
+ plain verdict intact. Failed enrichment can retain gaps. Transition telemetry emits once when
41
+ status changes. Unwritable verdicts leave claims continuing without executing handlers or providers.
42
+
43
+ Inline/direct execution does not increment the counter. These callers can now receive an error
44
+ whose name is RecoverableFinalizationError where 7.x returned failed; the class is not exported.
45
+ An unleased invocation remains running and is not claimed by workers. Callers must re-run
46
+ executeInvocation themselves and cap their own retries. Approval acceptance sets the counter to
47
+ at least 1 without incrementing it on resume. Approval's temporary lease uses the existing budget:
48
+ equal is admitted, above settles reconciliation. Approval does not reset the budget. A worker dying
49
+ before the check consumes a claim without calling a provider there. Parked handoffs are unleased
50
+ and are not claimed. ADR 0003 records the bounded exception to recoverable finalization.
51
+
52
+ An empty-reference handoff refusal retains its specific error if its verdict cannot be saved and
53
+ does not mark the step succeeded through generic degraded handling.
54
+
55
+ Both inline and leased non-idempotent interruption gates inspect adapter checkpoints: running rows
56
+ without a matching handoff settle unknown. Succeeded rows and durable handoffs end reconciliation
57
+ because a provider effect was applied. No rows or only failed rows retain failure. Idempotent
58
+ recovery still replays definite failed rows when invocation failure persistence was interrupted.
59
+ Known limit: if a later step's attempt-preparation write keeps failing, its row stays `running`
60
+ although the provider was not called, and a non-idempotent action's next claim settles that step
61
+ as an unknown outcome. Public options only grow; no store method is added. Adapter-row lease
62
+ fencing and overdue-sweep/late-callback behavior are unchanged.
63
+
64
+ - 4f56809: Fence adapter checkpoint creation and updates with the optional `writeAdapterInvocation` store
65
+ capability. Built-in stores atomically check the scoped parent lease or settled verdict and expected
66
+ adapter status; conditional writes cannot reset succeeded or ambiguous rows to running or failed.
67
+ Host captures execution credentials before reading live records. Lease/status refusal raises
68
+ `RecoverableFinalizationError`; changed verdict evidence is refused and reported through
69
+ `invocation.adapter_evidence_refused` telemetry. Memory transactions recheck at commit and
70
+ PostgreSQL locks the parent before updating or creating the child. Atomic finalizers require the
71
+ transaction-scoped capability when the store advertises it; callback writes use their transaction's
72
+ adapter-update seam. Parent enrichment joins the post-verdict evidence collector: a refusal is
73
+ reported through telemetry, a missing capability skips only enrichment, and a thrown error is raised
74
+ after the remaining evidence writes are attempted. Successful enrichment saves the parent error
75
+ and reconciliation outcome for replay. Verdict conditions carry `completionAdapterInvocationId`
76
+ and check the external-completion snapshot for that handoff row, preserving real callback output
77
+ even when its parent status/error
78
+ stays the same. Unrelated rows retain status/error and expected-row-status fencing. Isolation probes
79
+ retire their parent rows, and prepare-attempt lease refusals bypass provider retry/backoff.
80
+
81
+ This is part of the declared 8.0.0 major. No schema migration is added. Custom stores without the
82
+ capability remain compatible with unchecked adapter writes and a weaker stale-worker guarantee.
83
+ Inline guard reads and transactions apply only when the execution snapshot or patch carries a
84
+ handoff. Handoffs imported into running invocations by other writers are unsupported.
85
+ See MIGRATION-8.md.
86
+
87
+ Public API additions: `AdapterInvocationWriteCondition`, `AdapterInvocationPatch`,
88
+ `AdapterInvocationWriteInput`, and `UpdateSettledActionInvocationInput`;
89
+ optional `PlatformHostStore.writeAdapterInvocation` and `updateSettledActionInvocation`;
90
+ and optional `PlatformHostMutationTransaction.writeAdapterInvocation`.
91
+ `PlatformHostTelemetryEventName` gains `invocation.adapter_evidence_refused`, which requires
92
+ updates to exhaustive switches over that union. Subclasses customizing unchecked adapter writes
93
+ must also customize the checked write seam, or disable it and accept the weaker guarantee.
94
+
95
+ - 438be3f: Release note: late completion semantics (issue #79).
96
+
97
+ Overdue sweeps retain the handoff and set optional
98
+ `pendingCompletion.overdueAt` with the reconciliation verdict. Callbacks for
99
+ parked running invocations can settle even past their deadline before the
100
+ sweep. From reconciliation, settlement requires parking and the marker or the
101
+ fallback below. Callbacks for unparked invocations already
102
+ `reconciliation_required` retain the verdict and record redacted provider
103
+ evidence without result-schema validation. Unparked `running` invocations
104
+ still refuse callbacks with `still_executing`. Any parked row without the
105
+ marker, including a 7.x row, retains the weaker `"did not complete by"`
106
+ error-substring compatibility path. PostgreSQL hydrates the marker as a Date
107
+ from existing JSONB; no migration changes are required.
108
+
109
+ Sweeps re-read eligibility under the transaction row lock where supported and
110
+ use transaction-scoped reconciliation evidence when available. Inline
111
+ post-handoff writes preserve the sweep verdict while committing independent
112
+ adapter evidence. Reaching parking records the handler result and `parkedAt`
113
+ while keeping status, error, and `overdueAt`. Successful `after_adapters`
114
+ finalization commits its deferred domain events; without a local handoff it
115
+ records the result/events without fabricating parking. Inline callers receive
116
+ the latest reconciliation record observed by the preserving writer, merged
117
+ with its evidence update, without another read or a stale attempt fallback.
118
+ Leased paths retain `RecoverableFinalizationError` on fencing failure.
119
+ Verdict-first fallbacks attempt evidence writes independently, including
120
+ accepted/succeeded/ambiguous events, governance reconciliation, the outer
121
+ refused-handoff reconciliation append, and adapter checkpoints. A failed write
122
+ does not skip later evidence; the first failure raises
123
+ `RecoverableFinalizationError` after the remaining attempts. This also replaces
124
+ post-success fallback error suppression. The verdict remains durable and failed
125
+ writes can leave terminal evidence gaps. Atomic finalization and its outer
126
+ logging behavior retain their existing semantics; sweep errors are preserved.
127
+ Stores without the transaction lock retain a read/write race; without transaction-scoped reconciliation appends,
128
+ evidence can survive a rolled-back verdict. Neither check cancels an adapter
129
+ already in flight or undoes previously committed domain writes. Custom stores
130
+ must preserve and reload the optional marker.
131
+
132
+ Refused handoffs now retain their provider reference in the step's succeeded
133
+ event, including when explicit output omits it. Capable transactions append
134
+ the refusal reconciliation record with that evidence; fallback writes attempt
135
+ it independently of the other evidence writes. The outer append uses the same
136
+ stable id. An event failure does not skip reconciliation or the adapter marker.
137
+
138
+ Inline callers should retry only before a handoff is durable: re-running
139
+ `executeInvocation()` on an unleased running handoff returns without resuming
140
+ (it first settles any step already recorded `ambiguous`).
141
+ If execution stopped before parking, callbacks refuse with `still_executing`
142
+ until a sweep. Declare `completionDeadlineMs` for actions that hand off, then
143
+ reconcile surfaced rows per the external-completion runbook.
144
+
145
+ Handoff-free inline writes skip the guard's extra read and transaction. The
146
+ sweep accepts null lease owners from custom stores. Custom transactions should
147
+ implement both invocation locking and reconciliation appends to avoid blocking
148
+ an outer append on the invocation lock. Upgrade hosts and workers together:
149
+ 7.x sweeps omit the typed marker, and 7.x inline writers lack the new guard.
150
+
151
+ - 8d87a36: Version 8 changes the meaning of some invocation statuses: outcomes Host cannot know, and Host's own write failures after a provider call, settle `reconciliation_required` where 7.x reported `failed`. The public API only grows. `MIGRATION-8.md` in the package lists what to check before upgrading.
152
+
153
+ ### Minor Changes
154
+
155
+ - a205c48: Reconcile unknown adapter outcomes (issue #76). Platform's README describes the Host behavior; Platform runtime code is unchanged.
156
+
157
+ **Release note: failure semantics (issue #76).** Returned `timeout` outcomes, timeouts thrown from `adapter.execute` (including an aborted adapter deadline), contradictory results in either direction (`success: true` with a non-success outcome, or `success: false` with `outcome: "success"`), uninterpretable replies (non-objects, `null`, or arrays), getters that throw before Host interprets the reply, and unrecognised outcome strings such as `"timed_out"` now end `reconciliation_required` instead of `failed`, using the existing `adapter_outcome_ambiguous` kind and `AdapterInvocationAmbiguous` event payload and stable ID. Returned `ambiguous` outcomes use the same settlement path. These unknown outcomes are not retried by the retry helper. Governance stores receive `ExternalReconciliation` evidence even without provider `outcomeEvidence`, because a timed-out call may have applied its effect without returning an external reference.
158
+
159
+ Host detects a timeout thrown from `adapter.execute` from the adapter deadline's aborted signal or an error message matching `/timeout|deadline|timed?\s*out/i`. A definite rejection whose message matches therefore also routes to reconciliation; the matcher does not prove a provider timeout. Reverse contradictions are classified as ambiguous for retry and circuit-breaker accounting, rather than as success.
160
+
161
+ Host copies the reply's contract fields and other own enumerable fields into a plain object before interpretation, reading each top-level field once. A getter that throws during the copy takes the unreadable-reply path. Object replies retain Host's previous truthiness reading of `success`, including truthy non-boolean values from JavaScript adapters.
162
+
163
+ Unknown-outcome settlement uses `transactionWithEvents` when the transaction has `updateAdapterInvocation`, governance `appendExternalReconciliation` where applicable, and leased `updateLeasedActionInvocation` where required. Host commits the ambiguous checkpoint, stable ambiguity event, reconciliation record, and fenced terminal verdict together, in that write order. It retries the transaction once on a non-lease error; a second non-lease failure selects the verdict-first fallback. Lease loss raises `RecoverableFinalizationError` without retry or fallback.
164
+
165
+ The fallback writes `reconciliation_required` first through the existing verdict helper (one retry except on lease loss), then writes checkpoint, event, and reconciliation evidence. After the verdict commits, Host attempts each remaining evidence write (checkpoint, event, record), then surfaces the first error as `RecoverableFinalizationError`. This includes attempting the reconciliation record after an event-write failure and attempting both event and record when both checkpoint shapes fail. Evidence failure can leave a terminal evidence gap that recovery does not repair. If both verdict attempts fail, Host raises `RecoverableFinalizationError` without attempting an unfenced checkpoint. With the original action definition and step order available, recovery re-settles a saved ambiguous row before parked-handoff shortcuts, stale-action gates, authorization, or handler execution, for idempotent and non-idempotent actions. The pending-handoff shortcut retains its base condition (running with a pending handoff and no lease owner, without requiring `parkedAt`); its position after the new recorded-ambiguity check is the change. For a running invocation whose action resolves, `executeInvocation` reads each adapter step's row before the shortcut, stopping on recorded ambiguity or a failed read. A failed read surfaces `RecoverableFinalizationError`, where base 939bcad returned the parked result. If the action no longer resolves, the shortcut still returns the handoff before the unknown-action failure.
166
+
167
+ The optional `AdapterInvocationRecord.reconciliation` checkpoint preserves redacted reason, external reference, and the original event subject; the subject is excluded from the returned `AdapterReconciliationOutcome`. Recovery does not scan the space's event log. Legacy rows use the recorded error as message and reason, and default to the `AdapterInvocation` subject keyed by `adapterInvocationId`. Stable event and reconciliation IDs retain deduplication.
168
+
169
+ Ordinary adapter writes omit the migration 8 column. On checkpoint-write failure, fallback retries once without `reconciliation`, retaining legacy `status: "ambiguous"` plus `error`. Schemas without migration 8 still settle unknown outcomes, without the checkpoint metadata. Atomic PostgreSQL failures roll back before this legacy write runs outside the transaction. Apply migration 8 (`adapter_reconciliation_checkpoint`) through controlled `ensureSchema()` to retain metadata. Existing migration checksums are unchanged. Custom stores should persist the optional checkpoint through adapter create/update/read methods.
170
+
171
+ On a store without atomic support, if the verdict cannot be written at all, no durable write after the call succeeds (neither verdict nor checkpoint), as after a crash mid-call. Idempotent recovery may re-run a leftover running step under the same `adapterInvocationId` idempotency key. The same exposure applies if two atomic attempts roll back and the fallback verdict cannot be written. Non-idempotent recovery retains its stale-action gate without an ambiguous checkpoint. Before another provider attempt, Host resets a prior failed row to `running` and clears its old error.
172
+
173
+ Post-success Host write failures still end `failed`, as in 7.6.0, even though the provider succeeded. Accepted-handoff persistence and its `ExternalOperationAccepted` event, the succeeded marker, attestation extraction and recording, and `AdapterInvocationSucceeded` failures again write the adapter row `failed` and fail the invocation. Later execution returns that terminal verdict; lease loss retains its fencing error. If fencing refuses the failure verdict after the adapter row was saved as `failed`, idempotent recovery can re-run the step; this pre-existing 7.6.0 case remains for follow-up. A follow-up issue will cover these post-success failures. The existing 7.6.0 degraded-success paths for withheld output and refused handoffs retain their reconciliation semantics and documented re-call exposure when the verdict cannot be persisted. Final `after_adapters` domain-event/invocation completion retains its recovery path.
174
+
175
+ A deadline sweep followed by a late completion callback can settle `completed` when the `ambiguous` evidence of a later step was never written.
176
+
177
+ Circuit-open refusals, deadline expiry before execution (including between retry attempts), `prepareAttempt` store errors, returned `permanent_failure`, exhausted `transient_failure`, and exhausted non-timeout adapter throws retain `failed` and their existing event shapes. Adapters already express definite non-effects through `permanent_failure` and `transient_failure`; there is no new adapter-side definite-failure flag. `after_adapters` completion events remain withheld while reconciliation is required.
178
+
179
+ ### Patch Changes
180
+
181
+ - 939bcad: Build packages with tsdown in place of tsup. Preserve the published ESM/CommonJS
182
+ entry paths, ES2022 target, declaration filenames, and raw external module
183
+ specifier spellings, including the existing Node built-in spellings.
184
+
185
+ Generated JavaScript, source-map contents, declaration formatting, and internal
186
+ chunk layout change. Comparison with the saved tsup build found unchanged named
187
+ runtime exports and public declaration signatures. Declaration maps remain
188
+ unpublished. Facade maps are no longer published for Platform's root, evidence,
189
+ and policies entries, or experience-runtime's contracts entry (both `.js.map`
190
+ and `.cjs.map`). Their new JavaScript output has no source-map URL comment.
191
+
192
+ CommonJS interop: 29 of 72 baseline CJS files expose an `__esModule` marker;
193
+ 0 of 57 tsdown CJS files do. The compared package entries have no default runtime
194
+ export, and the emitted `.d.cts` files declare none. Typed consumers of the
195
+ unchanged declared exports are unaffected. For entries whose marker was removed,
196
+ an untyped CJS `import X from` compiled with a Babel-style default-interop helper
197
+ now yields the namespace instead of `undefined`.
198
+
3
199
  ## 7.6.0
4
200
 
5
201
  ### Minor Changes
package/MIGRATION-8.md ADDED
@@ -0,0 +1,247 @@
1
+ # Migrating to @fabricorg/platform-host 8
2
+
3
+ **Version 8 changes what some invocation statuses mean.** The public API only
4
+ grows; no export is removed or renamed. But an invocation that 7.x recorded as
5
+ `failed` may now end `reconciliation_required`, and code that reads `failed` as
6
+ "the provider did nothing" has to handle the new outcome. That change in meaning
7
+ is why this is a major release.
8
+
9
+ ## Unknown adapter outcomes settle `reconciliation_required` (issue #76)
10
+
11
+ In 7.x, an adapter call whose outcome Host could not know ended `failed`. That
12
+ covered a thrown timeout, a returned `timeout`, and a reply Host could not read.
13
+ The provider may have acted, so a caller that retried on `failed` could repeat
14
+ an SMS or a submission.
15
+
16
+ From 8.0.0 these outcomes settle like a returned `ambiguous` always has:
17
+
18
+ - the adapter row ends `ambiguous`;
19
+ - Host appends an `AdapterInvocationAmbiguous` event and, on governance stores,
20
+ an `ExternalReconciliation` record;
21
+ - the invocation ends `reconciliation_required` with `adapterReconciliation`.
22
+
23
+ Refusals that made no call (circuit open, an expired deadline) and definite
24
+ provider failures still end `failed`.
25
+
26
+ ### What to check
27
+
28
+ - **Callers and retry logic.** Treat `reconciliation_required` as "a person or
29
+ a reconciliation job must establish what happened". Do not retry it the way
30
+ you retry `failed`.
31
+ - **Status fences in your own store or domain code.** A fence that admits only
32
+ `completed` or `failed` as terminal statuses will refuse the new verdict, and
33
+ the invocation will stay `running` and be recovered again. Admit
34
+ `reconciliation_required`, at least when the invocation has an `ambiguous`
35
+ adapter row.
36
+ - **PostgreSQL.** Run `ensureSchema()` to apply migration 8, which adds
37
+ `adapter_invocations.reconciliation`. Host still settles on a schema without
38
+ it, but stores less evidence.
39
+ - **Custom stores.** Persist the optional
40
+ `AdapterInvocationRecord.reconciliation` field and accept it in
41
+ `updateAdapterInvocation` patches.
42
+
43
+ ## Truthful, bounded post-provider recovery (issue #79 part 1)
44
+
45
+ In 7.x, a Host write could fail after the provider reported success and leave
46
+ the invocation `failed`. Host 8 makes those errors recoverable once a success
47
+ marker or handoff is durable. Host store reads and writes stay recoverable
48
+ after that point in either event phase. This includes later checkpoints,
49
+ obligation reads, and the final completion write. Idempotent recovery repairs
50
+ the recorded step without calling its provider again. The handler still
51
+ re-runs before the adapter loop.
52
+
53
+ Non-idempotent recovery ends `reconciliation_required` instead of repairing
54
+ the invocation. A durable handoff takes precedence over its running row. Host
55
+ records it as an applied effect and keeps the external reference in the
56
+ message. A governance store also receives that reference in its reconciliation
57
+ record. A running row without a durable handoff is settled as unknown. A
58
+ succeeded row ends `reconciliation_required` as an applied effect. No rows or
59
+ only failed rows keep the existing interruption failure.
60
+
61
+ If neither checkpoint is durable, Host retries the failed write once. A
62
+ second failure selects degraded atomic finalization or a verdict-first
63
+ fallback. The fallback attempts the succeeded marker and safe events
64
+ independently, raising the first evidence error as
65
+ `RecoverableFinalizationError` after the remaining writes are attempted. A
66
+ terminal verdict can still have missing evidence. If the verdict cannot be
67
+ saved, Host raises `RecoverableFinalizationError`. The row can remain
68
+ running. An idempotent action can call the provider a second time for that
69
+ unrecorded outcome. It can also call the provider again for a definite
70
+ failed row whose invocation verdict was lost. An empty-reference handoff
71
+ refusal keeps its specific message if its verdict write fails. That failure
72
+ does not create a succeeded marker.
73
+
74
+ The optional `recoveryMaxAttempts` must be a positive integer. Its effective
75
+ default is `max(5, retryableExecutionMaxAttempts)`. Infinity is rejected. An
76
+ explicit recovery budget below the governance retry cap is rejected too. Both
77
+ settings count worker claims. The first execution claim counts as 1. With the
78
+ default of 5, the first run and four recoveries can execute. The sixth claim
79
+ settles `reconciliation_required` before the handler or adapters run.
80
+ Transient governance deferrals reach their own default cap on claim 5 first.
81
+
82
+ On exhaustion, Host writes a plain verdict before reading evidence. A readable
83
+ running step without a handoff is settled as unknown. A recorded ambiguous row
84
+ conditionally enriches the parent error and `adapterReconciliation` with its
85
+ saved evidence; successful enrichment makes response and replay agree. A handoff is recorded as an
86
+ applied effect instead. Failed reads leave the plain verdict already written
87
+ standing. Enrichment refusal or a missing capability skips only that parent write;
88
+ checkpoint, event and reconciliation evidence are still attempted. Store errors
89
+ are raised after the remaining independent writes. If the verdict itself
90
+ cannot be saved, later claims continue without running mutation code.
91
+
92
+ Inline and direct execution do not increment the claim counter. Approval
93
+ acceptance sets it to at least 1 without adding a claim on resume. Approval
94
+ resume uses its temporary lease and the existing count. A count equal to the
95
+ budget is admitted. A count above it settles reconciliation before the handler
96
+ runs. Approval does not reset the budget. A worker dying before the check
97
+ consumes a claim without calling a provider there. Parked handoffs have no
98
+ lease and are not claimed by workers.
99
+
100
+ Known limits remain. A later step's definite failure or pre-call refusal can
101
+ still end `failed` after an earlier success. Authorization and ordinary
102
+ handler failures on recovery keep their existing outcomes. Validation, state
103
+ guards, and unsatisfied obligations also keep their terminal outcomes. These
104
+ are not Host store errors. Saga compensation is unchanged.
105
+
106
+ ### What to check
107
+
108
+ - **Worker budgets.** If you raised `retryableExecutionMaxAttempts`, the
109
+ implicit recovery budget now rises with it. An explicit
110
+ `recoveryMaxAttempts` must be at least that retry cap. Both settings use the
111
+ same claim counter. Recovery also bounds evidence and completion failures.
112
+ - **Evidence extractors.** Return deterministic evidence for the same stored
113
+ input and output. Extraction can run again if the attestation was not saved.
114
+ Host reuses an existing stable-ID attestation and reapplies its obligation
115
+ updates. Missing output means extraction is skipped.
116
+ - **Store and action definitions.** Retain adapter rows, stable IDs, safe
117
+ input/output, and the original step order. No new store method is required.
118
+ Terminal verdicts can need manual evidence repair. Recovery reads
119
+ checkpoints before input generation and adapter lookup.
120
+ - **Callers.** Inline `submitAction()` without a dispatcher and direct
121
+ `executeInvocation()` can now throw where 7.x returned `failed`. Recognise
122
+ `name === "RecoverableFinalizationError"`. The class is not exported.
123
+ Without a lease, the invocation stays `running` and workers do not claim it.
124
+ Before a handoff is durable, re-run `executeInvocation()` yourself and cap
125
+ your own retries. Once a handoff is durable, re-running `executeInvocation()`
126
+ on the unleased running row returns without resuming execution (it first
127
+ settles any step already recorded `ambiguous`). If execution
128
+ stopped before parking, callbacks are refused with `still_executing` until a
129
+ sweep. Declare `completionDeadlineMs` for actions that hand off so the sweep
130
+ surfaces the row, then reconcile it per the
131
+ [external-completion runbook](../../docs/operations/runbooks.md#overdue-handoffs).
132
+ The worker budget does not bound unleased calls. A previously leased call can be
133
+ reclaimed after lease expiry. Handle terminal `reconciliation_required`
134
+ separately. Do not blindly resubmit an applied effect under a new key.
135
+
136
+ ## Late completion requires a parked, swept handoff (issue #79 part 3)
137
+
138
+ In 7.x, any reconciliation error containing `"did not complete by"` could let a
139
+ late callback settle an invocation, even if execution had stopped before parking.
140
+ From 8.0.0, the sweep writes optional `pendingCompletion.overdueAt` alongside its
141
+ verdict. Settlement from reconciliation requires `parkedAt` and that marker,
142
+ subject to the markerless fallback below. Callbacks for unparked invocations
143
+ already `reconciliation_required` record their redacted result/outcome as
144
+ evidence without result-schema validation and retain the existing verdict.
145
+ Callbacks for unparked `running` invocations are still refused with
146
+ `still_executing`.
147
+
148
+ Any parked row without the marker, including a 7.x row, still matches the old
149
+ substring. This path is weaker because unrelated error text on such a row can
150
+ also match.
151
+
152
+ ### What to check
153
+
154
+ - **Operators and callbacks.** A provider's completion does not establish that
155
+ an unparked invocation finished its remaining steps. Inspect its callback
156
+ evidence and reconcile that work explicitly.
157
+ - **Custom stores.** Preserve and hydrate optional `overdueAt` as a Date. No
158
+ PostgreSQL migration is needed for this JSONB member. Transaction-scoped
159
+ `getActionInvocationForUpdate` serializes the sweep's eligibility check with
160
+ the verdict; `appendExternalReconciliation` puts its evidence in the same
161
+ transaction. Without those capabilities, a read/write race or an evidence gap
162
+ remains; see the README.
163
+ - **Inline callers.** Independent adapter evidence commits without replacing
164
+ the sweep verdict. Reaching parking records the handler result and `parkedAt`
165
+ while retaining the sweep's status, error and `overdueAt`. Successful
166
+ `after_adapters` finalization commits its deferred domain events.
167
+ Inline entry points return the latest record observed by the preserving
168
+ writer, merged with its evidence update, after these writes succeed. No
169
+ post-attempt read or stale attempt fallback is used. Leased paths retain
170
+ `RecoverableFinalizationError` on fencing failure. In-flight adapters and
171
+ earlier domain writes are not undone; stores without a transaction lock
172
+ retain a read/write race. Evidence writes retain their existing retry and
173
+ error handling.
174
+ - **Mixed-version deployments.** Upgrade all inline hosts and workers together.
175
+ A 7.x sweep writes no `overdueAt`. An 8.x inline execution that was still
176
+ unparked under that verdict records its result but does not add `parkedAt`;
177
+ its callbacks stay evidence-only. A 7.x inline host can still overwrite an
178
+ 8.x sweep verdict because it lacks the new write guard.
179
+
180
+ ## Lease-checked adapter rows (issue #79 part 2)
181
+
182
+ Implement the optional `writeAdapterInvocation` on custom stores to
183
+ atomically authorize adapter creates and updates against their scoped
184
+ parent. Support both running owner/token lease conditions and exact settled
185
+ status/error conditions, plus expected row statuses. Host captures the handoff
186
+ row ID in `completionAdapterInvocationId`; only that row also checks
187
+ `externalCompletionDigest` (absence requires no recorded completion; a supplied
188
+ digest requires that same completion). A callback can retain status/error while
189
+ changing completion and the handoff row's output. Unrelated adapter rows still
190
+ use status/error and expected-row-status fencing, so their ambiguity evidence
191
+ can be recorded after that callback. Omitting the captured row ID retains the
192
+ conservative completion check on adapter writes. Reject conditional
193
+ `succeeded`/`ambiguous` to `running`/`failed` transitions even without an
194
+ expected-status list. Lock the parent before the adapter in PostgreSQL; an
195
+ unlocked `EXISTS` is insufficient against concurrent claims under READ
196
+ COMMITTED. Buffered memory implementations must recheck at commit. Expose
197
+ the same method on mutation transactions to retain atomic leased
198
+ finalization; otherwise Host chooses the verdict-first fallback before
199
+ evidence writes.
200
+
201
+ Built-in stores implement this without a new migration. Host captures
202
+ execution credentials and uses conditional writes for leased checkpoints and
203
+ post-verdict evidence. Lease/status refusal raises
204
+ `RecoverableFinalizationError`; post-verdict refusal leaves the adapter row
205
+ untouched and emits `invocation.adapter_evidence_refused` telemetry. Host
206
+ still attempts ambiguity and reconciliation evidence, and
207
+ withheld-output/refused-handoff success evidence may already have been
208
+ appended. Callback adapter writes share the callback's transaction when its
209
+ adapter-update member exists. A post-verdict refusal is not an
210
+ evidence-collector error. Thrown store errors are raised after every
211
+ independent fallback evidence write has been attempted, preserving the first
212
+ error.
213
+
214
+ A preserving inline write never adopts the sweep verdict as this execution's
215
+ own. Its adapter evidence keeps the unchecked inline path, and
216
+ settled-parent enrichment applies only to a verdict the execution wrote. The
217
+ inline guard reads the current row only when the execution snapshot or its patch
218
+ carries `pendingCompletion`. Handoffs written into running invocations by anything
219
+ other than their executing step are not supported.
220
+
221
+ Stores omitting the capability remain compatible and keep unchecked adapter
222
+ writes, including the risk that a stale worker overwrites a newer
223
+ checkpoint. After its own verdict, an inline execution uses verdict-conditioned
224
+ adapter writes too; earlier unleased writes retain their existing path. Implement
225
+ optional `updateSettledActionInvocation` to enrich a settled parent under the
226
+ full status/error/completion snapshot condition, regardless of adapter row ID.
227
+ The two optional methods are expected together. Without the parent-update method,
228
+ only parent enrichment is skipped; adapter checkpoints, events and reconciliation
229
+ evidence are still attempted. Refused enrichment emits
230
+ `invocation.adapter_evidence_refused` with `evidence: "parent_enrichment"` and is not
231
+ an error. A thrown enrichment error joins the evidence collector, which raises
232
+ the first error after the remaining writes are attempted. Successful enrichment
233
+ stores the budget error and `adapterReconciliation` before returning them;
234
+ skipped or refused enrichment can leave replay with only the plain verdict.
235
+
236
+ ### Built-in store subclasses
237
+
238
+ A subclass overriding `createAdapterInvocation` or `updateAdapterInvocation`
239
+ to encrypt or audit adapter data must also override
240
+ `writeAdapterInvocation`: checked writes can bypass the unchecked methods.
241
+ Disabling the capability preserves those overrides and accepts weaker
242
+ stale-worker protection. Parent annotations use
243
+ `updateSettledActionInvocation`; override that seam too when customizing
244
+ parent evidence persistence.
245
+
246
+ `PlatformHostTelemetryEventName` adds `invocation.adapter_evidence_refused`.
247
+ Update exhaustive switches over this union when upgrading.