symfonic.services.conversation.migration¶
migration ¶
Safe-boundary replay migration.
The governing clause: a legacy checkpoint may migrate only by replay from a contract-tested safe boundary; arbitrary mid-frame state is never translated.
That is enforced structurally, not by review. There is no function in this
module that reads a legacy frame and writes a migrated one — the only verbs
are plan and migrate, and migrate calls a replay port. A frame in
the middle of a node's execution carries invariants that node established and
nobody else re-validated; translating it would mean asserting those
invariants on somebody else's behalf.
Migration is idempotent by outcome, not by luck: the replayed checkpoint is
linked to the legacy ref in the registry, so a second call — from this
migrator, from another instance, or from another worker sharing the registry —
returns the first result and never replays twice. A lock keyed by the legacy
ref closes the window between "no link yet" and "link recorded", which spans an
awaited replay whose side effects the port has already committed — and that
lock is handed out by the registry (migration_lock), not held on the
migrator, because a per-instance lock is no lock at all once two migrators
share one registry. The legacy ref is never removed, because a rollback has to
find it there.
MigrationOutcome
dataclass
¶
MigrationOutcome(thread_id: str, checkpoint_id: str, migrated: bool, already_migrated: bool = False, boundary_id: str | None = None)
What migrating actually did.
MigrationPlan
dataclass
¶
MigrationPlan(strategy: str, ref: CheckpointRef, boundary: SafeBoundaryMarker | None, decision: HorizonDecision)
What migrating this artifact would involve, before anything runs.
ReplayPort ¶
Bases: Protocol
Re-executes a thread from a safe boundary and returns the new id.
A port because replay belongs to whoever owns the graph, not to the registry. This package decides whether and from where; it never decides how.
replay
async
¶
SafeBoundaryMigrator ¶
Plans and performs replay migrations under a resumability horizon.
Source code in src/symfonic/services/conversation/migration.py
horizon
property
¶
The horizon this migrator decides under.
Readable because a caller that requires a particular horizon kind (library mode requires a package-version horizon) must be able to check the wiring when it is built, not discover it mid-replay.
migrate
async
¶
Replay from the boundary and register the result as migrated.
Everything from the "already migrated?" question to recording the link happens under one lock, because the question is only answered correctly while nobody else can be mid-replay of the same legacy ref.
Source code in src/symfonic/services/conversation/migration.py
plan ¶
Decide the strategy without touching the thread.
Order is load-bearing: the horizon is consulted before the boundary lookup, so an expired artifact reports expiry rather than reporting that its (irrelevant) boundary is missing.