@fabricorg/platform-host 7.6.0 → 8.1.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 +263 -0
- package/MIGRATION-8.md +247 -0
- package/README.md +471 -47
- package/dist/index.cjs +5943 -4393
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1619 -1405
- package/dist/index.d.ts +1619 -1405
- package/dist/index.js +5934 -4388
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -68,7 +68,8 @@ delay is the remaining lease duration, followed by the worker's next claim; ther
|
|
|
68
68
|
retry or separate backoff. The next claim increments `attemptCount`. At the cap, or for an unleased,
|
|
69
69
|
non-idempotent, or approval-resume invocation, Host records `failed` with the reason retry was refused,
|
|
70
70
|
through the error-text redactor. Approval resume is excluded because its temporary lease does not
|
|
71
|
-
establish that a recovery worker is configured. Ordinary
|
|
71
|
+
establish that a recovery worker is configured. Ordinary application governance errors retain terminal failure behavior. Host store errors after
|
|
72
|
+
a recorded provider effect follow the recovery rules below.
|
|
72
73
|
|
|
73
74
|
The retry region includes Host's single-record store writes for mutation-governance and HITL
|
|
74
75
|
checkpoints. Later attempts reuse these persisted decisions through the `mutationFootprint` and
|
|
@@ -76,7 +77,9 @@ checkpoints. Later attempts reuse these persisted decisions through the `mutatio
|
|
|
76
77
|
A deferral happens before the attempt writes any policy evidence: the complete definition batch must
|
|
77
78
|
evaluate before Host starts appending policy records. Policy persistence, the `ComplianceBlocked`
|
|
78
79
|
event, obligation listing/resolution/persistence, state-machine checks, the handler transaction,
|
|
79
|
-
handlers
|
|
80
|
+
handlers and adapters retain their separate failure semantics. Finalization recovery is bounded by
|
|
81
|
+
`recoveryMaxAttempts`. Its effective default is at least `retryableExecutionMaxAttempts` because
|
|
82
|
+
both count worker claims.
|
|
80
83
|
A retryable obligation-resolver error fails terminally with the policy evidence from that attempt.
|
|
81
84
|
Preview evaluation does not enter this execution catch.
|
|
82
85
|
|
|
@@ -114,10 +117,10 @@ Host does not run adapters after this signal or add a new lifecycle event for it
|
|
|
114
117
|
committed by earlier attempts are not undone.
|
|
115
118
|
|
|
116
119
|
The signal does not change failures in pre-handler governance or validation, state-transition guard
|
|
117
|
-
exceptions, adapters, or finalization.
|
|
118
|
-
paths. State guards still fail closed.
|
|
119
|
-
`
|
|
120
|
-
|
|
120
|
+
exceptions, adapters, or finalization. Application governance errors and definite adapter failures retain their
|
|
121
|
+
`failed` paths; post-success errors follow the recovery rules below. State guards still fail closed.
|
|
122
|
+
Transient `after_adapters` finalization failures escape as `RecoverableFinalizationError` for
|
|
123
|
+
recovery within `recoveryMaxAttempts`. An ordinary handler error still records `failed`. This signal requests an operator decision,
|
|
121
124
|
not an automatic retry or a claim that the current transaction committed.
|
|
122
125
|
|
|
123
126
|
New rows bind that key to the action version, actor/authority binding, and a versioned canonical
|
|
@@ -127,16 +130,295 @@ parameter comparison and call `onLegacyRecord`; actor, authority-binding, and ac
|
|
|
127
130
|
still reject in enforce mode. See `MIGRATION-2-IDEMPOTENCY.md`.
|
|
128
131
|
|
|
129
132
|
Lifecycle records use deterministic checkpoint ids. Recovered idempotent actions do not repeat an
|
|
130
|
-
adapter that already reached `succeeded
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
-
|
|
133
|
+
adapter that already reached `succeeded`. Built-in stores deduplicate event and policy inserts by
|
|
134
|
+
checkpoint ID. Adapter rows are mutable; stale-worker protection requires the optional
|
|
135
|
+
`writeAdapterInvocation` capability described below. A
|
|
136
|
+
reclaimed non-idempotent action inspects its adapter checkpoints: running or succeeded rows route
|
|
137
|
+
to reconciliation; no rows or only failed rows retain terminal failure for manual review.
|
|
138
|
+
|
|
139
|
+
Returned `ambiguous` or `timeout` outcomes, timeouts thrown from `adapter.execute` (including an
|
|
140
|
+
aborted adapter deadline), contradictory successful results, and reverse contradictions
|
|
141
|
+
(`success: false` with `outcome: "success"`) route to `reconciliation_required`.
|
|
142
|
+
Host detects a thrown timeout from the adapter deadline's aborted signal or an error message matching
|
|
143
|
+
`/timeout|deadline|timed?\s*out/i`. A definite rejection whose message matches therefore also routes to
|
|
144
|
+
reconciliation; the message matcher does not prove that the provider timed out.
|
|
145
|
+
|
|
146
|
+
An uninterpretable provider reply (a non-object, `null`, or an array), a getter that throws before Host
|
|
147
|
+
can interpret the reply, and an unrecognised outcome string such as `"timed_out"` also take this path.
|
|
148
|
+
Host copies the reply's contract fields and other own enumerable fields into a plain object before
|
|
149
|
+
interpretation, reading each top-level field once. A getter that throws during the copy takes the
|
|
150
|
+
unreadable-reply path. These outcomes are not retried by the retry helper. Object replies retain the
|
|
151
|
+
previous truthiness reading of `success`, including truthy non-boolean values from JavaScript adapters.
|
|
152
|
+
The reconciliation kind remains `adapter_outcome_ambiguous`.
|
|
153
|
+
|
|
154
|
+
Unknown-outcome settlement uses `transactionWithEvents` when its transaction supplies
|
|
155
|
+
`updateAdapterInvocation`, `appendExternalReconciliation` on a governance-capable store, and
|
|
156
|
+
`updateLeasedActionInvocation` for a leased invocation. A store advertising conditional adapter writes
|
|
157
|
+
also needs transaction-scoped `writeAdapterInvocation` for leased finalization. In that transaction Host writes the
|
|
158
|
+
`ambiguous` checkpoint, `AdapterInvocationAmbiguous` with its stable event ID, governance
|
|
159
|
+
`ExternalReconciliation` evidence where supported, and the fenced `reconciliation_required` verdict,
|
|
160
|
+
in that order. These writes commit together. Host retries once on a non-lease error; a second
|
|
161
|
+
non-lease failure selects the verdict-first fallback. Lease loss raises
|
|
162
|
+
`RecoverableFinalizationError` without retry or fallback.
|
|
163
|
+
|
|
164
|
+
Without those transaction members, or after two non-lease transaction failures, the verdict-first
|
|
165
|
+
fallback writes the terminal verdict (retrying once except on lease loss), then the checkpoint,
|
|
166
|
+
event, and reconciliation record. If both checkpoint shapes fail, Host still attempts the ambiguity
|
|
167
|
+
event and reconciliation record. After the verdict commits, Host attempts each remaining evidence
|
|
168
|
+
write even if the event append fails, then surfaces the first error as `RecoverableFinalizationError`.
|
|
169
|
+
An evidence failure can leave a terminal verdict with missing evidence; a later direct execution returns that verdict
|
|
170
|
+
and workers exclude it from recovery, without repairing the evidence gap. If both verdict attempts
|
|
171
|
+
fail, Host surfaces `RecoverableFinalizationError` without attempting an unfenced checkpoint.
|
|
172
|
+
|
|
173
|
+
With the original action definition and adapter-step order available, recovery checks `ambiguous`
|
|
174
|
+
rows before the parked-handoff shortcut, stale-action gates, authorization, and handler execution.
|
|
175
|
+
This applies to actions declared idempotent and actions not declared idempotent. Settlement uses the
|
|
176
|
+
stored reconciliation metadata or the legacy row's error as its message and reason, without running
|
|
177
|
+
the handler or an adapter. For a running invocation whose action resolves, `executeInvocation` reads
|
|
178
|
+
each adapter step's row before the pending-handoff shortcut, stopping on recorded ambiguity or a
|
|
179
|
+
failed read. A failed read surfaces `RecoverableFinalizationError`, where base 939bcad returned the
|
|
180
|
+
parked result. The shortcut's condition is unchanged from base: a running invocation with
|
|
181
|
+
`pendingCompletion` and no lease owner returns early without requiring `pendingCompletion.parkedAt`.
|
|
182
|
+
If the action no longer resolves, the shortcut still returns the handoff before the unknown-action
|
|
183
|
+
failure. Completion callbacks that settle the invocation require parking, including after a deadline sweep.
|
|
184
|
+
|
|
185
|
+
`AdapterInvocationRecord.reconciliation` is an optional, redacted checkpoint preserving reason,
|
|
186
|
+
external reference, and the original `adapterEventSubject`. The subject stays out of the returned
|
|
187
|
+
`AdapterReconciliationOutcome`. Recovery uses this checkpoint without scanning `listEvents()`.
|
|
188
|
+
Without stored subject metadata it uses `{ subjectType: "AdapterInvocation", subjectId: adapterInvocationId }`;
|
|
189
|
+
the stable event ID deduplicates an event already written. Custom stores should persist the checkpoint
|
|
190
|
+
through adapter create, update, and read methods.
|
|
191
|
+
|
|
192
|
+
Ordinary adapter writes do not name the migration 8 column and remain compatible with schemas
|
|
193
|
+
lacking it. If a checkpoint write carrying `reconciliation` fails, fallback retries the row once
|
|
194
|
+
without that field, using legacy `status: "ambiguous"` plus `error`. Unknown outcomes therefore still
|
|
195
|
+
settle without migration 8, but without stored checkpoint metadata. Apply PostgreSQL migration 8
|
|
196
|
+
through `ensureSchema()` in controlled startup or migrations to retain that metadata. An error inside
|
|
197
|
+
an atomic PostgreSQL transaction first rolls it back; the legacy write runs outside that transaction
|
|
198
|
+
on the fallback path.
|
|
199
|
+
|
|
200
|
+
On a store without atomic support, if the verdict cannot be written at all, no durable write after the
|
|
201
|
+
call succeeds: neither verdict nor checkpoint is saved, as after a crash mid-call. A leftover `running`
|
|
202
|
+
step can then re-run on recovery for an idempotent action under the same `adapterInvocationId`
|
|
203
|
+
idempotency key, within the worker recovery budget. A reclaimed non-idempotent action with a
|
|
204
|
+
running row without a matching durable handoff settles that step as unknown. A succeeded row or
|
|
205
|
+
durable handoff instead ends `reconciliation_required` as an applied effect. Adapters should be
|
|
206
|
+
idempotent or carry that stable key. The same exposure applies if two atomic attempts roll back
|
|
207
|
+
and the fallback verdict cannot be written.
|
|
208
|
+
|
|
209
|
+
Once an invocation has a durable `succeeded` adapter row or accepted handoff, errors in Host
|
|
210
|
+
store reads or writes raise `RecoverableFinalizationError` instead of ending it `failed`.
|
|
211
|
+
This includes later adapter checkpoints, obligation reads, and the completion write in both
|
|
212
|
+
`before_adapters` and `after_adapters`. Recovery also protects Host store operations before
|
|
213
|
+
and around the handler when an earlier provider effect is recorded. Authorization, policy and handler callbacks
|
|
214
|
+
retain their own failure semantics. Idempotent recovery repairs recorded success/handoff evidence
|
|
215
|
+
without calling that step's provider. Non-idempotent interruption ends `reconciliation_required`
|
|
216
|
+
instead of repairing the invocation. Lease loss retains its fencing error.
|
|
217
|
+
|
|
218
|
+
If neither record is durable, Host retries the failed handoff or marker write once. After a
|
|
219
|
+
second failure it reuses degraded atomic finalization where available, otherwise writes the
|
|
220
|
+
`reconciliation_required` verdict first, then attempts the succeeded marker with safe output,
|
|
221
|
+
`ExternalOperationAccepted` when applicable, and `AdapterInvocationSucceeded`. Each fallback
|
|
222
|
+
evidence write is attempted independently; the first error raises `RecoverableFinalizationError`
|
|
223
|
+
after the remaining writes are attempted. One exception keeps its 7.x behaviour: when
|
|
224
|
+
unsatisfied obligations end a handed-off invocation `reconciliation_required` and the
|
|
225
|
+
reconciliation-log append fails, Host still returns the verdict. A terminal verdict can retain
|
|
226
|
+
missing evidence. If even the
|
|
227
|
+
verdict cannot be saved, Host throws `RecoverableFinalizationError`; the row can remain `running`.
|
|
228
|
+
Recovery may replay that unrecorded outcome for an idempotent action under its stable
|
|
229
|
+
`adapterInvocationId`, within the worker budget. A reclaimed non-idempotent action instead
|
|
230
|
+
ends `reconciliation_required`. A durable handoff takes precedence over its running row and is
|
|
231
|
+
recorded as an applied effect with its external reference. A running row without such a handoff
|
|
232
|
+
is settled as unknown. A succeeded row also ends `reconciliation_required`. With no rows or only failed rows, the existing
|
|
233
|
+
non-idempotent interruption gates still fail the invocation.
|
|
234
|
+
|
|
235
|
+
For idempotent recovery, the handler still runs before the adapter loop. The loop reads each
|
|
236
|
+
checkpoint before `getInput()` or adapter lookup. Succeeded rows and durable handoffs use stored
|
|
237
|
+
adapter type, operation, vendor, input and output; a falsy new input or removed adapter does not
|
|
238
|
+
skip evidence repair. Stable event IDs deduplicate replayed accepted and success events; newly
|
|
239
|
+
repaired success events include `recovered: true` and stored input/output when available.
|
|
240
|
+
|
|
241
|
+
Recovery looks up the stable attestation ID. If present, Host reuses it and reapplies its
|
|
242
|
+
`satisfiesObligations` updates without calling the extractor. If absent and stored output exists,
|
|
243
|
+
Host calls `extractExecutionAttestation` with stored input/output and checkpoint `createdAt` and
|
|
244
|
+
`updatedAt` as start/completion times. The extractor may run again after an interrupted append;
|
|
245
|
+
it must return deterministic evidence for the same input. A handoff checkpoint with no output,
|
|
246
|
+
or withheld output, skips extraction. Repair errors remain recoverable within the budget.
|
|
247
|
+
|
|
248
|
+
Known limits: a later step's definite provider failure or pre-call refusal can still end the
|
|
249
|
+
invocation `failed` after an earlier step succeeded. Authorization denials, ordinary handler
|
|
250
|
+
failures, validation/state-guard rejection, and unsatisfied policy obligations on recovery retain
|
|
251
|
+
their existing terminal outcomes. These are decisions or application failures, not Host store
|
|
252
|
+
errors. Idempotent recovery can call a provider again for an unrecorded running outcome, or for a
|
|
253
|
+
definite failed row whose invocation failure verdict was lost. The handler still re-runs before
|
|
254
|
+
the adapter loop. Saga compensation is unchanged. Withheld-output and refused-handoff
|
|
255
|
+
reconciliation retain the degraded pattern. Adapter-row fencing and overdue-sweep/late-callback
|
|
256
|
+
rules are described below.
|
|
257
|
+
|
|
258
|
+
Event appends are first-write-wins by event ID. Host re-appends lifecycle events under stable IDs
|
|
259
|
+
on retry and recovery, and each later attempt carries a fresh `sequence` and timestamps and can carry
|
|
260
|
+
a different payload. A custom store's `appendEvent` and `appendEventWithOutbox` must treat an ID it
|
|
261
|
+
already holds as a no-op that keeps the first event: do not throw, replace, or merge it. A store that
|
|
262
|
+
refuses these appends can leave recovery unable to converge, so the invocation stays `running` until
|
|
263
|
+
the recovery budget settles it, even when the adapter effect succeeded. Custom stores that currently
|
|
264
|
+
throw on a conflicting append need to change. The shipped stores enforce ID uniqueness across the
|
|
265
|
+
whole store, not per tenant or space. The rule covers the outbox too: `appendEventWithOutbox` for an
|
|
266
|
+
ID already stored adds no outbox row, including when the first append went through plain
|
|
267
|
+
`appendEvent`, so that event is never published later. Keep `outbox.shouldPublish` deterministic for
|
|
268
|
+
a given event ID. Custom store authors can verify these paths with the
|
|
269
|
+
[event append conformance checks](#event-append-conformance).
|
|
270
|
+
|
|
271
|
+
### Conditional adapter-row writes (8.0.0, issue #79)
|
|
272
|
+
|
|
273
|
+
`PlatformHostStore.writeAdapterInvocation(input)` is optional. Memory and PostgreSQL implement it;
|
|
274
|
+
custom stores can keep their existing methods. Without it, Host retains unchecked adapter creates
|
|
275
|
+
and updates, so a reclaimed worker can overwrite a newer worker's adapter checkpoint.
|
|
276
|
+
|
|
277
|
+
The input carries `scope` (tenant, space, invocation), a create or update `mutation`, a `condition`,
|
|
278
|
+
and optional `expectedStatuses`. A lease condition requires a `running` parent with the supplied
|
|
279
|
+
`leaseOwner` and `leaseToken`. A verdict condition requires the exact parent `status` and `error`
|
|
280
|
+
written by this execution. Host also captures the handoff row ID as
|
|
281
|
+
`completionAdapterInvocationId`, so `externalCompletionDigest` guards that row's callback output:
|
|
282
|
+
an absent digest requires no recorded completion; a supplied digest requires that same completion.
|
|
283
|
+
Other adapter rows retain the status/error and expected-row-status checks without the digest check.
|
|
284
|
+
Omitting the captured row ID retains the conservative digest check on adapter writes. Absent error
|
|
285
|
+
matches no error. Both parent and adapter must match scope.
|
|
286
|
+
A create must also match the record's scope and refuses a duplicate ID. The method returns `false`
|
|
287
|
+
when its conditions fail. Conditional updates reject `succeeded` or `ambiguous` to `running` or
|
|
288
|
+
`failed`, including when callers omit `expectedStatuses`.
|
|
289
|
+
|
|
290
|
+
Host captures the supplied execution credentials before loading the invocation. It uses them for
|
|
291
|
+
fenced invocation writes and leased adapter creation, retry preparation, success, failure, and
|
|
292
|
+
ambiguity checkpoints. A refused lease or row-status check raises `RecoverableFinalizationError`.
|
|
293
|
+
This prevents stale checkpoint writes; adapters still need stable idempotency keys because a lease
|
|
294
|
+
cannot cancel a provider call already in flight. Inline writes use the verdict condition after
|
|
295
|
+
this execution writes its own verdict; earlier unleased adapter writes retain their existing path.
|
|
296
|
+
|
|
297
|
+
Verdict-first fallbacks use the settled verdict condition after the terminal write releases the
|
|
298
|
+
lease. Their expected row status is the last state observed or written by that execution. A changed
|
|
299
|
+
parent status/error or row refuses the adapter update; a changed completion also refuses a write
|
|
300
|
+
to the captured handoff row. These refusals emit
|
|
301
|
+
`invocation.adapter_evidence_refused` (`fabric.platform.invocation.adapter_evidence_refused`) through
|
|
302
|
+
the telemetry hook. The adapter row stays untouched, with no unchecked retry. Host still attempts
|
|
303
|
+
`AdapterInvocationAmbiguous` and reconciliation evidence; withheld-output and refused-handoff
|
|
304
|
+
success evidence may already have been appended before the refused marker write. A refusal is not
|
|
305
|
+
an evidence-collector error. Thrown store errors are retained while each independent fallback
|
|
306
|
+
write is attempted, then the first error is raised as `RecoverableFinalizationError`.
|
|
307
|
+
|
|
308
|
+
An inline writer preserving a sweep verdict does not adopt it as this execution's own verdict.
|
|
309
|
+
Its adapter evidence keeps the unchecked inline path; conditional settled-parent enrichment is
|
|
310
|
+
used only for a verdict this execution actually wrote. The inline guard reads the current row
|
|
311
|
+
only when the execution snapshot or its patch carries `pendingCompletion`.
|
|
312
|
+
|
|
313
|
+
Budget exhaustion saves its plain verdict before reading evidence. It then enriches the parent
|
|
314
|
+
through optional `updateSettledActionInvocation`, comparing that execution's exact status, error,
|
|
315
|
+
and completion snapshot. Parent enrichment joins the independent evidence collector, alongside
|
|
316
|
+
adapter checkpoints, events and reconciliation records. Refusal emits
|
|
317
|
+
`invocation.adapter_evidence_refused` with `evidence: "parent_enrichment"` and is not an error;
|
|
318
|
+
a missing `updateSettledActionInvocation` skips only parent enrichment. Evidence writes still run.
|
|
319
|
+
A thrown store error is collected and raised as `RecoverableFinalizationError` after the remaining
|
|
320
|
+
writes are attempted. The two optional store methods are expected together. Successful enrichment
|
|
321
|
+
stores the error and `adapterReconciliation` before returning them; skipped or refused enrichment
|
|
322
|
+
can leave replay with only the plain verdict. Stores omitting `writeAdapterInvocation` retain
|
|
323
|
+
unchecked adapter writes. A retained handoff's reference is included in the initial budget verdict.
|
|
324
|
+
|
|
325
|
+
Subclasses of the built-in stores that override `createAdapterInvocation` or
|
|
326
|
+
`updateAdapterInvocation` for encryption or auditing must also override `writeAdapterInvocation`.
|
|
327
|
+
Checked writes can bypass the unchecked methods. Alternatively, explicitly disable the capability
|
|
328
|
+
and accept the weaker stale-worker guarantee. Parent annotations similarly have their own
|
|
329
|
+
`updateSettledActionInvocation` seam.
|
|
330
|
+
|
|
331
|
+
Atomic finalizers require transaction-scoped `writeAdapterInvocation` when the store advertises the
|
|
332
|
+
capability and execution holds a lease. A missing member selects verdict-first fallback before
|
|
333
|
+
transaction evidence writes. PostgreSQL locks the matching parent with `FOR UPDATE` before the
|
|
334
|
+
adapter mutation in one SQL statement. Memory rechecks buffered lease and adapter predicates at
|
|
335
|
+
commit and excludes claims and other conditional writes while applying the commit. A commit-time
|
|
336
|
+
conflict rolls back and raises `RecoverableFinalizationError`. No schema migration is added by this
|
|
337
|
+
capability; it uses the existing parent lease and verdict columns.
|
|
338
|
+
|
|
339
|
+
External-completion adapter writes use the callback's transaction-scoped `updateAdapterInvocation`
|
|
340
|
+
when available, sharing its invocation lock. These are callback writes rather than worker-lease
|
|
341
|
+
writes; legacy transactions without that member retain the outer-store write. The sweep retains
|
|
342
|
+
its transaction-scoped reconciliation append and parked/overdue settlement rules.
|
|
343
|
+
|
|
344
|
+
### Bounded worker recovery (8.0.0)
|
|
345
|
+
|
|
346
|
+
`recoveryMaxAttempts` is an optional positive integer. Its effective default is
|
|
347
|
+
`max(5, retryableExecutionMaxAttempts)`; `Infinity` is rejected. Both caps count the same worker
|
|
348
|
+
claims. Construction rejects an explicit `recoveryMaxAttempts` below
|
|
349
|
+
`retryableExecutionMaxAttempts`. The governance cap stops transient pre-policy deferrals at its
|
|
350
|
+
cap; the recovery budget stops resumed execution above its cap. With the defaults, governance
|
|
351
|
+
retry exhaustion therefore happens on claim 5 before recovery exhaustion could happen on claim 6.
|
|
352
|
+
|
|
353
|
+
Host checks a resumed leased `running` invocation immediately after lease ownership, before
|
|
354
|
+
normal action resolution, ambiguity scans, authorization, the handler, or adapter work. When
|
|
355
|
+
`attemptCount > recoveryMaxAttempts`, it settles `reconciliation_required` with
|
|
356
|
+
`recovery attempts exhausted (n/max)`. It writes that verdict before resolving the action or
|
|
357
|
+
reading adapter evidence, releasing the lease. A running row without a matching durable handoff
|
|
358
|
+
receives unknown-outcome settlement. A recorded ambiguous row enriches the parent error and
|
|
359
|
+
`adapterReconciliation`, as well as adapter/event evidence, using its stored reconciliation evidence
|
|
360
|
+
and a conditional parent update. A durable handoff takes precedence over that step's running row.
|
|
361
|
+
It is recorded as an applied effect with its external reference in the message and, on governance
|
|
362
|
+
stores, the reconciliation record. It is not marked ambiguous solely because its marker was lost.
|
|
363
|
+
If resolution or row reads throw, or no relevant row exists, the plain verdict already written
|
|
364
|
+
stands. Enrichment failure can retain an evidence gap. Transition telemetry is emitted once when
|
|
365
|
+
the status changes. If the verdict is unwritable, Host throws `RecoverableFinalizationError`;
|
|
366
|
+
later claims continue but do not execute mutation code.
|
|
367
|
+
|
|
368
|
+
The counter counts worker claims, including the first run, rather than provider calls or completed
|
|
369
|
+
recoveries. With the default of 5: the first run plus four recoveries; the sixth claim settles.
|
|
370
|
+
A raised governance cap raises the implicit recovery budget with it. Inline/direct execution does
|
|
371
|
+
not increment the counter. Approval acceptance sets it to at least 1 without incrementing it on
|
|
372
|
+
resume. Approval resume holds a temporary lease and uses the existing count: a count equal to the
|
|
373
|
+
budget is admitted, while a count above the budget settles reconciliation before the handler.
|
|
374
|
+
Approval does not reset an exhausted budget. A worker dying before reaching the check consumes a
|
|
375
|
+
claim without calling a provider at that point. Parked handoffs have no lease, are not claimed by
|
|
376
|
+
workers, and are outside this budget. The existing parked shortcut and ambiguity scan remain in
|
|
377
|
+
place. `retryableExecutionMaxAttempts` still limits pre-policy deferrals. No new store method is
|
|
378
|
+
required.
|
|
379
|
+
|
|
380
|
+
**Inline and direct callers.** Inline `submitAction()` without a dispatcher and direct
|
|
381
|
+
`executeInvocation()` can now reject with an error whose `name` is
|
|
382
|
+
`"RecoverableFinalizationError"` where 7.x returned `failed`. The class is not exported. Without a
|
|
383
|
+
lease, the invocation stays `running` and workers do not claim it. Before a handoff is durable,
|
|
384
|
+
call `executeInvocation()` again yourself and cap your own retries. Once a handoff is durable,
|
|
385
|
+
re-running `executeInvocation()` on the unleased running row returns without resuming execution
|
|
386
|
+
(it first settles any step already recorded `ambiguous`).
|
|
387
|
+
If execution stopped before parking, callbacks are refused with `still_executing` until a sweep.
|
|
388
|
+
Declare `completionDeadlineMs` for actions that hand off so the sweep surfaces the row, then
|
|
389
|
+
reconcile it per the [external-completion runbook](../../docs/operations/runbooks.md#overdue-handoffs).
|
|
390
|
+
The leased worker budget does not bound unleased calls. A
|
|
391
|
+
previously leased direct call retains its lease until expiry and can also be recovered by a
|
|
392
|
+
worker. Terminal degraded verdicts can retain evidence gaps and are not automatically repaired.
|
|
393
|
+
|
|
394
|
+
A transaction implementation may rethrow the handler's error wrapped in its own error; Host
|
|
395
|
+
still treats a failure inside the handler callback as a handler outcome.
|
|
396
|
+
|
|
397
|
+
**Known limit.** If a later step's adapter row is created but its attempt-preparation write keeps
|
|
398
|
+
failing, the row stays `running` although the provider was not called. For an action not declared
|
|
399
|
+
idempotent, the next claim settles that step as an unknown provider outcome.
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
A deadline sweep retains the handoff and sets `pendingCompletion.overdueAt` with its
|
|
403
|
+
`reconciliation_required` verdict. A late callback settles that verdict only when the handoff also
|
|
404
|
+
has `parkedAt`, proving Host reached parking after its remaining steps and finalization. An unparked
|
|
405
|
+
handoff stays in reconciliation even if later-step ambiguity evidence is missing; the callback's
|
|
406
|
+
redacted result and outcome are recorded as evidence for an operator without applying the action's
|
|
407
|
+
result schema. This evidence-only behavior applies to invocations already
|
|
408
|
+
`reconciliation_required`; callbacks for unparked `running` invocations are refused with
|
|
409
|
+
`still_executing`. Any parked row without `overdueAt`, including a 7.x row, still uses the old
|
|
410
|
+
`"did not complete by"` error substring. That compatibility path is weaker: an unrelated error
|
|
411
|
+
containing the substring on a parked row lacking the marker can also authorize settlement.
|
|
412
|
+
|
|
413
|
+
Circuit-open refusals and deadline expiry before execution retain `failed` semantics because
|
|
414
|
+
the refused attempt made no provider call. A `prepareAttempt` store error retains its prior failure
|
|
415
|
+
behavior only while the invocation has no durable success or handoff. With an earlier recorded
|
|
416
|
+
effect, an exhausted Host-store error raises `RecoverableFinalizationError`. Returned `permanent_failure`
|
|
417
|
+
and exhausted `transient_failure` outcomes, plus exhausted non-timeout adapter throws, also retain
|
|
418
|
+
`failed` semantics and their existing events, unless reading any field of the reply throws: Host
|
|
419
|
+
copies the reply before interpreting it, so such a reply takes the unreadable-reply path to
|
|
420
|
+
reconciliation. Adapters report definite non-effects with
|
|
421
|
+
`permanent_failure` or `transient_failure`; no additional definite-failure flag is needed.
|
|
140
422
|
|
|
141
423
|
### Adapter circuit breakers
|
|
142
424
|
|
|
@@ -250,9 +532,10 @@ in action parameters or workflow history.
|
|
|
250
532
|
Production stores can implement `AtomicMutationPlatformHostStore.transactionWithEvents`. The
|
|
251
533
|
transaction-scoped `db`, event sequence, event append, event listing, and invocation update methods
|
|
252
534
|
must all use the same database transaction.
|
|
253
|
-
The optional transaction-scoped
|
|
254
|
-
|
|
255
|
-
|
|
535
|
+
The optional transaction-scoped adapter-update and `appendExternalReconciliation` operations
|
|
536
|
+
extend this unit of work to degraded adapter finalization. Leased finalizers use
|
|
537
|
+
`writeAdapterInvocation` when the store advertises it, otherwise `updateAdapterInvocation`,
|
|
538
|
+
as described under redaction below.
|
|
256
539
|
|
|
257
540
|
`PostgresPlatformHostStore` enables this capability when constructed with a
|
|
258
541
|
`PostgresPlatformHostTransactionProvider`. The application owns that narrow binder because only the
|
|
@@ -265,7 +548,8 @@ With `transactionWithEvents`:
|
|
|
265
548
|
- `after_adapters`: declared domain events, the Host-generated `StateTransitioned` fact, and
|
|
266
549
|
invocation finalization commit together after required adapter steps succeed or accept a
|
|
267
550
|
parked external handoff. Handler writes have already committed separately. A finalization
|
|
268
|
-
failure leaves the invocation recoverable
|
|
551
|
+
failure leaves the invocation recoverable within `recoveryMaxAttempts`; recovery repairs evidence
|
|
552
|
+
for succeeded adapter checkpoints without repeating those calls.
|
|
269
553
|
|
|
270
554
|
Stores without `transactionWithEvents` retain the legacy non-atomic boundary for domain writes,
|
|
271
555
|
events, and invocation persistence. A partial append or finalization failure can leave some
|
|
@@ -341,7 +625,7 @@ Three optional `PlatformHostOptions` hooks cover other Host-recorded values.
|
|
|
341
625
|
| --- | --- |
|
|
342
626
|
| `redactAdapterOutput(actionId, adapterType, operation, output)` | Once after a successful adapter result is converted to output, before handoff handling. The adapter row, `AdapterInvocationSucceeded` payload, and `extractExecutionAttestation` input use the redacted output. |
|
|
343
627
|
| `redactErrorText(context, message)` | At the origin of caught handler/execution errors, adapter result errors and ambiguous-outcome reasons, and completion errors. The invocation/adapter errors, reconciliation messages and reasons, completion evidence, failure events, and returned error reuse that text. |
|
|
344
|
-
| `redactExternalCompletionResult(actionId, provider, result)` | After a completion matches its invocation, before
|
|
628
|
+
| `redactExternalCompletionResult(actionId, provider, result)` | After a completion matches its invocation, before persistence and digesting; successful settling callbacks also merge and validate against the result schema. See [External completion](#external-completion). |
|
|
345
629
|
|
|
346
630
|
`ErrorTextRedactionContext` is exported. It contains `actionId` and `source` (`handler`, `adapter`,
|
|
347
631
|
`external_completion`, or `execution`); adapter errors also supply `adapterType`, `operation`, and
|
|
@@ -367,40 +651,53 @@ callback can still land as evidence. References come from the original adapter r
|
|
|
367
651
|
For withheld output and refused handoffs (immediate completion or a second handoff), Host uses
|
|
368
652
|
`transactionWithEvents` when the transaction supplies `updateAdapterInvocation`,
|
|
369
653
|
`updateLeasedActionInvocation` for a leased invocation, and `appendExternalReconciliation` for
|
|
370
|
-
withheld output on a governance-capable store.
|
|
654
|
+
withheld output or refused handoffs on a governance-capable store. Leased finalization also needs
|
|
655
|
+
transaction-scoped `writeAdapterInvocation` when the store advertises it. These optional members must
|
|
371
656
|
share the event/verdict transaction; the adapter update must enforce the store method's supplied
|
|
372
657
|
tenant, space, and invocation scope. Both built-in stores provide them when configured with a
|
|
373
658
|
transaction provider. Custom stores must implement them to get this atomic behavior.
|
|
374
659
|
|
|
375
|
-
When atomic finalization succeeds, evidence events, any withheld-output
|
|
376
|
-
marker, and the terminal `reconciliation_required`
|
|
660
|
+
When atomic finalization succeeds, evidence events, any withheld-output or refused-handoff
|
|
661
|
+
reconciliation entry, the adapter's `succeeded` marker, and the terminal `reconciliation_required`
|
|
662
|
+
verdict commit together. Host writes evidence and
|
|
377
663
|
the marker before the fenced verdict, which releases the lease, then emits status telemetry after
|
|
378
664
|
commit. It retries the whole transaction once on a non-lease error. If the retry also fails with a
|
|
379
665
|
non-lease error, Host selects the verdict-first fallback described below. Lease loss is not retried
|
|
380
666
|
or converted to fallback; it raises `RecoverableFinalizationError`. Atomic rollback leaves the
|
|
381
667
|
invocation and adapter row `running` until a retry or fallback commits. If lease loss or an unavailable
|
|
382
668
|
fallback verdict prevents finalization, the invocation remains reclaimable and the adapter can run
|
|
383
|
-
again under the same `adapterInvocationId` idempotency key
|
|
384
|
-
|
|
669
|
+
again under the same `adapterInvocationId` idempotency key for idempotent actions within the
|
|
670
|
+
worker recovery budget. Non-idempotent reclaim inspects its checkpoints instead of replaying a call.
|
|
385
671
|
|
|
386
672
|
Without the required transaction members, or after two non-lease transaction failures, Host keeps
|
|
387
|
-
the verdict-first order: the `reconciliation_required` verdict precedes evidence and the adapter marker.
|
|
388
|
-
write once, except on lease loss.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
673
|
+
the verdict-first order: the `reconciliation_required` verdict precedes evidence and the adapter marker.
|
|
674
|
+
It retries the verdict write once, except on lease loss. After the verdict is durable, Host attempts
|
|
675
|
+
each evidence write independently: a failed event, reconciliation append, or adapter checkpoint does
|
|
676
|
+
not skip the remaining writes. Host raises the first evidence failure as `RecoverableFinalizationError`
|
|
677
|
+
only after those attempts finish. This includes unknown-outcome settlement and post-success degraded
|
|
678
|
+
finalization. Failed writes can still leave a terminal verdict with missing evidence; terminal replay
|
|
679
|
+
does not repair that evidence gap. If both verdict-write attempts fail, the adapter can run again on
|
|
680
|
+
recovery for an idempotent action within the worker recovery budget, the same exposure as a crash mid-step.
|
|
681
|
+
A reclaimed non-idempotent action settles a running checkpoint as unknown.
|
|
682
|
+
|
|
683
|
+
A refused handoff's provider reference is included in the step's `AdapterInvocationSucceeded` event
|
|
684
|
+
even when explicit adapter output omits it. Capable atomic transactions append its reconciliation
|
|
685
|
+
record with the step's evidence. The fallback independently attempts the reference-bearing event,
|
|
686
|
+
the governance reconciliation append, the outer `recordCompletionReconciliation` append, and the
|
|
687
|
+
adapter marker. Both reconciliation appends use the same stable id to deduplicate repeated attempts.
|
|
688
|
+
A failure of either append is reported after the remaining fallback evidence writes are attempted.
|
|
689
|
+
A non-sweep verdict may receive a log-unavailable error note; a preserved sweep verdict keeps its error.
|
|
690
|
+
After successful atomic finalization, the outer append retains its existing tolerated-failure behavior.
|
|
691
|
+
If its annotation write fails, Host raises `RecoverableFinalizationError` while keeping the committed
|
|
692
|
+
verdict and adapter marker intact; terminal replay retains the verdict.
|
|
397
693
|
|
|
398
694
|
`extractExecutionAttestation` receives the redacted output, so an extractor that needs a stripped
|
|
399
|
-
field can fail after the external effect
|
|
695
|
+
field can fail after the external effect. Host leaves a durable success checkpoint recoverable
|
|
696
|
+
within `recoveryMaxAttempts`; repair may call the extractor again.
|
|
400
697
|
|
|
401
698
|
`redactActionParameters` and `redactAdapterInput` behavior is unchanged. Independently of hook
|
|
402
|
-
configuration, a
|
|
403
|
-
|
|
699
|
+
configuration, a refused fenced invocation write inside an adapter step rethrows. With
|
|
700
|
+
`writeAdapterInvocation`, stale adapter updates also refuse before marking the new owner's row failed. Refused-handoff ordering and audit events also
|
|
404
701
|
change without the new hooks.
|
|
405
702
|
|
|
406
703
|
Applications own the safety of domain events/results, policy and HITL evidence, explicit
|
|
@@ -643,8 +940,10 @@ binding's target; otherwise Host's later fact sets the recorded state to the tar
|
|
|
643
940
|
|
|
644
941
|
Configure `redactExternalCompletionResult` to withhold sensitive completion fields. Host applies it
|
|
645
942
|
once to a supplied result (including a result attached to a failed callback). The redacted value
|
|
646
|
-
feeds `externalCompletion.result`; for a completed outcome it also feeds the adapter output
|
|
647
|
-
`ExternalOperationCompleted` payload
|
|
943
|
+
feeds `externalCompletion.result`; for a completed outcome it also feeds the adapter output and
|
|
944
|
+
`ExternalOperationCompleted` payload. Only a settling successful callback merges it into the
|
|
945
|
+
invocation result and validates that merge against the action's result schema. A non-settling
|
|
946
|
+
callback returns the stored invocation result and keeps the callback result in its evidence. A supplied
|
|
648
947
|
`completion.error` passes through `redactErrorText` before recording, including when attached to a
|
|
649
948
|
completed outcome. A missing error uses Host's fixed failure message, which bypasses the hook.
|
|
650
949
|
|
|
@@ -691,13 +990,55 @@ refused. A repeated identical completion is absorbed, identity being a digest of
|
|
|
691
990
|
reference, outcome, result, error, and evidence. A contradictory one, a flipped outcome or the same
|
|
692
991
|
outcome with different evidence, moves the invocation to
|
|
693
992
|
`reconciliation_required`, keeps the first outcome on record, logs the second, and emits
|
|
694
|
-
`ExternalOperationContradicted`.
|
|
695
|
-
|
|
696
|
-
|
|
993
|
+
`ExternalOperationContradicted`. With `transactionWithEvents` and transaction-scoped
|
|
994
|
+
`getActionInvocationForUpdate`, the callback read and verdict write share a lock. The
|
|
995
|
+
successful settling callback's redacted result is merged with the invocation result, stripped of
|
|
996
|
+
private fields, and validated against the action's result schema before writes. A non-settling
|
|
997
|
+
callback records its redacted result/outcome as evidence without schema validation or merging it
|
|
998
|
+
into the invocation result.
|
|
697
999
|
An invocation that ended for another reason while a handoff was pending keeps its status and records
|
|
698
1000
|
the callback as evidence. Declare `completionDeadlineMs` on `long-running` actions;
|
|
699
|
-
`reconcileOverdueCompletions()` moves
|
|
700
|
-
|
|
1001
|
+
`reconcileOverdueCompletions()` moves running, unleased, due handoffs to reconciliation and retains
|
|
1002
|
+
the handoff with `overdueAt` set to the sweep's current time. The worker cycle runs the sweep before
|
|
1003
|
+
claiming work. A callback can settle a parked running invocation, even past its deadline before the sweep. From
|
|
1004
|
+
reconciliation, settlement requires the parked, swept handoff or the substring fallback described above;
|
|
1005
|
+
a swept unparked handoff retains its verdict and records the callback in `externalCompletion`,
|
|
1006
|
+
adapter evidence, and a completion/failure event. It clears the pending handoff without claiming
|
|
1007
|
+
that the invocation's remaining steps ran. The markerless parked-row fallback is described above.
|
|
1008
|
+
|
|
1009
|
+
For each sweep candidate, Host re-reads status, lease ownership, handoff identity
|
|
1010
|
+
(`adapterInvocationId` and `externalReference`), due time, and absence of `overdueAt`. With
|
|
1011
|
+
`transactionWithEvents` and `getActionInvocationForUpdate`, this check and the verdict/marker write
|
|
1012
|
+
share the row lock. Reconciliation evidence joins that transaction when it exposes
|
|
1013
|
+
`appendExternalReconciliation`; otherwise evidence is written on the outer store and can survive a
|
|
1014
|
+
rolled-back verdict. A custom transaction exposing `getActionInvocationForUpdate` without
|
|
1015
|
+
`appendExternalReconciliation` makes the sweep append on the outer store while holding the
|
|
1016
|
+
invocation row lock. If the reconciliation table references that row through a foreign key, the
|
|
1017
|
+
outer append can block waiting for the held lock. Implement both members in the same transaction.
|
|
1018
|
+
Stores without a transaction lock use an immediate re-read, but retain a race
|
|
1019
|
+
between that read and the write: a concurrent callback or execution can still be overwritten.
|
|
1020
|
+
Custom stores must preserve and reload the optional `overdueAt` Date in the handoff JSON; PostgreSQL
|
|
1021
|
+
hydrates it without a new migration.
|
|
1022
|
+
|
|
1023
|
+
Inline writes bypass the extra guard read and transaction when neither the loaded invocation nor
|
|
1024
|
+
its patch carries `pendingCompletion`. Ordinary inline completion retains its base store cost.
|
|
1025
|
+
Writing a handoff into a running invocation from anything other than its executing step is not
|
|
1026
|
+
supported; an execution without that handoff in its snapshot does not guard against such imports.
|
|
1027
|
+
Inline execution preserves a stored `reconciliation_required` verdict while committing independent
|
|
1028
|
+
adapter checkpoints, events, and reconciliation records, including references from refused handoffs.
|
|
1029
|
+
If execution reaches parking after the sweep, it records the handler result and `parkedAt` on the
|
|
1030
|
+
same handoff, retaining the sweep's status, error, and `overdueAt`. Successful `after_adapters`
|
|
1031
|
+
finalization also commits its deferred domain events: the handler and remaining adapters have
|
|
1032
|
+
succeeded. Inline callers receive the latest
|
|
1033
|
+
record observed by the preserving writer, merged with its evidence update, after these writes
|
|
1034
|
+
succeed; there is no post-attempt read. Leased execution retains `RecoverableFinalizationError`
|
|
1035
|
+
on fencing failure. The transaction lock serializes the check with the sweep where available; stores
|
|
1036
|
+
without it retain a read/write race. This does not cancel an adapter in flight or undo earlier
|
|
1037
|
+
domain writes. Verdict-first fallbacks attempt independent evidence writes before raising the first
|
|
1038
|
+
failure as `RecoverableFinalizationError`. Reconciliation-log failures leave the sweep error unchanged. Concurrent inline executions of the
|
|
1039
|
+
same invocation can still raise `RecoverableFinalizationError` with `Invocation is no longer running`
|
|
1040
|
+
when another execution finalized the row first. This error is internal; callers classifying it can
|
|
1041
|
+
match its `name` and inspect the stored invocation before deciding how to recover.
|
|
701
1042
|
|
|
702
1043
|
The handoff is persisted the moment it is accepted, before the adapter step is marked done, so a
|
|
703
1044
|
worker that dies between the two leaves the handoff and not the marker. A custom store has four
|
|
@@ -736,10 +1077,93 @@ Host passes `AdapterInvocationScope` from the store-loaded action invocation to
|
|
|
736
1077
|
must match `tenantId`, `spaceId`, and `actionInvocationId` when scope is supplied. A mismatch returns
|
|
737
1078
|
`undefined` on read and leaves the row unchanged on update (a not-found error is permitted).
|
|
738
1079
|
The trailing argument is optional to preserve existing custom-store and direct-call compatibility;
|
|
739
|
-
calls without it retain ID-only behavior in the shipped stores.
|
|
1080
|
+
calls without it retain ID-only behavior in the shipped stores. When `writeAdapterInvocation` is present, the write probe also checks its scoped create/update
|
|
1081
|
+
refusals and an admitted update in our scope. The two adapter isolation checks
|
|
740
1082
|
fail for stores that ignore scope. Deployments running `runTenantIsolationChecks` against a custom
|
|
741
1083
|
store will see new failures until that store honours the argument.
|
|
742
1084
|
|
|
1085
|
+
## Event append conformance
|
|
1086
|
+
|
|
1087
|
+
`eventAppendChecks()` and `runEventAppendChecks(subject)` run six checks for first-write-wins
|
|
1088
|
+
event IDs: plain appends, outbox appends, outbox-after-plain, transactional appends,
|
|
1089
|
+
transactional outbox appends, and transactional outbox-after-plain. Each sends earlier and later
|
|
1090
|
+
duplicates with lower and higher sequences, earlier and later occurrence/recording times, and
|
|
1091
|
+
different remaining envelope and outbox metadata fields. Transaction checks cover buffered
|
|
1092
|
+
duplicates and duplicates against a committed event; they do not require transaction reads to
|
|
1093
|
+
expose buffered writes. Each committed-duplicate transaction appends a fresh plain control event
|
|
1094
|
+
after the duplicate and verifies its presence after commit, detecting a silently rolled-back unit
|
|
1095
|
+
of work. Event-count checks count only the original probe ID; controls use distinct sequences.
|
|
1096
|
+
|
|
1097
|
+
The checks detect common first-write-wins violations: throwing, replacing or merging events,
|
|
1098
|
+
keeping both events, selecting the earlier-ordered or later-ordered duplicate, adding outbox
|
|
1099
|
+
rows, and resetting or rewriting an existing outbox row in the probed states. They are not
|
|
1100
|
+
exhaustive. Known limits include rules that compare ordering fields in mixed directions,
|
|
1101
|
+
store-wide uniqueness across spaces or tenants, the dead-letter state, and claims that lease
|
|
1102
|
+
foreign rows without returning them. They also cannot detect a buffering transaction that merges
|
|
1103
|
+
fields the store never returns (such as `includeProvenance`) from a buffered duplicate, or
|
|
1104
|
+
duplicate handling backed by an in-process cache that works during one run but fails across
|
|
1105
|
+
restarts. Stores requiring gapless sequences can fail falsely: the first event uses sequence 50
|
|
1106
|
+
in a fresh space. So can stores with a unique key over fields the control events share with the
|
|
1107
|
+
probe event, such as `actionInvocationId` with the subject.
|
|
1108
|
+
|
|
1109
|
+
Outbox-creating checks verify the first event and metadata, then compare whole-row snapshots
|
|
1110
|
+
while pending, leased by the probe worker, retry-pending, and settled as published. Retry-pending
|
|
1111
|
+
accepts a cleared lease, an expired lease with no owner or the probe worker, or the probe owner
|
|
1112
|
+
retained as a record without an expiry. A probe lease that still expires more than five seconds
|
|
1113
|
+
after the probe's clock fails with `markOutboxFailed did not release the lease`; an expiry inside
|
|
1114
|
+
that margin is treated as clock skew and waited out before the row is claimed again. A lease held
|
|
1115
|
+
by another worker fails with the request to pause relays. The checks prescribe neither a positive attempt
|
|
1116
|
+
count nor error formatting; they snapshot the values the store returns. Retry availability uses
|
|
1117
|
+
whole seconds a few seconds in the past, tolerating coarse timestamp precision and small clock
|
|
1118
|
+
skew, then the probe reclaims the row before publication. Snapshots include the full envelope,
|
|
1119
|
+
lease and publication fields. Comparisons sort object keys, normalize dates and date strings to milliseconds, treat null like undefined, and
|
|
1120
|
+
compare numeric strings in `sequence`, `attemptCount` and `leaseToken` as numbers. An absent
|
|
1121
|
+
actor disclosure is accepted; included disclosure or actor identity on the first outbox envelope
|
|
1122
|
+
fails because the first metadata does not opt in. A store's first returned records supply the
|
|
1123
|
+
snapshot references; extra envelope fields and fields the store omits or returns as null are
|
|
1124
|
+
accepted, including required fields. This tolerance does not validate envelope completeness. For
|
|
1125
|
+
buffered duplicates in the first transaction, exposed fields that differ between the first input
|
|
1126
|
+
and a duplicate must retain the first value. Outbox-after-plain checks write a fresh control
|
|
1127
|
+
event through the matching outbox seam and require its row to be the only outbox row visible in
|
|
1128
|
+
the probe space, so absence verification cannot pass solely because `listOutbox` returns
|
|
1129
|
+
nothing.
|
|
1130
|
+
|
|
1131
|
+
`EventAppendSubject<TDb>` supplies `store`, `tenantId`, and optional `reset()`. Only
|
|
1132
|
+
`appendEvent` and `listEvents(tenantId, spaceId)` are required; the subject does not need a full
|
|
1133
|
+
recoverable store. The result has `passed` and per-check `id`, `status`, `evidence`, and
|
|
1134
|
+
optional `error`, following the tenant-isolation suite. Absent outbox or transaction append
|
|
1135
|
+
capabilities produce `skipped: ` evidence; `passed` means no failures, so inspect skips for
|
|
1136
|
+
capability coverage. An available outbox append without `listOutbox`, `claimOutbox`,
|
|
1137
|
+
`markOutboxPublished` or `markOutboxFailed` fails because the check cannot verify or clean up.
|
|
1138
|
+
Append errors and probe retirement failures also fail.
|
|
1139
|
+
|
|
1140
|
+
Run against test or staging stores, preferably with a dedicated probe tenant. **Pause outbox
|
|
1141
|
+
relays for the probe tenant.** Event timestamps and written availability values (including
|
|
1142
|
+
retirement) use whole seconds a few seconds in the past. Claims and publication markers use
|
|
1143
|
+
real current time. Probe rows are expected to be due when availability follows `recordedAt`;
|
|
1144
|
+
stores assigning availability by their own clock may need additional setup. Visible foreign-worker
|
|
1145
|
+
leases or status changes before the probe acts fail with a request to pause relays. This detects visible interference; it does not prevent a relay race.
|
|
1146
|
+
Append-only events remain in fresh `event-append-probe-${uuid}` spaces. Successful
|
|
1147
|
+
outbox-creating checks leave their row **published although nothing was sent**; the space name
|
|
1148
|
+
identifies probe rows. Published is needed to exercise duplicate appends against that delivery
|
|
1149
|
+
state. Absence-control rows and failure rows instead go through final cleanup, which attempts
|
|
1150
|
+
to dead-letter visible probe rows still pending with `event append probe; never published`.
|
|
1151
|
+
Operator tooling can requeue those dead-lettered rows for real publication: exclude probe
|
|
1152
|
+
spaces from real publication and requeue workflows.
|
|
1153
|
+
|
|
1154
|
+
Claims use the real current time, a 30-second lease, and a limit equal to the pending probe row
|
|
1155
|
+
count. Cleanup filters listed rows locally by tenant and space and writes only to probe rows. If
|
|
1156
|
+
a claim returns foreign rows, the failure names their IDs, the checks write nothing to those
|
|
1157
|
+
rows, and no further claims occur in that run. Later outbox checks fail with an explanation that
|
|
1158
|
+
they were skipped because of the earlier filter violation; a standalone `check.run(subject)`
|
|
1159
|
+
stops claims for that check. Cleanup after a filter violation can dead-letter only probe rows
|
|
1160
|
+
returned by the violating claim, using the probe worker and that claim's tokens without another
|
|
1161
|
+
claim. The failure lists visible probe rows still pending, including unreturned rows or failed
|
|
1162
|
+
retirements, and instructs the operator to dead-letter them by hand. A filter-ignoring claim can
|
|
1163
|
+
still impose a short lease and make whatever writes the claim itself makes, such as incrementing attempt counts. Those leases lapse
|
|
1164
|
+
within 30 seconds of the claim if the store honours the requested duration; the checks do not
|
|
1165
|
+
release foreign leases or change foreign statuses, availability or errors.
|
|
1166
|
+
|
|
743
1167
|
## Runtime observability and health
|
|
744
1168
|
|
|
745
1169
|
Platform Host exposes vendor-neutral lifecycle telemetry through the optional `telemetry` sink on
|