Rebased lane/daemon onto main first — fast-forward, no conflicts (lane/daemon was already fully merged; main has since taken lane/gui, lane/pkg-qa, lane/proto). 1. capture.offer and capture.getRules were stubs, buried inside D3's generic stub list with no flag for how load-bearing capture.offer is: it's a CLAUDE.md §4 non-negotiable (750ms deadline, fails open) and AGENT-DAEMON.md build step 6, and against the real daemon it 500s to -32603 every time — the extension captures nothing, not "falls through to Firefox on a slow path." Split into their own D7/D8 rows so they stop being invisible. 2. settings.get/settings.set (D9, closed). Four pointer-to-member tables in dispatcher.cpp (bool / ranged int / plain string / string array), five enum-typed keys handled individually since parse_XXX already validates those — covers all 43 SettingKeys without ~40 repetitive hand blocks. store::Settings::kDefaults grew from 14 entries to 43 (the schema itself carries no "default" keyword anywhere, so these are hand-chosen — conservative for the ones ADR 0012 doesn't speak to; capture.monitoredExtensions defaults to the union of every builtin category's extensions rather than an arbitrary list of its own). settings.set validates every field before writing any of them: numeric min/max (hand-checked — the generated parser only checks JSON type, not schema constraints) and saveTo.* paths via fs::resolve_target/canonicalize_root (-32011) — allowedRoots entries checked as roots in their own right, defaultDir/tempDir checked as paths resolving inside the (possibly just-updated, same call) root list. Reports exactly the keys whose effective value actually changed, publishes event.settings.changed with that list, and calls the scheduler's reload_config() when a connection.* key took effect — live, not on next restart. Verified against real veloxd: all 43 keys round-trip with defaults, a keys subset filters, an out-of-range value rejects the whole call, a bad saveTo.defaultDir is -32011, allowedRoots+defaultDir set together cross-validate against the new roots, event.settings.changed fires over a live subscription. New dispatcher_settings_test covers the same ground without a socket. Full ctest: 53/53 (excluding a pre-existing, unrelated conformance failure — see below). Also found, not fixed (not this lane): tests/conformance/run.sh's live-veloxd leg fails "pairing failed: no token issued" reproducibly, isolated, on main before any of this session's changes — it launches the isolated veloxd without VELOX_PAIR_AUTO=1, so EnvAutoApprover denies every session.pair and the WS leg's session.hello never gets a token. tests/conformance/ is PROTO/QA-owned; flagging rather than editing across the lane boundary. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
15 KiB
15 KiB
DAEMON — deferred work, tracked
Things that are deliberately incomplete in daemon/ right now, with why and when they
close. Kept here (not buried in commit messages) so the next pass can see them at a glance.
| # | What | Where | Why deferred | Closes when |
|---|---|---|---|---|
| D7 | capture.offer → -32603. This is a CLAUDE.md §4 non-negotiable ("Capture fails open... the extension gives up" — but a fail-open answer still has to arrive; a hard error is not a substitute for ignore) and AGENT-DAEMON.md build step 6 ("capture.offer must answer within 750 ms, always"). Against the real daemon right now, every offer 500s, so the extension captures nothing at all — not "falls through to Firefox on a slow path," genuinely nothing, because the extension is waiting on a promise that never resolves with a decision it can act on. This was buried inside D3's stub list ("...capture.*") with no flag for how load-bearing it is; called out on its own row now. |
rpc/dispatcher.cpp |
rules table has no matcher yet, no dedup-against-active-tasks, no category-folder resolution | build step 6 |
| D8 | capture.getRules → -32603. The extension mirrors this into its own capture policy on connect (docs/05 §4) so the two can never disagree about what should be intercepted; until this returns something real the extension runs with an empty/stale policy. Also buried in D3's list before this pass. |
rpc/dispatcher.cpp |
same rules-engine gap as D7 | alongside D7 |
| D1 | Pairing prompt is EnvAutoApprover (needs VELOX_PAIR_AUTO=1) |
rpc/pairing.hpp, main.cpp |
A GUI dialog / org.freedesktop.Notifications approver is integration work |
Build step 7 (systemd + notifications) |
| — | D1, checked this pass, not attempted: libdbus-1-dev (or libsystemd-dev for sd-bus) has no headers installed in this build environment — only the runtime .sos (dpkg -l/apt-cache policy confirm libdbus-1-3 present, libdbus-1-dev not, "Candidate" available but not installed). A real notification-backed approver needs one of those linked into veloxd, which is a new build dependency for daemon/CMakeLists.txt (find_package/pkg_check_modules) and — since packaging manifests need to know about it too — arguably a decision to surface rather than something to reach for silently mid-session. PairingApprover::approve() is also still synchronous by shape (its own doc comment already says so: "the real notification-backed approver will run async and is not this shape") — swapping it for the async pattern this session built for download.probe (rpc::TaskActionPort + the server-layer deferred-reply special-case) is the right shape once there's a real implementation to justify the churn; reshaping the interface with nothing behind it yet would just be churn. Left EnvAutoApprover in place rather than build a fragile hand-rolled D-Bus wire client to avoid the missing headers — a broken pairing approver is worse than an honest stub. |
rpc/pairing.hpp |
missing dev headers + an undiscussed new dependency | once libdbus-1-dev/libsystemd-dev is available and the dependency is approved |
Closed — download.probe is real on both transports. It's genuinely async (the engine's probe pool, up to the schema's 30s x-deadlineMs) and so cannot fit VeloxDispatcher::on_download_probe's synchronous HandlerResult<T> return — uds_server.cpp/ws_server.cpp special-case "download.probe" before the generic dispatch(), exactly the way they already special-case session.hello/session.subscribe, and queue the reply whenever the callback fires. rpc::TaskActionPort::probe_now (kept in proto/std terms, no vdm::net::*, so veloxd_rpc never needs core/include's vdm headers) is what both transports call; sched::Scheduler::probe_now is the implementation — builds a vdm::net::ProbeRequest, runs it on the engine's probe pool, maps a failure to -32013 ProbeFailed (with data.httpStatus when there was one), and fills suggestedCategoryId/suggestedSaveDir with a plain extension match against the categories table (not the real rules engine — that's still D3). Verified live: a real probe answers in ~5ms; a bad host maps to -32013; a connection issuing a 10s slow-loris probe does not block a second connection's download.list (answered in ~1ms) — confirms the async design actually keeps the loop free, not just compiles. |
rpc/task_action_port.hpp, rpc/{uds_server,ws_server}.{hpp,cpp}, sched/scheduler.{cpp,hpp} |
— | done | |
| D3 | Stub handlers for the rest: download.refreshUrl/update, rules.*, limiter.*, schedule.*, queue.reorder, grabber.*, media.* |
rpc/dispatcher.cpp |
No store/scheduler wiring behind them yet | Per method, as each wires to the store/scheduler |
Closed — settings.get/settings.set. The field <-> SettingKey <-> JSON-type mapping is four pointer-to-member tables in dispatcher.cpp (one per C++ field type: bool, ranged int, plain string, string array) plus five enum-typed keys handled individually (parse_XXX already validates those); every one of the 43 SettingKeys now has a real default (store::Settings::kDefaults grew from 14 entries to 43 — capture.monitoredExtensions's default is the union of every builtin category's extensions, so the two never drift apart). settings.get honors keys: null = everything. settings.set validates every field before writing any of them (numeric min/max — the schema itself carries none of this, so it's hand-checked against each key's documented range; -32602 names the offending key, its value, and its bounds) and validates saveTo.* paths against fs::resolve_target/canonicalize_root (-32011) — saveTo.allowedRoots entries are checked as roots in their own right, saveTo.defaultDir/.tempDir are checked as paths resolving inside the (possibly, in the same call, just-updated) root list. Reports exactly the keys whose effective value actually changed (a set to the value already in effect reports changed: [], not the key), publishes event.settings.changed with that same list, and calls TaskActionPort::apply_settings_reload() (-> Scheduler::reload_config()) when any connection.* key took effect, live rather than waiting for a restart. Verified against real veloxd: all 43 keys round-trip with sane defaults, a keys subset filters correctly, an out-of-range value is rejected with nothing else in the same call landing, saveTo.defaultDir outside every allowed root is -32011, setting allowedRoots and defaultDir together cross-validates against the new roots, and event.settings.changed fires over a live subscription. New dispatcher_settings_test covers the same ground without a socket. |
rpc/dispatcher.cpp, store/settings.{hpp,cpp}, rpc/task_action_port.hpp, sched/scheduler.hpp |
— | done | |
Closed — category.upsert/category.remove: store/categories.hpp gains get/upsert/remove. upsert generates an id when absent (create) and always ignores the payload's builtin (preserved from the existing row on replace, false on create — a client can never mint or revoke it); the saveDir goes through the same fs::resolve_target canonicalize-and-root-check as download.add (-32011 on failure). remove refuses a builtin at both layers (dispatcher pre-checks for the -32602 error text; the store's own DELETE ... AND builtin = 0 is defense in depth) and reassigns member tasks to reassignTo (default "general") inside one transaction before deleting the row. Note: the categories table (0001) has no columns for Category.mimeTypes/.sortOrder — accepted on upsert but not persisted. |
store/categories.{hpp,cpp}, rpc/dispatcher.cpp |
— | done, mimeTypes/sortOrder gap noted |
|
Closed — queue.upsert: store/queues.hpp gains get/upsert (set_state already existed from D4b). Same create-generates-id pattern as categories; taskIds in the payload is ignored (schema's own note) and a create always starts 'stopped' while a replace keeps the queue's current run state — queue.upsert edits config, not run state (that's queue.start/stop). Also fixed: on_complete was a real column since 0001 but Queues::list/get never projected it onto Queue.onComplete — now they do. |
store/queues.{hpp,cpp}, rpc/dispatcher.cpp |
— | done | |
Closed — download.remove: cancels with discard_partial=true through TaskActionPort (always drops any .veloxpart/.veloxpart.meta — the row is gone either way, unlike download.cancel, which keeps them), deletes the finished file only when deleteFile is true and the task was complete (best-effort — a missing file doesn't fail the call), deletes the row (segments cascade via the FK), and publishes event.task.removed (closing the last open note under D5). download.addBatch: on_download_add's body is now a shared add_one(), called once per item after merging each item's unset fields against params.defaults. download.provideAuth: forwards to EnginePort::provide_auth through a new TaskActionPort::provide_auth; remember/persisting to the Secret Service is accepted but not acted on — nothing in this build talks to libsecret yet (verified: no such integration exists anywhere in the tree). Verified against real veloxd + tools/testserver: category create/replace/remove-with-reassignment, queue create/replace-keeps-state, a batch add with shared defaults.saveDir, and remove-with-deleteFile actually deleting the file and the task then 404ing download.get with -32010. |
rpc/dispatcher.{hpp,cpp}, rpc/task_action_port.hpp, sched/scheduler.{cpp,hpp} |
download.provideAuth's remember (needs the Secret Service, unbuilt) |
done, remember persistence gap noted |
|
Closed — sched/engine_port_core.hpp wraps vdm::Engine + segment_budget(); main.cpp constructs Engine + Scheduler, calls reconcile_after_restart / reload_config / tick at startup |
— | — | done (lane/core stage 8 merged) |
|
Closed — download.pause/resume/start/cancel and queue.start/stop all drive the scheduler now, and apply immediately (not deferred to the next tick — pausing/resuming/cancelling a live transfer can't wait up to 1s, and per ADR 0013 §3 the governor never touches a user-owned pause on its own). New rpc::TaskActionPort interface (owned by rpc/, implemented by sched::Scheduler) is the seam dispatcher.hpp depends on instead of sched/scheduler.hpp directly — avoids a real veloxd_rpc <-> veloxd_sched circular library dependency (veloxd_sched already links veloxd_rpc for EventHub). Scheduler::user_pause/resume/start/cancel + pause_queue engine-call-then-eager-transition, matching tick()'s existing to_pause pattern. Fixed a real bug hit while building this: transition() always overwrote pause_reason to NULL when the engine's own delayed pause-ack callback arrived with no explicit reason, clobbering whatever the actual initiator (user or governor) had just written — now it preserves the stored reason when none is supplied. Verified against real veloxd + tools/testserver: pausing a live single-segment throttled transfer freezes downloadedBytes, resume continues it from that point, cancel stops it; queue.stop(pauseRunning:true) pauses the queue's running task immediately. NOTE: download.start's contract "a task in 'queued' jumps its queue" (priority bump) is not implemented — admission is still plain FIFO by created_at. |
sched/scheduler.{cpp,hpp}, rpc/task_action_port.hpp, rpc/dispatcher.{hpp,cpp}, store/queues.{cpp,hpp} |
— | done, except the queue-jump priority bump noted above | |
Mostly closed — rpc/event_hub fans out per-subscription; session.subscribe on both transports registers/updates/tears down a real subscription; Scheduler::transition() publishes event.task.state (with previousState) on every state change, scheduler-driven or engine-reported; dispatcher::on_download_add publishes event.task.added; a 250 ms timer batches Scheduler::progress_snapshot() into one event.task.progress array per AGENT-DAEMON.md item 5 / the schema's x-maxRateHz: 4. Verified live end to end. |
— | event.task.removed has no source yet (download.remove is D3); event.speed.global, event.notify, event.auth.required, event.settings.changed, event.grabber.progress are unpublished — each lands with its owning handler |
as each owning D3 handler lands | |
Closed — engine numbers now reach the store: Scheduler::tick() probes (EnginePort::probe) before every start(), persisting sizeBytes/resumable/validators via Tasks::set_probe_result before a byte moves; Scheduler::persist_progress() (called from progress_snapshot() and once more from on_engine_state right before release()/unmap on every terminal transition) writes downloadedBytes/speedBps/segments/segmentDetail from the engine's Progress, so a task that finishes between two 250 ms ticks (the common case for anything small or fast) still leaves real numbers instead of the pre-persistence defaults. TaskSummary.segments is sourced from segments.size() when the task has any (matching what actually lands in segmentDetail, per the schema's "exactly segments entries"), falling back to the engine's effective_segments (budget slots held, not necessarily physical range count — see core/include/vdm/task/download.hpp's Progress comment) only pre-segmentation. Tasks::set_final_bytes tops up on_finished's byte count as a last-resort backstop. Migration 0002 adds speed_bps to both tasks and segments, and fixes segments.state's CHECK to include 'pending' (0001 omitted it, so a pre-connect snapshot could never be written). Verified against real veloxd + tools/testserver (not just unit tests): download.list/download.get correct immediately after completion and after a daemon restart. |
sched/scheduler.{cpp,hpp}, store/{tasks,segments}.{cpp,hpp}, store/migrations/0002_*.sql |
— | done | |
| — | vdm::task::Progress.speed_bps reads back as 0 for the whole lifetime of a live, real (non-fake) throttled download, despite downloadedBytes visibly advancing between polls — core/src/task/download_task.cpp's per-worker EWMA never seems to produce a nonzero aggregate in this build. DAEMON passes EnginePort::progress()'s speed_bps straight through (Scheduler::persist_progress); nothing in this lane drops it. Still reproduces in the D4b live checks above (0 throughout a paused/resumed/cancelled transfer whose downloadedBytes visibly moved) — not re-filed, since it's already CORE's. |