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]>
7.0 KiB
01 — Architecture
1. Decision summary
| Concern | Decision | Why this and not the alternative |
|---|---|---|
| Engine language | C++23 (libveloxcore) |
You asked for speed and it is the right call here: segment scheduling, a 64 KiB–8 MiB write path, and 32 concurrent sockets are exactly where a GC and a per-chunk allocation tax show up. Rust would be an equally good engine choice, but Qt's C++ API means C++ removes an entire FFI boundary from the GUI lane. |
| HTTP stack | libcurl (multi interface, curl_multi_poll) |
Writing an HTTP/1.1+2 client on raw epoll costs weeks and buys nothing: curl already does connection reuse, HTTP/2 multiplexing, TLS, proxies, SOCKS5, Digest/NTLM auth, redirect and cookie semantics. One curl_multi handle per worker thread, N easy handles = N segments. |
| GUI toolkit | Qt 6 Widgets (not QML, not GTK4) | IDM's UI is a dense data grid, a toolbar of big icons, a tree, and ~12 tabbed dialogs. QTreeView + a custom QAbstractItemModel renders 100k rows without breaking a sweat; QML would need that grid hand-built. GTK4/libadwaita fights you the moment you want a non-GNOME look, and gtkmm's tree/column story is worse. Qt also gives you QSystemTrayIcon, drag-and-drop, a global clipboard API, and QSS theming to get the IDM look. LGPLv3 dynamic linking is fine for an open-source app. |
| Process model | Daemon + thin clients | Downloads must survive closing the window, and the extension must work when no window is open. One owner of state also kills every "GUI and extension disagree" bug class. |
| Wire protocol | JSON-RPC 2.0 over Unix domain socket (+ loopback WebSocket for the extension fallback) | One protocol for GUI, CLI, and extension = one contract to keep in sync, and it is trivially mockable so three lanes can build in parallel. D-Bus is more idiomatic on Linux but is painful to speak from a WebExtension and adds a second schema. A D-Bus shim can be added in M5 for desktop integration only. |
| Persistence | SQLite (WAL) | Task list, segment bitmaps, categories, rules, queues, history. Crash-safe, zero admin, one file. |
| Extension | MV3 WebExtension, TypeScript | Firefox MV3 still allows blocking webRequest, which is what makes true "intercept before the browser downloads it" possible — the thing Chrome MV3 took away. |
| Build | CMake ≥ 3.28 + Ninja + presets | Presets mean every agent and CI run the identical configure line. |
2. Processes
veloxd — the daemon
Owns everything: task list, scheduler, queues, disk, sockets, settings.
- Single instance, enforced by an abstract-namespace lock socket.
- Started by systemd user unit
velox.service, socket-activated byvelox.socket. Also auto-started by the GUI or nmhost if not running (systemctl --user start velox). - Listens on:
$XDG_RUNTIME_DIR/velox/velox.sock(mode 0600) — GUI, CLI, nmhost. Peer credentials checked viaSO_PEERCRED; same-UID only. No token needed, the socket permission is the authorization.127.0.0.1:<ephemeral, recorded in $XDG_RUNTIME_DIR/velox/ws.port>— loopback WebSocket for the extension fallback transport. Token-authenticated + pairing prompt. Seedocs/05-extension-spec.md§4.
- Threads: 1 RPC/event loop, 1 curl-multi transfer thread per ~8 active segments (capped), 1 disk writer thread per active task, 1 timer thread for the scheduler. Never block the RPC loop on disk or DNS.
velox-gui — Qt 6 client
Stateless view. On start: connect → session.hello → download.list → subscribe to
event.*. On daemon restart: exponential-backoff reconnect with a banner, never lose the
window. Everything it displays comes from the daemon; it holds no download state of its own.
velox — CLI
Same RPC, scriptable: velox add <url> --dir ~/ISOs --segments 16, velox ls,
velox pause <id>. Falls out nearly free once the generated client exists, and it is the
fastest way to test the daemon before the GUI is ready.
velox-nmhost — native-messaging bridge
Deliberately trivial and boring: reads Firefox's 4-byte-length-prefixed JSON from stdin, writes it to the Unix socket, pumps replies back. No business logic — ever. Under 300 lines. It exists only because Firefox's native messaging speaks stdio and the daemon speaks sockets. If a feature needs logic, it belongs in the daemon.
3. The layering rule (enforced in review)
libveloxcore → knows nothing about RPC, JSON, SQL, or Qt.
Input: a DownloadSpec. Output: bytes on disk + callbacks.
veloxd → knows RPC, SQL, scheduling. Depends on core. No Qt.
velox-gui → knows Qt and the generated client. Zero engine code.
extension → knows the browser and the generated TS client. Zero download logic.
If the GUI ever needs to know what a "segment steal" is, the layering has been violated.
The daemon's job is to project the engine's state into the contract's TaskDetail type,
and that is the only shape the GUI ever sees.
4. How three lanes build at the same time without breaking each other
This is the part that makes parallel agents work, so it is a hard process, not a habit:
contracts/is the single source of truth and is owned by exactly one lane (PROTO). JSON Schema for every type, method, and event, plus an OpenRPC document.- Codegen, not hand-written types.
contracts/codegen/gen_cpp.pyemitslibveloxproto(structs + (de)serialization);gen_ts.pyemits@velox/protocol. Generated files are committed so nobody is blocked on running the generator. Hand-editing a generated file is a merge-blocking offence. - Golden fixtures. Every method has request/response JSON examples in
contracts/fixtures/.tests/conformance/replays them against both the C++ server and the TS client. Green fixtures = the two lanes are compatible, without them ever having run against each other. tools/mockd— a TS mock daemon that serves the fixtures and fakes progress events. The GUI and extension lanes develop against it from day one and never wait forveloxd.- Protocol changes are a PR to
contracts/only, with aVERSIONbump and updated fixtures. Adding an optional field = minor. Removing/renaming/retyping = major, andsession.hellorefuses a mismatched major with a clear error the GUI shows as "Update Velox".
5. Data locations
| What | Path |
|---|---|
| Config | ~/.config/velox/settings.json |
| Database | ~/.local/share/velox/velox.db |
| Logs | ~/.local/state/velox/velox.log (rotated, 5×2 MiB) |
| Runtime sockets | $XDG_RUNTIME_DIR/velox/ |
| Temp/partial data | configurable; default ~/.local/share/velox/temp/ |
| Default download root | ~/Downloads/ with category subfolders |
Partial files: the target file is created sparse and preallocated at final size in the
destination directory as <name>.veloxpart, with <name>.veloxpart.meta beside it. On
completion the part file is renamed in place — no copy, no second full-size write, and a
half-finished download is never mistaken for a real file by other apps.