symfonic.services.conversation.checkpoint¶
checkpoint ¶
Checkpointer readiness, flush, and close — the conversation-side owner.
This is the object T2.3.4's RunLifecycle registers as its
CheckpointerPort. Three of its rules come straight from that contract and
are the reason it exists as a service rather than as three flags on an engine:
ensure_readyis lazy and idempotent per instance, because readiness can open a pool or run a migration, and the call site that happened to trigger it is not the call site that should own its teardown;- a failed readiness does not latch — the next call retries, because a transient backend outage that permanently poisoned a run would be worse than the outage;
flushreports failure and never raises, so a flush failure can never blockclose. A checkpointer that refuses to close because it could not write is a leaked pool.
A deployment with no checkpointer at all is a supported configuration, not an error: when neither durable driver is enabled, nothing is wired, and every verb here is a no-op.
CheckpointAdapterPort ¶
Bases: Protocol
The durable-state handle this service governs.
Structurally a superset of the kernel's CheckpointerPort (readiness,
flush, close) plus the two reads restart recovery needs. Adopters
implement it with their own saver; nothing here knows what a checkpoint
physically is.
CheckpointService ¶
CheckpointService(*, adapter: CheckpointAdapterPort | None, registry: CheckpointRegistry | None = None, clock: object | None = None)
Owns readiness and teardown for one run's durable conversation state.
Source code in src/symfonic/services/conversation/checkpoint.py
flush_failures
property
¶
Flush failures, as safe-to-log text. Never exception objects.
close
async
¶
Release the durable handle, flushed or not. Idempotent.
Source code in src/symfonic/services/conversation/checkpoint.py
ensure_ready
async
¶
Open durable state once. Idempotent on success, retried on failure.
The lock is what makes "once" true under concurrency: readiness may
open a pool or run a migration, and two coroutines of the same run that
both observed _ready is False would otherwise do it twice. The
second waiter re-checks after acquiring, so it costs one flag read on
the hot path once readiness is established.
Source code in src/symfonic/services/conversation/checkpoint.py
flush
async
¶
Push buffered writes. Failure is recorded, never raised.
Source code in src/symfonic/services/conversation/checkpoint.py
record_write
async
¶
record_write(thread_id: str, checkpoint_id: str, *, safe_boundary: bool = False, sequence: int | None = None, digest: str | None = None, package_version: str | None = None, tenant_id: str | None = None, finalized: bool = True) -> CheckpointRef
Register a checkpoint this service wrote as authoritative.
A safe-boundary write also marks the boundary, because a boundary nobody recorded is a boundary no future migration can replay from — and the moment a boundary is written is the only moment its sequence and digest are known for free.
That marker lives in a process-local registry, so the durable half
is the caller's: stamp registry.latest_safe_boundary(thread_id)'s
boundary_id and sequence into the envelope
(:func:~symfonic.services.conversation.compat.encode_envelope) that
goes out with the state. Restart adoption re-adopts the boundary under
exactly that id and never re-derives one, so the id a migration replays
from is the id a writer marked — before and after a restart alike.
finalized=False opens the crash window on this ref. Its durable
half is the caller's in the same way: stamp
encode_envelope(..., finalized=False) into the metadata that goes
out with the write, and a finalized=True envelope when the write is
closed. A restart adopts what the envelope says, so an unfinalized row
left behind by a crash that ended the process is expired by
reconcile_crash_expiry with a reason rather than resumed mid-frame.