libdbus-1-dev / libsystemd-dev have no headers installed in this build environment (only the runtime .so's — apt-cache policy confirms libdbus-1-dev is available but not installed). A real org.freedesktop.Notifications-backed PairingApprover needs one of those linked into veloxd, which is a new build dependency for daemon/CMakeLists.txt and, since packaging manifests would need to know about it too, a decision to surface rather than reach for silently mid-session. PairingApprover::approve() is also still synchronous by shape — its own doc comment already says the real approver "will run async and is not this shape." The async pattern this session built for download.probe (rpc::TaskActionPort + the server-layer deferred-reply special-case in uds_server.cpp/ws_server.cpp) is the right shape to reuse once there's a real implementation to justify reshaping the interface; doing that with nothing behind it yet would just be churn. Left EnvAutoApprover in place rather than hand-roll a D-Bus wire client to route around the missing headers — a broken pairing approver is a worse outcome than an honest, already-documented stub. Findings recorded in deferrals.md for whoever picks this up once the dependency is available and approved. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
13 KiB
13 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 |
|---|---|---|---|---|
| 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.*, settings.*, limiter.*, schedule.*, queue.reorder, grabber.*, media.*, capture.* |
rpc/dispatcher.cpp |
No store/scheduler wiring behind them yet, or (settings.*) sound but large — see the note below. category.list/upsert/remove, queue.list/upsert/start/stop, download.remove/addBatch/provideAuth are done |
Per method, as each wires to the store/scheduler |
| — | settings.get/settings.set specifically, not started: proto::Settings is a flat struct of ~43 std::optional fields, one per SettingKey (~50 keys) in Settings.schema.json; store::Settings already has get_raw/set_raw/overrides keyed by the same dotted strings the JSON uses ("connection.maxSegmentsPerDownload", …). The handlers are a mechanical field <-> key <-> JSON-type mapping table in both directions (get: row-or-default -> struct field; set: struct field -> validate against the key's schema type -> set_raw, collecting changed) — real work, just long and repetitive rather than hard. Left alone this pass rather than rushed; every other read of settings in this codebase already goes through store::Settings's typed helpers directly (reload_config, on_download_add's segment default, capture.offer's allowed roots when that lands), so nothing downstream is blocked on the RPC surface existing. |
rpc/dispatcher.cpp, store/settings.{hpp,cpp} |
the mapping table is genuinely large, not genuinely hard | its own pass |
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. |