Skip to content

amend(dn-supervision-and-liveness): A1 the power axis + graduate bp-154 — the scheduler learns to see the battery - #30

Merged
ascalva merged 2 commits into
mainfrom
design/dn-supervision-power-axis
Aug 8, 2026
Merged

amend(dn-supervision-and-liveness): A1 the power axis + graduate bp-154 — the scheduler learns to see the battery#30
ascalva merged 2 commits into
mainfrom
design/dn-supervision-power-axis

Conversation

@ascalva

@ascalva ascalva commented Aug 6, 2026

Copy link
Copy Markdown
Owner

⚑ Merging this blesses Amendment A1 and readies bp-154 in one act

closes #12

Two commits: the power-axis amendment to dn-supervision-and-liveness (ratified, track: ops),
and the one build plan graduated from it. The plan's design_ref becomes ratified by the same
merge that readies the plan — the two gates collapse into one, per the regime. No status field is
edited.

Kept separate from #29 deliberately: that PR is track: code-ingest and its merge means "I
ratify dn-vector-membership-store."
Mixing an ops amendment in would make one merge mean two
unrelated things, in a repo where the merge log is the build log.


Why — three emergencies, not the two the issue title names

when what happened
Jul 24 Drained to 1% during the v1.18.0 deploy night. The embedder starved under critical-battery throttle → code_backfill timed out → code_sync wedged → the daemon died unwitnessed and stayed dead three days.
Jul 28 100% → 8% in 2h40m during the revival's backlog drain, Python:100% throughout. Caught at the wire; zero casualties under launchd KeepAlive.
Aug 1 Fatal — the machine died mid-run. Run #39 came up in recovery.

The battery hardware is healthy and stays healthy — re-measured 2026-08-05: Condition Normal,
95% maximum capacity, 128 cycles (up 4 from the frozen finding's 124). The drain is load, not
degradation. The scheduler is the defect.

§2.7 gave the supervisor exactly one resource axis to refuse on — memory (NN-8, "the scheduler
refuses breaching work"
). That principle was never memory-specific; it simply never had a second
axis. A1 grows the same dispatch-time refusal a power axis: remaining energy is a schedulable
resource, and work that would spend the last of it is breaching work.

The design got much smaller once grounded

Issue #12's direction text implied new sensing infrastructure. The code says otherwise — the shed
machinery already exists
, so this adds a predicate, not machinery:

  • HEAVY_TIERS = frozenset({"synthesis", "stretch"}) (supervisor.py:59) is already shed by
    blocked_tiers() on presence.foreground_active() (:130-135).
  • Composition happens at the ONE claim siteblocked_tiers() | model_blocked_tiers()
    (:177), pinned at :153: "Enforced at the ONE claim site … no new queue."
  • scheduler/presence.py:46-59 is an exact template — injectable probe, threshold, and the
    fail-closed default assume_present_when_unknown: bool = True.

⚑ The one hard pin, and it comes from the code's own words

The power rule gets its own predicate — power_blocked_tiers() — never folded into
blocked_tiers().
supervisor.py:131-134 already ruled this shape:

THE FOREGROUND GATE, and nothing else. Deliberately not extended with the single-model-in-flight
rule (bp-110 §7 Item 4's invariant: "the foreground gate keeps its meaning and is not
overloaded"
) — two different reasons to refuse a tier, conflated into one predicate, is how a
reader later cannot tell which rule refused a job.

model_blocked_tiers() (:137) is the precedent for a sibling. Power is the third. Three
predicates, three questions, separately readable.

⚑ A1.3 amends the issue's own direction text

Issue #12 says "health/preflight reads pmset -g batt". Preflight is the wrong home. It runs
in the caller's environment, not the daemon's — and open #19 is the standing proof (status
reports sandbox: present while every live run booted with it off). Confirmed live this session:
palace status reported sandbox: podman present from my shell, which says nothing about what the
daemon is under. Refusal must bind where dispatch happens. Preflight may display power state; it
may never enforce it.

Enforcement tier, stated honestly

Tier 5 with a tier-4 test — deliberately identical to what §2.7 claims for the memory ceiling
itself. Power is a sampled reading of the physical world; no value can be made to not inhabit "the
battery is low", so tier 1 is unreachable, and claiming it would be exactly the overclaim §0's
ladder names as the foot-gun.

The honest limit, recorded rather than hidden (A1.4): a dispatch-time refusal cannot stop a
job already running.
Jul 24's code_backfill was already in flight when the throttle hit — so
this design would not have prevented that one outright. In-flight energy bounding needs the
job-timeout machinery (finding-0178) and is explicitly not designed here.

What the builder must not do (tripwires, not omissions)

  • scheduler/presence.py is out of write scope — the template is read, never edited. The
    foreground gate keeps its meaning.
  • tests/integrity/test_shadow_isolation.py is out of write scope — it asserts
    shadow_job.tier in HEAVY_TIERS (:96-107). A red there means the tier set was reshaped
    instead of a predicate added: the wrong approach, and a stop-and-raise rather than a test edit.
  • A guard nobody calls is not a guard. Items 2 and 3 are deliberately split so the predicate
    lands inert and Item 3's whole content is "and now it is actually consumed". Item 3's acceptance
    is that deleting the new term from :177 must redden a test — the finding-0187 shape this
    must not repeat (deleting bp-105's sweep call left 85/85 green).
  • Fail closed. assume_discharging_when_unknown — an unreadable battery is treated as
    discharging. None is a reachable, ordinary state here (absent tool, timeout, OSError), not a
    theoretical one, which is what makes this the plan's most important test.

How it was verified

Docs-only diff — zero Python touched. Per the pr skill this is a read-through plus structural
validation, not a code gate; I did not run pytest (and per #22 the local gate is unrunnable on a
main checkout). CI runs the verdicts.

What I did run: every path:line re-opened at HEAD 174d06c; the acceptance-reachability check
(every §7 criterion's files appear in §5); structural validation (sections 0–12 complete, five
items, write_scope entries bare globs); and the enrollment checks — mypy is directory-scoped so
scheduler/power.py needs no entry, and subprocess is already established in scheduler/ by
presence.py:14-33, so there is no import-firewall question.

§8 is marked N/A with a reason: a threshold comparison and a set union are not mathematical
objects, and inventing a field-guide entry would be the formalism §8 exists to refuse.

Sequencing note

parallelizable_with: [bp-151, bp-152, bp-153] — disjoint write scopes (scheduler/** vs
core/** + ops/**), different track. But the coupling runs one way and is worth stating:
this plan protects bp-153. That rebuild is a long, sliced, resumable job whose D7 slicing exists
because the lane wedged once already — and the Jul 24 wedge was caused by a battery drain.
Landing bp-154 before bp-153 Item 3 runs is cheap insurance.

🤖 Generated with Claude Code

https://claude.ai/code/session_011LZZQPyGsoeGL73cbbEp3U

ascalva added 2 commits August 5, 2026 22:50
…r learns to see the battery

Warrant: issue #12 (type:direction, route:orchestrator), migrated from frozen
finding-0279. THREE measured emergencies, not the two its title names — Jul 24
(drained to 1%; the embedder starved under critical-battery throttle, code_backfill
timed out, code_sync WEDGED, the daemon died unwitnessed and stayed dead three days),
Jul 28 (100%→8% in 2h40m, caught at the wire under KeepAlive), and Aug 1 (fatal —
the machine died mid-run; run #39 came up in recovery).

Battery hardware is healthy and stays healthy: re-measured 2026-08-05 at Condition
Normal, 95% maximum capacity, 128 cycles. The drain is load, not degradation. The
scheduler is the defect.

§2.7 gave the supervisor exactly one resource axis to refuse on — memory (NN-8,
"the scheduler refuses breaching work"). That principle is not memory-specific; it
was simply never given a second axis. A1 grows the same dispatch-time refusal a
POWER axis: remaining energy is a schedulable resource, and work that would spend
the last of it is breaching work.

This amends rather than founding a new note because it adds a DIMENSION to machinery
this note already decided, not a subsystem. Grounded on the code, not asserted:

  A1.1  The shed mechanism already exists — HEAVY_TIERS (supervisor.py:59) shed by
        blocked_tiers() on presence.foreground_active() (:130-135), composed at THE
        ONE claim site (:177, pinned at :153). This adds a PREDICATE, not machinery.
        Load-bearing pin: power_blocked_tiers() is its OWN predicate, never folded
        into blocked_tiers() — quoting the code's own reasoning (:131-134), "two
        different reasons to refuse a tier, conflated into one predicate, is how a
        reader later cannot tell which rule refused a job."

  A1.2  The sensor mirrors Presence (presence.py:46-59) including its fail-closed
        default: assume_discharging_when_unknown. An unreadable battery is treated
        as discharging — a sensor that fails open re-creates the failure exactly
        when the system is least healthy.

  A1.3  AMENDS issue #12's own direction text. It says "health/preflight reads
        pmset"; preflight is the WRONG HOME — it runs in the caller's environment,
        not the daemon's, and open issue #19 is the standing proof (status reports
        sandbox present while every live run booted with it off). Refusal binds
        where dispatch happens. Preflight may display power state, never enforce it.

  A1.4  Tier 5 with a tier-4 test, stated as such — deliberately identical to what
        §2.7 claims for the memory ceiling. Power is a sampled reading of the
        physical world; tier 1 is unreachable and claiming it is the overclaim the
        enforcement ladder names as THE foot-gun. Honest limit recorded: dispatch-time
        refusal cannot stop an already-running job, so this would NOT have prevented
        Jul 24 outright — in-flight bounding is finding-0178's, not designed here.

  A1.5  The /tmp sampler is gone — cleared by the Aug 1 reboot. The tool built to
        diagnose the battery problem was destroyed by the battery problem, which is
        the argument for promotion in its sharpest form. Constrained by §1.2's
        anti-goal: ONE sensor feeding ONE predicate, not a family of ad-hoc probes.

A1.6 carries the non-goals (no in-flight bounding, no thermal/CPU axis, no embedder
residency — dn-local-model-runtime's boundary is untouched, no policy engine, no
preflight enforcement), inferred ones marked [INFERENCE]. A1.7 carries four
falsifiers, per §2.10 — the owner ratifies falsifiers, not proofs.

Agent-drafted; lands only by the owner's merge. Frontmatter untouched.
…status proposed

One plan, five items, 200k opus. Graduated from Amendment A1 in the preceding
commit, so this plan's design_ref becomes ratified by the same merge that readies
the plan — the two gates collapse into one, per the merge-gated regime.

Grounded pass against HEAD 174d06c. The design turned out much smaller than issue
#12's direction text implied, because the machinery already exists:

  Q1  A shed mechanism is already there — HEAVY_TIERS (supervisor.py:59), shed by
      blocked_tiers() on presence.foreground_active() (:130-135). This adds a
      predicate, not machinery.
  Q2  The ONE enforcement point is supervisor.py:177 — blocked_tiers() |
      model_blocked_tiers(), pinned at :153 ("Enforced at the ONE claim site …
      no new queue"). The power predicate joins that union; nothing else changes.
  Q3  The power rule may NOT live inside blocked_tiers() — the code says so in its
      own words (:131-134) and model_blocked_tiers() (:137) is the precedent for a
      sibling predicate. This is the plan's one hard constraint.
  Q4  presence.py:46-59 is the template: injectable probe, threshold, and the
      fail-closed default assume_present_when_unknown=True. Injectability is what
      makes the gate testable with no hardware.
  Q5  Preflight is not a valid home (scripts/palace.py, the caller's process) —
      issue #19 is the standing proof. Confirmed live: palace status reported
      "sandbox: podman present" from my shell, which says nothing about the daemon.
  Q6  Battery hardware healthy — Condition Normal, 95% capacity, 128 cycles
      (2026-08-05). The drain is load, not degradation.
  Q7  The /tmp sampler no longer exists to promote; the builder writes a sensor
      from the Presence template rather than porting a script.

Item ordering IS the blast-radius order: sensor (pure) → predicate (computes a set
nobody consumes) → compose (first behavior change) → the floor (the only write) →
surface repair. Items 2 and 3 are deliberately separate so the predicate can land
and be reviewed as inert, and Item 3's whole content is "and now it is actually
consumed" — the finding-0187 failure this must not repeat (deleting bp-105's sweep
call left 85/85 green). Item 3's acceptance is therefore that DELETING the new term
from the union must redden a test.

Two out-of-scope files are tripwires rather than omissions: presence.py (the
template is read, never edited — the foreground gate keeps its meaning) and
tests/integrity/test_shadow_isolation.py (asserts shadow_job.tier in HEAVY_TIERS;
a red there means the tier set was reshaped instead of a predicate added — the
wrong approach, and a stop-and-raise rather than a test edit).

Acceptance-reachability checked: every §7 criterion's files appear in §5; mypy is
directory-scoped so scheduler/power.py needs no enrollment; subprocess is already
established in scheduler/ by presence.py, so no import-firewall question.

§8 marked N/A — a threshold comparison and a set union are not mathematical objects,
and inventing a field-guide entry would be the formalism §8 exists to refuse.

Parallelizable with bp-151..153 (disjoint scope, different track). Noted in §12:
this plan PROTECTS bp-153 — that rebuild is a long sliced job routing around a wedge
the Jul 24 battery drain caused.
@ascalva
ascalva merged commit 68d8d39 into main Aug 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant