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
The scriptable client, built now rather than last: it is how the daemon
gets exercised before the GUI is pointed at it (AGENT-DAEMON.md).
- src/client — synchronous blocking RPC over the Unix socket: resolve
$XDG_RUNTIME_DIR/velox/velox.sock, connect, session.hello, one framed
request/reply per call. Distinguishes transport failure (exit 3) from
a daemon-returned error (exit 1).
- src/main — subcommands add/ls/pause/resume/rm; --json prints the raw
JSON-RPC result or error; --dir/--out/--segments on add;
--delete-file on rm. Usage errors exit 2.
ls works end to end against veloxd today (empty table). add and the
bulk verbs reach the daemon and surface its "not implemented" (-32603)
cleanly until the store lands — the plumbing is done, the commands
light up as handlers do.
Test velox.client: the real Client against an in-process UdsServer —
no-daemon path, session.hello, download.list, and a not-implemented
method surfacing as an RPC error rather than a transport error.
ASan+UBSan clean; full tree (21 tests, incl. conformance) green.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
First real code in daemon/. veloxd now listens on
$XDG_RUNTIME_DIR/velox/velox.sock (0600, SO_PEERCRED same-UID check),
frames NDJSON, and routes every method through the generated
velox::proto::dispatch(). The CLI and GUI have a server to talk to.
Modules:
- rpc/ndjson.hpp — newline-delimited framing, 8 MiB frame cap, CRLF-
tolerant, partial-tail buffering. Header-only, tested.
- rpc/event_loop — single-threaded poll(2) reactor; never blocks the
loop. stop()/wake() are async-signal-safe (eventfd).
- rpc/runtime_dir — $XDG_RUNTIME_DIR/velox resolution, 0700, owner-checked;
refuses an insecure fallback rather than using /tmp.
- rpc/uds_server — listener + non-blocking per-conn read/write with
backpressure; handles session.hello (protocol-major
check -> -32001, sessionId, transport=uds) and
session.subscribe in the server layer; routes the
rest through dispatch().
- rpc/dispatcher — VeloxDispatcher : proto::Dispatcher, all 39 methods.
download.list answers an empty table; the rest return
"not implemented" (-> -32603) until the store lands.
- main.cpp — abstract-namespace single-instance lock, signal ->
clean shutdown, socket unlinked on exit.
Tests (ASan+UBSan and TSan clean):
- veloxd.ndjson — framing edge cases
- veloxd.uds_roundtrip — real socket: hello ok / version mismatch / empty
list / -32601 / -32700 / pipelined requests, and a
guard on the -32603 collapse documented in P1.
Known gap, filed not worked around: daemon/docs/proto-requests-m1.md P1 —
the generated Dispatcher has no error channel below -32603, so handlers
cannot yet return -32010/-32011/-32013 with their data payloads. The
server layer handles -32001/-32002/-32003 around dispatch(); genuine
in-handler errors collapse to -32603 until PROTO gives handlers a real
error return. Three error fixtures are non-conformant until then.
Not in this drop: rpc/ws_server (next; needs the store for hashed pairing
tokens), store/, sched/, cli/. WS reuses this event loop.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
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
vdm/ids.hpp — TaskId, an opaque engine handle (DAEMON keeps the wire
UUID <-> TaskId map; the engine never sees the UUID).
segment/segmenter — per-download range management (docs/04 §3). Initial
lazy split; assign_slot() splits the largest remaining range when the
budget grants a slot; on_complete(may_steal) either *steals* the second
half of the largest remaining range for the same worker (slot-neutral) or
returns nullopt so the caller *yields* the slot (ADR 0011 A1); on_failed()
returns requeue only on the 3rd consecutive connection error with a mirror
present — the remaining range is orphaned and re-split. Non-resumable or
unknown-size => exactly 1 segment; never split below min_segment_bytes
(1 MiB). Resume ctor rebuilds from a persisted table (falls back to a
fresh layout if it doesn't tile [0,total)). One mutex == "the task lock";
segment fields are std::atomic and the store is a std::deque so a steal's
append never moves a worker's record.
segment/budget — the global allocator (ADR 0011). Owns exactly one
ceiling (maxActiveSegments) and min-1-before-seconds fairness: a two-pass
allocation (guarantee pass gives every wanting task 1 slot in DAEMON's
priority order, then a growth pass round-robins the rest up to each
task's effective cap = min(per_task_cap, host cap, 1 if non-resumable)),
recomputed from scratch on every edge so a live set_max_active_segments
cut naturally yields the excess lowest-priority-first, never a
mid-segment kill. DAEMON-facing surface exactly as promised in
daemon/docs/core-requests-m1.md / ADR 0011: budget(), segments_active(),
starved_tasks(), starved_since(), set_max_active_segments (drain),
set_host_segment_cap, set_task_order, on_budget_changed (a jthread
coalesces at <=4 Hz; the tasks_starved 0<->nonzero edge fires
immediately). Callbacks are copied out and run after the lock is
dropped.
Tests: segmenter split/steal/requeue/resume math + a concurrent
steal-and-advance run; budget min-1 under a tight budget, round-robin
growth, host-cap and non-resumable clamps, live-lower shedding
lowest-priority-first, starvation below the task count, starved-edge
notification, and a concurrent set_want hammer. Green under ASan/UBSan;
the steal path and the budget green under TSan.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
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
R3: the three GUI DoD gates (10k rows at 60fps, flat RSS over 10 min,
--slow/--flaky/--drop-connection recovery) have nowhere to run. GUI owns the
harness, PKG/QA owns the job. tests/integration/README.md records the wiring
contract — driver invocation, exit-code and --json semantics — and carries
the pre-drafted per-PR and nightly job stanzas with TODO(GUI) markers for the
harness path. Wire for real when GUI files the follow-up.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw
PROTO wired the suite into ctest (label "conformance": the end-to-end
`conformance` test that shells to run.sh, plus the native `conformance_cpp`);
the CI job called run.sh directly. Two entry points, and the required M0 gate
exercised only one of them, so the ctest registration could rot.
The conformance job now configures, builds velox_conformance_cpp, and runs
`ctest --preset dev -L conformance`. The dev test preset's
noTestsAction: error is the rot guard: an empty label match exits non-zero
instead of the old silent pass. build/sanitizers exclude the heavy e2e test
with -E '^conformance$' (the dedicated job owns that run; conformance_cpp
still runs under every sanitizer). ADR 0014 records the decision.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw
libFuzzer writes crash-* / oom-* / leak-* / timeout-* into CWD on a find and
each holds the crashing input verbatim. None were ignored; CORE caught two by
hand before committing. Prevention, not cleanup.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw
--check verified outcomes — binaries on PATH, pkg-config modules — on a box
that already installed everything. It never looked at the APT_* names, which
is the only part that breaks on a clean machine of the wrong release, as R1
just showed. Add a loop over the assembled PKGS array that fails on any name
with no installable candidate (apt-cache policy; no root, no network).
All-missing is treated as stale lists (warn), not 36 bad names.
The bootstrap-script job runs on a 24.04 runner, where the bad name still
resolves, so name validation there checks the wrong archive. Add
bootstrap-script-2604: a real --with-clang install in an ubuntu:26.04
container — the release the project ships on, and the first time the
fuzz-toolchain half of bootstrap is exercised anywhere (CORE had been running
clang++-21 directly). Also pass --with-clang/--packaging to the 24.04 --check
so the optional and M6 names can't rot unnoticed. BRANCH_PROTECTION.md gains
the 2604 row and the stale "when X merges" rows are corrected to "now".
gui/docs/pkg-qa-requests-m1.md R2 + the --with-clang note.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw
`apt-cache policy libqt6svg6-dev` is "Candidate: (none)" on 26.04; the package
that carries the Svg headers and Qt6SvgConfig.cmake is qt6-svg-dev. Since
gui/CMakeLists.txt landed on main the root build's
find_package(Qt6 ... Svg REQUIRED) is live, so a clean 26.04 box could not
configure the project at all. Same name in README.md and AGENT-PKG-QA.md.
Reported in gui/docs/pkg-qa-requests-m1.md R1.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw
core/docs/proto-requests-conformance-cmake.md: the runner's header comment
said it links libveloxproto, but it actually compiled
core/generated/velox_proto.cpp straight into the executable and found
nlohmann_json itself — the only option while ADR 0009's target didn't exist.
It exists now on main as velox::proto (core/CMakeLists.txt, PUBLIC generated
include dir, PUBLIC nlohmann_json).
if(TARGET velox::proto): link it. else: fall back to compiling the generated
.cpp directly, for a configure with no core/ in the tree. Both paths verified
— full tree links libveloxproto.a (compiled once, by veloxproto's own
target); with core/CMakeLists.txt hidden the fallback compiles the .cpp and
finds nlohmann itself. conformance_cpp passes either way.
Beyond tidiness: once GUI links velox::proto too, the conformance runner
linking the same target is what guarantees the suite and the clients exercise
byte-identical generated code, rather than two compiles of one .cpp under two
warning configs — the exact skew a conformance suite exists to catch.
run.sh's own direct g++ compile is unaffected and stays independent by
design; this only changes the ctest-driven path CI and lanes use.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
The header-hook manager's core call: capture when any positive signal holds
and none of the vetoes do (docs/05 §2). Vetoes are absolute and checked
first — a monitored .zip on an excluded host is not captured.
Decision table written first (tests/capture/rules.test.ts, 22 rows); every
branch here exists to satisfy one. Covers the M1 DoD set: attachment,
monitored extension, monitored MIME, size threshold, excluded host (exact +
wildcard), HTML navigation, blob:/data: origin, bypass modifier, streaming
media (HLS MIME and resourceType 'media'), a page-issued range request, plus
non-GET, redirect status, sub-threshold, and large-but-renderable.
Pure function of (candidate, rules); rules are the daemon's, mirrored via
capture.getRules, so the decision never drifts from daemon policy.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
Review feedback on the filed request:
- R1: gui/CMakeLists.txt is on main now, so root's
find_package(Qt6 ... Svg REQUIRED) is live — a missing SVG dev package
is a hard configure failure for the whole project, not a skipped guard.
Added the reason CI hasn't caught it: ubuntu-latest is 24.04 (where
libqt6svg6-dev likely resolves), the project targets 26.04 (where it
does not). Wrong name + runner/target release mismatch = the class of
bug PKG/QA owns is currently unobservable in CI. That's the argument
for R2, folded in.
- R3: corrected — CI does build the GUI and runs its three ctests under
the ci preset. What has no home is the non-unit-test DoD: 10k-row
60fps, flat RSS over a 10-minute run, and mockd
--slow/--flaky/--drop-connection recovery. Asked for those specifically.
- gui/docs/ext-requests-m1.md: CLAUDE.md §3 says the no-download-logic
rule applies to extension/ too; GUI made its half an executable ctest,
EXT's half is still prose. Suggested the ESLint equivalent for the
existing extension-lint job.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
util/crc32.hpp — header-only CRC-32 (zlib polynomial, reflected), used to
integrity-check the sidecar.
meta/veloxpart — the <name>.veloxpart.meta resume file (docs/04 §5).
Little-endian, versioned, CRC-32 over the whole record. Layout: magic,
version, flags, total_size, downloaded, url set (original/effective/
mirrors), etag/last-modified/content-type, segment records (start, end
INCLUSIVE, completed), optional sha256 streaming-hash blob.
parse_veloxpart() is the attacker-facing surface (the file sits in a
world-writable-ish download dir) and is total on any byte string: CRC
checked before any field is interpreted; magic, a version it understands,
every count and length bounded by a hard cap AND checked against the
remaining buffer; ByteReader latches on overrun; trailing bytes rejected.
Every malformation is meta_corrupt / meta_version_unsupported, never a
crash or an unbounded allocation. serialize_veloxpart() is deterministic
(unchanged sidecar isn't rewritten). File helpers write atomically
(temp + rename) and fdatasync the file and its directory.
Tests: crc32 known vector; full + minimal round-trips; deterministic
serialize; file round-trip; and a truncation/corruption table — bad
magic, CRC mismatch (payload and CRC-field flips), future version,
truncation at every stage, hostile url_count / segment_count / lp_string
length (the case the brief singles out), trailing bytes, impossible
segment.completed.
tools/fuzz/fuzz_veloxpart — feeds raw bytes and bytes-with-valid-CRC
(so the field parser and ByteReader bounds checks are actually reached),
and round-trip-stability-checks anything accepted. Ran 1.1M execs clean
under ASan+UBSan+libFuzzer (clang++-21); fuzz_content_disposition and
fuzz_url likewise re-run to 1.1M. tools/fuzz gains a -runs=0 seed-replay
CTest smoke per target (regression tripwire; the campaign stays manual).
Fuzz-found and fixed: parse_content_disposition could emit a filename
containing NUL / control bytes from a mangled filename* ext-value —
strip_path only removed path separators. Now sanitize_leaf() also drops
C0 controls and DEL (rules/ still owns the authoritative sanitize; `..`
and printable-unsafe content pass through as before).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
onHeadersReceived (where shouldCapture runs) doesn't carry the headers the
browser actually sent; signed-URL and referrer-gated CDNs need them. Stash
from onBeforeSendHeaders keyed by requestId, read back at capture time.
This map sees every request, so it is bounded both ways — oldest-out past a
size cap (default 2048) and a 5-minute TTL, swept on a 60 s timer and on read
— and attach() clears an entry the moment its request completes or errors.
normalizeHeaders(): Array<{name,value}> -> lower-cased map, repeats joined
with ", ". 13 tests: eviction order, redirect re-put refresh, TTL expiry,
sweep, and the onBeforeSendHeaders/onCompleted/onErrorOccurred wiring.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
Firefox-only extension, so webextension-polyfill (a Chrome shim) is dead
weight and forces a bundler just to resolve one bare import. Use the native
`browser.*` global with @types/firefox-webext-browser instead.
- manifest.json: MV3, event-page background (dist/background.js), the
docs/05 §7 permission set (<all_urls> in host_permissions),
strict_min_version 128.0, data_collection_permissions none.
- scripts/build.mjs: esbuild bundle of src/background/index.ts -> dist/,
esm, target firefox128. Wired to `prepare` so `npm ci` produces the
bundle and CI's `web-ext lint` (which needs it to exist) passes with no
added CI step. dist/ stays gitignored.
- src/background/index.ts: event-page entry — brings the transport up,
holds the shared reference. Capture surfaces attach here next.
- transport/storage.ts, transport/index.ts: use the browser global.
- tests/setup.ts: stub the browser global instead of mocking a module.
web-ext lint clean (0/0/0). typecheck clean. 38 tests still green.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
Spike S1 — run on the target machine through real snap confinement
(apparmor snap.firefox.firefox enforced; web-ext's direct-exec of the inner
binary bypasses it, so runs were forced through `snap run firefox`):
- manifest in ~/.mozilla/native-messaging-hosts/ -> WORKS; host launched
unconfined with real $HOME and real $XDG_RUNTIME_DIR, bound a socket in
the real /run/user/<uid>. Corroborated by the machine's 1Password host.
- ~/snap/firefox/common/.mozilla/native-messaging-hosts/ -> not read
- /usr/lib/mozilla/native-messaging-hosts/ -> not read
- flatpak path -> N/A (snap Firefox)
Decision: WebSocket stays the default; native messaging is an opportunistic
upgrade taken only when its handshake succeeds. docs/05 §4 corrected in this
commit to point the snap manifest at ~/.mozilla and mark /usr/lib as
deb/tarball-only. ADR carries a self-contained reproduction; the scratch
harness has been removed.
transport/ (build order item 1):
- types.ts VeloxTransport interface + error taxonomy
- rpc.ts JSON-RPC id correlation, per-call deadline, AbortSignal
- backoff.ts exponential backoff with jitter
- discovery.ts 52000-52016 scan ordering (last-good port first)
- websocket.ts scan -> session.hello -> auto-pair (token in
storage.local) -> reconnect; -32001 fatal, refused/
rate-limited pairing latches needsPairing (no retry storm);
a mid-handshake drop aborts hello immediately
- native.ts connectNative(); distinguishes "not installed" (fatal,
lets the picker fall through) from a crash (reconnect)
- index.ts createTransport() runtime picker + persisted Options override
Toolchain: package.json / tsconfig (strict) / vitest; webextension-polyfill
mocked. 38 tests, incl. the WS suite against a real loopback ws server.
No manifest.json yet, so CI's extension-lint guard stays a no-op.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
Follow-up hardening after a review noted the RTL "check" verified nothing
(a .ts stub that no test loads), matching a session-wide pattern of
checks written against what should be true rather than what would break.
- Split the non-main() code into velox-gui-lib (STATIC) so tests link the
real widgets/models, not a reimplementation.
- tst_rtl: builds the real MainWindow, flips layoutDirection, asserts the
direction propagates to the central widget AND that the offline-banner
QHBoxLayout actually mirrors (label x-position LTR vs RTL differs by
>100px). Verified it fails when the banner is pinned LtR.
- gui_no_download_logic: a ctest that greps gui/src for curl_*/pwrite/
sqlite/QSqlDatabase/QNetworkAccessManager and fails on a hit — CLAUDE.md
§3 as an executable check. Verified it fails when a curl_ token is added.
- Still uncovered (noted, not claimed): that the translation catalogue
loads and the right context/strings resolve at runtime.
Not covered here because the files are PKG/QA-owned: tools/bootstrap.sh
ships a package name that does not exist on 26.04 (libqt6svg6-dev; the
real one is qt6-svg-dev), and --check validates pkg-config outcomes
rather than the apt names it would install. Both, plus the same name in
AGENT-PKG-QA.md and the README, are written up apply-ready in
gui/docs/pkg-qa-requests-m1.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
First vertical slice of velox-gui, built entirely against tools/mockd
(no daemon dependency):
- rpc/: RpcConnection runs a QLocalSocket on a worker thread with
newline-delimited JSON-RPC framing, drives the session.hello /
session.subscribe handshake, and reconnects with exponential backoff
(250 ms -> 8 s). RpcClient is the main-thread face: marshals calls onto
the worker, delivers replies as main-thread callbacks, re-emits server
notifications as typed Qt signals, and issues the one-shot download.list
on reaching Connected.
- models/DownloadTableModel: QAbstractTableModel over TaskSummary. A
progress batch is a row patch with a narrow dataChanged over the value
columns only; beginResetModel() is reserved for the initial load and a
reconnect resync.
- widgets/ProgressDelegate: in-cell progress bar for the Status column.
- mainwindow/MainWindow: the table, a status-bar connection dot, an
offline banner instead of a modal, dialog-free pause/resume/stop
actions, and QSettings column/geometry persistence.
- gui/CMakeLists.txt links velox::proto (never velox::core, ADR 0009) and
self-guards on the veloxproto target so main keeps configuring if it is
ever absent again.
- i18n from the first commit: every string via tr(), plus an Arabic .ts
stub for the RTL check.
- tests/: headless QTest for the model — proves the progress patch is a
narrow dataChanged and never resets the model.
Verified end-to-end against `mockd --tasks 300`: handshake, initial list,
live progress batches applied to the model, and a clean
Reconnecting -> Connected recovery when mockd is bounced mid-run.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
io/sparse_file — the single O_WRONLY output file (docs/04 §4). open()
posix_fallocate's the full size (falls back to ftruncate on
EOPNOTSUPP/ENOSYS, reported via preallocated()); write_at() pwrites at an
absolute offset, looping short writes and retrying EINTR; sync() is
fdatasync (timer/pause only); advise_dontneed() is
posix_fadvise(DONTNEED); resize() trims a preallocated tail or sizes a
chunked download. errno -> vdm::Error (ENOSPC->disk_full,
EACCES->permission_denied, ENOENT/ENOTDIR/...->path_rejected). No lock on
write_at — POSIX makes each pwrite atomic for a regular file, so N
segment threads writing disjoint ranges is safe (tested, TSan-clean).
io/write_buffer — per-segment accumulate-and-flush buffer, preallocated
at construction; append() only memcpys (no allocation on the write-
callback hot path, docs/04 §8 — asserted by a global-new counter in the
test). Flushes on fill via a caller-supplied FlushFn; a chunk >= capacity
arriving on an empty buffer writes straight through. On a flush error
next_offset() stays at the last durable position. Single-threaded; the
disk-writer-thread handoff is stage 8.
Also: vtest.hpp VT_CHECK_EQ/NE now copy operands (auto, not auto&&) — an
assertion must not outlive a temporary the expression returned a
reference into (ASan caught this on Result<void>{}.error().code).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
tests/conformance/cpp/CMakeLists.txt (PROTO's lane) compiles
core/generated/velox_proto.cpp directly while its comment claims it links
libveloxproto. Now that the veloxproto target exists it should link
velox::proto, with a TARGET-guarded fallback to the direct-compile for
standalone configures. Filed, not edited — not CORE's file.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
core/ now produces two libraries as ADR 0009 specifies:
- veloxcore — the engine; still links only Threads + CURL, no JSON.
- veloxproto — generated/velox_proto.cpp, generated/ as a PUBLIC include
dir, nlohmann_json linked PUBLIC. velox::proto alias.
Consumed by veloxd / CLI / GUI / the conformance runner; veloxcore must
never link it. nlohmann_json is found here too (the root only finds it
when daemon/ has landed) so core builds standalone. Generated code is
built -Wall -Wextra -Wno-error — it is committed and never hand-edited, so
a codegen quirk must not break the build. A configure-time FATAL_ERROR
trips if veloxcore ever links veloxproto.
Verified: libveloxproto.a builds clean; veloxcore's link deps contain no
proto/nlohmann; full suite green.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
PROTO closed the one remaining contract gap as contracts/ 1.3.0
(lane/proto commit 6db304a): event.task.state.error / TaskSummary.error
now populate on a paused transition CORE entered unilaterally, not just
on failed/retry_wait. Minor widening of an existing field's presence
condition, no retype, no new field, per contracts/README.md rule 4.
Updates every place in the ADR that referred to this as an open
question or unresolved gap: the status line, the pause-reason
bookkeeping in §2, the alternatives-considered pointer, and the
contract-gap section itself (renamed "surfaced, now closed"). Notes
1.3.0 is on lane/proto but not yet merged to main (still 1.1.0) —
daemon/src/sched/'s pause/resume logic should be written once that
merge lands, not before.
All four of ADR 0013's open items are now resolved: CORE confirmed
tasks_starved's structural exclusion and pause()/resume() idempotency
explicitly (verdict: "accept as written", not hedged), and adopted
"auto-pause" with no new wire term.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
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
CORE accepted ADR 0013 as written, no amendments
(core/docs/adr-0013-core-response.md, lane/core@c65e664). Folds in:
- pause()/resume() idempotency contract, precisely: no-op success on
an already-paused task, ALSO on a terminal task (pause racing
completion isn't an error), resume() no-op on a non-paused task, the
only error is task_not_found, and no state-change event fires for a
no-op call.
- tasks_starved pinned as {connecting, downloading} AND
segments_active == 0 — a structural exclusion of retry_wait and
auto-paused tasks rather than a special case, with the full state
table CORE gave.
- restart handling confirmed fully; two non-blocking notes from CORE
about work-interruption during verifying/assembling.
- "auto-pause" adopted as the term, no new wire/API surface.
Status updated: accepted by CORE; PROTO's item 3 (permit `error` on
event.task.state when state=="paused") is the one remaining blocker
before daemon/src/sched/'s pause/resume logic can be written
correctness-preservingly.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
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
Verified PKG's landed CI (.github/workflows/ci.yml, db7650b/8f45815) against
this lane's actual tree, per the standing instruction that conformance as a
required check is this lane's DoD to verify, not PKG's. It wasn't running:
the `conformance` job's presence-check looks for tests/conformance/CMakeLists.txt
or tests/conformance/package.json, and neither existed -- the job was
silently short-circuiting to a green "skipped" on every PR, forever. The M0
exit gate was not gating anything.
tests/conformance/CMakeLists.txt registers one ctest entry, labeled
"conformance", that shells out to run.sh -- the exact command
tests/conformance/README.md tells a human to run locally, so there is one
definition of "the suite passed", not a CMake-flavoured near-duplicate of it.
cpp/CMakeLists.txt's existing conformance_cpp test gets the same label, for a
lane iterating on core/generated/ who wants the fast native-only path.
Fixed a second landmine found while wiring this: the root CMakeLists.txt only
find_package(nlohmann_json)'s when daemon/CMakeLists.txt exists, since
daemon is its real consumer -- but daemon hasn't landed yet, so
add_subdirectory(tests/conformance) would have failed to configure the
moment this file existed, on every machine, until daemon merges. Fixed inside
tests/conformance/cpp/CMakeLists.txt with an if(NOT TARGET) guard rather than
widening the root file's condition, which is PKG's to change.
run.sh now installs its own Python deps (jsonschema, referencing) on demand:
they aren't in tools/bootstrap.sh's apt list -- that's PKG's script, these
are this suite's own dependency -- so a bare CI image would otherwise fail
check_contract.py with an ImportError before this suite even started.
Verified end to end: `cmake --preset dev && ctest --test-dir build/dev -R
'^conformance$'` passes in 23.8s, exercising the exact command and label the
CI job uses.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
Accept as written — no amendments. Answers to the four open items:
1. tasks_starved / starved_tasks() count ONLY tasks in {connecting,
downloading} with segments_active == 0 (the allocator owes a slot to a
task that is asking). retry_wait and paused (either-initiated) are
outside that state set, so they are excluded by construction, not by a
special case. Design commitment; the accessor is stage 6/8.
2. pause() is idempotent: no-op success on an already-paused or terminal
task; resume() no-op success on a non-paused task; only task_not_found
errors. No state-change event for a no-op.
3. PROTO's item — CORE confirms its half: auto-pause reports the
transition with ErrorInfo populated (auth_required / server_file_changed
/ disk_full / path_rejected), already in util/error.hpp. Ready once
PROTO permits error on state=="paused".
4. CORE adopts "auto-pause"; the wire/API discriminator stays
state==paused + presence of the Error code.
Notes back: pause during verifying re-hashes from scratch on resume;
pause during assembling is M4; restart handling in §5 agreed —
start(TaskId) re-derives resume position from .veloxpart.meta + If-Range.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
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
GUI's M1 definition of done is "10 000 synthetic rows scroll at 60 fps with
flat memory over 10 minutes (mockd --tasks 10000)". This flag was missing from
the four unhappy-path flags that did land; the brief's own flag list omitted
it, which is corrected here too.
--tasks seeds a plausible population rather than N copies of one row: varied
state, size (log-uniform 50 KB - 20 GB), category, queue position and
description, drawn from the same category.list / queue.list fixtures the rest
of mockd already serves so a synthetic task can never name a category or
queue those methods don't also return. State distribution is roughly
55% complete / 8% failed / 4% cancelled / 6% paused / 2% retry_wait / 25%
queued, using the new TaskErrorCode taxonomy for failures.
"Progress advances across the whole set, not a handful of live rows" ruled
out the obvious cheap answer. A bounded, rotating pool of concurrently-active
downloads (--active-cap, default 24) is fed continuously from each queue's
FIFO — with the rest of that queue's queuePosition renumbered on every
promotion, as a real scheduler would — and a small fraction of active tasks
hit a transient failure and cycle through retry_wait before rejoining, so the
pool keeps rotating through new rows for the whole run instead of draining
once. Verified over a 10000-task, 60-second run: 61.5 MB RSS flat, and the
active pool's membership meaningfully different after 60s.
tick() only ever walks the active pool plus due retry-wait entries, never the
full task list, so its cost stays flat regardless of --tasks. A manual
download.add is still admitted immediately regardless of --active-cap — a
human driving the GUI by hand must never wait behind synthetic load.
Fixed a latent double-push while building this: any task 'connecting' at the
top of a tick was pushed to the progress batch once for the transition and
again at the loop's unconditional final push, inflating event.task.progress
payloads with a duplicate entry for that taskId. It predates this change (the
original tick() had the same shape) but only became visible once several
tasks are legitimately 'connecting' in the same tick, which --active-cap's
continuous promotion now does routinely.
--seed makes a run reproducible, which matters when a GUI bug only shows up
at a particular row.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
Drafts the CORE/DAEMON split PROTO raised as D1 and CORE already agreed
to in contracts/proto-answers-m1.md, since neither lane would write it
down alone. DAEMON owns new/queued and pause-for-schedule; CORE owns
probing through complete|failed and cancelled-from-anywhere is
DAEMON-driven; paused is shared.
Ties into ADR 0011 in two places:
- retry_wait looks identical to segment starvation from the budget
accessor's point of view (zero segments, deliberately) and must be
excluded from tasks_starved by construction, not by DAEMON guessing
from timing.
- restart handling: CORE holds no persistent state, so any CORE-owned
TaskState reloads as queued and re-admits through the scheduler;
paused tasks reload with their pauseReason intact.
Surfaces one real contract gap while drafting, not just an open
question: event.task.state's error field is schema-scoped to
failed/retry_wait only, so CORE auto-pausing for auth_required or
server_file_changed currently has no wire signal telling DAEMON why —
needed before the resume-must-not-cross-reasons rule (§3) can be
implemented at all. Filed as a PROTO follow-up in the ADR itself.
Status: proposed, needs CORE + PROTO sign-off (four open items at the
end) before daemon/src/sched/'s pause/resume logic is written against
it.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
net/content_disposition — total parser for the mojibake-prone header:
RFC 6266 filename (quoted/token), RFC 5987 filename* ext-values
(charset'lang'pct-encoded, incl. RFC 2231 continuations), legacy RFC 2047
encoded-words (=?UTF-8?B?..?= / ?Q?), and raw Latin-1 bytes; prefers
filename* over filename; strips path components AFTER decoding (a base64
payload can hold '/'). 22-case test table.
net/text_codec (internal) — percent-decode, UTF-8 validation, Latin-1->
UTF-8, base64, RFC 2047 — shared by the CD parser and the URL splitter.
net/url — a small total URL splitter (scheme/userinfo/host/port/path/
query/fragment, http(s) validity) and url_filename() for the last path
segment; used for the filename fallback.
net/probe — HEAD then a ranged GET bytes=0-0 that PROVES resumability
(206 + matching Content-Range + a validator), rather than trusting
Accept-Ranges which servers lie about; the ranged GET is also the HEAD-
refused (403/405/501) fallback. 401/407 -> success result with
requires_auth, not an error. Runs on its own pool (max_concurrent,
default 4) outside the segment budget per ADR 0011 §5. suggest_filename()
does the resolution order (explicit -> disposition -> URL -> download.bin)
with a light strip; rules/ (stage 9) owns the authoritative sanitize.
tools/fuzz — libFuzzer targets for the CD parser and the URL splitter,
compiling the parser sources directly so they're fully instrumented;
self-guards on VELOX_BUILD_FUZZ + Clang (the top-level CMake adds every
tools/* unconditionally). Seed corpora included.
Fixed on the way: a p -> Transfer -> State -> cbs -> p reference cycle in
Prober that leaked every probe (drop the stored Transfer; the worker
keeps State alive). Tests green under ASan/UBSan and TSan.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
The conformance job was a required check that ran nothing. Two bugs:
1. Its guard tested for tests/conformance/CMakeLists.txt or package.json.
The suite ships as tests/conformance/run.sh; neither file exists, so the
guard was always false and the job took the "skipped" (success) branch.
2. Even forced true, it ran `ctest --preset dev -L conformance` — no test
carries that label, so ctest reported "Total Tests: 0" and exited 0.
Replace the job body with PROTO's intended wiring from
tests/conformance/README.md: bootstrap the toolchain, pin Node 22 (apt ships
< 20; the TS replay runner needs >= 20), and run ./tests/conformance/run.sh
directly. The suite starts its own mockd and builds its own C++ runner, so no
cmake configure is needed. Verified it goes red: an enum-invalid fixture makes
run.sh exit 1; reverting it returns to green.
check_contract.py imports jsonschema and referencing, which bootstrap.sh did
not install. Add python3-jsonschema / python3-referencing to the apt set and
to --check, so one command still provisions the whole suite.
Guards now fail loudly instead of passing quietly:
- conformance has no skip branch any more. run.sh has landed; the job runs
it unconditionally and errors if the entrypoint is missing.
- extension-lint keyed "has EXT landed?" to extension/package.json — the
same single-filename trap. Key it to a manifest instead, and once a
manifest exists, treat a missing package.json as a hard failure rather
than a green skip.
Mark conformance required now in BRANCH_PROTECTION.md — it is the M0 exit gate.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0143aKiohmDiyefJBwHDJJqw