Skip to main content
Version: 1.0.0

Explicit bounded compensation

A compensation requests the declared business inverse of one confirmed external effect, such as releasing a reservation. It is never implicitly triggered by failure, fallback, cancellation or deployment rollback.

The original effect remains applied. The inverse has its own run, effect, budgets, secrets and authority. Its outcome is completed, partial, unavailable, failed or unknown; only an observed inverse outcome can determine that result.

Declare the inverse​

A touba-manifest-v1 external-write binding identifies an existing strategy in its effect contract:

touba.manifest.json
{
"manifest_version": "touba-manifest-v1",
"bindings": {
"tool:reservations/CreateReservation": {
"handler": {
"kind": "process",
"command": "node",
"args": ["handlers/create-reservation.mjs"]
},
"grants": ["network"],
"secrets": ["RESERVATION_API_TOKEN"],
"timeout_ms": 5000,
"max_payload_bytes": 65536,
"effect_contract": {
"kind": "external_write",
"retry_mode": "idempotency_key",
"compensation": {
"entry_ref": "strategy:reservations/ReleaseReservation"
}
}
}
}
}

The inverse graph contains pure bind nodes, at most one human approval, exactly one external tool call and a terminal give or stop. Branches, loops, fallbacks, nested calls, interactions, a second effect and recursive compensation are refused.

Handler contract​

For one declared input, request contains its value directly:

release-reservation.handler.mjs
import { createToolResult, defineTool } from "@touba/sdk";

await defineTool({
toolRef: "tool:reservations/ReleaseReservation",
inputSignature: [
{ name: "reservation_id", type_ref: "builtin:Text" },
],
outputType: "builtin:Text",
run: async ({ request: reservation_id, context }) => {
const response = await releaseReservation({
reservationId: reservation_id,
originalReceipt: context.compensation.original_effect_receipt,
idempotencyKey: context.effect.effect_id,
});

return createToolResult(response.message, {
effectReceipt: context.createEffectReceipt({
outcome: "applied",
receipt_ref: response.receiptRef,
}),
compensationReceipt: context.createCompensationReceipt({
result: response.complete ? "completed" : "partial",
}),
});
},
});

releaseReservation is your business adapter. See the runnable callback test.

The inverse handler receives context.compensation.original_effect_receipt and context.effect.effect_id. It returns an effect receipt created with context.createEffectReceipt(...) and a compensation receipt created with context.createCompensationReceipt(...). Keep the original receipt in control data; do not copy it into business arguments, logs or public traces.

The host separately requests creation and resume. Each command checks target authority before reading the journal, then revalidates authority before progress and immediately before spawning a handler. Resume supplies the current revision and digest. An exact already-handled replay is read-only: no handler or new trace. A fresh approval can replace an expired approval for the same subject and reference; approval is not inherited from the original run.

Meaning of completion​

Completion means the declared inverse was applied, not that the external world was perfectly restored. The supplied SQLite store atomically checks revisions and digests across local processes. The host coordinates executions and backups; this store supplies no multi-host lease or tamper-proof history. Existing JSON state uses the explicit migration in Storage and retention.

See External effects.