Skip to content

symfonic.services.conversation.errors

errors

The conversation-service error taxonomy.

One root (ConversationServiceError) so an adopter can catch the whole package, and one leaf per decision a caller can actually act on differently. The two that matter most are deliberately not merged:

  • :class:UnsafeBoundaryError — "we will not translate this state" — is a permanent property of the artifact, and
  • :class:ResumabilityHorizonExpiredError — "we no longer support this format" — is a property of the support window, and carries the route to a human.

Collapsing them would tell an adopter with recoverable state to open a support ticket, and an adopter with expired state to keep retrying.

AmbiguousThreadKeyError

Bases: SessionIdentityError

A thread key's tenant segment cannot be reconstructed unambiguously.

Raised in both directions, because the ambiguity is symmetric:

  • forward — a tenant_id / sub_tenant_id carrying the thread-key separator is refused, so no new key can be minted that parses back as a different tenant, and
  • backward — a key read out of a backend whose tenant segment is not provably whole (more separators than the derivation puts there) is quarantined for attribution: it may still be parsed positionally and resumed, but it may not be used to name a tenant.

The migrated path never re-attributes such state. Pre-existing state written by the legacy path under a separator-bearing tenant stays readable at its key and stays quarantined for tenant attribution until an operator records the owning tenant explicitly (CheckpointRef.tenant_id), which is the one attribution this package treats as authoritative.

CheckpointFormatError

Bases: ConversationServiceError

Persisted state does not match a format this package can vouch for.

Covers an envelope from a future format version, a corrupt envelope, and a reserved key already occupied by something else. Every one of those is a refusal, never a silent downgrade to "assume legacy".

ConversationServiceError

Bases: SymfonicError

Root of the conversation/session/transcript/checkpoint taxonomy.

IssuanceFrozenError

Bases: ConversationServiceError

New checkpoint issuance is frozen; only reads and replays remain.

The freeze is how a retirement drain reaches a fixed point: no new state of the retiring shape can appear while the existing state is migrated out.

So the drain itself is not what this refuses. Registering the output of a safe-boundary replay, and adopting rows read back out of the backend after a restart, both stay open while frozen — a freeze that closed them would guarantee that nothing could ever be drained. Raised while frozen only for genuinely new state, including a "migration" of a legacy ref the registry never accounted for, which is new state under another name.

ResumabilityHorizonExpiredError

ResumabilityHorizonExpiredError(message: str, *, support_route: str)

Bases: ConversationServiceError

The artifact is past its published support horizon.

Always carries the support route in its message: an expiry an adopter cannot act on is indistinguishable from a bug.

Source code in src/symfonic/services/conversation/errors.py
def __init__(self, message: str, *, support_route: str) -> None:
    super().__init__(f"{message} Support route: {support_route}")
    self.support_route = support_route

SessionIdentityError

Bases: ConversationServiceError

A session identity could not be derived, parsed, or trusted.

Raised rather than defaulted: a guessed tenant is a cross-tenant read, and a guessed thread id silently forks one conversation into two.

TranscriptUnavailableError

Bases: ConversationServiceError

The transcript cannot be served — no durable source, or no timestamps.

Distinct from "the transcript is empty", which is a legitimate answer.

UnsafeBoundaryError

Bases: ConversationServiceError

The state is mid-frame and has no contract-tested safe boundary.

There is no translation path by design. Arbitrary mid-frame state carries node-local invariants nobody re-validated, so the only supported crossing is replay from a boundary that was tested as a boundary.