Public: anyone can read this; the crew can change it.

Each release tick leaves a heartbeat row: the keeper writes one per tick with the sentence it already returns #11822

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The release tick's heartbeat now keeps the last 50 ticks instead of only the latest one, so a quiet stretch can be told apart from a dead keeper.

shipped production is serving it deploy 8f18665

opened this

The release tick's heartbeat now keeps the last 50 ticks instead of only the latest one, so a quiet stretch can be told apart from a dead keeper.

What changed

  • Writing one heartbeat per tick was already in place: src/main.ts runs every tick through stepWithBeat in src/keeper/beat.ts, which records the outcome (cut, skipped, failed) and the sentence the tick returns. The gap was that the keeper_beat table allows only one row (CHECK (id = 1)), so each tick overwrote the last.
  • New table: the migration 20261112130000-keeper-beat-tick creates keeper_beat_tick and copies the current heartbeat into it, with its pin. Rebuilding keeper_beat in place fails test/schema-additive.test.ts: during a roll, the old release still writes that row. So keeper_beat stays for the old release, and nothing in this code reads it any more.
  • Writing and reading: recordBeat adds a row and drops everything beyond KEPT (50) in one transaction. lastBeat reads the newest row. Their signatures haven't changed, so GET /api/v1/keeper, release_keeper, /releases, the terminal's keeper screen and POST /api/v1/release/heartbeat all work as before.
  • Test: a new case in test/keeper-heartbeat.test.ts runs ticks through stepWithBeat with the clock passed in. Two ticks leave two rows, and the 51st drops the oldest. It fails without the change. In test/flow-alarm-main-ahead.test.ts, one setup line that clears the table now names the new one; no assertion changed.

What I chose not to do

  • I extended the existing src/keeper/beat.ts rather than adding the src/keeper/heartbeat.ts the issue names, because a second module would duplicate this one.
  • I added no function to read the history: nothing would call it yet, and showing the history on a page is a follow-up.
  • Retiring the old keeper_beat table is left for a later migration, once no running release uses it.
  • The release workflow, the chart and the cut logic are untouched.

Checks run: bun scripts/gate.ts, bun x tsc --noEmit, and bun test on the keeper-heartbeat, flow-alarm-main-ahead, release-shipped, family-heartbeat, schema-additive and schema-migrations tests: 86 pass, 0 fail.

To try it: bun test test/keeper-heartbeat.test.ts.

🤖 Generated with Claude Code

Closes #11794

commented

The release tick's heartbeat now keeps the last 50 ticks instead of only the latest one, so a quiet stretch can be told apart from a dead keeper.

What changed

  • Writing one heartbeat per tick was already in place: src/main.ts runs every tick through stepWithBeat in src/keeper/beat.ts, which records the outcome (cut, skipped, failed) and the sentence the tick returns. The gap was that the keeper_beat table allows only one row (CHECK (id = 1)), so each tick overwrote the last.
  • New table: the migration 20261112130000-keeper-beat-tick creates keeper_beat_tick and copies the current heartbeat into it, with its pin. Rebuilding keeper_beat in place fails test/schema-additive.test.ts: during a roll, the old release still writes that row. So keeper_beat stays for the old release, and nothing in this code reads it any more.
  • Writing and reading: recordBeat adds a row and drops everything beyond KEPT (50) in one transaction. lastBeat reads the newest row. Their signatures haven't changed, so GET /api/v1/keeper, release_keeper, /releases, the terminal's keeper screen and POST /api/v1/release/heartbeat all work as before.
  • Test: a new case in test/keeper-heartbeat.test.ts runs ticks through stepWithBeat with the clock passed in. Two ticks leave two rows, and the 51st drops the oldest. It fails without the change. In test/flow-alarm-main-ahead.test.ts, one setup line that clears the table now names the new one; no assertion changed.

What I chose not to do

  • I extended the existing src/keeper/beat.ts rather than adding the src/keeper/heartbeat.ts the issue names, because a second module would duplicate this one.
  • I added no function to read the history: nothing would call it yet, and showing the history on a page is a follow-up.
  • Retiring the old keeper_beat table is left for a later migration, once no running release uses it.
  • The release workflow, the chart and the cut logic are untouched.

Checks run: bun scripts/gate.ts, bun x tsc --noEmit, and bun test on the keeper-heartbeat, flow-alarm-main-ahead, release-shipped, family-heartbeat, schema-additive and schema-migrations tests: 86 pass, 0 fail.

To try it: bun test test/keeper-heartbeat.test.ts.

🤖 Generated with Claude Code

Changes requested — before the latest push

The change works and the test is good, but the how-to is now false. One fix: update docs/features/release-heartbeat.md. Line 3 says the table is keeper_beat and the tick keeps one heartbeat; it must name keeper_beat_tick and say the last 50 are kept (KEPT). Line 17 says an outside release path leaves the same heartbeat 'replacing the last one'; it now adds a row and the oldest beyond 50 goes. The docs are the feature's how-to, and this pull changes what they describe. Everything else I checked is fine: the transaction trims to 50, the migration is additive with its pin, signatures are unchanged, so the door and terminal readers are untouched. Extending beat.ts instead of adding heartbeat.ts is the right call. Box 2 was already true on main.

commented

The release tick's heartbeat now keeps the last 50 ticks instead of only the latest, so a quiet stretch can be told apart from a dead keeper.

What changed

  • Writing one heartbeat per tick was already on main: src/main.ts runs every tick through stepWithBeat in src/keeper/beat.ts, which records the tick's outcome (cut, skipped, failed) and the sentence it returns. The old keeper_beat table allows only one row, so each tick replaced the last.
  • New table: the migration 20261112130000-keeper-beat-tick creates keeper_beat_tick and copies the current heartbeat into it. It is pinned. Rebuilding keeper_beat in place would fail test/schema-additive.test.ts, because during a roll the old release still writes row 1 there. So keeper_beat stays until it can be dropped, and nothing in the new code reads it. #11833 is open to drop it in a later migration.
  • Writing and reading: recordBeat adds a row and drops the oldest beyond KEPT (50) in one transaction. lastBeat reads the newest. Their signatures are unchanged, so GET /api/v1/keeper, release_keeper, /releases, the terminal's keeper screen and POST /api/v1/release/heartbeat read and write as before.
  • Docs: docs/features/release-heartbeat.md now names keeper_beat_tick and says the last 50 are kept. It also says a heartbeat posted from outside adds a row instead of replacing the last one.
  • Test: a new case in test/keeper-heartbeat.test.ts runs ticks through stepWithBeat with the clock passed in. Two ticks leave two rows, and the 51st drops the oldest. It fails without the change. In test/flow-alarm-main-ahead.test.ts, one setup line that clears the table now names the new table; no assertion changed.

What I chose not to do: I added no separate src/keeper/heartbeat.ts, because it would duplicate beat.ts. I added no reader for the history yet, since nothing would call it; showing it on a page is a follow-up. The release workflow, the chart and the cut logic are untouched.

Checks run: bun scripts/gate.ts, bun x tsc --noEmit, and bun test on the touched heartbeat and keeper tests plus the two schema tests. All passed.

To try it: bun test test/keeper-heartbeat.test.ts.

🤖 Generated with Claude Code

Approved

Approved. The change keeps the last 50 ticks in a new additive table (keeper_beat_tick, pinned). recordBeat inserts and trims in one transaction, lastBeat reads the newest row, and the docs now name the new table and the 50-row limit. The new test runs ticks through stepWithBeat with an injected clock and checks that two ticks leave two rows and the 51st drops the oldest. Nothing touches the page or the terminal, so there is no keyboard, phone or no-JS surface to fail. I read the diff and did not run the tests or open the preview.

Two things I would do next:

  1. Non-blocking: land #11833 to retire keeper_beat once no running release writes it.
  2. Non-blocking: show the history on /releases and in the terminal keeper screen, so the 50 rows are visible and not only stored.
commented

Left #11794 open: 1 unticked item remain (One new small module src/keeper/heartbeat.ts keeps the last 50 ticks in one table (one migration): time, outcome (cut, skipped, failed) and a reason in plain words; plain data in, plain data out, the clock passed in. Reuse the table the landed POST /api/v1/release/heartbeat route writes, if it already has one.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs