@fabricorg/platform-host 7.0.0 → 7.1.1

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,25 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 7.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 91f7d7e: A repeated completion was judged identical by outcome alone, so a second `completed` callback carrying a different result was absorbed as a replay and its evidence discarded. A recorded completion now carries a canonical digest of provider, reference, outcome, result, error, and evidence, with observation time left out because a redelivery carries a new one. A repeat with the same digest is absorbed; the same outcome with a different digest is a contradiction and is handled as one: reconciliation, the first outcome kept, the second logged, `ExternalOperationContradicted` emitted.
8
+
9
+ ## 7.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 4c358ad: The host now reads an action's `completion` mode. Before this the contract could declare `accepted` or `long-running` and nothing in the host behaved differently, so a render job, a financing decision, or an order acknowledgment that arrives by callback had nowhere to land except reconciliation.
14
+
15
+ An adapter that returns `acceptedExternalOperation` has its handoff persisted on the invocation the moment it is accepted, before the adapter step is marked done, so a worker that dies between the two leaves the handoff and not the marker; on re-execution the step is skipped and the handoff kept. Once finalized the invocation is `running` with no lease and a recorded `pendingCompletion`. The recovery worker does not claim it, a direct re-execution returns it as it stands, and an idempotent resubmission reports the pending completion. Domain events still go out at accept time, because the handler's writes committed.
16
+
17
+ `completeExternalInvocation` completes a parked invocation by external reference. Where the store is atomic, the read and write happen under one lock so two callbacks racing cannot both find nothing recorded. A reference the invocation is not waiting on is refused. A repeated identical completion is absorbed. A contradictory completion moves the invocation to `reconciliation_required`, keeps the first outcome on record, logs the second for reconciliation, and emits `ExternalOperationContradicted`. The callback's result is validated against the action's result schema and stripped of private fields. An invocation that ended for another reason while a handoff was pending, because a later step failed or a contract was violated, keeps its status and records the callback as evidence. A handoff under an `immediate` contract, or a second handoff in one invocation, is routed to reconciliation with the reference kept and a reconciliation row written, since the external effect is in flight; if the reconciliation log is unavailable the reference is still kept and the error says so. A callback that arrives before the invocation has parked, whether under a worker's lease, during an inline execution's remaining steps, or before a dispatched invocation runs, is refused rather than settling mid-execution; the handoff carries a `parkedAt` set at finalization and that is what the ingress checks. Refusals throw `ExternalCompletionError`, which carries a `refusal` code and a `retryable` flag; only `still_executing` is retryable and an unknown invocation is `not_found`, so a webhook handler maps refusals to responses without matching strings. Crash recovery between the durable handoff and the step marker replays the accept event by its stable id.
18
+
19
+ `reconcileOverdueCompletions` moves handoffs past their `completionDeadlineMs` to `reconciliation_required` while keeping the handoff, so a late completion can still land; the worker cycle runs the sweep before it claims, and a failure in the sweep is reported rather than stranding claimed work. `ListActionInvocationsInput` gains `awaitingCompletion`, `completionDueBefore`, and `unleased` so the sweep reads only what it needs; the sweep and the contradiction path refuse without a governance-capable store. `PostgresPlatformHostStore` gains migration 3 with the `pending_completion`, `external_completion`, and `completion_due_at` columns and a partial index on the due instant, verified against PostgreSQL 16. The worker accepts `sweepOverdueCompletions: false` for a store without the governance seam. Unsatisfied policy obligations after a handoff route to reconciliation rather than failure.
20
+
21
+ Disclosed as interface growth: `GovernedActionHost` gains two members, `completeExternalInvocation` and `reconcileOverdueCompletions`, which only the host implements in this repository. `PlatformHostStore.updateActionInvocation` and the transaction-scoped sibling accept the wider `ActionInvocationPatch`, which carries `pendingCompletion` and `externalCompletion`. A custom store has new obligations for those fields, documented on `ActionInvocationPatch` and in the README; a store that ignores them lets the worker re-run a handoff. The local certification suite gains an `accepted-completion-callback` case and its contract version moves to 3.
22
+
3
23
  ## 7.0.0
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -247,6 +247,59 @@ stores should expose transaction-scoped `getEntityState()` so the guard reads th
247
247
  snapshot used by the handler and event append. Existing custom stores without that optional
248
248
  transaction method fall back to the store-level authoritative read.
249
249
 
250
+ ## External completion
251
+
252
+ An action declaring `completion: "accepted"` or `"long-running"` may hand its work to an external
253
+ system. The adapter returns `acceptedExternalOperation: { externalReference }`; the Host parks the
254
+ invocation as `running` with no lease, emits `ExternalOperationAccepted`, and leaves it for the
255
+ external system to finish. The recovery worker never claims a parked invocation, and re-executing it
256
+ returns it as it stands.
257
+
258
+ ```ts
259
+ await host.completeExternalInvocation(actionInvocationId, tenantId, spaceId, {
260
+ externalReference: "vendor-op-123",
261
+ outcome: "completed",
262
+ result: { receipt: "r-1" },
263
+ observedAt: new Date(),
264
+ });
265
+ ```
266
+
267
+ Matching is by reference: a completion naming a reference the invocation is not waiting on is
268
+ refused. A repeated identical completion is absorbed, identity being a digest of provider,
269
+ reference, outcome, result, error, and evidence. A contradictory one, a flipped outcome or the same
270
+ outcome with different evidence, moves the invocation to
271
+ `reconciliation_required`, keeps the first outcome on record, logs the second, and emits
272
+ `ExternalOperationContradicted`. Under an atomic store the read and write happen under one lock. The
273
+ callback's result is validated against the action's result schema and stripped of private fields.
274
+ An invocation that ended for another reason while a handoff was pending keeps its status and records
275
+ the callback as evidence. Declare `completionDeadlineMs` on `long-running` actions;
276
+ `reconcileOverdueCompletions()` moves overdue handoffs to reconciliation while keeping the handoff so
277
+ a late completion can still land, and the worker cycle runs that sweep before it claims.
278
+
279
+ The handoff is persisted the moment it is accepted, before the adapter step is marked done, so a
280
+ worker that dies between the two leaves the handoff and not the marker. A custom store has four
281
+ obligations here, documented on `ActionInvocationPatch`: a patch setting `pendingCompletion` with
282
+ `status: "running"` parks the invocation and clears its lease; one setting it without a status keeps
283
+ the lease; `pendingCompletion: undefined` clears it; and the claim query must never claim a running
284
+ invocation without a lease. The recovery worker's guarantee of never re-running a handoff rests on
285
+ those. A callback that arrives before the invocation has parked, whether a worker still holds the lease
286
+ or an inline execution is still running the steps after the handoff, is refused with
287
+ `ExternalCompletionError` whose `refusal` is `still_executing` and whose `retryable` is true, since
288
+ settling mid-execution would report completion before the remaining steps ran; the handoff carries a
289
+ `parkedAt` set at finalization, and that is what the ingress checks; every other refusal
290
+ is permanent and typed the same way, so a webhook handler maps them to a retryable or a final
291
+ response without matching strings. An expired lease a dead worker left behind refuses the same way
292
+ until the claim loop reclaims the row; the overdue sweep also leaves leased rows alone, so both defer
293
+ to the worker fleet and the health snapshot's expired-lease count is where a stalled fleet shows.
294
+ The contradiction path and the overdue sweep refuse without a governance-capable store, and the
295
+ worker's sweep can be turned off with `sweepOverdueCompletions: false` for such a store; a refused
296
+ handoff keeps its reference on the invocation even when the reconciliation log is unavailable, and
297
+ says so in its error. Unsatisfied policy obligations after a handoff route to reconciliation rather
298
+ than failure, since a recovered handoff cannot attest what died with its worker. `PostgresPlatformHostStore`
299
+ migration 3 adds the `pending_completion`, `external_completion`, and `completion_due_at` columns and
300
+ a partial index on the due instant the overdue sweep uses; the due instant is a real column because a
301
+ cast in an index expression is not immutable and PostgreSQL refuses it.
302
+
250
303
  ## Runtime observability and health
251
304
 
252
305
  Platform Host exposes vendor-neutral lifecycle telemetry through the optional `telemetry` sink on