Checkpointer and Graph Evolution¶
This documents current behavior. Details may change between versions — there are no stability guarantees yet.
See also: Event migrations — the full migration story for renamed / relocated event classes and added required fields, including the coverage gates that fail CI when a baselined event identity is no longer covered.
Existing threads survive graph modifications via graceful degradation — no crashes, but some changes have silent side effects.
What's Safe¶
| Change | Behavior |
|---|---|
| Add a handler | dispatch() is rebuilt from current handlers; new handler participates immediately. |
| Add an event type | New events can be emitted and matched normally. |
| Remove an event type | Existing events stay in the log but no handler matches them — inert. Not true for an Interrupted subclass a thread is still paused on — see the row below. |
What to Watch Out For¶
| Change | Risk |
|---|---|
| Remove a handler (normal checkpoint) | Events only the removed handler subscribed to become undeliverable. Graph halts early — no crash, incomplete execution. |
| Remove a handler (interrupted checkpoint) | If the graph was paused inside the removed handler via Interrupted, graph.resume(value) raises UnresumableError by default (set EventGraph(on_unresumable="halt"\|"warn") to handle it non-fatally). Declare @on(previously=...) to keep it resumable. To retire the Interrupted subclass itself, call graph.abandon(config) or .aabandon(). It settles the paused thread without ever answering it. See Ending a pause without answering it. See Handler renames. |
Remove an Interrupted subclass |
A thread paused on it still holds a pending __interrupt__ write naming the class. Deleting the class without a migration breaks revival on that write, not just dispatch. Call graph.abandon(config) (or .aabandon()) on every thread paused on it first, then delete the class. See Ending a pause without answering it and Retiring an Interrupted subclass. |
| Rename a handler | An Interrupted checkpoint targeting the old node is lost — unless you declare @on(previously="old_name"), which keeps it resumable. See Handler renames. |
| Rename / relocate an Event class | Old checkpoints fail revival under the default serde — author a migration; the coverage gates catch an uncovered rename in CI before deploy. A renamed Interrupted subclass stays resumable under @migrate_from. Removing it is different: it needs abandon() first (see above). |
| Add a reducer | New reducer starts cold — misses default values and all historical projections. Only post-resume events contribute. |
| Remove a reducer | Channel data is silently dropped from the checkpoint. |
Best Practices¶
- Renaming a handler with active interrupted threads is safe if you declare
@on(previously=...)— for an inlineCommand, thepreviously: ClassVar = (...)class attribute — (or pin@on(node_name=...)up front); the handler coverage gate flags an undeclared rename in CI. - Treat reducer addition as a fresh start; for full history, use a new thread.
- Prefer additive changes (add handlers/events; removal is safe only with no in-flight threads). For an
Interruptedsubclass,abandon(config)on each paused thread produces exactly that state. - Use a new
thread_idafter structural changes to avoid all edge cases.