Files
vdm/core/docs/proto-requests-m1.md
T
samiandClaude Sonnet 5 c13b5dff21 core: answer PROTO's bufferBytes range question; accept inclusive endByte
buffer-sizing.md: the frozen 4 KiB–8 MiB and docs/04's 64 KiB–64 MiB both
miss. Recommend 64 KiB – 16 MiB, default 2 MiB, max_total_buffer_bytes
unchanged at 256 MiB:
 - 4 KiB floor is smaller than one libcurl write callback -> a syscall per
   chunk; 64 KiB is the smallest floor that coalesces.
 - throughput vs write size is flat past ~8 MiB on NVMe; 8–16 MiB is
   disk-stall absorption headroom for the fast-pipe/slow-disk case; 64 MiB
   is cache pressure for zero gain.
 - 32 segments x 64 MiB = 2 GiB vs the 256 MiB cap means the docs/04 max is
   unreachable past 4 total active segments — a misleading Options value.
   16 MiB is reachable for single-/light-multitask and clamps to 8 MiB
   under heavy parallelism, which is correct.
 - default 4 MiB x 20 downloads = 80 MiB, busting the "<=60 MB RSS / 20
   downloads" DoD; 2 MiB fits. Filed as request B4.

proto-requests-m1.md: B3 endByte accepted as inclusive (HTTP Range
semantics, no curl-boundary off-by-one); [start,end) ask withdrawn; stage
6 designed against inclusive. New B3a: the Content-Length: 0 whole-file
case needs a representable zero-length segment — min_segment_bytes means
CORE never makes empty segments mid-download, so it's only the degenerate
case; mild preference for startByte+length over an endByte=startByte-1
sentinel.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
2026-09-09 19:57:46 +04:00

189 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CORE → PROTO — requests before the `VERSION` freeze
Status: **open**. Raised by lane CORE against `contracts/` at `1.0.0-draft`.
This is a request list, not a decision record. PROTO owns every item here. Once PROTO
answers D1, that one becomes a three-way ADR (`docs/adr/`) per `contracts/README.md` rule 4.
Ranking is by `contracts/README.md` rule 4, not by intuition:
new method or new optional field = **minor**, can land anytime;
rename / remove / retype / default change = **major** + ADR.
---
## Freeze-blockers — get these into `1.0.0` (major if we guess wrong)
### B1. A frozen wire enum for `TaskSummary.error.code`
`TaskSummary.error.code` is currently a bare `integer`. CORE cannot import a protocol
header (layering rule — `core/` never sees JSON/RPC), so CORE has its own failure
taxonomy **regardless** of what PROTO picks; DAEMON owns the projection from one to the
other. The ask is not "who owns the enum" — it is: **freeze a wire enum with enough
cardinality that DAEMON's mapping from the CORE taxonomy below is lossless.**
If the wire enum is coarser than this table, DAEMON has to collapse distinct engine
failures into one code and the GUI can't tell "server file changed" from "checksum
mismatch". Retyping `error.code` later is a major bump.
CORE failure taxonomy (this *is* `vdm::Error` in `core/include/vdm/util/error.hpp`):
| CORE `Error` | Meaning | `retryable` | carries `httpStatus` |
|---|---|---|---|
| `canceled` | user/daemon cancelled | no | no |
| `resolve_failed` | DNS resolution failed | yes | no |
| `connect_failed` | TCP connect failed | yes | no |
| `tls_failed` | TLS handshake / cert rejected | no | no |
| `connection_reset` | peer reset mid-transfer | yes | no |
| `timeout` | connect or idle/stall timeout | yes | no |
| `too_many_redirects` | redirect limit hit | no | no |
| `http_client_error` | 4xx not otherwise classified | no | yes |
| `http_server_error` | 5xx | yes | yes |
| `auth_required` | 401 / 407 — task pauses, not terminal | no | yes |
| `forbidden` | 403 after the referrer-retry (`docs/04` §7) | no | yes |
| `not_found` | 404 | no | yes |
| `range_not_satisfiable` | 416 — triggers re-probe/re-split | yes | yes |
| `gone` | 410 / expired signed URL, no mirror left | no | yes |
| `server_file_changed` | `200` where `206` expected, or `If-Range` / ETag drift (`docs/04` §5) | no | yes |
| `content_length_mismatch` | bytes received ≠ declared length | yes | no |
| `checksum_mismatch` | post-download hash ≠ expected | no | no |
| `disk_full` | ENOSPC | no | no |
| `io_error` | `pwrite` / `fallocate` / `fdatasync` failed | no | no |
| `path_rejected` | outside allowed roots or not writable | no | no |
| `permission_denied` | EACCES on the destination | no | no |
| `meta_corrupt` | `.veloxpart.meta` failed CRC / bad magic | no | no |
| `meta_version_unsupported` | meta file from a newer engine | no | no |
| `probe_failed` | probe could not determine file info | maybe | yes |
| `unsupported_url_scheme` | not http/https | no | no |
| `max_retries_exhausted` | backoff loop gave up (`data` names the last underlying `Error`) | no | maybe |
| `internal` | invariant violation / bug | no | no |
Concretely: a **string enum** on `error.code` mirroring these names is simplest to keep
lossless; if it stays an `integer`, please reserve grouped ranges (e.g. 1xx network,
2xx http, 3xx local-io, 4xx metadata, 5xx internal) with room to grow inside each.
### B2. Freeze the meaning of `TaskSummary.segments` — *major, retype-of-meaning*
`docs/04` §4/§3 require the *effective* value (after the per-host cap and the
non-resumable→1 demotion) to reach the UI. `TaskSummary.segments` currently has no stated
meaning. Please freeze it as **"effective segment count in use right now"**, not the
requested value. This is the only genuinely freeze-blocking half of the buffer/segment
story — pinning a field's meaning after `1.0.0` is a major bump under rule 4. It stands
alone; do not defer it together with B2a below.
### B2a. A readable effective buffer size — *minor, but note the trap*
Split out from B2 on purpose so PROTO can't use the cheap half to defer the major half.
`bufferBytes` is **write-only**: accepted by `download.add` and `download.update.patch`,
readable on **no** type. `docs/04` §4 says the clamped / "silently reduced to fit" value
is "reported back to the UI", so CORE will expose an effective value and PROTO needs a
frozen field to carry it. Adding an optional field is minor under rule 4 — **except**
that `TaskSummary` is `additionalProperties: false`, so a daemon cannot tack it on as an
extension; it has to be in the schema or it's dropped by every conformance replay. Name
and placement are PROTO's call (`effectiveBufferBytes` on `TaskSummary` / `TaskDetail`,
or a dedicated readback). Land it whenever — it just can't be improvised later.
### B3. Freeze `Segment.schema.json` field names
`Segment` is already on PROTO's type list (`contracts/README.md`) and `TaskDetail`
"includes `segments[]`", so this is mostly field-name matching — but do it **before**
freeze or each fix is a rename = major.
CORE will emit per segment:
| CORE field | Type | Note |
|---|---|---|
| `index` | int ≥ 0 | **`event.task.progress` currently says `i`.** Pick one name for both. |
| `startByte` | int ≥ 0 | absolute byte offset, inclusive |
| `endByte` | int | absolute byte offset, **inclusive** — range is `[startByte, endByte]` (see below) |
| `completed` | int ≥ 0 | bytes written in this range so far |
| `speedBps` | int ≥ 0 | current per-segment rate |
| `state` | enum | `connecting` \| `downloading` \| `stalled` \| `complete` \| `failed` |
**Resolved by PROTO at freeze:** `endByte` is **inclusive** (matches HTTP `Range`
semantics — `Range: bytes=start-end` is inclusive — and removes an off-by-one at the curl
boundary). CORE designs stage 6 (segmenter/stealer) against inclusive. The earlier
`[start, end)` ask is withdrawn.
Still open: (a) reconcile `i` vs `index`; (b) confirm the segment `state` enum values;
(c) **empty-segment representation** — see B3a.
### B3a. Zero-length segment must be representable — *PROTO is fixing; CORE's requirement*
With `endByte` inclusive and `minimum: 0`, a zero-length segment (`endByte = startByte -
1`) at offset 0 is `endByte = -1`, which the schema forbids. The one case CORE actually
needs: a **whole-file zero-length download** (`Content-Length: 0`) — one segment, length
0. It is a valid HTTP response and the daemon/GUI must be able to hold it.
CORE will **not** produce empty segments mid-download: the `min_segment_bytes` floor
(1 MiB, `docs/04` §3) means the segmenter never splits below 1 MiB and the stealer only
takes a half-range if it is ≥ that floor. So B3a is purely about the degenerate
whole-file case.
Preference: encode segments as `startByte` + `length` (+ `completed`) rather than an
inclusive `endByte` with a `startByte - 1` sentinel — `length: 0` is then the natural
representation and there is no negative value to allow. If `endByte` inclusive stays,
then a 0-byte task needs an explicit encoding (an `empty`/`length` field, or permitting
`endByte = startByte - 1` with `minimum: -1`) — any of those work for CORE as long as
total length 0 round-trips. Flag back if the chosen fix needs anything else from CORE.
### B4. `bufferBytes` range is wrong in the frozen schema — *see `buffer-sizing.md`*
`contracts/` froze `bufferBytes` at 4 KiB 8 MiB; `docs/04` §4 says 64 KiB 64 MiB
default 4 MiB; the RSS DoD (`docs/04` §8) can't hold either default. CORE's analysis and
the recommended range (**64 KiB 16 MiB, default 2 MiB**, `max_total_buffer_bytes`
unchanged at 256 MiB) with the global-cap arithmetic is in
[`core/docs/buffer-sizing.md`](buffer-sizing.md). PROTO to land schema + `docs/04` §4 +
ADR together.
---
## Do not hold the freeze for these
All minor under rule 4 (new methods / new events / a pattern on an existing optional
string). `contracts/README.md` rule 5 wants contract changes "cheap and frequent" — raise
these now, land them as small follow-up PRs, but **do not gate `1.0.0` on them.** Each is
needed *within* M1.
### F1. A "needs user decision" carrier — *new event + method, minor*
`docs/04` §5 mandates a "File on server has changed — restart download?" prompt, and §7
has 416/stale-metadata. The event surface has only `event.notify` + task states. Either:
- confirm `state: paused` + an `event.notify {level:"warn", …}` is the intended carrier
(CORE reports `server_file_changed`, DAEMON pauses + notifies, GUI offers restart), **or**
- add `event.task.decision {taskId, kind, options[]}` + `download.decide {taskId, choice}`.
CORE needs to know which, but a string comparison on `error.code` covers the engine side
either way, so this doesn't block freeze.
### F2. A credential return path for 401/407 — *new method, minor, M1-needed*
`event.auth.required` exists; there is **no method to hand credentials back to a paused
task**. `session logins` / Secret Service is M3, but "401 handled" is in CORE's M1 DoD.
Requesting `download.provideAuth {taskId, username, password, save?}`
`{ok}`. Secrets go to the Secret Service, never SQLite/logs (`CLAUDE.md` §4) — `save`
just tells DAEMON whether to persist.
### F3. `checksum` string format — *pattern on an existing optional field, borderline*
`download.add {checksum?}` has no shape. CORE's verifier will accept `"<algo>:<hex>"`
with `algo ∈ {md5, sha1, sha256}` (appendix lists MD5/SHA-256). Adding a `pattern` to an
existing optional string is close to free; PROTO's call whether it's worth a fixture now
or a doc note.
---
## Three-way ADR material (after PROTO answers)
### D1. State-machine ownership split — CORE / DAEMON / PROTO
`docs/04` §1 hands CORE the lifecycle, but `queued` and every scheduler transition are
DAEMON's. Proposed split, to be recorded as an ADR once all three agree:
- **CORE owns:** `probing → connecting → downloading ⇄ paused → retry_wait → assembling
→ verifying → complete | failed`, plus `cancelled` from any state.
- **DAEMON owns:** `new`, `queued`, and pause-for-schedule (enters CORE's `paused`).
- **Shared:** `paused` is reachable from either side; both must be idempotent about it.
This needs an ADR regardless of the answer (`contracts/README.md` rule 4: the lifecycle
enum is a frozen type and any change to who drives it is a documented decision).