Root cause of the M7 RSS gap (core/docs/m7-baseline.md: ~70 MB measured against a 60 MB target): SegmentBudget::confirm_slot() only checked a task's own held count against its own target -- never the engine-wide active_ sum. reallocate_locked()'s two-pass fairness allocation does bound sum(target) <= max_active_ at the moment it computes a plan, but that bound says nothing about sum(held): a task can be legitimately holding more than its own just-lowered target for a while (yield is deferred to a segment boundary, never mid-segment -- ADR 0011 A1), and another task's target can correctly rise to claim that capacity before the first task has physically released it. Both confirm_slot() calls could then succeed against their own, individually-correct targets while sum(held) exceeded max_active_ -- tools/bench heap-profile caught this directly: budget.active reading 56-86 against a total of 32. confirm_slot() now also checks active_ < max_active_, unconditionally, as a backstop that doesn't depend on any task's target bookkeeping being in sync with what every other task holds. That creates a liveness question the original design never answered: a task denied only by this new check has a target that's already correct, so it never changes again and reallocate_locked()'s plain "fire a callback when a task's target changes" mechanism never revisits it. Task gained a waiting_for_slot flag, set on exactly this denial; release_slot()/deregister_task() (the only two places that free real capacity) now hand a freed slot directly to the highest-priority waiting task via wake_one_waiter_locked(), if reallocate_locked()'s own plan didn't already produce a callback for anyone. download_task.cpp's fill_slots_locked() needed a matching fix: a woken task's stalled segments (SegState::stalled -- backed off mid-retry, its own release_slot() already called) have no live worker and never surface through Segmenter::assign_slot(), which only hands out unassigned or fresh ranges. fill_slots_locked() now restarts any stalled segment with no live worker directly (bounded by slot_target, same as its assign_slot() loop) before looking for new work; a segment it doesn't get to keeps its own scheduled retry_worker() timer as a second chance. Also fixes a real TSan-caught data race this work surfaced: SegWorker:: speed_bps was written only by its own segment's curl callback and, before Progress.speed_bps's polled-path fix, only ever read from that same thread -- safe without synchronization. snapshot_progress() reading it from whatever thread calls DownloadHandle::progress() broke that invariant (workers_mu's shared_lock protects the workers map's structure, not an individual SegWorker's fields). Now std::atomic<double> with relaxed ordering -- an informational EMA, nothing synchronizes real state on it -- rather than adding a lock to the write side. core/tests/segment/budget_test.cpp adds two tests reproducing the actual gap (budget_active_never_exceeds_max_active_segments_under_concurrent_load, budget_wait_list_wakes_a_task_whose_target_never_changed) plus a sanity baseline (budget_release_wakes_a_denied_waiter), and introduces AsyncFakeTask + TestTimer for the one existing test that drives the budget from multiple concurrent threads -- mirroring production's real dispatch (register_task()'s on_target lambda posts through host.schedule(), download_task.cpp, never a synchronous call) rather than adding reentrancy-guarding machinery to SegmentBudget itself to compensate for a synchronous test double being unlike production. See docs/adr/0017 for the full writeup, including what an earlier version of this fix got wrong chasing a same-thread reentrancy hazard that doesn't actually exist in production. core/docs/m7-baseline.md updated: the RSS number now clears the DoD line (45.41 MiB via heap-profile), root-caused rather than just re-measured. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q3QrF7rCt21bkAjt9BCDFQ
Velox Download Manager (VDM)
An IDM-class download manager for Ubuntu 26.04 LTS: multi-segment accelerated HTTP(S) downloading, resume, categories and automatic file distribution, queues and scheduler, speed limiter, a Firefox extension that captures downloads automatically, and clipboard link capture.
Name is a placeholder. Binaries are
veloxd,velox-gui,velox,velox-nmhost. Rename before first release if you want something else — do it in M0, never later.
Status
Phase M0 — scaffolding. No implementation code exists yet. This repository currently contains the architecture, the wire contract, the roadmap, and one brief per build lane so that several agents can work in parallel without colliding.
Start here:
| Document | What it answers |
|---|---|
| docs/01-architecture.md | Process model, why four binaries, framework choices and why |
| docs/02-roadmap.md | Milestones M0–M7, what runs in parallel, exit gates |
| docs/03-gui-spec.md | IDM-parity UI: every window, dialog, column, menu |
| docs/04-engine-design.md | Segmentation, resume, buffers, rate limiting, disk I/O |
| docs/05-extension-spec.md | Firefox capture, transports, pairing, media grabber |
| docs/06-risks-and-spikes.md | Snap Firefox, Wayland clipboard, and the other landmines |
| docs/07-packaging.md | .deb/PPA, Flatpak, AMO signing, install layout |
| contracts/README.md | The interface. Both sides build against this |
| CLAUDE.md | Rules of engagement for agents working in this repo |
Per-lane briefs live in docs/agents/ — one per agent, each with an owned directory list, a definition of done, and the files it must never touch. To dispatch the agents, use docs/agents/PROMPTS.md — six copy-paste prompts plus the worktree commands and the wave order.
Architecture in one picture
┌────────────────────┐ native messaging (stdio JSON) ┌──────────────┐
│ Firefox extension │◄───────────── or ──────────────►│ velox-nmhost │
│ (MV3, TS) │ loopback WS 127.0.0.1 + token └──────┬───────┘
└────────────────────┘ │
│
┌────────────────────┐ ▼
│ velox-gui (Qt 6) │◄──── JSON-RPC 2.0 over Unix socket ──► ┌─────────────┐
└────────────────────┘ $XDG_RUNTIME_DIR/velox/velox.sock │ veloxd │
│ (daemon) │
┌────────────────────┐ │ │
│ velox (CLI) │◄───────────────────────────────────────┤ libveloxcore│
└────────────────────┘ └──────┬──────┘
│
SQLite + sparse files
The daemon owns all state and all sockets. The GUI is a view — closing it does not stop a download. The extension never touches the disk; it hands URL + headers + cookies to the daemon and gets a task id back.
Repository layout
vdm/
├── contracts/ ⭐ Wire contract: JSON Schema, fixtures, codegen. Frozen per version.
├── core/ C++23 libveloxcore — engine. No UI, no RPC, no SQL.
├── daemon/ C++23 veloxd — RPC server, scheduler, queues, SQLite store.
├── gui/ C++23 velox-gui — Qt 6 Widgets, IDM-parity UI.
├── cli/ C++23 velox — scriptable client.
├── nmhost/ C++23 velox-nmhost — Firefox native-messaging bridge (thin pipe).
├── extension/ TS Firefox MV3 WebExtension.
├── tools/
│ ├── mockd/ TS mock daemon — lets GUI + extension work before veloxd exists.
│ ├── testserver/ Deliberately hostile HTTP server (no Range, flaky, redirects, auth).
│ ├── bench/ Throughput and CPU benchmarks.
│ └── fuzz/ libFuzzer targets for parsers.
├── tests/
│ ├── conformance/ Protocol suite. Every lane must pass it. Gate for merging.
│ ├── integration/ veloxd + testserver.
│ └── e2e/ Playwright: real Firefox + real daemon + real file on disk.
├── packaging/ debian/, flatpak/, appimage/, native-host manifests.
└── docs/ Everything above, plus adr/ and agents/.
Toolchain bootstrap
Surveyed on this machine 2026-09-09 — most of it is already installed:
| Present | Version |
|---|---|
| git · cmake · ninja · g++ · gdb | 2.53.0 · 4.2.3 · — · 15.2.0 (C++23 ready) |
| qt6-base-dev · qt6-tools-dev · qt6-tools-dev-tools | ✓ |
| libcurl4-openssl-dev · libsqlite3-dev · nlohmann-json3-dev · libssl-dev | ✓ |
| libavformat-dev · libavcodec-dev · ffmpeg | ✓ |
| clang-format · clang-tidy · python3 · pkg-config | ✓ |
| python3-jsonschema · python3-referencing (conformance static runner) | ✓ |
Only these four are missing:
sudo apt update && sudo apt install -y \
qt6-svg-dev \ # GUI: SVG icon rendering
libsecret-1-dev \ # DAEMON: Secret Service for site logins
nodejs npm \ # EXT + PROTO: extension build, mockd, conformance runner
clang # optional: libFuzzer targets in M7
Node in the 26.04 archive may lag; if the extension toolchain needs 22+, use nvm.
Verify with cmake --preset dev && cmake --build --preset dev once lane CORE lands its
first target. Note CMake 4.2.3 is installed — newer than the 3.28 floor in
CMakeLists.txt, and it hard-errors on cmake_minimum_required below 3.5, so no
dependency may ship a pre-3.5 CMake file.