Files
vdm/daemon/docs/engine-api-review.md
samiandClaude Sonnet 5 ea8fd7ca8f daemon: fold CORE's confirms into the engine-API review
CORE resolved all four (lane/core@3da4cd6): vdm::TaskId is hashable and
DAEMON never constructs one; DAEMON mkdir -p's save_path's parent
(engine -> Error::path_rejected if missing); sha512 added as the 4th
Checksum::Algo so no -32602 at the RPC edge; on_state(cancelled) then
on_finished(Err{Error::canceled}) -- note the one-L spelling in the
error taxonomy. Also notes rate/token_bucket + Engine::rate_limiter()
for the limiter.set wiring.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
2026-09-10 15:51:16 +04:00

5.6 KiB

DAEMON review — CORE's engine API (core/docs/engine-api-m1.md, lane/core@d6cf1fe)

Verdict: sign off. Nothing here forces a daemon/src/sched/ or RPC-dispatch rewrite. The value types are final enough to build sched/ against now; Engine / DownloadHandle bodies landing in stage 8 is fine. The split matches ADR 0011 and ADR 0013 exactly.

Answers to the five open questions, then the small things to confirm.

Answers

Q1 — probe_hint: keep it optional, as sketched

DAEMON has a ProbeResult on exactly one path: the File Info dialog, where the user already waited for download.probe and then clicked Download. Every other entry — capture.offer → take, velox add, download.addBatch, the restart flow — has no probe in hand. Forcing the engine to always probe adds a round trip to the one case where the user just sat through one; forcing DAEMON to always probe first means reimplementing the engine's probe on the daemon side. Pass probe_hint when we have it, omit it otherwise — the two code paths are worth keeping.

Q2 — one cancel(discard_partial), not a separate remove()

The two wire methods map cleanly onto the one call:

wire live task already terminal
download.cancel handle.cancel(discard_partial=false) — keeps .veloxpart no-op on the handle; DAEMON marks the row cancelled
download.remove {deleteFile} handle.cancel(discard_partial=true) no-op on the handle; DAEMON deletes the row and, if deleteFile, the finished file

download.remove on a completed task is pure DAEMON-side work (row + optional file); the handle is already terminal so cancel() no-ops, which is exactly what we want. No handle.remove() needed.

Q3 — {restart, keep_partial, abort} is enough, if the engine owns the mechanical 416 retry

For server_file_changed the three options are right and complete. For range_metadata_stale (416): docs/04 §7 already has the engine re-probe and re-split automatically. Keep that — DAEMON does not want to be in the loop for a routine 416. Only escalate to on_decision_needed{range_metadata_stale} when the automatic re-probe/re-split also fails to reconcile, and at that point "retry the same range once more" is not a useful fourth option (the engine already exhausted it). So: no fourth value, provided the engine handles the common 416 without a callback.

Q4 — per-task 4 Hz on_progress is fine; on_progress_batch optional

DAEMON already has to coalesce across tasks: event.task.progress is "batched ≤ 4 Hz into a single array message" (AGENT-DAEMON.md item 5). So 80 per-task callbacks/s land in DAEMON's fan-out queue and are re-emitted as one array at 4 Hz regardless. Per-task keeps the handle↔callback correspondence simple and is not a bottleneck. If the engine's timer thread is already walking every task to build those callbacks, a on_progress_batch(span<Progress>) is strictly less work for both sides and we'd take it — but it is not needed for M1 and should not hold stage 8.

Q5 — refresh_url while downloading: restart all segments on the new URL

download.refreshUrl's contract is the signed-URL-expiry case: "re-probes and compares size and validator; if they still match, the transfer resumes from where it stopped." That wants consistency — every segment on the new URL once the re-probe validates. Mirror rotation (finish in-flight on the old host, new work elsewhere) is a different mechanism and it is already DownloadSpec.mirrors + the segmenter's 3-failure requeue, not refresh_url. So: on refresh_url, re-probe, and if size+validator match, move all segments to the new URL from their current offsets; if they don't match, surface it (on_decision_needed or a refresh_url error) rather than silently restarting.

Confirmed by CORE (lane/core@3da4cd6)

  1. vdm::TaskId — cheap-copy and std::hash-able; DAEMON never constructs one, only receives it from start() / callbacks and passes it back to set_task_order(). sched/ keeps the vdm::TaskId → wire taskId map keyed off handle.id().
  2. Parent directory — DAEMON mkdir -ps save_path's parent before start(); the engine opens the file and fails the task with Error::path_rejected if it is missing. Now explicit on DownloadSpec's doc comment.
  3. sha512 — added as the fourth Checksum::Algo, matching the wire set. No -32602 at the RPC edge; DAEMON passes it straight through.
  4. Cancel orderingon_state(_, cancelled, nullopt) then on_finished, always in that order. Note the taxonomy value is spelled canceled (one L): the finish is on_finished(Err{Error::canceled}), error.code == canceled. sched/ keys on that to write a cancelled history row rather than a user-facing failure. (The wire TaskState / EngineState spelling stays cancelled; only vdm::Error is one-L.)

rate/token_bucket — the global → queue → task speed-limiter hierarchy (docs/04 §6), reached via Engine::rate_limiter(). DAEMON's limiter.set {globalBps, enabled} maps to rate_limiter().set_global_limit(...); limiter.get reads it back. Wire this alongside the download.addstart() glue.

Integration timing

Wire it after sched/ lands — the scheduler is what calls start() / pause() / resume() / cancel() and drives segment_budget().set_task_order(). Order: sched/ (against these headers) → download.addstart() glue → the callback→wire projection. sched/ does not need the Engine bodies, only the signatures in this doc, so stage 8 and sched/ proceed in parallel.