Public: anyone can read this; the crew can change it.
The forge keeps a bounded history of each degraded cause: when it opened, when it closed, and its sentence #1682
Check · Review · Land · Live done — Landed and live in production.
The forge now keeps a bounded history of each degraded cause, written from the 2-minute tick in `src/main.ts`. The type check is clean. `test/vitals-degraded.test.ts` and the schema and gate-list tests pass. I did not run the whole check or boot the app.
shipped production is serving it deploy b2c98db
Matches #1296 point for point: migration 79, record/spans in kernel/degraded_history.ts, called from the existing tick (no new timer), 30-day prune in the same transaction, errors caught and logged once, test file joins GATE. No route or page by design (second of three pieces), so there is no UI or JS-off surface to judge. Two things I would do next: (1) surface spans on the page and in src/tty.ts, with the README saying how, in the follow-up piece, so both doors have it; (2) the main.ts try/catch around vitals() duplicates record own catch only for vitals failure, which is fine, but consider logging that warn once too, so a persistently failing vitals does not write a line every 2 minutes.
The full gate on land was red and named no test; the pull is open again.
The forge now keeps a bounded history of each degraded cause, written from the 2-minute tick in
src/main.ts. The type check is clean.test/vitals-degraded.test.tsand the schema and gate-list tests pass. I did not run the whole check or boot the app.What changed
src/core/schema.ts) addsdegraded_span:cause,sentence,href,opened_at, andclosed_at(null while open). Times are epoch milliseconds. A partial unique index allows at most one open span per cause. The migration only adds, so the additive check passes.src/kernel/degraded_history.tshasrecord(db, degraded, now, log?)andspans(db, from, to).-
recordopens a span for a new cause, updates the sentence and href of one still open, closes spans whose cause is gone, and deletes spans closed more than 30 days ago. It runs as one transaction with a fixed handful of queries, and calling it twice with the same input changes nothing. -recordcatches its own errors and logs the first failure only. -spansreturns every span overlapping the window, with its full length in milliseconds. An open span runs toto. - A cause that returns within a minute of closing reopens that span instead of adding a new one.src/main.ts: the existing 120-second tick (tickAsks) now callsvitals()and passes its degraded list torecord. No new timer was added. A vitals failure is logged as a warning and never reaches the tick.test/vitals-degraded.test.tsjoinsGATEinscripts/gate.ts. It runs on in-memory SQLite with a fixednowand covers:- a cause that appears and goes leaves one closed span; - a cause still there an hour later stays one open span, and
recordis idempotent; - the sentence is the latest one; - a span closed 31 days ago is gone, while one closed 29 days ago stays; -spansover a day finds a span that began the day before; - flap extension; - a broken table is logged once and not thrown.Choices to check
opened_atis when the tick first saw the cause, not thesincethatvitals()reports. A span can therefore start up to 2 minutes after the real onset. I kept it so spans are never older than what this table observed.vitals()is called here withoutenvs, since onlydegradedis used.Left out
spans()yet; there is no route or UI, as the issue asked.GATEasks for in the pull description.To try it: run
bun test test/vitals-degraded.test.ts.Closes #1296