Atomic Regions and Durability Controls in Effect Golem Agents
Golem agents are durable by default. Use these advanced controls only when replay behavior must
change for a specific group of externally observable effects. Import the Effect SDK API from the
package root; do not translate callback helpers from @golemcloud/golem-ts-sdk.
import { Duration, Effect, Schema } from "effect";
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from "effect/unstable/http";
import { Durability, Retry } from "@golemcloud/effect-golem";
Atomic Regions
Durability.atomically accepts an Effect, not a synchronous or async callback. If execution
fails after only part of the region completed, recovery re-executes the region from its begin
marker instead of replaying from the middle.
const placeOrder = Durability.atomically(
Effect.gen(function* () {
const reservation = yield* reserveInventory(itemId, quantity);
const charge = yield* chargePayment(customerId, price);
return { reservation, charge };
}),
);
reserveInventory and chargePayment above are assumed to return Effects. Yield every operation
inside the body so the atomic region controls their ordering and failure.
Use an atomic region for:
- two or more external effects that must replay together after a crash; or
- one external effect followed by validation that may fail, when that failure must cause the effect itself to run again instead of replaying its recorded response.
External effects can still be repeated. Atomicity here means whole-region re-execution during durable recovery, not ACID isolation or rollback in the external system. Design remote operations to tolerate duplicates or use a stable idempotency key.
An atomic region only controls one durable execution. It does not deduplicate a later, independent invocation of the same method: that invocation runs the method, including work after the atomic region, again. When validating a state-changing recovery example, start the atomic work once against a fresh agent identity. Do not run it during build/deploy validation if a test harness will start the same identity afterward.
Atomic Outgoing HTTP Effects
Use Effect's HttpClient when the atomic region must call an external service. The stable request
ID in this example is supplied by the caller, and each state-changing remote operation gets a
distinct idempotency key so whole-region replay can safely repeat it:
const record = (event: string, requestId: string) =>
HttpClientRequest.post("https://effects.example.com/record").pipe(
HttpClientRequest.setHeader("Idempotency-Key", `${requestId}:${event}`),
HttpClientRequest.bodyJson({ event }),
Effect.flatMap(HttpClient.execute),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.asVoid,
);
const shouldRetry = (requestId: string) =>
HttpClientRequest.get(
`https://effects.example.com/decide?requestId=${encodeURIComponent(requestId)}`,
).pipe(
HttpClient.execute,
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.flatMap(HttpClientResponse.schemaBodyJson(Schema.Boolean)),
);
const runAtomicWork = (requestId: string) =>
Effect.gen(function* () {
yield* Durability.atomically(
Effect.gen(function* () {
yield* record("a", requestId);
yield* record("b", requestId);
if (yield* shouldRetry(requestId)) {
return yield* Effect.die(
new Error("retry the complete atomic region"),
);
}
yield* record("c", requestId);
}),
);
yield* record("d", requestId);
}).pipe(Effect.provide(FetchHttpClient.layer), Effect.orDie);
The remote service must actually deduplicate each stable key. Merely setting an idempotency header
does not make the operation idempotent. FetchHttpClient.layer provides the HTTP service at the
handler boundary, and HTTP transport, status, encoding, and decoding failures inside the region
all trigger the same atomic trap behavior as any other unsuccessful Effect. The read-only decision
must eventually change after the condition that caused the trap is repaired; an unchanged true
response makes recovery fail repeatedly.
Failure Semantics
Durability.atomically has intentionally stronger failure behavior than a normal Effect scope:
- On success it writes the matching end marker and returns the body's value.
- On a typed failure, defect, or interruption it leaves the region open and invokes the host's uncatchable trap so durable recovery can retry the complete region.
- An outer
Effect.catchAll,Effect.either, or similar operator cannot handle a body failure and continue the same invocation after the region.
The trap-recovery guarantee is a worker execution guarantee, not a promise that the original HTTP, RPC, or CLI caller waits for replay. Do not externally retry with a new invocation merely because the first caller observed a failure: that would start the method again and can duplicate work after the atomic region.
Use Effect.fail for a condition that should trigger this trap just as a defect does; the high-level
atomic combinator traps every unsuccessful exit. Do not put caller-visible business failures inside
an atomic region if the invocation should recover from them and continue normally.
Failure-Path Verification Limit
The pinned SDK has unit tests proving that an unsuccessful body leaves its marker open and calls the host trap, but its real-runtime integration test covers only a successful atomic region. It exposes no public Effect that waits for trap recovery, reports the recovery attempt, or guarantees that the original caller eventually receives the replayed result. A fire-and-forget typed call only confirms host acceptance; it does not provide a completion handle for the trapped invocation.
Consequently, use a successful Durability.atomically body for portable end-to-end application
tests. Verify deliberate crash/recovery behavior with executor-specific worker status and oplog
tooling rather than making a normal awaited method or polling loop depend on the trap completing.
Keep deterministic failures out of production atomic bodies: if the condition is unchanged after
recovery, the region can fail repeatedly.
What Atomic Regions Are Not
- Do not wrap ordinary
Refupdates just to make in-memory state “transactional.” Durable replay already rebuilds agent state, and agents process invocations sequentially. - Do not use atomic regions to reduce oplog size or speed up recovery. Use snapshot-based recovery for that purpose.
- Do not claim that nested regions are supported, rejected, flattened, or merged; the pinned SDK does not define or test nesting behavior.
Manual Begin and End Markers
Prefer Durability.atomically. When imperative control is genuinely required, begin is an Effect
value and end is a function taking the returned bigint oplog index:
const manuallyBracketed = Effect.gen(function* () {
const begin = yield* Durability.beginOperation;
const result = yield* performExternalWork;
yield* Durability.endOperation(begin);
return result;
});
Manual markers do not trap or pair themselves on failure. Never put endOperation(begin) in an
unconditional finalizer: closing the marker after failure commits the region instead of leaving it
open for recovery.
Persistence Level
Persistence-level controls are primarily for Golem-specific libraries implementing custom durable behavior, not ordinary application optimization. Prefer the scoped combinator, which restores the previous level when the Effect exits:
const result = Durability.withPersistenceLevel(
Durability.PersistenceLevel.persistNothing,
customDurabilityEffect,
);
Available values are:
| Value | Meaning |
|---|---|
Durability.PersistenceLevel.smart |
Default host-managed durable behavior |
Durability.PersistenceLevel.persistRemoteSideEffects |
Persist remote side effects for replay |
Durability.PersistenceLevel.persistNothing |
No replay or state-restoration guarantee for the section |
persistNothing is not an oplog-size switch. Code using it must implement the required live/replay
behavior itself. For low-level integration work, Durability.getPersistenceLevel is an Effect
value and Durability.setPersistenceLevel(level) changes the host mode directly; ordinary code
should use withPersistenceLevel so restoration is scoped.
Idempotence Mode
The host default is true. In that mode, side effects are treated as idempotent and Golem
guarantees at-least-once semantics. This does not make an inherently non-idempotent external
operation safe. Opt out only for a side effect where a duplicate is worse than a missing call:
const atMostOnce = Durability.withIdempotenceMode(false, nonIdempotentEffect);
false selects at-most-once handling; the executor fails the agent when it cannot determine whether
the side effect already ran. The scoped combinator restores the prior mode. Low-level code can yield
Durability.getIdempotenceMode and call Durability.setIdempotenceMode(value), but should not
manually change the mode when the scoped form is sufficient.
Oplog Replication Barrier
Wait for the oplog to reach the requested replica count by yielding the Effect:
const replicationBarrier = Effect.gen(function* () {
yield* Durability.oplogCommit(3);
});
The replica count must be a safe integer from 0 through 255; the SDK forwards that value. The
host waits for at least that many replicas, or its maximum replica count if fewer are available.
This is a replication barrier, not an atomic-region commit.
Durable Idempotency Keys
generateIdempotencyKey is an Effect value, not a function:
const makeRequest = Effect.gen(function* () {
const key = yield* Durability.generateIdempotencyKey;
return { key };
});
The host persists and commits the generated key, and replay returns that same key rather than
generating another one. The generated TypeScript value is a UUID record with highBits and
lowBits bigint fields, not a UUID string. Convert it explicitly at an external API boundary if
text is required.
Scoped Retry Policies
Retry policy control belongs to the separate Retry namespace. Effect Golem manages named host
policies rather than replacing one anonymous global policy:
const paymentRetry = Retry.NamedPolicy.named(
"payment",
Retry.Policy.periodic(Duration.seconds(1)),
);
const result = Retry.withPolicy(paymentRetry, paymentEffect);
Retry.withPolicy attempts to restore the previous policy with the same name, or remove the
temporary one, when the Effect exits; cleanup failures are ignored. Other named policies remain
active. The combinator temporarily changes the host-visible policy set—it does not retry the whole
Effect like Effect.retry. Do not substitute a local Effect retry schedule when durable,
host-visible retry behavior is required.
Key Constraints
- Import
DurabilityandRetryfrom@golemcloud/effect-golem; do not import internal host clients or layers. - Normal
defineAgent(...).implement({ init, methods })execution supplies the services required by these Effects. Do not manually provide SDK-internal layers in application code. - Keep agent methods Effect-based; do not wrap
Durability.atomicallyin anasynccallback. - Prefer
Durability.atomicallyover manual begin/end markers. - Keep externally observable actions duplicate-safe because recovery may execute them again.
- Use Effect
HttpClientwithFetchHttpClient.layerfor outgoing HTTP rather than wrapping rawfetchinEffect.tryPromise. - Do not assume an awaited caller or fire-and-forget trigger can observe completion after an intentional atomic trap; use executor-specific recovery tooling for that test.