daemon: mark ADR 0013 fully accepted — PROTO landed the error-on-paused widening

PROTO closed the one remaining contract gap as contracts/ 1.3.0
(lane/proto commit 6db304a): event.task.state.error / TaskSummary.error
now populate on a paused transition CORE entered unilaterally, not just
on failed/retry_wait. Minor widening of an existing field's presence
condition, no retype, no new field, per contracts/README.md rule 4.

Updates every place in the ADR that referred to this as an open
question or unresolved gap: the status line, the pause-reason
bookkeeping in §2, the alternatives-considered pointer, and the
contract-gap section itself (renamed "surfaced, now closed"). Notes
1.3.0 is on lane/proto but not yet merged to main (still 1.1.0) —
daemon/src/sched/'s pause/resume logic should be written once that
merge lands, not before.

All four of ADR 0013's open items are now resolved: CORE confirmed
tasks_starved's structural exclusion and pause()/resume() idempotency
explicitly (verdict: "accept as written", not hedged), and adopted
"auto-pause" with no new wire term.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
This commit is contained in:
2026-09-10 00:07:28 +04:00
co-authored by Claude Sonnet 5
parent 62cda074c1
commit 27e9ce9fb5
+34 -25
View File
@@ -1,13 +1,15 @@
# ADR 0013 — Task state-machine ownership: CORE, DAEMON, and the shared `paused`
**Status:** accepted by CORE, PROTO's open item 3 outstanding · **Date:** 2026-09-09
**Status:** accepted by CORE and PROTO; all four open items resolved · **Date:** 2026-09-09
· **Lane:** DAEMON, drafted at PROTO's request (D1 in `core/docs/proto-requests-m1.md`).
**CORE's response:** `core/docs/adr-0013-core-response.md` (`lane/core`, commit `c65e664`)
— accepted as written, no amendments, plus the pinned `tasks_starved` definition and
`pause()`/`resume()` idempotency contract folded in below.
**Remaining blocker:** PROTO landing the `error`-on-`paused` widening (open item 3) —
`daemon/src/sched/`'s pause/resume logic is unblocked from CORE's side already, but not
buildable correctness-preservingly until that lands.
**PROTO's response:** the `error`-on-`paused` widening landed as `contracts/` **1.3.0**,
`lane/proto` commit `6db304a` — no retype, no new field; only the presence condition on
the existing `error: TaskError | null` field widens to include a daemon-initiated pause.
Not yet merged to `main` (main is at 1.1.0 as of this writing) — `daemon/src/sched/`'s
pause/resume logic should be written against `1.3.0` once that merge lands, not before.
## Context
@@ -95,10 +97,10 @@ The contract that makes this safe, per CORE's sign-off (`core/docs/adr-0013-core
- **DAEMON persists *why* a task is paused**, in the `tasks` table, not in `TaskState`
itself (the wire type stays a flat enum — this is DAEMON-local bookkeeping, not a
contract change). A `pauseReason` distinguishing at least `user`, `schedule`,
`queue_stopped`, `admission_reconcile`, and CORE's `error.code` when auto-paused. This
is what makes §3's resume rule possible — but it needs a contract fix first: see "A
contract gap this ADR surfaces" below. `error.code` is not currently carried on a
transition into `paused` at all.
`queue_stopped`, `admission_reconcile`, and CORE's `error.code` when auto-paused
now readable off `event.task.state.error` since PROTO's 1.3.0 widening (see "A
contract gap this ADR surfaced, now closed" below). This is what makes §3's resume
rule possible.
- **CORE does not need to track why it's paused past the current occurrence.** Once
paused, CORE's job is done; DAEMON is the only side that later decides whether to
resume, and DAEMON is also the only side with persistent storage to remember the
@@ -209,21 +211,25 @@ without CORE reporting them; forcing DAEMON to poll or reimplement that timing d
etc.).** Rejected — it roughly doubles the enum for a fact only DAEMON's resume logic
needs, and adding wire cardinality for internal bookkeeping is the kind of thing that
becomes a compatibility problem the moment a client starts branching on it. The value
DAEMON needs (CORE's reason) can travel on the existing `error` field instead — see the
contract gap in open item 3 — without touching `TaskState` itself.
DAEMON needs (CORE's reason) travels on the existing `error` field instead — see the
closed contract gap below — without touching `TaskState` itself.
## A contract gap this ADR surfaces, not just an open question
## A contract gap this ADR surfaced, now closed
`event.task.state.schema.json`'s own description scopes `error` to "whenever the new
state is failed or retry_wait" — **not** `paused`. The one fixture
(`event.task.state.json`) only exercises the `failed` case. So today, when CORE
auto-pauses for `auth_required` or `server_file_changed`, DAEMON has no signal on the wire
telling it *why* — §2/§3 of this ADR are unbuildable without one. This needs a PROTO
follow-up (minor: widening an existing field's presence condition, per
`contracts/README.md` rule 4 — no new field, no retype) to also populate `error` when
`state == "paused"` and the pause was CORE-initiated. DAEMON is not asking for a way to
tell CORE-paused from user-paused on the wire in general — `error: null` on a
DAEMON-initiated pause is sufficient, since DAEMON already knows it just did that.
`event.task.state.schema.json`'s description scoped `error` to "whenever the new state is
failed or retry_wait" — **not** `paused`, at the time this ADR was drafted, and the one
fixture (`event.task.state.json`) only exercised the `failed` case. So CORE auto-pausing
for `auth_required` or `server_file_changed` had no wire signal telling DAEMON *why* — §2/
§3 of this ADR were unbuildable without one.
**Closed by PROTO**, `contracts/` 1.3.0 (`lane/proto` commit `6db304a`, see the status
line at top for the merge caveat): minor widening of the existing field's presence
condition, per `contracts/README.md` rule 4 — no new field, no retype, `error` stays
`TaskError | null`. It is now populated on a `paused` transition whenever CORE entered it
unilaterally; `error: null` on a DAEMON-initiated pause is unchanged. New fixture
`event.task.state.auto-paused.json` exercises the auto-pause case; the existing
`event.task.state.json` fixture's stale "exactly when failed or retry_wait" assertion was
corrected in the same change.
## Resolution of the four open items
@@ -236,10 +242,13 @@ DAEMON-initiated pause is sufficient, since DAEMON already knows it just did tha
task (pause racing completion isn't an error), and no state-change event fires for a
no-op — DAEMON reads resulting state from the callback/`TaskDetail`, never from the
call's return value.
3. **Still open — PROTO.** CORE's half is ready (`ErrorInfo` is populated on every
auto-pause transition today's callback path would carry, using the existing B1
taxonomy); the wire only needs `error` permitted when `state == "paused"`. This is the
one remaining blocker on `daemon/src/sched/`'s pause/resume logic.
3. **Landed — PROTO, `contracts/` 1.3.0** (`lane/proto` commit `6db304a`, not yet merged
to `main`). `event.task.state.error` and `TaskSummary.error` are now populated "on
every `failed` or `retry_wait` transition, and on a `paused` transition the daemon
entered unilaterally" — a deliberate pause still carries `error: null`. New fixture
`event.task.state.auto-paused.json` exercises an `auth_required` auto-pause directly.
DAEMON's §3 resume rule can be implemented once `sched/` is built against `main` at
1.3.0 or later.
4. **CORE adopts "auto-pause".** No new wire or API term — the discriminator stays
`state == paused` plus the presence of an `Error` (present ⇒ CORE-initiated, absent ⇒
DAEMON-initiated), exactly as §2 already specified.