Side-Effect Tracking
Fuze distinguishes between read-only operations and operations that change the real world. This is critical for safe retries and rollback.
Marking side-effects
// Read — safe to retry
const search = guard(searchFn)
// Changes the world — needs special handling
const sendInvoice = guard(invoiceFn, {
sideEffect: true,
compensate: cancelInvoice,
})Why it matters
When an agent run fails at step 5, Fuze needs to know:
- Steps 1-3 were reads — safe to ignore on rollback
- Step 4 sent an invoice — must call compensation function
- Step 5 failed — this is where we are
Without side-effect tracking, you either retry everything (duplicate invoices) or retry nothing (lost progress).
Compensation functions
A compensation function undoes a side-effect:
const sendInvoice = guard(
async (customerId: string, amount: number) => {
return await stripe.createInvoice(customerId, amount)
},
{
sideEffect: true,
compensate: async (result) => {
// result is the return value of the original call
await stripe.voidInvoice(result.invoiceId)
},
}
)On rollback, Fuze calls compensation functions in reverse order — last side-effect first.
Compensation timestamp accuracy
When a compensation handler runs, Fuze captures compensationEndedAt after the handler completes (not before). This ensures the timestamp accurately reflects when the compensation finished, which is important for audit trails and SLA tracking.
Compensation hash chain verification
Every compensation record is included in the audit hash chain. You can verify the integrity of the full chain — including compensation entries — by calling verifyHashChain(). This ensures that no records have been tampered with or inserted after the fact.
const result = await sideEffectLog.verifyHashChain()
// result includes both original side-effect records and compensation recordsIdempotency keys
Fuze generates idempotency keys for side-effect calls. If the same call is retried with the same arguments, the key ensures no duplication.
Idempotency keys are automatically cleaned up when you call purgeOlderThan() as part of data retention. This prevents stale keys from accumulating over time.
Rollback flow
Step 1: search(query) → read, skip
Step 2: search(refined) → read, skip
Step 3: send_invoice(cust, $) → SIDE EFFECT → call compensate()
Step 4: send_email(to, body) → SIDE EFFECT → call compensate()
Step 5: update_db(record) → FAILED HEREFuze walks back from step 4 to step 3, calling each compensation function in reverse.
Non-compensable side-effects
If a side-effect has no compensation function, Fuze logs it with status no_compensation and sets escalated: true. The incident is recorded in the audit trail for human review.
Data retention
The purgeOlderThan() method cleans up old records including side-effect entries, compensation records, and idempotency keys. Both compensation records and side-effect records are included in verifyHashChain(), so you should verify chain integrity before purging if you need a final audit check.