sequenceDiagram participant Old as Old transition participant Runtime participant New as New configuration Old->>Runtime: start against Epoch 7 / MCP_A New->>Runtime: MCP_A removed, MCP_B installed Runtime-->>New: publish Epoch 8 Old->>Runtime: resume after await Runtime-->>Old: epoch mismatch: abort and unwind Runtime->>New: restart against Epoch 8 / MCP_B
Async Reloads, Epochs and the Race You Probably Missed
await Changes the Problem
Suppose an MCP plugin starts like this:
async def start_mcp(ctx):
transport = ctx.services["mcp_transport"]
connection = await transport.connect()
tools = await connection.list_tools()
ctx.services["tools"].register_many(tools)Look at the two await points.
At the beginning:
mcp_transport = server A
During:
await transport.connect()the configuration changes.
Now:
mcp_transport = server B
But the coroutine still holds:
transport = server_AIt eventually registers tools discovered from A into a runtime whose current provider is B.
We have produced:
current configuration: server B
installed tools: server A
The component may appear active, but it is active against the wrong dependency world.
This Is a Coherence Problem
Our simple state machine saw only:
before: requirements satisfied
after: requirements satisfied
So:
ACTIVE → ACTIVE
looks fine.
But identity changed.
The paper introduces an epoch associated with the concrete resolved dependency configuration.(Cordiverse 2026)
For dependency specification d, the intuition is:
\epsilon_d(\sigma) = \langle \sigma(k) \mid k \in d \rangle
Think of it as a dependency fingerprint.
Example: Dependency Epochs
Our component requires:
requires = {
"llm",
"tools",
}Suppose the runtime resolves:
llm → DeepSeekProvider instance #17
tools → ToolRegistry instance #5
Conceptually, an epoch could identify:
(
id(llm_provider_17),
id(tool_registry_5),
)Call that:
Epoch 41
The component starts loading against Epoch 41.
Now the LLM changes:
llm → DeepSeekProvider instance #18
The dependency set remains satisfied.
But the epoch becomes:
Epoch 42
The runtime now knows:
The component’s target configuration changed even though all dependency keys remain present.
Add Epochs to Our Python Runtime
class Component:
def compute_epoch(self, runtime):
if not self.satisfied(runtime):
return None
return tuple(
id(runtime.services[name])
for name in sorted(self.requires)
)A Boolean tells us:
ready / not ready
An epoch tells us:
ready against exactly this dependency resolution
That is a stronger statement.
Why a Boolean Is Not Enough
These all collapse to True:
{llm=A, tools=X}
{llm=B, tools=X}
{llm=C, tools=Y}
An epoch distinguishes them:
{llm=A, tools=X} → epoch 1
{llm=B, tools=X} → epoch 2
{llm=C, tools=Y} → epoch 3
A component is not merely:
ACTIVE
It is:
ACTIVE against configuration N
That distinction is what lets the runtime reason about stale bindings.
DeepSeek Harness’s current client HMR documentation explicitly notes that dependent reloads are driven through Cordis using service-provider identities in a fiber’s activation epoch.(DeepSeek Harness contributors 2026)
Example: The MCP Race
Start:
Epoch 7
transport = MCP_A
tools = registry_1
Component begins:
connection = await MCP_A.connect()While waiting:
MCP_A removed
MCP_B installed
Now:
Epoch 8
transport = MCP_B
tools = registry_1
When the old transition resumes, it should not blindly publish work derived from Epoch 7 into Epoch 8.
A simplified policy is:
if current_epoch != starting_epoch:
abort_old_transition()Then:
abort old transition
│
▼
undo partial managed effects
│
▼
reload against latest epoch
Effects Need Interruption Boundaries Too
Suppose startup performs:
async def load():
register_prompt()
await connect()
register_tools()
await warm_cache()
register_handlers()The epoch changes during:
await warm_cache()What should be undone?
Only operations that have actually completed:
register_prompt ✓
connect ✓
register_tools ✓
warm_cache incomplete
handlers not started
Recovery should undo completed managed effects in reverse order.
This is why lifecycle machinery often needs incremental transition boundaries rather than one monolithic load() callback.
A More Realistic Python Sketch
class Transition:
def __init__(self):
self.undo = []
async def step(self, operation, epoch_ok):
if not epoch_ok():
raise EpochChanged()
undo = await operation()
self.undo.append(undo)
if not epoch_ok():
raise EpochChanged()
async def rollback(self):
while self.undo:
undo = self.undo.pop()
result = undo()
if hasattr(result, "__await__"):
await resultPlugin startup:
async def start_mcp(runtime, component, epoch):
tx = Transition()
def valid():
return component.compute_epoch(runtime) == epoch
try:
await tx.step(connect_transport, valid)
await tx.step(register_tools, valid)
await tx.step(register_prompts, valid)
except EpochChanged:
await tx.rollback()
raiseAgain, this is not Cordis.
It exposes the race that the more complete lifecycle model must handle.
Inertia: Do Not Race Reload Against Unload
Consider rapidly changing configuration:
t0 provider A exists
t1 provider disappears
t2 provider B appears
t3 provider B disappears
t4 provider C appears
while startup itself takes two seconds.
A naïve system might concurrently spawn:
reload A
unload
reload B
unload
reload C
Now lifecycle transitions race with one another.
A stronger design runs state migrations one at a time. If a newer target arrives during a transition, the system remembers it and handles it after the current transition reaches a safe boundary.
Conceptually:
target changes
│
▼
RELOAD starts
│
│ target changes again
│
▼
RELOAD finishes
│
▼
Is target still current?
│
no
│
▼
reconcile toward latest target
Not:
reload ──────┐
unload ──────┼── uncontrolled race
reload ──────┘
Relatable Example: Device Lifecycle Transitions
Kernel device lifecycle operations are not instantaneous. Probe, suspend, resume, bind and unbind can have ordering and concurrency constraints.(The Linux kernel developers n.d.)
The general OS lesson is:
A lifecycle transition has duration and needs coordination.
You cannot always model:
OLD → NEW
as a single assignment.
Real systems look like:
OLD
│
▼
TRANSITIONING
│
▼
NEW
Cordis brings similar concerns into component composition.
Relatable Example: TOCTOU
There is also a resemblance to a time-of-check/time-of-use problem.
A program observes:
resource = X
then time passes.
Later it uses X.
The world may have changed between observation and use.
Our component does:
llm = resolve("llm")
await initialise()
use(llm)The lookup was correct when performed.
That does not mean the dependency configuration is still current when startup commits.
An epoch provides a versioned answer to:
I started against configuration N.
Am I still completing against configuration N?
The analogy is useful, but an epoch in Cordis is a lifecycle and dependency tool, not a general-purpose check-then-use tool.
HMR Is Less Magical Once We See the Lifecycle
Once we have:
revertible managed effects
+
dependency re-resolution
+
epoch-aware component lifetimes
hot module replacement becomes easier to reason about.
Conceptually:
Plugin v1
│
▼
remove v1 contribution
│
▼
resolve current dependencies
│
▼
Plugin v2
rather than:
mutate a live module
and hope every reference follows
This architecture still does not imply automatic recovery from every failed hot update.
DeepSeek Harness’s current client HMR README explicitly states a known limitation: there is no failure rollback; if a reload fails, the entry remains FAILED and the previous bundle is not automatically restored.(DeepSeek Harness contributors 2026)
That limitation becomes very important in the final security discussion.
Previous: Reactive Coeffects: Dependencies That Control Component Lifetime · Next: From Clever Plugin Runtime to Programming Paradigm