DAEMON's daemon/docs/proto-requests-m1.md P1: velox::proto::Dispatcher's
on_* methods returned Result<T> = expected<T, ParseError>, and dispatch()
mapped every handler error to -32603 InternalError. A handler had no way to
return -32010 (download.get not-found), -32011 (download.add invalid-path)
or -32013 (probe-failed) with their data payloads -- three error fixtures a
conformant server must satisfy were unreachable, blocking DAEMON's
"conformance as a server" M1 DoD.
Two error channels now, kept separate on purpose:
- parse: Result<T> / ParseError -- dispatch() failing to turn the wire into
typed params. Always -32602, always structural.
- handler: HandlerResult<T> / HandlerError -- a handler deciding the request
can't be fulfilled. Carries any ErrorCode + message + free-form data.
struct HandlerError {
ErrorCode code{ErrorCode::InternalError}; // bare {} is a valid -32603
std::string message;
nlohmann::json data = nullptr; // straight into the error's data
};
template <class T> using HandlerResult = std::expected<T, HandlerError>;
dispatch()'s handler branch is now
make_error(id, r.error().code, r.error().message, r.error().data)
instead of a hard-coded InternalError. -32001/-32002/-32003 stay the server
layer's to raise around dispatch(), as DAEMON already does.
Verified end to end against the real dispatch() path: a handler returning
TaskNotFound/InvalidPath/ProbeFailed produces -32010/-32011/-32013 with the
data object intact, and a bare HandlerError{} still yields a clean -32603
with no data field. The `= nullptr` on the member (not `{nullptr}`) matters:
brace-init of nlohmann::json from nullptr is the array [null], not JSON null.
FixtureDispatcher regenerated to HandlerResult; conformance_main.cpp only
inspects dispatch()'s JSON and needed no change. TS side is untouched beyond
the version string -- no server Dispatcher is generated there.
P2 also handled: session.hello.version-mismatch's data.expected was a stale
"1.0.0"; now $any, with a note that the error-fixture compare is on `code`
only so a server echoing kProtocolVersion there is fine.
Version: minor, 1.3.0 -> 1.4.0. Wire is byte-identical (no schema, fixture,
or OpenRPC change) but every Dispatcher implementer must swap Result ->
HandlerResult on regen, and the bump is how lanes are told to. Not an ADR:
one lane consumes this binding, it's the one that asked, and the shape is
the one they proposed. Answered in contracts/proto-answers-daemon-m1.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
contracts/codegen — the generators
Four generators, one IR. Everything is derived from contracts/schema/; nothing here is
a second source of truth.
schema_ir.py loads schema/ and lowers it to a small IR
├── gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
├── gen_ts.py -> extension/src/shared/protocol/*.ts
├── gen_openrpc.py -> contracts/openrpc.json
└── gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Regenerate everything:
for g in gen_cpp gen_ts gen_openrpc gen_cpp_conformance; do
python3 contracts/codegen/$g.py
done
tests/conformance/check_contract.py re-runs all four and fails if any committed output
differs, so stale generated code cannot be merged.
The supported JSON Schema subset
The generators refuse to guess. Anything outside this subset raises SchemaError at
generation time rather than emitting subtly wrong code — a schema that cannot be generated
from is a contract bug, and it should stop the build.
| Supported | Emitted as |
|---|---|
object + properties |
struct / interface |
object + typed additionalProperties |
std::map<std::string, T> / Record<string, T> |
string + enum |
enum class / string-literal union |
integer + enum + x-enum |
enum class : int32_t / as const object |
array + items |
std::vector<T> / T[] |
$ref to a types/*.schema.json |
the named type |
["X", "null"], or oneOf: [X, {type: null}] |
std::optional<T> / T | null |
{} |
nlohmann::json / unknown |
minimum maximum minLength maxLength pattern minItems maxItems |
runtime checks in both languages |
Deliberately unsupported: allOf, anyOf, general oneOf, patternProperties, tuple
items, recursive types. If the contract needs one, extend schema_ir.py in the same PR
that needs it.
Two rules the generated code follows
Nothing throws on the inbound path. gen_cpp.py emits parse<T>() -> std::expected<T, ParseError> and deliberately does not emit nlohmann's ADL from_json,
whose failure mode is an exception. A malformed frame off the wire is an ordinary value the
RPC loop handles, not a throw unwinding through the daemon.
Constraints are checked, not just documented. A maximum in a schema becomes an if
in both languages. The daemon is not allowed to trust the extension and the extension is
not allowed to trust the daemon — ws://127.0.0.1 is reachable by every local process, so
a type declaration proves nothing at runtime.
Absent and null mean the same thing
Both generators treat a missing field and an explicit null identically. A client that
omits a nullable field and one that sends null get the same result, in both languages.
This is stated here because it is the kind of asymmetry that otherwise surfaces as a
cross-language bug six months later.