Three corrections into 1.0.0, all of which would be major bumps once the contract has landed. It has not: main still carries 1.0.0-draft, so these are corrections to an unpublished version rather than changes to a released one. ADR 0010 records that and the reasoning behind each. B1 — TaskError.code was a bare integer, and the integer space in the contract is JSON-RPC's, which is a different thing; TaskError's own description said so while typing its code as one. Freeze TaskErrorCode: a string enum mirroring vdm::Error by name and in order, all 27 failure values, verified against core/include/vdm/util/error.hpp mechanically. ErrorCode says why a call failed; TaskErrorCode says why a download failed, and a download fails while every RPC succeeds. Adds TaskError.cause so max_retries_exhausted names what kept failing. B2 — TaskSummary.segments is now explicitly the effective count in use right now, after the per-host cap and the non-resumable demotion to 1. DownloadSpec.segments and download.update's patch say they are the requested value. B3 — Segment.endByte's "minimum: 0" contradicted the description's own empty-range encoding of startByte - 1, which is -1 for the first segment of every download. Empty ranges are no longer representable and are not needed. The range stays CLOSED and INCLUSIVE, matching the HTTP Range header the two fields are copied into verbatim, and that is now stated in the schema, the README, an ADR, a fixture assertion and a conformance check. CORE asked for half-open and gets a written notice rather than a silent schema edit. Segment state spells 'downloading' as CORE asked, not 'receiving'. check_contract.py now enforces segment contiguity, coverage of exactly [0, sizeBytes-1], downloadedBytes within the range size, and the entry count matching TaskSummary.segments. The download.get fixture claimed 8 segments while carrying 2; it now carries 8 contiguous ones covering the whole file. contracts/proto-answers-m1.md answers every item in core/docs/proto-requests-m1.md, including the ones not being landed now: B2a and F2 accepted as follow-ups, F1 answered with the notify path for M1, F3 already frozen as a Checksum object rather than a string, and D1 left for DAEMON to draft as the three-way ADR it is. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
4.5 KiB
ADR 0005 — Protocol v1.0.0 freeze and the versioning rule
Status: accepted · Date: 2026-09-09 · Lane: PROTO
Context
M0 exists to produce one thing: an interface the CORE, DAEMON, GUI and EXT lanes can build against in parallel without meeting. Everything after M0 is four lanes working from the same contract and not talking to each other for weeks. The cost of getting this wrong is not a bug; it is an M2 integration rewrite.
The failure mode this is designed against is specific and predictable: a lane needs a field that isn't in the contract, adds it locally "just for now", and nobody finds out until four implementations disagree at once.
Decision
contracts/VERSION is frozen at 1.0.0. The surface is 38 methods, 9 events, 26 named
types and the JSON-RPC envelope, exactly as contracts/README.md documents it.
Three corrections landed into 1.0.0 before it reached main, answering CORE's
freeze-blockers: a TaskErrorCode wire taxonomy, the effective-vs-requested meaning of
TaskSummary.segments, and the segment range convention. See ADR 0010 — including why
correcting an unpublished 1.0.0 in place is not the major bump this ADR's own rule would
otherwise demand.
Semantics of a change:
| Change | Bump | Also needs |
|---|---|---|
| New method, new event, new optional field | minor | fixtures for it |
| New enum value | minor | both generators handle it; check client default: arms |
| Rename, remove, retype, change a default | major | an ADR with a migration note |
session.hello compares majors only. A mismatch is refused with -32001 and a message
the GUI renders as "Velox needs updating"; a differing minor or patch is always accepted.
Failing loudly at connect is the point — the alternative is failing subtly at the tenth
field, three weeks later.
Process: changes arrive as a PR touching contracts/ alone, containing the schema edit,
the fixtures, the regenerated code and the VERSION bump. Lanes rebase onto it. This is the
only synchronization point in the project, so it is kept cheap and frequent rather than big
and rare.
What enforces this rather than hoping for it
- Generated code is committed, so no lane is blocked on running Python, and a stale regeneration is a diff in the PR rather than an invisible skew.
tests/conformance/check_contract.pyre-runs all four generators and fails if any committed output differs. Hand-editing generated code cannot be merged.- The generators refuse schema constructs they cannot lower, so an unrepresentable schema stops the build instead of producing subtly wrong code in one language only.
- Every method must have a success fixture, checked mechanically.
SettingKeyandSettings.propertiesmust name the same keys, checked mechanically — the GUI cannot invent a settings key that isn't in the contract.
Consequences
- Adding a field costs a round trip through PROTO. That is the price of the guarantee, and it is deliberately much cheaper than the M2 rewrite it prevents.
- Four generators must stay in step with
schema_ir.py. They share one IR precisely so this is one change, not four. - The frozen surface has known asymmetries, left in on purpose rather than invented around:
there is no
queue.removeand norules.remove(rules are deleted throughrules.upsert'sremovelist, queues not at all in v1). These were not added becausecontracts/README.mddoes not list them, and quietly widening the surface during the freeze is the exact habit this ADR exists to prevent. They are minor bumps whenever a lane actually needs them.
Alternatives rejected
Hand-written types per lane. This is the default and it is how projects like this fail. Four hand-maintained copies of a type diverge silently; the divergence is discovered at integration, when all four are load-bearing.
Protobuf or Cap'n Proto. Better wire types, but the extension must speak this protocol
from a WebExtension, and JSON-RPC over JSON is what a browser speaks natively. A binary
codec buys efficiency on a control plane that moves a few hundred small messages a second,
and costs a build dependency in every lane plus a much worse debugging story: nc and a
browser devtools console can both read this wire.
Not freezing, and letting the contract evolve continuously. The whole parallel-lane plan depends on the interface being still. An unfrozen contract means every lane rebases onto a moving target, which is the serialized dependency M0 exists to remove.