# DAEMON → CORE — the engine API `sched/` needs before it can be written Status: **open**. Companion to `docs/adr/0011-admission-control-and-the-segment-budget.md`, which settles *who* enforces what. This doc is the concrete API ask that ADR depends on. Also carries one contract gap (C4) that is PROTO's, not CORE's, filed here because it was found while writing the ADR. Ranking follows `contracts/README.md` rule 4 conventions even though this isn't a `contracts/` change: new API surface = cheap, land anytime; a behavioural promise (min-1 fairness) = needs CORE's explicit sign-off before DAEMON builds on the assumption. --- ## C1. Occupancy read-out, not inference DAEMON's governor (ADR 0011 §1) must never count segments to make an admission decision, which means it needs to *read* engine occupancy rather than derive it from how many tasks it thinks it started. Requesting: - `EngineBudget budget() const` — `{ total: uint32, active: uint32, tasks_starved: uint32 }` where `tasks_starved` is the count of running tasks currently holding zero segments (ADR 0011 §3.6 — DAEMON asserts this is 0 in steady state and logs if it isn't). - `uint32_t segments_active(TaskId) const` — for `TaskSummary.segments` projection (ADR 0010: the *effective* count) and for `velox ls --json`. - A coalesced `on_budget_changed(callback)`, batched at the same ≤4 Hz as `event.task.progress` (brief item 5) — DAEMON is not polling this on a tighter loop. ## C2. `set_max_active_segments(uint32_t)` and its live-apply semantics Brief already has this as a settings value CORE enforces. DAEMON needs to know: does lowering it live drain existing segments down to the new ceiling (segments finish, no new ones start until under budget), or does it kill in-flight segments? DAEMON's assumption, pending your answer, is drain-not-kill — a live cut to 8 must not abort 24 in-flight segments and lose their partial ranges. ADR 0011 open item 3. ## C3. `set_host_segment_cap(host, uint32_t)` The per-host table is DAEMON state (SQLite `settings`/a host-cap table); CORE enforces it in segment units. Requesting a push API so there is one source of truth for the table and two enforcement points, per ADR 0011 §4. DAEMON derives its own per-host **task** cap from the same table value — same number, different unit, pushed once. ## C4. Contract gap: `connection.maxActiveSegments` isn't in `Settings.schema.json` Not CORE's item — flagging because ADR 0011 depends on it and `core/docs/buffer-sizing.md` already asked for it. `SettingKey` is a closed enum (`additionalProperties: false`), so DAEMON cannot expose this key through `settings.get`/`settings.set` until PROTO lands it. Until then DAEMON holds the value (default 32) locally and passes it to `set_max_active_segments()` at startup only — no wire exposure, so `Options → Connection` can't show or set it yet. Same ask as CORE's B2a bundle; landing them together is fine. ## C5. Fairness rule sign-off (ADR 0011 §3) Not new API — a behavioural contract. DAEMON's governor is built assuming: 1. Min-1 before seconds: no running task gets a second slot while another running task has zero. 2. Admission implies progress, subject only to connect timeout and per-host cap. 3. A steal is slot-neutral. 4. Priority order for slot distribution beyond the first comes from DAEMON (queue order, then FIFO by admission time, unless you'd rather take an explicit ordered list per tick — ADR 0011 open item 4). If any of this can't be built as stated inside the stealer, say so before `sched/` exists — this is the one thing in this doc that changes DAEMON's design rather than just its plumbing. ## C6. Probe pool sized outside the segment budget ADR 0011 §5: `download.probe` must not spend a segment slot, and `capture.offer`'s 750 ms deadline can't wait on the segment budget being free. Confirming CORE's probe path already runs its own small worker pool (proposed size 4) independent of `maxActiveSegments` — if not, this is a request to make it so. --- Raise objections against ADR 0011 directly; this doc is downstream of it and changes if the ADR does.