Review of core/docs/engine-api-m1.md (lane/core@d6cf1fe) in daemon/docs/engine-api-review.md. Sign off: nothing forces a sched/ or RPC-dispatch rewrite; the split matches ADR 0011 and ADR 0013. Answers: (1) keep probe_hint optional — DAEMON has a ProbeResult only on the File Info path; (2) one cancel(discard_partial), download.remove is cancel(true) + DAEMON-side row/file cleanup; (3) {restart, keep_partial, abort} suffices if the engine owns the mechanical 416 re-probe/re-split; (4) per-task 4 Hz progress is fine — DAEMON re-batches across tasks for event.task.progress anyway; (5) refresh_url restarts all segments on the new URL (the signed-URL case), mirror rotation is spec.mirrors not refresh_url. Four things to confirm, none blocking: vdm::TaskId copy/hash semantics and that DAEMON never constructs one; who mkdir -p's save_path's parent; sha512 (in the wire Checksum, not the engine enum) rejected at the RPC edge; on_finished(Err{cancelled}) code + ordering vs on_state(_, cancelled, _). Integration timing: wire after sched/ lands. sched/ builds against these signatures in parallel with CORE stage 8. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
5.4 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.
To confirm — none block sign-off
vdm::TaskId↔ wire UUID.sched/keeps avdm::TaskId → wire taskIdmap, keyed offhandle.id()atstart(). Confirmvdm::TaskIdis cheap to copy and hashable (std::unordered_mapkey), and that DAEMON is never expected to construct one — it only ever receives them fromstart()/ callbacks and passes them back toset_task_order().- Parent directory creation. The engine "never canonicalises or root-checks"
save_path— does itmkdir -pthe parent (the category folder may not exist yet), or must DAEMON ensure the directory exists beforestart()? DAEMON assumes the latter (it owns category/path logic) unless you say otherwise. Checksum::Algohas nosha512, butChecksum.schema.jsonaccepts it (PROTO F3). DAEMON will rejectalgorithm: "sha512"at the RPC edge with-32602beforestart()unless CORE would rather carry a fourth enum value. Either is fine; say which.on_finished(Err{cancelled})— DAEMON keys onErrorInfo.code == cancelledto write acancelledhistory row rather than surfacing it as a user-facing failure. Confirmcancelledis the code and thaton_state(_, cancelled, nullopt)always precedes it (the §4 table implies this).
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.add → start() 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.