@fabricorg/platform-host 8.1.2 → 8.3.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,84 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 8.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 749c507: Add host-owned staged descendant invocations. Actions declare an allowlist and return typed child
8
+ requests; Platform Host validates and redacts them atomically, releases them only after parent
9
+ completion, and dispatches them through fresh entitlement, authorization, admission, runtime
10
+ evidence, policy, and HITL decisions.
11
+
12
+ PostgreSQL migration 11 adds the staged-invocation ledger and caller-unforgeable child-origin unique
13
+ index. Child identity is a Host-only parent-invocation/stage-key origin. Child creation is
14
+ atomically fenced on the stage's pending status, lease owner, lease token, and unexpired lease;
15
+ PostgreSQL locks that stage row `FOR SHARE` while the fenced insert is in flight.
16
+
17
+ Each attempt settles a stage through one sequence. It first looks the child up by origin: a child
18
+ created by an earlier attempt is handed to the dispatcher again with its stable workflow ID while it
19
+ remains pending, without pre-submission checks or repeated governance, even after a deploy changes
20
+ the child's schema. Otherwise Host submits the child. When that submission did not itself return
21
+ the child (a failed check, a refusal, an error, or a timeout), Host rotates the lease token through
22
+ the store operation `rotateStagedInvocationLease` before an authoritative lookup, so the attempt's
23
+ own timed-out submission can no longer create the child. A stage is rejected only when that lookup
24
+ finds no child, and a dead-lettered stage records the child ID whenever the attempt found or created
25
+ a child. A stage becomes dispatched only after confirmed handoff, when no dispatcher is configured,
26
+ or when the child has already left pending. A failed child lookup settles the stage for retry and
27
+ does not dead-letter it. Settlements are compare-and-set on the lease owner and current token and
28
+ keep a child ID recorded earlier. A store error on one stage is reported through the sweep's
29
+ `onError` option (the Platform worker passes its own) and does not stop the rest of the batch.
30
+ Handoff failures retry after a constant one-second delay by default; callers provide `retryDelayMs`
31
+ for backoff. Public entitlement and authorization refusals now expose `SubmissionRefusalError` with
32
+ stable `code: "ACTION_SUBMISSION_REFUSED"`. The child inherits the parent's actor type: a
33
+ natural-person parent therefore does not enter agent HITL, while an agent parent follows the normal
34
+ agent approval route.
35
+
36
+ ## 8.2.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 71d306f: Add optional action-level finalization for atomic, immediate after_adapters actions, including non-idempotent actions. Export JSON finalization types, registration validation, a manifest protocol descriptor and defineFinalizingAction to guard handlers against older hosts.
41
+
42
+ Host requires atomic transactions, invocation locking and transaction-scoped lease fencing before application execution. On normal success the hook consumes stored redacted outputs and commits transaction-local domain writes, declared events, outbox and the merged result together. Hook or validation failure rolls back finalization and settles reconciliation_required when durable provider effects exist, or failed when there are none, with redacted error text. Lease loss escapes without a stale-worker verdict. Handler preparation writes remain separately committed.
43
+
44
+ If entry to or COMMIT of the final success transaction (after adapters) fails with an unknown outcome, Host raises RecoverableFinalizationError and records no verdict. An uncommitted invocation stays running for recovery; a committed completed invocation retains its stored verdict and result for re-execution.
45
+
46
+ Explicit undefined on the optional hook return fields events, resultPatch and eventSchemaVersion is treated as omitted. Payloads and result-patch values retain strict JSON validation and reject undefined.
47
+
48
+ Failed, ambiguous, degraded and accepted/handoff outcomes do not invoke the hook. Existing actions retain their lifecycle.
49
+
50
+ Before adapters, normalize hook parameters and public handler data through a JSON round-trip, stripping private handler carriers. Dates become ISO strings and undefined object properties drop; serialization failures such as cycles or BigInt fail before provider execution. Handler events are normalized separately. Hook event schema versions default to the action version when omitted.
51
+
52
+ Add optional Host eventContracts name validation for events returned by the finalize hook. It checks eventType and subjectType membership only; handler events, payload schemas and schema-version compatibility are outside this option's scope. Recovery refusal also preserves ambiguity evidence for in-flight adapter rows alongside succeeded effects.
53
+
54
+ - d5e7caa: Add optional adapter-step deadlineMs durations overriding the Host-wide duration,
55
+ including when the Host duration is unset or zero. Action and module registration, manifest export,
56
+ and manifest normalization reject step durations that are not positive finite integers.
57
+ Host validates resolved steps before handler or adapter execution. Invalid
58
+ durations fail a first execution without invoking adapters; retries with recorded
59
+ applied effects or unknown adapter outcomes require reconciliation instead.
60
+ Manifests preserve valid step durations.
61
+
62
+ Correct adapterDeadlineMs documentation: it is a duration starting just before
63
+ step retry execution, not an epoch timestamp. Host-wide behavior is unchanged.
64
+ Budgets span retries; expiry prevents a subsequent attempt and signals cooperative
65
+ cancellation of an in-flight call. An aborted call that throws retains the existing
66
+ ambiguous-outcome reconciliation semantics.
67
+
68
+ - f32aae6: Commit versioned finalization preparation with handler writes, bounded to 256 KiB of canonical UTF-8 JSON. Recovery skips the handler, reuses succeeded adapter outputs and durable skips, and finalizes through the locked success transaction, including non-idempotent actions. Finalizing getInput callbacks receive normalized public data; legacy callbacks retain raw inputs.
69
+
70
+ Unknown adapter outcomes, incompatible protocol/action versions/ordered step identities, handoffs and governance refusal after effects retain reconciliation semantics. Missing checkpoints retain prior recovery behavior. Preparation validation/write failures roll back before adapters; unknown commit outcomes and lease loss remain recoverable without a stale verdict.
71
+
72
+ Apply migration 10 with ensureSchema before deploying checkpoint-writing Hosts. Updates omitting the field do not require its column.
73
+
74
+ A checkpoint bypasses non-idempotent interruption gates only when recorded adapter rows are succeeded. Failed rows preserve interruption settlement without repeating the provider call: failed without recorded success, reconciliation_required after another step succeeded. Leased non-idempotent replay additionally requires store-level and transaction-level writeAdapterInvocation fencing. Idempotent finalizing actions retain failed-row retry under existing adapter retry rules, using saved preparation.
75
+
76
+ On the live path, a finalizing action with an earlier succeeded adapter and a later permanent adapter failure now settles reconciliation_required instead of failed, without running the hook. Subsequent execution failures after a recorded provider effect likewise preserve reconciliation of partial progress.
77
+
78
+ - 3d24330: Permit bounded leased retries of non-idempotent actions after retryable pre-effect governance errors. The additive deferredBeforeEffectAttempt record field proves only the immediately preceding claim deferred before handler or adapter effects. Missing or stale proof retains interruption failure. Marker-write failure retains terminal failure where persistence succeeds; lease loss preserves fencing errors. Approval resume, inline execution, exhausted budgets, and errors after the governance window retain their existing refusal semantics. Workspace dependents receive compatible patch releases.
79
+
80
+ PostgreSQL migration 9 (`pre_effect_governance_deferral`) adds the nullable marker column. Run `ensureSchema()` before deploying Hosts that may defer non-idempotent actions. Invocation updates that do not carry the marker omit its column, preserving writes on schemas without migration 9.
81
+
3
82
  ## 8.1.2
4
83
 
5
84
  ### Patch Changes
package/README.md CHANGED
@@ -55,7 +55,8 @@ immediately before the first policy-evaluation record is written. Host rethrows
55
55
  without a failure/status write when:
56
56
 
57
57
  - the invocation has a lease owner and a defined lease token;
58
- - the action declares `idempotent: true`;
58
+ - the action declares `idempotent: true`, or Host successfully records the current claim in
59
+ `deferredBeforeEffectAttempt` through a lease-fenced write;
59
60
  - `attemptCount` is below `retryableExecutionMaxAttempts`, a positive integer option defaulting to 5; and
60
61
  - execution is not a `resumeApprovedInvocation()` call.
61
62
 
@@ -65,12 +66,24 @@ deferrals do not increment `failed`. The worker counts by error type: a `Retryab
65
66
  that a store wrapper throws from Host's own reads or status writes also lands in `deferred`. In that
66
67
  case the row likewise stays leased for a later claim, but outside this cap. Retry becomes eligible when the current lease expires, so the
67
68
  delay is the remaining lease duration, followed by the worker's next claim; there is no immediate
68
- retry or separate backoff. The next claim increments `attemptCount`. At the cap, or for an unleased,
69
- non-idempotent, or approval-resume invocation, Host records `failed` with the reason retry was refused,
69
+ retry or separate backoff. The next claim increments `attemptCount`. At the cap, or for an unleased
70
+ or approval-resume invocation, Host records `failed` with the reason retry was refused,
70
71
  through the error-text redactor. Approval resume is excluded because its temporary lease does not
71
72
  establish that a recovery worker is configured. Ordinary application governance errors retain terminal failure behavior. Host store errors after
72
73
  a recorded provider effect follow the recovery rules below.
73
74
 
75
+ Non-idempotent governance retry requires a durable `deferredBeforeEffectAttempt` equal to the
76
+ preceding claim count. Each retry must record its own pre-effect deferral to permit another claim.
77
+ A missing or stale marker retains interruption failure and manual reconciliation. Marker-write
78
+ failure ends terminally when the verdict can be persisted; lease loss retains the recoverable
79
+ fencing error. Handler and adapter errors are outside this deferral window.
80
+
81
+ PostgreSQL migration 9 (`pre_effect_governance_deferral`) adds the nullable marker column.
82
+ Run `ensureSchema()` before deploying Hosts that may defer non-idempotent actions. Invocation
83
+ updates without the marker omit the column and continue to work on schemas without migration 9.
84
+ If a marker write fails, Host refuses retry with `pre-effect marker could not be recorded` when
85
+ it can persist the terminal verdict; lease loss preserves its fencing error.
86
+
74
87
  The retry region includes Host's single-record store writes for mutation-governance and HITL
75
88
  checkpoints. Later attempts reuse these persisted decisions through the `mutationFootprint` and
76
89
  `hitlRoute` guards. Callbacks should be safe to repeat and return stable invocation-scoped evidence.
@@ -133,7 +146,7 @@ Lifecycle records use deterministic checkpoint ids. Recovered idempotent actions
133
146
  adapter that already reached `succeeded`. Built-in stores deduplicate event and policy inserts by
134
147
  checkpoint ID. Adapter rows are mutable; stale-worker protection requires the optional
135
148
  `writeAdapterInvocation` capability described below. A
136
- reclaimed non-idempotent action inspects its adapter checkpoints: running or succeeded rows route
149
+ reclaimed non-idempotent action without compatible finalization preparation inspects its adapter checkpoints: running or succeeded rows route
137
150
  to reconciliation; no rows or only failed rows retain terminal failure for manual review.
138
151
 
139
152
  Returned `ambiguous` or `timeout` outcomes, timeouts thrown from `adapter.execute` (including an
@@ -201,8 +214,8 @@ On a store without atomic support, if the verdict cannot be written at all, no d
201
214
  call succeeds: neither verdict nor checkpoint is saved, as after a crash mid-call. A leftover `running`
202
215
  step can then re-run on recovery for an idempotent action under the same `adapterInvocationId`
203
216
  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
217
+ running row without a matching durable handoff settles that step as unknown. Without compatible
218
+ finalization preparation, a succeeded row or durable handoff instead ends `reconciliation_required` as an applied effect. Adapters should be
206
219
  idempotent or carry that stable key. The same exposure applies if two atomic attempts roll back
207
220
  and the fallback verdict cannot be written.
208
221
 
@@ -212,7 +225,7 @@ This includes later adapter checkpoints, obligation reads, and the completion wr
212
225
  `before_adapters` and `after_adapters`. Recovery also protects Host store operations before
213
226
  and around the handler when an earlier provider effect is recorded. Authorization, policy and handler callbacks
214
227
  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`
228
+ without calling that step's provider. Without compatible finalization preparation, non-idempotent interruption ends `reconciliation_required`
216
229
  instead of repairing the invocation. Lease loss retains its fencing error.
217
230
 
218
231
  If neither record is durable, Host retries the failed handoff or marker write once. After a
@@ -229,10 +242,12 @@ Recovery may replay that unrecorded outcome for an idempotent action under its s
229
242
  `adapterInvocationId`, within the worker budget. A reclaimed non-idempotent action instead
230
243
  ends `reconciliation_required`. A durable handoff takes precedence over its running row and is
231
244
  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
245
+ is settled as unknown. Without compatible finalization preparation, a succeeded row also ends `reconciliation_required`. With no rows or only failed rows, the existing
233
246
  non-idempotent interruption gates still fail the invocation.
234
247
 
235
- For idempotent recovery, the handler still runs before the adapter loop. The loop reads each
248
+ For idempotent recovery without an action finalizer, the handler still runs before the adapter loop.
249
+ Finalizing actions with compatible durable preparation replay without rerunning the handler;
250
+ missing or incompatible preparation retains effect-aware refusal. See [action-level finalization](#action-level-finalization). The loop reads each
236
251
  checkpoint before `getInput()` or adapter lookup. Succeeded rows and durable handoffs use stored
237
252
  adapter type, operation, vendor, input and output; a falsy new input or removed adapter does not
238
253
  skip evidence repair. Stable event IDs deduplicate replayed accepted and success events; newly
@@ -245,7 +260,7 @@ Host calls `extractExecutionAttestation` with stored input/output and checkpoint
245
260
  it must return deterministic evidence for the same input. A handoff checkpoint with no output,
246
261
  or withheld output, skips extraction. Repair errors remain recoverable within the budget.
247
262
 
248
- Known limits: a later step's definite provider failure or pre-call refusal can still end the
263
+ Known limits for actions without checkpointed finalization: a later step's definite provider failure or pre-call refusal can still end the
249
264
  invocation `failed` after an earlier step succeeded. Authorization denials, ordinary handler
250
265
  failures, validation/state-guard rejection, and unsatisfied policy obligations on recovery retain
251
266
  their existing terminal outcomes. These are decisions or application failures, not Host store
@@ -398,7 +413,6 @@ still treats a failure inside the handler callback as a handler outcome.
398
413
  failing, the row stays `running` although the provider was not called. For an action not declared
399
414
  idempotent, the next claim settles that step as an unknown provider outcome.
400
415
 
401
-
402
416
  A deadline sweep retains the handoff and sets `pendingCompletion.overdueAt` with its
403
417
  `reconciliation_required` verdict. A late callback settles that verdict only when the handoff also
404
418
  has `parkedAt`, proving Host reached parking after its remaining steps and finalization. An unparked
@@ -466,6 +480,22 @@ in the `before_adapters` phase stay appended. If the breaker opens between retry
466
480
  retrying and records the refusal. See the `@fabricorg/platform` README for thresholds and outcome
467
481
  mapping.
468
482
 
483
+ ## Adapter execution deadlines
484
+
485
+ `PlatformHostOptions.adapterDeadlineMs` is a duration in milliseconds, starting
486
+ after step input preparation, just before the step enters retry execution.
487
+ An action's `AdapterStep.deadlineMs` overrides that duration for the step, including
488
+ when the Host option is unset or zero. Step durations must be positive finite integers;
489
+ action and module registration, manifest normalization and export reject invalid
490
+ values. Host also validates resolved steps before handler or adapter execution.
491
+
492
+ The budget spans retry attempts and delays; it is not renewed per attempt.
493
+ Host supplies an epoch-millisecond `AdapterExecutionContext.deadlineMs` and an
494
+ `AbortSignal`. Adapters must observe the signal to abandon an in-flight call.
495
+ Expiry before an attempt prevents that attempt; an aborted call that throws follows
496
+ the existing ambiguous-outcome reconciliation path. With no step duration and an
497
+ unset or zero Host duration, neither context field is supplied.
498
+
469
499
  ## Execution authorization
470
500
 
471
501
  Submission authorization proves that a caller may create an invocation. Applications that delegate
@@ -562,7 +592,116 @@ evidence, so concurrent transitions serialize. Retrying the same identity and ev
562
592
  reusing it with contradictory evidence fails closed, and a distinct transition cannot rewrite a
563
593
  terminal parent. `createSagaParentActivities()` supplies deterministic identities for Temporal callers.
564
594
 
565
- Actions can optionally declare `resultSchema`. The Host validates public handler result data after
595
+ ### Action-level finalization
596
+
597
+ Define provider-dependent domain work with `defineFinalizingAction({ ...action, finalize })`.
598
+ This helper guards the handler against older hosts by requiring
599
+ `ctx.finalizationProtocol === "atomic-after-adapters-v1"`, including when no handler was
600
+ provided. An optional callback alone cannot make an older host refuse execution.
601
+
602
+ Finalizing actions declare `eventPhase: "after_adapters"`, atomic kind, and immediate
603
+ completion (or omit kind/completion). Non-idempotent actions are supported. Host refuses
604
+ execution before handler or adapters unless the store provides atomic mutation transactions,
605
+ invocation locking, and transaction-scoped lease fencing. There is no fallback.
606
+
607
+ `finalize(ctx, { params, handlerData, adapterOutputs })` receives transaction-bound `ctx.db`
608
+ and identity/provenance, without services. Each ordered step is either skipped or succeeded
609
+ with its index, adapter type, operation, invocation ID, and stored redacted JSON output.
610
+ Before adapters, Host strips `eventResultFields` from handler data and normalizes the hook's
611
+ parameters and public handler data using `JSON.parse(JSON.stringify(value))`, then validates
612
+ the normalized JSON. Dates become ISO strings, undefined object properties drop, and class
613
+ instances use their JSON representation; non-finite numbers become null. Cycles, BigInt or
614
+ other serialization failures fail the handler phase before adapter calls or adapter rows.
615
+ Handler events are normalized in that phase too. The hook receives copies of the saved
616
+ normalized inputs, so adapter input preparation cannot change them.
617
+ Perform transaction-local domain writes here; provider calls belong in adapters. Earlier
618
+ handler writes commit separately and are outside this atomic completion.
619
+
620
+ Return only `{ events?, resultPatch? }` with JSON-compatible values. Explicit `undefined` on
621
+ `events`, `resultPatch`, or an event's `eventSchemaVersion` is treated as omitted; undefined
622
+ payloads or nested payload/result-patch values are rejected. Events require non-empty
623
+ event/subject identifiers, a payload, and an optional positive integer schema version;
624
+ event types must be declared in `emitsEvents`. Host checks event and subject names against
625
+ `eventContracts` when supplied for hook-returned events only; handler events are outside
626
+ that option's scope. It checks name membership, without validating payload schemas or schema
627
+ version compatibility. Host owns envelope identity, scope, actor and timestamps.
628
+ Hook and handler events default `eventSchemaVersion` to `action.version` when omitted;
629
+ an explicit event schema version is preserved.
630
+ Handler events use `domain:<index>`, hook events use `finalize:<index>`, followed by
631
+ `state:transition`. The result patch merges shallowly over the public handler data; keys
632
+ in `eventResultFields` are rejected. For these actions `resultSchema` validates the merged
633
+ result inside the final transaction.
634
+
635
+ The final transaction locks and checks the running invocation before calling the hook.
636
+ Hook, validation, event-append or completion-write failure rolls back finalizer domain
637
+ writes, events, outbox and completion, then settles `reconciliation_required` through
638
+ applied-effect settlement when durable provider effects exist. With zero steps or all steps
639
+ skipped, it settles `failed` without claiming a provider effect. Error text uses Host error
640
+ redaction. Lease loss rolls back and
641
+ escapes without a stale-worker verdict. Failed, ambiguous, degraded and accepted/handoff
642
+ outcomes do not call the hook.
643
+
644
+ If transaction entry or COMMIT fails with an unknown outcome, Host raises
645
+ `RecoverableFinalizationError` and records no verdict. An uncommitted invocation stays
646
+ `running` for recovery; if COMMIT succeeded despite the error, its stored `completed`
647
+ verdict and result remain intact and are returned on re-execution.
648
+
649
+ **Durable preparation and recovery.** The handler transaction commits a JSON
650
+ `finalizationCheckpoint` with protocol `atomic-after-adapters-v1`, action version, ordered
651
+ adapter type/operation identities, normalized parameters and public handler data, handler
652
+ events, public result, optional pending transition event and skipped indexes. Adapter inputs
653
+ are excluded. Parameters derive from the already-redacted durable invocation input.
654
+ Finalizing `getInput` callbacks receive copies of normalized parameters and public handler
655
+ data on both paths; other actions retain raw inputs. Skip decisions are computed before
656
+ preparation commits. Replay does not call `getInput` for recorded skips or succeeded rows.
657
+ For an unstarted step, it recomputes input from the saved normalized values; callbacks must
658
+ be deterministic for those values.
659
+
660
+ On resumed running invocations or later attempts, a compatible checkpoint bypasses the
661
+ handler, including for non-idempotent actions. Succeeded rows supply their stored redacted
662
+ outputs; steps without rows can run because Host creates a running row before calling the
663
+ provider. Recovery retains entitlement, authorization and policy checks. A refusal after
664
+ an applied effect requires reconciliation. Finalization uses the same locked, lease-fenced
665
+ success transaction as live execution. A completed invocation returns its stored result.
666
+
667
+ Running or ambiguous adapter rows require reconciliation, before checkpoint compatibility
668
+ checks. Protocol, action version or step count/order/type/operation changes refuse replay;
669
+ retain compatible definitions while work is recoverable. Handoffs cannot replay a finalizer.
670
+ Refusals reconcile when an effect exists or may exist, and fail when no effect is recorded.
671
+ Missing checkpoints retain the previous recovery rules, including reconciliation after
672
+ succeeded adapter evidence. Recovery-attempt budgets and deadline/capability refusals remain
673
+ in force. A checkpoint bypasses non-idempotent interruption gates only when each recorded
674
+ adapter row is succeeded; missing rows may start, but failed rows retain interruption
675
+ settlement without another provider call. Failed rows do not prove the provider had no effect.
676
+ Settlement fails without recorded success and reconciles when another step succeeded.
677
+ For leased non-idempotent replay, both the store and mutation transaction must provide
678
+ `writeAdapterInvocation` for lease-fenced adapter creation and updates. Without either
679
+ capability the existing interruption gates remain in force. Idempotent finalizing actions
680
+ retain failed-row retry through the adapter loop under existing retry rules, using saved
681
+ preparation rather than rerunning the handler.
682
+
683
+ On the live path, a finalizing action whose earlier adapter succeeded and whose later adapter
684
+ returns a permanent failure settles `reconciliation_required`, rather than `failed`. The hook
685
+ does not run. This also applies to subsequent execution failures after a recorded provider
686
+ effect, so the terminal status preserves the need to reconcile partial progress.
687
+
688
+ Canonical checkpoint JSON is limited to **256 KiB in UTF-8**, a conservative fixed bound on
689
+ durable evidence and transaction size. Oversize or unserializable preparation fails before
690
+ adapters and rolls back handler writes. Store entry/commit errors with unknown outcomes raise
691
+ `RecoverableFinalizationError` without a terminal verdict. Checkpoint write failures roll
692
+ back preparation, so adapters do not run.
693
+
694
+ **Rollout:** apply PostgreSQL migration **10**, `finalization_preparation_checkpoint`, using
695
+ `ensureSchema()` before deploying Hosts that write checkpoints. It adds nullable
696
+ `action_invocations.finalization_checkpoint jsonb`. Updates without the checkpoint field do
697
+ not name the column and remain usable on older schemas. Custom stores must persist the field
698
+ atomically with handler writes and honor explicit `undefined` clearing. Memory storage follows
699
+ the same patch semantics. The saved transition event preserves the original emission decision;
700
+ `pendingStateTransition` alone cannot encode suppression by a handler-emitted transition.
701
+
702
+ ### Result validation
703
+
704
+ Actions without a finalizer can optionally declare `resultSchema`. The Host validates public handler result data after
566
705
  the handler and before canonical event append or adapter execution. Invalid results fail the invocation,
567
706
  emit no domain/completion event, invoke no adapter, and persist only a sanitized validation failure.
568
707
  Domain writes roll back only when the configured store transaction provider includes those writes.
@@ -778,6 +917,11 @@ Only JSON-data parameters can be admitted. Anything else is refused as `invalid_
778
917
  directly after `authorize`, before the composition callback and the check run. A SharedArrayBuffer or a view over one is refused this way,
779
918
  since the caller could still write to its memory (test "a parameter backed by a SharedArrayBuffer …").
780
919
 
920
+ Entitlement and submission-authorization refusals throw `SubmissionRefusalError`. It remains an
921
+ `Error` with the existing message and adds stable `code: "ACTION_SUBMISSION_REFUSED"`, `actionId`,
922
+ and `refusal: "entitlement" | "authorization"` fields so internal dispatchers can distinguish a
923
+ definitive refusal from a retryable failure.
924
+
781
925
  Only `{ admitted: true }`, read as an own data property, admits. Each of these makes
782
926
  `submitAction` throw `ActionAdmissionError` (`code: "ACTION_ADMISSION_REFUSED"`, `refusal`, and
783
927
  `refusalCode` for `refused`):
@@ -891,7 +1035,7 @@ must match the current state and invocation. Missing provenance leaves normal va
891
1035
  custom stores can continue compiling without the optional method.
892
1036
 
893
1037
  After a valid transition for a binding with a non-empty entity ID and target, Host appends
894
- `StateTransitioned` immediately after the handler's domain events, following the action's
1038
+ `StateTransitioned` after the handler's domain events and any action-finalizer events, following the action's
895
1039
  `eventPhase`. Its subject is the bound entity and its version-1 payload is
896
1040
  `{ entityType, entityId, fromState, toState, causedByAction }`. `fromState` is the trusted state
897
1041
  read before the handler, and `causedByAction` is the action ID. The event uses a stable
@@ -900,7 +1044,8 @@ can filter it. It does not need an `emitsEvents` declaration from the action.
900
1044
 
901
1045
  For `before_adapters`, the transition is appended with the handler's domain events. For
902
1046
  `after_adapters`, it is deferred to finalization after required adapter steps succeed or
903
- accept a parked external handoff, immediately after domain events. Adapter failure withholds
1047
+ accept a parked external handoff, after domain events and any action-finalizer events.
1048
+ Actions with `finalize` reconcile accepted handoffs without calling the hook. Adapter failure withholds
904
1049
  both domain events and the transition, leaving the built-in stores' ledger-derived entity state
905
1050
  unchanged by that invocation even though handler writes may have committed. Custom stores that
906
1051
  read domain rows can already observe the target state at this point. With `transactionWithEvents`, the
@@ -1066,11 +1211,13 @@ cast in an index expression is not immutable and PostgreSQL refuses it.
1066
1211
 
1067
1212
  ## Tenant isolation
1068
1213
 
1069
- `tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run eight checks against a host
1214
+ `tenantIsolationChecks()` and `runTenantIsolationChecks(subject)` run nine checks against a host
1070
1215
  and its store. They prove scope on the invocation reads and writes, idempotency keys, event streams,
1071
- worker claims, external completion, an optional governed read, and adapter checkpoint reads and
1072
- writes they probe. The subject supplies a host, a recoverable store, a registered action, and two
1073
- scopes in different tenants. Adapter checks create probes in dedicated spaces and retire them afterwards.
1216
+ worker claims, staged-invocation lists and claims, external completion, an optional governed read,
1217
+ and adapter checkpoint reads and writes they probe. The subject supplies a host, a recoverable
1218
+ store, a registered action, and two scopes in different tenants. Adapter and staged checks create
1219
+ scoped probes and retire their parent invocations afterwards. The staged check is skipped for a
1220
+ store that does not advertise transactional staged invocations.
1074
1221
 
1075
1222
  Host passes `AdapterInvocationScope` from the store-loaded action invocation to
1076
1223
  `getAdapterInvocation(id, scope)` and `updateAdapterInvocation(id, patch, scope)`. Custom stores
@@ -1205,6 +1352,129 @@ CI certifies concurrent migrations, command idempotency, and schema behavior aga
1205
1352
  Lakebase failover, backup, and restore remain adopter certification gates and are not implied by the
1206
1353
  PostgreSQL CI result.
1207
1354
 
1355
+ ## Staged descendant invocations
1356
+
1357
+ An action declaring `stagesInvocations` may return up to 32 child requests from its handler or
1358
+ `after_adapters` finalizer. A request key is 1–128 characters and matches
1359
+ `[A-Za-z0-9][A-Za-z0-9._:-]*`. The Host accepts at most 64 KiB of canonical JSON per request and
1360
+ 256 KiB for the canonical batch. Keys are unique within the combined handler/finalizer batch.
1361
+ Every action ID must be allowlisted and registered, and each parameter object must pass the current
1362
+ child schema before the parent transaction commits. `redactActionParameters` runs during staging;
1363
+ the redacted, schema-valid parameters are the only parameters written to the stage record and later
1364
+ used to create the child invocation.
1365
+
1366
+ Staging requires `transactionWithEvents`, a transaction-scoped `stageInvocations`, and a store whose
1367
+ `transactionalStagedInvocations` flag is true. Host construction checks the advertised staged-store
1368
+ operations and atomic-store capability: scoped origin lookup, lease-fenced child creation, claim,
1369
+ renewal, lease-token rotation, settlement, cancellation, listing, and `transactionWithEvents`. The transaction object itself is available only when execution begins, so
1370
+ a missing transaction-scoped `stageInvocations` method is caught by the first staging action's
1371
+ execution preflight and interrupts that invocation. For `before_adapters`, the handler's batch
1372
+ commits with the handler mutation. For `after_adapters`, handler and finalizer requests commit with
1373
+ events and the completed parent verdict. A finalizing action retains its handler requests in the
1374
+ durable finalization checkpoint. Replaying an identical parent batch is a no-op; any changed key,
1375
+ action, version evidence, parameter digest, parameter value, batch size, or batch digest moves the
1376
+ parent to reconciliation without changing stored requests.
1377
+
1378
+ A pending stage is claimable only while its immutable parent scope matches and the parent is
1379
+ `completed`. Parents ending `failed`, `blocked_by_policy`, or `validation_failed` have pending stages
1380
+ cancelled by the sweep. `reconciliation_required` and nonterminal parents leave stages pending and
1381
+ unclaimed. `host.dispatchStagedInvocations` uses renewable leases, a monotonic lease token,
1382
+ per-submission timeout, retry availability, a default ten-row claim batch, and a default ten-attempt
1383
+ dead-letter budget. The normal Platform worker runs this sweep by default only when its Host registry
1384
+ contains an action with a non-empty `stagesInvocations` allowlist. `dispatchStagedInvocations: true`
1385
+ forces a sweep and `dispatchStagedInvocations: false` disables it. A deployment that has no staging
1386
+ declarations therefore does not query the migration-11 tables by default.
1387
+
1388
+ The child has a Host-only `stagedOrigin` of parent invocation ID plus stage key, backed by a separate
1389
+ unique index. It carries no idempotency key, so a public caller cannot pre-claim its identity with a
1390
+ staged-looking string. A crash after parent commit is recovered by a later sweep. A crash after child
1391
+ creation but before the stage checkpoint resolves the same child by `stagedOrigin`. PostgreSQL
1392
+ migration 11 creates `staged_invocations`, adds the nullable origin columns and partial unique index,
1393
+ and adds the claim and parent indexes.
1394
+
1395
+ Each claimed row goes through one settlement sequence (`dispatchStagedRow` in `src/host.ts`):
1396
+
1397
+ 1. Host renews the claim's lease, skipping the row if the lease is gone, and looks the child up by
1398
+ `stagedOrigin` in the parent's tenant and space. Every claim replaces the lease token, so a
1399
+ creation still in flight from an earlier attempt holds an older token and fails the creation
1400
+ fence; this lookup therefore settles what earlier attempts did. A child found here goes to step 4
1401
+ without the pre-submission checks or any entitlement, authorization, or admission call.
1402
+ 2. With no child, Host checks that the parent is still `completed` and that the child action is
1403
+ still registered, does not require capture-time authority, and accepts the stored parameters. It
1404
+ then submits through the shared submission pipeline under the claim's lease token, bounded by
1405
+ `submissionTimeoutMs`.
1406
+ 3. If that submission's own fenced create returned the child, Host settles with its ID: `dispatched`
1407
+ when the handoff succeeded or no dispatcher is configured, otherwise a retry that records the ID
1408
+ (`dead_lettered` once the attempt budget is spent). In every other case (a failed pre-submission
1409
+ check, a refusal, an error, or a timeout) Host first rotates the lease token with
1410
+ `rotateStagedInvocationLease`. The only other possible creator is this attempt's own timed-out
1411
+ submission, which still holds the old token, so after the rotation it can no longer create the
1412
+ child and the lookup that follows is authoritative. A child found there goes to step 4. With none,
1413
+ a definitive refusal settles `rejected`, and anything else settles for retry or, at the attempt
1414
+ limit, `dead_lettered`.
1415
+ 4. For a known child that is still `pending`, Host calls the configured dispatcher again with the
1416
+ stable workflow ID `action-invocation-<id>`, bounded by `submissionTimeoutMs`. A confirmed
1417
+ handoff, no dispatcher, or a child that has left `pending` settles `dispatched`; a failed or
1418
+ unconfirmed handoff settles for retry or `dead_lettered`. Both record the child ID.
1419
+
1420
+ Each settlement is one compare-and-set on the row's lease owner and current token, and a settlement
1421
+ that carries no child ID keeps one recorded earlier (both stores; shared contract test "keeps a
1422
+ recorded child id when a settlement carries none"). A settlement that finds the lease gone writes
1423
+ nothing, and the row's next claim starts again at step 1. A child lookup that fails settles the row
1424
+ for retry with the error `Staged child lookup failed.`, not as `rejected` or `dead_lettered`, even
1425
+ past the attempt limit. That attempt still counts, so the next attempt that can read the lookup and
1426
+ fails is dead-lettered if the budget is spent. A store
1427
+ error from renewal, rotation, or settlement ends only that row: the sweep counts it as failed,
1428
+ passes the error to `onError`, and continues with the next row. The Platform worker passes its own
1429
+ `onError`. Without a dispatcher, the submission that creates a child also executes it inline, as
1430
+ `submitAction` does; the stage settles `dispatched` once the child exists, even when that execution
1431
+ outlasts `submissionTimeoutMs`.
1432
+
1433
+ Child creation is fenced in the store operation that inserts the invocation. The stage must still be
1434
+ `pending`, owned by the submitting worker at the matching lease token, and unexpired. PostgreSQL
1435
+ locks the qualifying stage row `FOR SHARE` in a CTE before `INSERT ... SELECT`. A token rotation of
1436
+ that row therefore waits for an in-flight insert to commit, and an insert under the old token that
1437
+ reaches the row while a rotation is uncommitted waits for it and then inserts nothing; the live
1438
+ PostgreSQL contract tests observe both orders. The memory store's fence, insert, rotation, and
1439
+ settlements run without an intervening await. An existing child is returned regardless of the fence,
1440
+ but a stale attempt with no child reports lease loss and writes nothing. While a submission or a
1441
+ handoff runs, a heartbeat renews the row's current lease token.
1442
+
1443
+ Dispatch inherits the immutable parent's tenant, space, actor ID/type, and correlation ID; causation
1444
+ is the parent invocation ID. It does not copy the parent's authorization binding or approval decision.
1445
+ The child receives current entitlement, submission authorization, `admitSubmission`, runtime
1446
+ evidence, dispatcher, execution authorization, policy, and HITL handling as a fresh governed
1447
+ decision. Consequently, a natural-person parent produces a natural-person child and does not trigger
1448
+ agent HITL; an agent parent produces an agent child and follows the normal agent route. A definitive
1449
+ admission refusal, a submission authorization or entitlement refusal, or a failed pre-submission
1450
+ check settles `rejected` only after the token rotation and an authoritative lookup that finds no
1451
+ child. When that lookup finds one, as after a deploy that changes the schema of a child whose earlier
1452
+ handoff failed, the stage takes the existing-child path instead, so a `rejected` stage has no child.
1453
+ Admission failures/timeouts, store failures, and submission timeouts before child creation retry and
1454
+ eventually become `dead_lettered`. The default retry delay is a constant one second; callers provide
1455
+ `retryDelayMs(attemptCount)` when they need linear, exponential, jittered, or another backoff policy.
1456
+ A stage becomes `dispatched` only when the child exists and the current dispatcher handoff succeeds,
1457
+ no dispatcher is configured, or the child is no longer `pending`. A dispatcher failure after creation
1458
+ leaves the stage pending with the child ID and consumes the attempt. Exhausting the attempt budget
1459
+ moves it to `dead_lettered` while retaining that child ID: this terminal state means Host could not
1460
+ confirm dispatcher handoff, not that no child was created. A `dead_lettered` stage without a child ID
1461
+ was settled after an authoritative lookup found none. Later child policy blocks, approval waits,
1462
+ validation failures, or execution failures do not rewrite a dispatched stage. Stored stage errors
1463
+ are fixed messages and do not include request parameters or caught secret text:
1464
+
1465
+ - `Staged invocation dispatch failed.`
1466
+ - `Staged invocation submission was definitively refused.`
1467
+ - `Staged child dispatcher handoff was not confirmed.`
1468
+ - `Staged child lookup failed.`
1469
+
1470
+ `dispatchStagedInvocations` returns counters for the sweep. Each claimed row is counted once, as
1471
+ `dispatched`, `rejected`, or `failed`; `failed` also covers rows whose lease was lost before
1472
+ settlement and rows whose processing threw, and `deadLettered` is the part of `failed` settled as
1473
+ `dead_lettered`.
1474
+
1475
+ This release does not provide operator requeue of dead-lettered stages, lifecycle events for stage
1476
+ transitions, a parent-result field listing children, or per-stage release options.
1477
+
1208
1478
  ## Durable outbox egress
1209
1479
 
1210
1480
  Configure `outbox` on `createGovernedActionHost()` only with an `OutboxPlatformHostStore`.