download.probe was a stub (-32603). It needs an HTTP round trip on the engine's probe
pool (up to the schema's 30s x-deadlineMs), which cannot fit VeloxDispatcher's
synchronous on_download_probe -> HandlerResult<T> return without blocking the RPC
loop for the duration — a hard no per CLAUDE.md ("never block the RPC loop") and
AGENT-DAEMON.md build step 1.
uds_server.cpp and ws_server.cpp special-case "download.probe" before the generic
dispatch(), exactly the way they already special-case session.hello/session.subscribe:
parse the params, call the port, and queue the reply whenever the callback fires
(dropped silently if the connection is gone by then).
rpc::TaskActionPort gains probe_now(DownloadProbeParams, callback) — kept in proto/std
terms, no vdm::net::* in the signature, so veloxd_rpc never needs core/include's vdm
headers just to declare this. sched::Scheduler::probe_now is the implementation:
builds a vdm::net::ProbeRequest, runs it on the engine's probe pool, marshals the
engine-thread callback back onto the loop (deps_.post_to_loop, same as every other
engine callback here), maps a probe failure to -32013 ProbeFailed (data.httpStatus set
when there was an HTTP response), and fills suggestedCategoryId/suggestedSaveDir with a
plain extension match against the categories table — not the real rules engine, which
is still D3; noted in a comment.
Verified against real veloxd + tools/testserver, not just unit tests: a real probe
answers in ~5ms with size/resumable/etag/redirect chain; a bad host maps to -32013;
and — the actual point of the async design — a connection running a 10s slow-loris
probe does not block a second connection's download.list, which answers in ~1ms while
the probe is still outstanding.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GRDjHGgpYmMoPE2UFbe7pP
7.2 KiB
7.2 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) |
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.remove/addBatch/refreshUrl/provideAuth/update, rules.*, settings.*, limiter.*, schedule.*, queue.upsert/reorder, category.upsert/remove, grabber.*, media.*, capture.* |
rpc/dispatcher.cpp |
No store/scheduler wiring behind them yet. category.list, queue.list, queue.start/stop are done |
Per method, as each wires to the store/scheduler |
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. |