Problem
RUNTIME_HOST_COMPATIBILITY_EPOCH in packages/runtime-host/src/protocol/index.ts is a hand-edited integer, and every contributor who changes the wire contract has to guess a value no one else has taken. Right now main is at 27 while three independent open PRs each set it to 28 for different, mutually incompatible wire changes — #3236 (access credential prepare/finalize), #3199 (goal.arm), #3133 (session trace cursor pages) — and #3299 takes 29 on the assumption that exactly one 28 lands.
What makes this worth fixing rather than just coordinating around is that the collision is silent by construction:
- Every branch writes the same text to that line, so git's three-way merge resolves it with no conflict. Only the adjacent comment block conflicts, and keeping both comments is the natural resolution.
- Per-PR assertions of the form
assert epoch > 27 still pass on all of them.
- Nothing compares the merged value against the merge base.
So two incompatible protocols end up advertising one epoch. client/connection.ts then admits the peer because the epochs match, and the unknown operation fails decode and tears down the transport — bypassing the structured incompatible handshake the epoch exists to provide. The failure surfaces to a user as a connection that dies instead of a version mismatch that explains itself.
This is not the first time; it recurs whenever two protocol changes are open at once, and rebasing does not surface it.
Desired outcome
Two peers advertise the same compatibility discriminator if and only if they actually share a wire contract, without any contributor having to know what other branches are doing.
A useful constraint for whoever picks this up: the epoch is never ordered at runtime. Every comparison is equality — client/connection.ts:1287, :1328, :1448, and the corresponding check in host-kernel.ts — and the - 1 occurrences are test fixtures constructing "some other epoch". So the value does not have to be a monotonically increasing integer. It only has to differ whenever the contract differs.
Alternatives or workarounds
Derive it from the protocol surface. Replace the hand-edited constant with a digest computed at build time over the canonical contract — the aggregated *_OPERATION_SPECS in protocol/operations.ts together with the frame and codec key lists. Two branches that add different operations then produce different digests on their own, which makes the collision impossible rather than merely detectable, and removes the "what number is free?" question entirely. It also gets the converse right: a refactor that preserves the contract yields the same digest, whereas the manual scheme encourages a bump whenever someone is unsure.
Costs worth weighing before committing to this:
- The value stops being human-readable. Mitigation is to keep today's comment log as a plain changelog, decoupled from the wire value.
- Canonicalization has to be deliberate — sorted operation names, explicit key lists, no incidental dependence on declaration order or
Object.keys ordering. If the input is sloppy the digest churns on unrelated edits, which is worse than the status quo.
- Someone has to decide what enters the hash. Operation names alone are too coarse: a change to an existing operation's payload shape without a rename must still move the digest.
Keep the manual bump and add a merge-base check. A CI check that compares the epoch on the PR head against the epoch on the merge base, failing when they are equal while anything under protocol/ changed. Much cheaper, and it can land today. It does not remove the manual work, and being per-PR it still cannot see a sibling branch — but it converts a silent bad merge into a red run for the second PR to land, which is the part that currently has no guard at all.
These are not exclusive; the check is a reasonable interim step either way.
Whichever direction is chosen, this is a material decision about a public compatibility contract, so it probably belongs on dev@maka.apache.org rather than being settled in a PR thread. The immediate three-way collision also needs resolving before those PRs land, independent of this issue.
Filed with Claude Code (Opus 5) assistance. The epoch values were read from main and from each referenced PR head, and the equality-only comparison claim was verified against every use of the constant in the repository.
Problem
RUNTIME_HOST_COMPATIBILITY_EPOCHinpackages/runtime-host/src/protocol/index.tsis a hand-edited integer, and every contributor who changes the wire contract has to guess a value no one else has taken. Right nowmainis at 27 while three independent open PRs each set it to 28 for different, mutually incompatible wire changes — #3236 (access credentialprepare/finalize), #3199 (goal.arm), #3133 (session trace cursor pages) — and #3299 takes 29 on the assumption that exactly one 28 lands.What makes this worth fixing rather than just coordinating around is that the collision is silent by construction:
assert epoch > 27still pass on all of them.So two incompatible protocols end up advertising one epoch.
client/connection.tsthen admits the peer because the epochs match, and the unknown operation fails decode and tears down the transport — bypassing the structuredincompatiblehandshake the epoch exists to provide. The failure surfaces to a user as a connection that dies instead of a version mismatch that explains itself.This is not the first time; it recurs whenever two protocol changes are open at once, and rebasing does not surface it.
Desired outcome
Two peers advertise the same compatibility discriminator if and only if they actually share a wire contract, without any contributor having to know what other branches are doing.
A useful constraint for whoever picks this up: the epoch is never ordered at runtime. Every comparison is equality —
client/connection.ts:1287,:1328,:1448, and the corresponding check inhost-kernel.ts— and the- 1occurrences are test fixtures constructing "some other epoch". So the value does not have to be a monotonically increasing integer. It only has to differ whenever the contract differs.Alternatives or workarounds
Derive it from the protocol surface. Replace the hand-edited constant with a digest computed at build time over the canonical contract — the aggregated
*_OPERATION_SPECSinprotocol/operations.tstogether with the frame and codec key lists. Two branches that add different operations then produce different digests on their own, which makes the collision impossible rather than merely detectable, and removes the "what number is free?" question entirely. It also gets the converse right: a refactor that preserves the contract yields the same digest, whereas the manual scheme encourages a bump whenever someone is unsure.Costs worth weighing before committing to this:
Object.keysordering. If the input is sloppy the digest churns on unrelated edits, which is worse than the status quo.Keep the manual bump and add a merge-base check. A CI check that compares the epoch on the PR head against the epoch on the merge base, failing when they are equal while anything under
protocol/changed. Much cheaper, and it can land today. It does not remove the manual work, and being per-PR it still cannot see a sibling branch — but it converts a silent bad merge into a red run for the second PR to land, which is the part that currently has no guard at all.These are not exclusive; the check is a reasonable interim step either way.
Whichever direction is chosen, this is a material decision about a public compatibility contract, so it probably belongs on
dev@maka.apache.orgrather than being settled in a PR thread. The immediate three-way collision also needs resolving before those PRs land, independent of this issue.Filed with Claude Code (Opus 5) assistance. The epoch values were read from
mainand from each referenced PR head, and the equality-only comparison claim was verified against every use of the constant in the repository.