From 27e9ce9fb541e488556e822dd28bd957d7e6e88d Mon Sep 17 00:00:00 2001 From: sami Date: Thu, 10 Sep 2026 00:07:28 +0400 Subject: [PATCH] =?UTF-8?q?daemon:=20mark=20ADR=200013=20fully=20accepted?= =?UTF-8?q?=20=E2=80=94=20PROTO=20landed=20the=20error-on-paused=20widenin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig --- docs/adr/0013-task-state-machine-ownership.md | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/docs/adr/0013-task-state-machine-ownership.md b/docs/adr/0013-task-state-machine-ownership.md index 612e08b..eefbd99 100644 --- a/docs/adr/0013-task-state-machine-ownership.md +++ b/docs/adr/0013-task-state-machine-ownership.md @@ -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.