Files
vdm/daemon/docs/proto-requests-m1.md
samiandClaude Sonnet 5 4b279e8271 daemon: store/ — SQLite WAL schema + forward-only migrator (build step 3)
The daemon's persistent state. SQLite in WAL mode, foreign keys on,
5 s busy timeout so a writer waits rather than SQLITE_BUSY under the
RPC loop.

- store/sqlite — RAII Db/Stmt over the C API; errors returned as
  DbResult<T> (std::expected), never thrown — the RPC loop must not
  unwind. transaction() helper: BEGIN / fn / COMMIT, ROLLBACK on error.
- store/migrations/0001_initial.sql — the eight tables from the brief:
  settings, categories, queues, tasks, segments, rules, history,
  pairings. Notable choices:
    * tasks columns project onto proto TaskSummary with no computation;
      requested vs effective segments/buffer split per ADR 0010/0012;
      pause_reason column per ADR 0013.
    * segments end_byte is NOT constrained >= 0 so a whole-file
      zero-length download is one row with end_byte = -1 (ADR 0010 B3a).
    * pairings stores only token_sha256 — the plaintext token is
      returned once from session.pair and never persisted (CLAUDE.md §4).
    * indices on tasks(state), (category_id), (queue_id, queue_position),
      (created_at), (completed_at) for the "1000 tasks, download.list
      under 50 ms" DoD.
    * six built-in categories + a Main queue seeded.
- store/migrations — runs every embedded migration past PRAGMA
  user_version, each in its own transaction, forward-only. SQL files
  are embedded at build time by cmake/embed_migrations.cmake.

Test veloxd.store_migrations (ASan+UBSan and TSan clean): fresh DB ->
head, all tables present, seed rows, FK cascade (segment orphan
rejected, task delete cascades), the end_byte=-1 zero-length case,
idempotent re-run, and forward-only from every released user_version.

Also: daemon/docs/proto-requests-m1.md — P1 marked landed on lane/proto
as 1.4.0 (HandlerError/HandlerResult), to be adopted in rpc/ once that
merges to main; P2 resolved.

Not linked into the running daemon yet — the store is wired to the
dispatcher when download.add/list/get get real bodies, next.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
2026-09-10 15:27:20 +04:00

4.7 KiB

DAEMON → PROTO — requests against contracts/ (and its codegen)

Status: open. Raised by lane DAEMON while building rpc/ against 1.3.0. PROTO owns contracts/, including contracts/codegen/. Ranking per contracts/README.md rule 4: a codegen output-shape change that every server must adopt is effectively major for the C++ binding even when the wire is untouched — it needs a version note and a regen, not a silent change.


Status

  • P1 — landed on lane/proto as contracts/ 1.4.0 (commit 5e3e215), as the HandlerError / HandlerResult<T> sketch below. Wire is byte-identical; C++-binding bump only. rpc/ adopts it (the predicted Result<T>HandlerResult<T> swap on the on_* overrides) once lane/proto merges to main — not against the unmerged branch. uds_roundtrip's -32603-collapse guard flips to -32010 in the same change.
  • P2 — resolved. session.hello.version-mismatch's data.expected is now $any; the error-fixture compare is on code only, so rpc/ echoes kProtocolVersion there. PROTO's writeup: contracts/proto-answers-daemon-m1.md.

P1. The generated Dispatcher has no error channel below -32603blocking a conformant server

velox::proto::Dispatcher's 39 methods each return Result<T> = std::expected<T, ParseError>, and dispatch() maps every handler error to ErrorCode::InternalError (-32603):

auto r = handler.on_download_get(*p);
if (!r)
    return make_error(id, ErrorCode::InternalError, r.error().message,
                      nlohmann::json{{"path", r.error().path}});

So a handler cannot return any of the contract's own error codes. The error fixtures in contracts/fixtures/errors/ that a live server must satisfy (DAEMON DoD: "passes the full conformance suite as a server, over both transports") include:

Fixture Expected code data Originates
download.get.not-found -32010 {taskId} inside the handler
download.add.invalid-path -32011 {path} inside the handler (after canonicalization)
download.probe.probe-failed -32013 {httpStatus} inside the handler
session.pair.rate-limited -32014 {retryAfterSec} server-side gate, but cleanest expressed as a handler result
session.hello.version-mismatch -32001 {expected, actual} can be done server-side around dispatch()
session.hello.not-paired -32002 server-side WS auth gate, around dispatch()

-32001, -32002, -32003 DAEMON can and will handle in the server layer that wraps dispatch() (-32003 is already in dispatch() itself). But -32010, -32011, -32013 are per-method handler outcomes — the daemon knows "no such task" only after the store lookup, "outside allowed roots" only after realpath(). There is no correct way to surface them today except misreporting as -32603, which the TS conformance replay will reject on the code compare.

Requested: give the generated handler methods an error return that carries an ErrorCode, a message, and a free-form data object. Shape is PROTO's call; a minimal one that keeps ParseError for the parse path and adds a handler-error type:

struct HandlerError {
    ErrorCode   code{ErrorCode::InternalError};
    std::string message;
    nlohmann::json data{nullptr};
};
template <class T> using HandlerResult = std::expected<T, HandlerError>;
// Dispatcher::on_* return HandlerResult<T>; dispatch() forwards code/message/data
// straight into make_error() instead of hard-coding InternalError.

FixtureDispatcher and conformance_main.cpp would need the trivial follow-on edit (they only ever return success today, so it is a type-name swap).

Until this lands, DAEMON's rpc/ server layer handles -3200x around dispatch() where it can, and every genuine in-handler failure collapses to -32603 with a clear message — visibly non-conformant on three error fixtures, tracked here, not worked around by inventing a side channel.


P2. SessionHelloResult.transport and -32001 data.expected — minor clarifications

  • session.hello.version-mismatch's data.expected is "1.0.0" in the fixture, i.e. the daemon's current protocol version string, not a bare major. DAEMON will echo kProtocolVersion ("1.3.0") there unless PROTO wants the fixture's literal "1.0.0" preserved — flag if the conformance compare is exact on that field rather than structural.
  • SessionHelloResult.transport is std::optional — DAEMON intends to always populate it ("uds" / "ws") so a client knows its privilege level up front, as the field's own description invites. No change requested; noting the intent so a later "why is this always set" review has the answer.