symfonic.services.conversation.refs¶
refs ¶
Checkpoint-shaped value types: safe boundaries and checkpoint refs.
Split out of :mod:symfonic.services.conversation.values so each module holds
one vocabulary — session/transcript values there, the checkpoint registry's
vocabulary here — and re-exported from values so every existing import
site keeps working.
Both types encode a governance rule rather than a data shape:
:class:SafeBoundaryMarker derives its id, so marking one boundary twice
yields one marker; :class:CheckpointRef records expiry instead of deleting,
and refuses to attribute a checkpoint whose thread key cannot name its tenant.
CheckpointRef
dataclass
¶
CheckpointRef(thread_id: str, checkpoint_id: str, writer_line: WriterLine, format_version: int, created_at: datetime, package_version: str | None = None, safe_boundary: bool = False, finalized: bool = True, expired: bool = False, expiry_reason: str | None = None, tenant_id: str | None = None)
One checkpoint, as the authoritative registry knows it.
writer_line and format_version are what make a rollback decidable;
expired plus expiry_reason are what make an expiry explicit rather
than an absence.
attribution_is_certain
property
¶
Whether this ref can name its tenant without guessing.
Two ways to be certain: the tenant was recorded on the ref (the migrated writers do this), or the thread key carries exactly the two separators the derivation introduces, so its first segment is provably the whole tenant id.
owning_tenant
property
¶
The tenant this checkpoint belongs to. Refuses to guess.
:class:SessionIdentity refuses to mint a key from a separator-
bearing tenant, but that guard never applied to a key read back out of
a backend — the legacy engine derived keys without validating either
id. So for a key with more separators than the derivation introduces
(acme:eu:_:s1), the first segment may be a prefix of the tenant
rather than the tenant, and this raises instead of answering.
That matters because this value addresses expiry notices
(TenantNotificationPolicy): answering 'acme' here would deliver
tenant acme:eu's thread id and checkpoint id to a different tenant.
A recorded tenant_id is the authoritative escape hatch and is used
whenever present.
owning_tenant_or_none
property
¶
:attr:owning_tenant, or None where it would refuse.
For callers that must partition a mixed set — notify what can be addressed, quarantine the rest — rather than abort the whole batch.
expire ¶
Return an expired copy. Expiry is recorded, never a deletion.
SafeBoundaryMarker
dataclass
¶
SafeBoundaryMarker(thread_id: str, boundary_id: str, sequence: int, writer_line: WriterLine, created_at: datetime)
A contract-tested point a thread may be replayed from.
boundary_id is derived from the thread, sequence, and state digest, so
marking the same boundary twice — from a retry, a second process, or a
replayed migration — produces the same identifier and therefore one marker.
create
classmethod
¶
create(*, thread_id: str, sequence: int, digest: str, writer_line: WriterLine, created_at: datetime, boundary_id: str | None = None) -> SafeBoundaryMarker
Build a marker, deriving the id unless the writer's is supplied.
boundary_id is for rehydration only: it re-adopts a boundary under
the id the writer recorded, rather than re-deriving one from inputs
(sequence, digest) that a restarted process cannot recover.