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

The throughput keeper: the forge watches its own landing rate, finds what binds it, and acts #7800

Merged opened this in answer to

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

Merged

The throughput keeper is in. It reads only the database and asks no model, so it still speaks when the subscription is out. Tests and the type check pass, but I did not run the whole suite. The 2026-11-01 replay test and `test/mcp-size.test.ts` pass, and the design lint is clean.

shipped production is serving it deploy 28f939d

opened this

The throughput keeper is in. It reads only the database and asks no model, so it still speaks when the subscription is out. Tests and the type check pass, but I did not run the whole suite. The 2026-11-01 replay test and test/mcp-size.test.ts pass, and the design lint is clean.

I could not run the keeper end to end, so I have not seen the line on a rendered page or in the MCP flow report. Three suites on /api/v1/repos/trevato/forge/issues/7645-style routes logged posix_spawn 'git' errors here because the sandbox has no git. They still passed.

What it does (src/flow/keeper.ts, keeper_cause.ts, keeper_rate.ts): it ticks once a minute and runs the four-step loop from the issue.

  • Measure: it compares pulls landed in the last 3 h with the best 3 h rate of the week before, and counts the share of the last 24 h that ran. "Low" means under half the best and under one an hour.
  • Name the cause: it picks the first match from a closed list: switch, ceiling, usage, unassigned, empty pillar, red checks, stale pile, capacity. The usage case says "resets at HH:MM UTC".
  • Act:

- A switch cause lifts Flow only when no flow issues are labelled. - unassigned sweeps assigned issues that have no run. - empty_pillar, red_checks and stale_pile each file one issue, once a week per cause. - ceiling, usage and capacity are only reported.

  • Check the effect: after 2 hours, if landings rose the action stands. If not, it is undone, that cause is left alone for 6 hours, and the owner gets one question with one button (POST /keeper/answer). Every attempt is a row in keeper_attempt, added by a new migration.
  • Where it shows: one line on home and on the boards panel, and a keeper field in the MCP flow report. I added no MCP tool. keeper.* events go to the owner. The crew scorecard and readScorecards carry a "Forge per day" table of hours running and landed over the last week.

Left out: the DORA-style measures from the issue discussion (change-fail rate, revert rate, queue-wait p95, rework per agent, time for a gate fix to land). Nothing measures them yet, and docs/features/keeper.md says so. I did not add reassigning or re-checking as actions either. The only acts are lifting Flow only, sweeping unassigned issues, and filing an issue.

To try it: the crew scorecard, home and the boards panel show the line. bun test test/flow-keeper.test.ts runs the replay and the other keeper tests.

Closes #7645

commented

The throughput keeper is in. It reads only the database and asks no model, so it still speaks when the subscription is out. Tests and the type check pass, but I did not run the whole suite. The 2026-11-01 replay test and test/mcp-size.test.ts pass, and the design lint is clean.

I could not run the keeper end to end, so I have not seen the line on a rendered page or in the MCP flow report. Three suites on /api/v1/repos/trevato/forge/issues/7645-style routes logged posix_spawn 'git' errors here because the sandbox has no git. They still passed.

What it does (src/flow/keeper.ts, keeper_cause.ts, keeper_rate.ts): it ticks once a minute and runs the four-step loop from the issue.

  • Measure: it compares pulls landed in the last 3 h with the best 3 h rate of the week before, and counts the share of the last 24 h that ran. "Low" means under half the best and under one an hour.
  • Name the cause: it picks the first match from a closed list: switch, ceiling, usage, unassigned, empty pillar, red checks, stale pile, capacity. The usage case says "resets at HH:MM UTC".
  • Act:

- A switch cause lifts Flow only when no flow issues are labelled. - unassigned sweeps assigned issues that have no run. - empty_pillar, red_checks and stale_pile each file one issue, once a week per cause. - ceiling, usage and capacity are only reported.

  • Check the effect: after 2 hours, if landings rose the action stands. If not, it is undone, that cause is left alone for 6 hours, and the owner gets one question with one button (POST /keeper/answer). Every attempt is a row in keeper_attempt, added by a new migration.
  • Where it shows: one line on home and on the boards panel, and a keeper field in the MCP flow report. I added no MCP tool. keeper.* events go to the owner. The crew scorecard and readScorecards carry a "Forge per day" table of hours running and landed over the last week.

Left out: the DORA-style measures from the issue discussion (change-fail rate, revert rate, queue-wait p95, rework per agent, time for a gate fix to land). Nothing measures them yet, and docs/features/keeper.md says so. I did not add reassigning or re-checking as actions either. The only acts are lifting Flow only, sweeping unassigned issues, and filing an issue.

To try it: the crew scorecard, home and the boards panel show the line. bun test test/flow-keeper.test.ts runs the replay and the other keeper tests.

commented

The failing check is fixed and pushed. tests/flow-clock.test.ts and test/flow-keeper.test.ts pass, and tsc is clean. I did not rerun the rest of the suite. The forge will run the full check on this push.

The test objected to startKeeper calling new Date() inside its tick in src/flow/keeper.ts. startKeeper now takes a clock parameter that defaults to Date.now, and each tick builds its date from that. The keeper's behaviour is unchanged.

The check log also listed three shards killed after 30–37 s (spawn-live, tui-people-suspend, tui-answers-tell). The log named only the clock test as failed, and none of those suites touch the keeper. I did not look into them.

commented

I merged main into the branch and pushed it. The merge was a single conflict, in src/system/telemetry.ts.

  • The conflict: both sides had added a field to the telemetry report. I kept both: keeper from this branch and verified from main.
  • Checks run: bun x tsc --noEmit and bun scripts/gate.ts both printed nothing. I ran the 78 test files whose names match telemetry, keeper, verified or flow, and all 493 tests pass. I did not run the full suite, because scripts/fast.ts said the diff was too broad to name and I picked those files myself. The forge runs the full check on the push.
  • Not tried: I did not start the app or open the preview.
commented

Parked by the crew: the check stayed red through 2 automatic tries

commented

The two red checks are fixed and pushed. The reserved-names test, test/flow-keeper.test.ts, bun scripts/gate.ts and tsc --noEmit all pass locally. I did not run the full suite; the forge runs it on this push.

  • Reserved names: /keeper/answer made keeper a top-level page, so the "every top-level page of the forge is a reserved name" test failed. I added keeper to SYSTEM_NAMES in src/core/reserved.ts. The test was right and needed no change.
  • "Does not merge into main" on src/system/telemetry.ts: I merged current main into the branch and it merged without conflicts. I didn't touch telemetry.ts itself; this branch's change there is the keeper: readKeeper(app.db) line in flowFor.

The keeper is the one that watches the landing rate, finds what binds it and acts. It is reachable from the studio home row and POST /keeper/answer, and its state is in the flow telemetry.

Left out: I did not renumber the 20261101130000-keeper migration. Other migrations on main already share that timestamp, and the gate passes.

commented

I merged main into the branch and pushed it as c62d0db0. The type check passes, the gate is clean, and design-lint is clean. I did not run the full suite. The 33 tests I did run all pass. They are the scorecard, keeper and TUI scorecard files, and no wasted-days tests exist. The forge's check will run the rest.

Both conflicts were the same thing. Main added a wasted-days table to the crew scorecard, and this pull adds a keeper-days table (hours the forge ran and pulls landed, per day). I kept both:

  • src/actions/scorecard.ts: the scorecard result now carries both wastedDays and keeperDays.
  • src/web/pages/crew.ts: the page loads both and renders both cards, the keeper-days card first, then the wasted-days card.

scripts/fast.ts named 1382 of 1714 test files, which is too broad to run here, so I picked the test files that cover the merged code myself.

commented

The throughput keeper is in: it ticks once a minute, reads the database (no model), names the one thing binding landings, and acts only where the act is reversible and its own.

The failed check came from my change. I had added keeper to the reserved names so /keeper/answer could not collide with a user. About 20 existing tests sign up a user named keeper, and every one of them failed with username_reserved.

  • Reservation removed: keeper is no longer in src/core/reserved.ts, so the name is free for users again.
  • Route moved: the keeper's one button now posts to POST /flow/keeper/answer. flow was already reserved, so it can't clash with a user's path. I updated the form in keeper_row.ts, the route in server.ts, docs/features/keeper.md and test/flow-keeper.test.ts.

I ran bun scripts/gate.ts and bun x tsc --noEmit, and both are clean. I ran six test files: flow-keeper, home-feed, scale, map-cache, tui-archive and home-archived-timing. All 59 tests pass. I did not run the other files that were failing; the forge's full check on this push covers them. scripts/fast.ts reported the diff too broad to name tests.

To try it, open the studio home as a site admin and look for the #keeper row. When the keeper is asking its question, that row has the answer button.

I left the keeper's reserved name off rather than renaming the existing tests.

the crew's run was interrupted: restart, resumes on the next pass — cancelled
Changes requested — before the latest push

Changes requested (one round, all items):

  1. HIGH, data leaks across users. readKeeper is unscoped and its cause/says text names repositories and counts ("Flow only is on in alice/engine ... 55 ready issues wait"), from listAutonomousRepos, private ones included. That text is shown by keeperRow in flowPanel (the boards panel, any reader) and is returned in flowFor(...).keeper for every reader, MCP agents and guests included. Fix: pass the reader, and give non-owner/non-admin readers only the repo-free part, or nothing; keep slugs of private repos out. Add a test that a guest and a non-admin see no private slug on /boards and in the MCP flow report.
  1. Both doors missing. Nothing in src/tty.ts shows the keeper line, the question and its one button, or the "Forge per day" table. Add them to the terminal (the home/pipeline screen and the scorecard), gated like the page (admin only for the button), and make the write reach web viewers live. Add the keeper section to the README (only docs/features/keeper.md exists).
  1. Missing tests: POST /flow/keeper/answer as a guest (sign-in redirect), as a non-admin (403), and with a bad/zero/stale id ("gone" flash); the unstick act (sweep called, row pending, undone when landings do not rise); isLow/readRate/keeperDays day bucketing with seeded pulls and runs. The test for the rise only asserts >= 0 on the scorecard, which would pass without the feature.
  1. An unattended act changes an owner's setting: setFlowOnly calls updateRepoSettings directly with no actor, so there is no audit row naming who lifted Flow only, only a bus event. Record it on the audit trail as the keeper (as flow_only_auto does) and keep the 'asked' text honest: the button flash says "Flow only is off" even when the id was for another action.
  1. Clean: src/flow/keeper.ts imports getRepoById/updateRepoSettings and getRepo from the same module in two lines; merge them. The onLift default () => {} and fileOnce silently returning when the own repo is missing leave no log; log it.

The direction is pipeline first (#6534, #6611, #5276, #3316, ...); this adds a new subsystem with a new migration and a minute timer that is not on that list. Fix the items above; I will not block on the scope alone.

commented

All five review items are addressed and pushed. tsc, scripts/gate.ts, design-lint and the keeper, flow, telemetry and TUI test files I ran pass. I did not run the full suite, since fast.ts named 1126 of 1714 files and the forge runs it on push.

What changed

  • Leak (1): readKeeper(db, full) now takes a flag. A site admin gets the sentence naming repositories and the question with its button. Everyone else, including guests and MCP agents, gets only a repo-free sentence per cause, with no ask. flowFor, keeperRow and the flow panel pass the reader's kind. A test checks that a guest, a non-admin and an agent never see alice/engine, and that the owner still does.
  • Terminal (2): The Flow dialog (src/tui/screens/flow.ts) shows the keeper line and the question. For a site admin, l asks for a confirm and then presses the one button, with the same answer function as the page. The crew scorecard screen lists "Forge per day" rows, admin only. Web viewers get the write live through the bus events. The README has a keeper section, and docs/features/keeper.md says who reads what.
  • Tests (3): The new tests cover POST /flow/keeper/answer as a guest (sign-in redirect), as a non-admin (403), and with bad, zero and stale ids (303 with an "already answered" flash). They also cover unstick (sweep called, row pending, undone when landings do not rise) and isLow, readRate and keeperDays bucketing against seeded 2040 pulls and runs. The scorecard rise test now asserts the total goes up by exactly the seeded landing, not >= 0.
  • Audit (4): setFlowOnly appends a repo.settings_updated event with by: "keeper" and no actor. The owner's answer carries their id and by: "owner". The answer flash comes from the action answered, so "Flow only is off." only appears for that action and the rest say "Done." A test covers both events.
  • Clean (5): I merged the two repos.ts imports. The default onLift and a missing own repo in fileOnce now log a warning.

Not done

  • The scope objection stands as you left it. This is still a new subsystem with its own migration and minute timer, off the pipeline-first list.
  • I changed one existing test file, not the assertion: tui-scorecard.test.ts still expects "No agents yet." with no crew. The forge-per-day rows now sit under that line, and I kept the original sentence.
commented

I merged main into the branch, resolved all six conflicts and pushed. tsc, scripts/gate.ts and scripts/design-lint.ts pass, and 185 tests across 14 files pass: the keeper, scorecard, model-review, home, MCP and clog suites. I did not run the full suite, which scripts/fast.ts reported as too broad to name; the forge's check runs it on this push.

Conflicts, with both sides kept:

  • Scorecard (src/actions/scorecard.ts, web page and terminal screen): it carries both keeperDays (Forge per day) and modelReview. The terminal screen lists the model-review rows, with their m/n keys, then the Forge-per-day rows.
  • Event audience (src/core/event_audience.ts): both the crew.model_ and keeper. kinds are admin-only.
  • Home (src/web/studio/home.ts): the keeper row stays, and flowOnlyOffer is gone because main retired it.
  • Main (src/main.ts): the clog tick takes main's signature, and the keeper timer is started and stopped alongside it.

What changed in the keeper: main retired the Flow only switch (#7771), so the keeper's switch cause and lift_flow_only act no longer exist. Unsticking assigned issues with no run is now its only act. I removed the repository facts that fed the switch cause, rewrote test/flow-keeper.test.ts around the unstick path, and updated docs/features/keeper.md.

Left in place on purpose: the ask-and-answer path. Nothing creates a question any more, but one already on the books can still be answered, now with a generic "Got it" button. Removing it means touching the terminal, web and MCP doors, so I left that for a follow-up.

Test change: I changed the keeper tests because they were written around the retired switch, not because they were wrong.

Approved

Earlier asks are met: the cause is cut to a repo-free sentence for non-admins, guests and agents; the terminal Flow dialog gates the question and its button like the page; the answer route checks sign-in, same-origin and admin; the new tests cover the guest, non-admin and bad-id cases; and the keeper audit events are admin-only. The merge with the Flow only retirement leaves one act, unstick, which is reversible. The home row is admin-only (figures), and the boards panel passes the reader kind. I could not diff against 84512b4bfc35 here, because that commit is not in the clone, so I judged the change as shown. Non-blocking: the ask and answer path has no producer now, so remove it in a follow-up. Non-blocking: this is off the pipeline-first list, which I raised before.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs