12 Commits
Author SHA1 Message Date
samiandClaude Sonnet 5 4e177ec809 proto: stop conformance from downloading real files, ADR the null-clearing gap
1. download.add.json had startMode "now" against a real, large (~6 GB)
   Ubuntu ISO with saveDir hardcoded to /home/sami/Downloads/Programs.
   Against a real veloxd (tests/conformance/run.sh) that's a real
   download into the real user's real home, every single run — it had
   already happened twice. startMode -> "later" (exercises the add path,
   hands nothing to the engine) and saveDir is dropped entirely (resolves
   to saveTo.defaultDir instead, checked against allowedRoots the same
   way). Documented the rule this fixture was breaking in
   contracts/fixtures/README.md so it doesn't happen a third time.

   Auditing the rest for the same shape (now real: capture.offer, D7)
   found a second, subtler instance: capture.offer.take.json's "take"
   admits a real, immediately-started task the same way download.add
   does, and the "Programs" category's saveDir is a migration-seeded
   builtin (~/Downloads/Programs) that no isolated test setup can
   redirect -- so even after pointing the URL at example.org (RFC 2606),
   a real ~6 GB sparse .veloxpart still landed in the real home on the
   declared Content-Length alone. Shrunk to a plausible-but-small 5 MiB.
   Also scoped to "transport": "uds" -- a real "take" persists an active
   task, so replaying the same fixture again on the second live transport
   against the same shared daemon was hitting capture.offer's own
   dedupe-by-URL and failing on a missing taskId, not a bug.
   download.add's other real-URL siblings (errors/*.invalid-path,
   *.invalid-params, *.disk-full) all fail before admission or are
   requires-gated; left alone.

2. ADR 0018: DAEMON can set a nullable field through download.update /
   settings.set but never clear it back to null, because the generated
   C++ parser collapses "absent" and "explicit null" to the same
   std::nullopt for every optional field (contracts/codegen/gen_cpp.py,
   on purpose, and correct for create-style params -- just wrong for
   patch-style ones, which is the only place the schema documents
   "explicit null clears"). Decision: an opt-in x-clearable schema
   annotation makes just those fields std::optional<std::optional<T>> in
   C++ (TS already round-trips this natively); not a blanket rule
   (would retype response fields like TaskSummary.effectiveUrl that have
   no clear-vs-absent distinction to make), not an explicit clear-list
   field (would redesign a wire contract DAEMON already built against
   just to route around a generator gap). Recorded, not implemented here
   -- that's its own PROTO PR (schema annotations + gen_cpp.py + gen_ts.py
   + regeneration + a minor VERSION bump per ADR 0015), not bundled into
   a fixture-safety pass. Left a pointer to the ADR at the generator
   comment it concerns.

3. Re-verified every xfail entry against current deferrals.md rather
   than trust the reasons already on file: D7/D8 (capture.offer/
   getRules), D3d/e/f/g/h/i (rules, queue.reorder, schedule, limiter,
   download.update/refreshUrl) and D9 (settings) have all closed since
   the list was last pruned, so most of it was stale. Removed everything
   that now cleanly passes; kept and re-reasoned everything that doesn't:
   - errors/download.provideAuth.not-found.json stays, as asked: real
     bug, on_download_provideAuth never checks the task exists.
   - category.list.json (mimeTypes -- documented D3a gap), schedule.set.json
     (nextRunAt -- documented D3f gap): unchanged in substance, reason
     text was already accurate.
   - download.probe/get/list/update.json, session.hello.json,
     queue.start/reorder.json, category.remove.json: not bugs -- each
     golden depicts a richer lifecycle/config state (a probed download,
     real queue or category membership, media/grabber capabilities) than
     this harness's fresh, never-started bound tasks and empty isolated
     DB can produce.
   - limiter.get.json: real fixture bug, not a daemon one -- applyToRunning
     is a write-only instruction on limiter.set, on_limiter_get never
     returns it; the golden shouldn't have had it either. Fixed the
     fixture and tools/mockd's own limiter.get, which had the same field
     hardcoded into its in-memory state independent of the fixture file.
   - grabber.*/media.*: still genuinely stub (M4 territory).
   Only remaining unexpected-pass surfaced while re-verifying
   (errors/capture.offer.ignore.json, always "take" instead of "ignore")
   traced to capture.minSizeBytes defaulting to 0 on a fresh daemon,
   making its below-minimum-size scenario unreachable -- not a bug, so
   raised the setting in run.sh's isolated seeding instead of xfailing it.

ctest -L conformance: green, 100% (2/2), ~87s.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
2026-09-12 16:56:35 +04:00
samiandClaude Sonnet 5 e30d994d74 proto: fix conformance run.sh flakiness, prune the veloxd xfail list
Two run.sh fixes plus the xfail prune, all requested together:

1. VELOX_PAIR_AUTO=1 for the isolated veloxd. Pairing is the D1 dev stub
   (EnvAutoApprover) and denies without it, so session.pair never issued
   a token and the WS half of the veloxd step could never even connect.

2. WS_PORT was hardcoded to 52080 with no free-port search, so one leaked
   mockd made every future run fail EADDRINUSE. free_port() binds :0 and
   asks the kernel instead. The EXIT trap's stop() used `pkill -P "$pid"`,
   which only reaps direct children — tsx's actual listener is often a
   grandchild, which that missed and left holding the port. Every server
   (mockd, slow mockd, veloxd) now launches under `setsid`, making it the
   leader of its own process group, so stop() does `kill -TERM -"$pid"`
   (a process-group kill) and reaches everything it spawned in one shot.

3. Pruned the xfail list now that D2, D4b and most of D3 have landed.

Pruning surfaced two more bugs than expected, both in the test harness
itself, not veloxd — worth recording since they were indistinguishable
from real daemon hangs until isolated:

- errors/session.hello.version-mismatch.json documents that the *server*
  closes the connection after replying (correct, intended behavior). The
  harness replays every fixture on one shared connection per transport,
  so once this fixture ran, every later UDS fixture sent into the dead
  socket and just sat there until its own timeout — including ones still
  on the xfail list, which applyXfail waved through as "expected -32603"
  regardless of the real reason. Fixed with a `closesConnection` fixture
  flag: replay() reconnects (fresh session.hello) right after such a
  fixture instead of leaving the rest of the run to time out one by one.
  This is what was actually behind queue.*/session.*/download.remove
  appearing to hang — none of them do; verified individually and via a
  raw probe script before finding the real cause.
- category.remove.json (deletes the "firmware" category) sorted before
  category.upsert.json (creates it) alphabetically, so it was failing
  -32602 "no such category" against a fresh DB — never a daemon bug.
  Added it to DESTRUCTIVE so it now replays after every other fixture.

Also fixed while verifying "confirm each really passes": download.addBatch.json's
`defaults.categoryId` was "compressed", a category nothing ever creates —
real veloxd correctly enforces the FK on tasks.category_id, so all three
batch items failed instead of the two expected. Changed to "programs" (a
migration-seeded builtin).

Of the 15 fixtures named for pruning, 10 turned out to cleanly pass and
are gone from the list entirely: download.pause/resume/start/cancel,
download.remove, download.addBatch, queue.upsert/stop, download.probe's
success path (D2, including errors/download.probe.probe-failed.json),
and category.upsert. Two do NOT cleanly pass and are kept, with reasons
rewritten to match what's actually happening now instead of the stale D3
text: download.probe.json (see below) and errors/download.provideAuth.not-found.json,
a real bug — on_download_provideAuth never checks the task exists, so an
unknown taskId gets a normal `{ok:false}` result instead of -32010.

Five more fixtures newly needed xfail entries to reach green, none of
them stubs:
- category.list.json — documented gap (deferrals.md's D3a note): the
  categories table has no mimeTypes/sortOrder columns.
- download.probe.json, download.get.json, download.list.json,
  session.hello.json — not bugs. Each golden depicts a richer lifecycle
  state (a probed/in-progress download, a daemon with media/grabber/
  Secret Service implemented) than this harness's bound tasks, which are
  always fresh and never started, can produce. Optional/omit-if-absent
  fields (effectiveUrl, requiresAuth, capabilities) are correctly absent;
  the mismatch is against the golden's illustrative values, not the
  contract.
- queue.start.json, category.remove.json — same class: startedTaskIds /
  reassignedTaskIds are correctly empty because this run's queue/category
  have no real membership.

`ctest -L conformance` is green: 100% (2/2), 81.7s (down from ~240s now
that pairing and the port/reconnect fixes remove the retries and the
5-10s timeouts the connection-death bug was producing).

One thing NOT fixed here, flagged for a follow-up decision rather than
touched mid-task: download.add.json's fixture is `startMode: "now"`
against a real, large (~6GB) Ubuntu ISO on the real internet, with
saveDir hardcoded to /home/sami/Downloads/Programs. Every run against a
real veloxd writes a real multi-GB file into that path — confirmed by
running this repeatedly during verification. Isolating the daemon's XDG
dirs doesn't isolate this. Worth its own change (startMode: "later"
would still exercise the add path without the transfer) but out of scope
for a fixture I wasn't asked to touch beyond what blocked this task.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
2026-09-12 14:04:18 +04:00
samiandClaude Sonnet 5 dd1676bc0a proto: renumber ADR 0015 (0014 collided with PKG's), fix stale README versions
PKG landed docs/adr/0014-conformance-runs-through-ctest.md (dad88fc) and this
lane's 0014-generated-binding-changes-and-versioning.md landed in the same
integration round, both as 0014. Renumbered this one to 0015 — second merger
renumbers. Updated the two cross-references (contracts/README.md rule 4,
proto-answers-daemon-m1.md P1) and the in-file header.

Also while in contracts/README.md: the file-tree comment and the "Method
surface" heading still said v1.2.0; both now v1.4.0 to match VERSION.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 15:31:01 +04:00
samiandClaude Sonnet 5 768f4f8e53 proto: ADR 0014 — versioning a generated-binding break with an unchanged wire
Records the rule 1.4.0 applied, so the next generated-binding retype cites it
instead of relitigating README rule 4's "retype → major + ADR" from scratch.

The rule: VERSION tracks the wire protocol, not any binding's API or ABI. A
change that leaves the wire byte-identical but breaks a generated binding's
source API (a C++ virtual's return type, a struct name) is a minor bump plus
a migration note — major would make session.hello refuse a client whose wire
behaviour is unchanged, which is worse than the problem. An ADR is still
required when the change encodes a design decision; "only one lane consumes
it" is not a reason to skip that, since GUI already links velox::proto and
the next such change starts with more than one consumer.

Rule 4 in contracts/README.md now points here so its "major + ADR" line
isn't read in isolation. proto-answers-daemon-m1.md's P1 writeup references
it as the durable home for the reasoning that was otherwise only in a commit
message.

Also names the nlohmann brace-init hazard the 1.4.0 work hit: json{nullptr}
is the array [null], not JSON null, so HandlerError::data is `= nullptr`. The
kind of thing a regeneration reintroduces; caught here only by an end-to-end
assertion on dispatch() output, which is called out to keep.

Docs only — no schema, VERSION, or generated-code change.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 15:24:53 +04:00
samiandClaude Sonnet 5 5e3e21543a proto: give the generated C++ Dispatcher a real error channel (P1, 1.4.0)
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
2026-09-10 15:10:13 +04:00
samiandClaude Sonnet 5 d3ab00d67c proto: fix daemon/engine attribution in two buffer-clamp descriptions
DAEMON's rebase audit caught it: TaskDetail.effectiveBufferBytes said "the
daemon reduces every live segment's buffer" to fit maxTotalBufferBytes, but
ADR 0011's ownership table (line 55) assigns bufferBytes/maxTotalBufferBytes
to CORE in bytes-units -- DAEMON counts tasks, CORE counts segments and
bytes. "The engine" is correct.

Same error, same root cause, in DownloadSpec.segments: "the daemon lowers it
to the per-host cap" attributes the per-host *segment* cap to DAEMON, but
that's CORE's (ADR 0011 line 54, "CORE enforces per-host segment caps -- it
owns the connections and is the only place segments are counted"). DAEMON's
own per-host cap is a *task*-level admission cap (line 50), a different
thing entirely -- conflating the two in the schema's own prose is exactly
how the clamp ends up implemented twice, once in each lane, disagreeing.

Description-only, no version bump: the JSON Schema shape is untouched, only
which component the prose names as doing the reducing. Regenerated code
diffs are comment-only (doc comments in the generated header and TS types).

Checked every other buffer/segment-clamp description for the same mistake;
the rest either already said "CORE"/"the engine" or used passive voice that
doesn't misattribute (Settings.connection.maxTotalBufferBytes,
Settings.connection.bufferBytes, docs/04, ADR 0012).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 00:07:05 +04:00
samiandClaude Sonnet 5 6db304a0ae proto: widen error-on-paused for ADR 0013's auto-pause signal (1.3.0)
DAEMON's docs/adr/0013-task-state-machine-ownership.md needs a wire signal
for the difference between a paused task the daemon entered unilaterally
(auth_required, server_file_changed, disk_full) and one that was requested
(user, schedule, queue stop, admission reconcile) -- without it, DAEMON's §3
resume rule ("resume only when the reason matches the event that justifies
resuming") has nothing correctness-preserving to key on, and would have to
guess from timing. CORE has already accepted the ADR; this was the sole
remaining blocker per DAEMON's own status line on it.

No retype, no new field -- error was already TaskError | null on both
event.task.state and TaskSummary, exactly as DAEMON characterized the ask.
Only the *description* of when it is populated widens: previously "failed or
retry_wait", now also "paused, when the daemon entered it on its own
initiative". A deliberate pause still carries error: null. TaskError's own
top-level description gets the same widening, since it previously also said
"failed or retry_wait" and would otherwise contradict the field that embeds
it.

New fixture (event.task.state.auto-paused.json) exercises the case directly:
an auth_required pause with error populated, contrasted in its own
description against download.pause.json's error: null for a requested pause.
The existing event.task.state.json fixture's first assertion was stale
("error is present exactly when failed or retry_wait") and is corrected.

Minor bump, 1.2.0 -> 1.3.0: a description widening on an already-nullable,
already-optional field changes no JSON Schema shape, but it is a real
behavioral commitment change worth a version bump so downstream regenerates
and notices, per the same reasoning ADR 0010 applied to TaskErrorCode.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 00:04:35 +04:00
samiandClaude Sonnet 5 2d36e9fef0 proto: land F2 — download.provideAuth (1.2.0)
The last contract gap blocking an M1 definition-of-done item: CORE's "401
handled" has no return path without it, and B2a's sibling F2 was accepted in
proto-answers-m1.md but never actually landed.

download.provideAuth {taskId, username, password, save?} -> {ok}, exactly as
proposed there. Privileged and Unix-socket-only: a credential-bearing method
must never be reachable from the browser, which is the other half of the
promise event.auth.required's own description already makes ("never back
through this event, never into a log"). It answers the challenge; it does not
itself resume the task -- the daemon retries with the credential attached and
the ordinary event.task.state reports the task leaving retry_wait, the same
as any other state change.

save only tells the daemon whether to persist the credential in the Secret
Service for next time, or use it for this attempt alone -- it never touches
SQLite or a log either way, in keeping with CLAUDE.md's secrets rule.

Three fixtures: the success path, -32010 for a task that no longer exists
(credentials submitted for it are simply discarded), and -32003 confirming
the extension has no path to this method under any transport.

mockd gets a real handler rather than falling through to the generic fixture
responder: it validates the taskId exists (so the -32010 fixture is
replayable) and actually transitions the task out of retry_wait.

Minor bump, 1.1.0 -> 1.2.0: additive method, no existing type touched.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 00:01:58 +04:00
samiandClaude Sonnet 5 60363a7142 proto: land B4 and B2a — buffer bounds, budget knobs, effective readback (1.1.0)
Minor bump on 1.0.0, per core/docs/buffer-sizing.md.

B4 — bufferBytes bounds corrected in all four locations (DownloadSpec,
TaskDetail, download.update's patch, Settings.connection.bufferBytes): was
4 KiB-8 MiB with no stated default, now 64 KiB-16 MiB with a 1 MiB default.
64 KiB because 4 KiB is smaller than one libcurl HTTP/2 write-callback delivery;
16 MiB because throughput from write size is flat past ~1-4 MiB and past 16 MiB
there is stall-cover left to buy but no memory left to spend it on; 1 MiB
default because it is the only candidate for which docs/04's 60 MB RSS target
actually holds once buffers are counted per segment, not per download.

Two new settings keys: connection.maxTotalBufferBytes (128 MiB default) and
connection.maxActiveSegments (32 default). Without them CORE's clamp — reduce
every live segment's buffer to fit the global cap — has no wire configuration
surface, and "20 active downloads" has no meaning distinct from 160 live TLS
connections.

B2a — TaskDetail.effectiveBufferBytes: what a segment is actually using right
now, after the clamp. Placed on TaskDetail next to bufferBytes, following the
requested/effective pattern ADR 0010 already established for segments. The
download.get fixture now demonstrates a real clamp (16 MiB requested, 4 MiB
effective) rather than a case where the cap happens not to bind.

docs/04-engine-design.md §4 and §8 updated in the same change per CORE's
request and CLAUDE.md rule 5: the RSS target is now stated as conditional on
maxActiveSegments = 32, and the old 4 MiB/64 MiB/256 MiB numbers are corrected
to match the schema. ADR 0012 records the reasoning and explicitly keeps the
60 MB target over CORE's offered 120 MB alternative, with the arithmetic that
makes 60 MB achievable with margin.

Numbered 0012 rather than 0011: DAEMON is independently drafting ADR 0011
(admission control / segment budget split) in a peer session at time of
writing, so 0011 was reserved to avoid a collision.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-09 23:20:58 +04:00
samiandClaude Opus 5 2c8f5e5d7d proto: answer CORE's freeze-blockers before 1.0.0 lands
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
2026-09-09 20:01:47 +04:00
samiandClaude Opus 5 53421d6cb8 proto: freeze the wire contract at 1.0.0
Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.

Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:

  gen_cpp.py             -> core/generated/velox_proto.{hpp,cpp}
  gen_ts.py              -> extension/src/shared/protocol/
  gen_openrpc.py         -> contracts/openrpc.json
  gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp

Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.

59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.

run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.

core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-09 19:55:54 +04:00
samiandClaude Opus 5 8bb683b09d scaffold: project structure, wire contract, roadmap and agent briefs
Lays out Velox Download Manager (IDM-class download manager for Ubuntu
26.04) as a monorepo ready for parallel lane development. No implementation
code by design.

- docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design,
  Firefox extension spec, risks/spikes, packaging
- contracts/: wire-contract skeleton (JSON Schema + fixture templates) —
  the single synchronization point between lanes
- docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA)
  with owned directories, build order and definition of done
- CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables
- CMake scaffolding with dev/tsan/release/ci presets

Two environment findings shape the design: Firefox here is the Mozilla snap
(native-messaging risk, so the extension carries a loopback-WebSocket
fallback), and Wayland forbids passive clipboard monitoring (so clipboard
capture is explicit-action-first).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 18:21:11 +04:00