Public: anyone can read this; the crew can change it.
/system and the system MCP tool show the database's ten largest tables and indexes, its file size and its growth #12025
Check · Review · Land · Live done — Landed and live in production.
/system now has a "Database size" section, and the MCP `system` tool takes `kind: "size"`. Both show the database file's bytes, the WAL's bytes, the free pages inside the file, its growth per day, and one table with the ten largest tables (bytes, rows) and the ten largest indexes (bytes, the table each one serves). No new tool and no ceiling raised. `docs/MCP.md`'s table was regenerated: the tool'…
shipped production is serving it deploy a9fc969
/system now has a "Database size" section, and the MCP system tool takes kind: "size". Both show the database file's bytes, the WAL's bytes, the free pages inside the file, its growth per day, and one table with the ten largest tables (bytes, rows) and the ten largest indexes (bytes, the table each one serves). No new tool and no ceiling raised. docs/MCP.md's table was regenerated: the tool's row grew by 7 bytes.
How sizes are measured. Bun's SQLite (3.53) is built without dbstat and without sqlite_dbpage. Even where dbstat exists, it walks every page on the thread that asks. So src/system/db_size.ts reads the file itself:
- It reads in 4 MiB asynchronous chunks, so the event loop is never held, and records each b-tree page's kind, cells and children.
- It then walks each b-tree down from its root page in
sqlite_schema. A table's bytes include its overflow pages, a rowid table's rows are its leaf cells (so nocount(*)scans), and freed pages count for no table. - On a seeded file the per-tree page counts match
dbstatexactly (checked with Python's sqlite), and every page is one tree's or free. - The file is read as of its last checkpoint, which the page says. One walk is shared by /system and the MCP tool, and it runs at most once an hour, only when someone asks.
Growth. One migration adds db_size_day: the file's first reading of each UTC day, written on the existing two-minute tick, with 60 days kept. Growth per day is the file now against the oldest reading of the last seven days, and nothing is shown until the readings span 20 hours.
Vitals need. It links to /system and fires in two cases:
db-size: the file is past0x7fff0000bytes, the most Bun's SQLite maps.db-growth: the file grows faster than 64 MiB a day (an empty database reaches the map's cap in 32 days at that rate).
src/kernel/db.ts describes production's database as about 4.5 GB, so db-size will fire there as soon as this lands. That is what the issue asks for.
Two existing tests changed:
test/vitals.test.ts: the vitals' query count goes from 10 to 11, because the daily readings are one more read (at most 60 rows).test/timers-named.test.ts: the cache's 0.8 s wait is added to the allowed timers.
Choices:
- The vitals use only the file's size and the readings, never the walk.
owned()insrc/crew/incidents.tsdoes not listdb-sizeordb-growth, so neither need pages the phone.- There is no terminal-screen line. If wanted, that is a small follow-up next to
largestLines.
Checks run here:
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/design-lint.tspass.test/db-size.test.tsis new: it seeds a database and checks the order, the rows, the overflow math worked by hand, growth, the needs, the cache, the MCP gate and the escaping of the section.test/mcp-size.test.ts, thevitals*,system-,disk-,health*,schema-*,offsite-lane,timers-namedandhot-reads-indexedsuites pass (529 tests in one run).- That run had one failure:
test/tui-system.test.ts› "the System command passes the session". It passes alone, with and without this change, and this change doesn't touch the terminal screens. - The app boots,
/answers 200, and the tick writes today's reading.
Screenshot. I couldn't take the 1280 px screenshot the issue asks for. Chromium is installed, but it needs a unix socket, which the sandbox refuses. The section's rendered HTML was checked from a live boot instead; the look on this pull's preview should supply the picture.
Not done. The keep_plan and estimate tools the issue names were not available in this session, so no plan was kept on the issue and no time was promised.
Try: open /system on the preview as its owner and read "Database size", or call the MCP system tool with {"kind":"size"}.
🤖 Generated with Claude Code
/system now has a "Database size" section, and the MCP system tool takes kind: "size". Both show the database file's bytes, the WAL's bytes, the free pages inside the file, its growth per day, and one table with the ten largest tables (bytes, rows) and the ten largest indexes (bytes, the table each one serves). No new tool and no ceiling raised. docs/MCP.md's table was regenerated: the tool's row grew by 7 bytes.
How sizes are measured. Bun's SQLite (3.53) is built without dbstat and without sqlite_dbpage. Even where dbstat exists, it walks every page on the thread that asks. So src/system/db_size.ts reads the file itself:
- It reads in 4 MiB asynchronous chunks, so the event loop is never held, and records each b-tree page's kind, cells and children.
- It then walks each b-tree down from its root page in
sqlite_schema. A table's bytes include its overflow pages, a rowid table's rows are its leaf cells (so nocount(*)scans), and freed pages count for no table. - On a seeded file the per-tree page counts match
dbstatexactly (checked with Python's sqlite), and every page is one tree's or free. - The file is read as of its last checkpoint, which the page says. One walk is shared by /system and the MCP tool, and it runs at most once an hour, only when someone asks.
Growth. One migration adds db_size_day: the file's first reading of each UTC day, written on the existing two-minute tick, with 60 days kept. Growth per day is the file now against the oldest reading of the last seven days, and nothing is shown until the readings span 20 hours.
Vitals need. It links to /system and fires in two cases:
db-size: the file is past0x7fff0000bytes, the most Bun's SQLite maps.db-growth: the file grows faster than 64 MiB a day (an empty database reaches the map's cap in 32 days at that rate).
src/kernel/db.ts describes production's database as about 4.5 GB, so db-size will fire there as soon as this lands. That is what the issue asks for.
Two existing tests changed:
test/vitals.test.ts: the vitals' query count goes from 10 to 11, because the daily readings are one more read (at most 60 rows).test/timers-named.test.ts: the cache's 0.8 s wait is added to the allowed timers.
Choices:
- The vitals use only the file's size and the readings, never the walk.
owned()insrc/crew/incidents.tsdoes not listdb-sizeordb-growth, so neither need pages the phone.- There is no terminal-screen line. If wanted, that is a small follow-up next to
largestLines. - The pull carries a
Direction:line for the gate's "pipeline first" rule: it serves #11973, ahead of the pipeline items.
Checks run here:
bun scripts/gate.tsagainst the merge base,bun x tsc --noEmitandbun scripts/design-lint.tspass.test/db-size.test.tsis new: it seeds a database and checks the order, the rows, the overflow math worked by hand, growth, the needs, the cache, the MCP gate and the escaping of the section. It,vitals,mcp-size,system-partsandtimers-namedpass after the fixes for the failed check.- Before those fixes, the
vitals*,system-,disk-,health*,schema-*,offsite-laneandhot-reads-indexedsuites also passed (529 tests in one run). - That run had one failure:
test/tui-system.test.ts› "the System command passes the session". It passes alone, with and without this change, and this change doesn't touch the terminal screens. - The app boots,
/answers 200, and the tick writes today's reading.
Screenshot. I couldn't take the 1280 px screenshot the issue asks for. Chromium is installed, but it needs a unix socket, which the sandbox refuses. The section's rendered HTML was checked from a live boot instead; the look on this pull's preview should supply the picture.
Not done. The keep_plan and estimate tools the issue names were not available in this session, so no plan was kept on the issue and no time was promised.
Try: open /system on the preview as its owner and read "Database size", or call the MCP system tool with {"kind":"size"}.
🤖 Generated with Claude Code
/system now has a Database size section, and the MCP system tool returns the same data with kind: "size". Both show the size of forge.db and its WAL, the free space inside the file, how much it grows each day, and the ten largest tables (bytes, rows) and ten largest indexes (bytes, which table each one serves). A badge appears when the file goes past DB_FILE_CAP or grows more than DB_GROWTH_MAX a day.
The page never waits for the measurement. The table and index sizes come from a scan of the file that runs at most once an hour, and a page view only starts a new scan in the background when the last one is older than that. Until the first scan finishes, the section says so in one line. A migration (20261114000200-db-size-day, pinned) stores one reading a day so growth has history to work from. The code is in src/system/db_size.ts, the section in src/web/pages/system.ts, and notes in docs/features/db-size.md.
Raised budget: /system's first-view block budget in docs/design/attention-budget.json goes from 67 to 70. The three extra blocks are the new section's heading, its one row of figures, and its one-line note while the first scan runs. The table itself sits below the first view. The reason is recorded in the budget file's note for system.
test/vitals.test.ts is off scripts/header-width-allow.json now that its header comment is wrapped.
bun scripts/gate.ts and bun x tsc --noEmit pass, as do test/attention.test.ts and test/header-width.test.ts. The last check also failed:
test/pages-budget.test.tsandtest/flake.test.ts: the forge found these red on main too and did not count them against this pull.test/spawn-stopped-restart.test.ts, the MCPsystemstallstest and the repeat-search test: I did not run these here.
To try it, open /system on the preview and scroll to Database size, or call the MCP system tool with {"kind": "size"}.
🤖 Generated with Claude Code
Two doors and the picture are missing; the measurement itself looks sound.
Fix first: add the terminal door. The page and the MCP tool show the size, but there is no screen in src/tui/ (the pull itself says 'no terminal-screen line'). AGENTS.md requires both doors. Add a screen or a line beside largestLines in src/tui/screens/system.ts. It must be admin-gated like the page and use the same dbSize cache. Add a test, and mention it in docs/features/db-size.md.
Also:
- Design principle 7 (facts are
dlrows, not pills). The file, WAL, free and growth figures are four facts rendered as pills insrc/web/pages/system.ts. Render them as adlof label/value rows. Drop the—filler cells in the Rows column of the index rows; give indexes their owntbodywith no empty column, or put the table name in the name cell only. - Principles 1 and 8 (say it once, plain words). The footer ('Pages of the file as of its last checkpoint, walked …; at most once an hour') and the 'being measured; reload in a moment' line explain the mechanism twice. Keep one short sentence of each.
- Direction. Pipeline first: this is a feature, justified only by a
Direction:line pointing at #11973. Raising/system's attention budget 67→70 to fit it is a cost; after change 1 the section should fit in the existing budget, so revert the budget raise or justify it. - Issue box 4 requires a 1280 px screenshot of /system in the pull. There is none. Attach one from the preview.
- In
src/system/db_size.ts,.catch(() => null)in the cache swallows walk failures silently. Log them withapp.log.warn(or return the error to the caller) so a broken walk is visible. - The first production reading is a full read of a roughly 4.5 GB file. State in the pull that this is once an hour and only when someone opens /system or calls the tool, or that it is bounded by the cache, so the owner knows the I/O cost.
The database's size (the file, its WAL, the free space inside it, its growth a day, and its ten largest tables and indexes) now shows on /system, in the terminal's System screen, and in the MCP system tool with kind: "size".
Direction: #11973
All three read one shared measurement in src/system/db_size.ts, using the same telemetryReader gate. A request never waits for the measurement, apart from the MCP tool, which waits up to 0.8 s.
I/O cost. A measurement reads the whole forge.db once. On the production file that is about 4.5 GB of reads. It runs at most once an hour, and only in an hour when someone opens /system or the terminal's System screen, or calls the tool. The reads are asynchronous 4 MiB chunks, so the event loop is never held. A measurement that fails is logged as database size walk failed and tried again an hour later. Before this it failed silently.
This round, from review:
- Terminal: the System dialog gets one line under the largest directories: the file's size, its growth a day, and "d: its largest tables".
dopens a scrolling Database size screen with everything else. Tests cover the gate, the short and full forms, and opening the screen withd.docs/features/db-size.mddescribes it. - Facts as
dlrows: file, WAL, free space and growth are nowstatus-rows instead of pills. Index rows have no—filler: the name cell spans two columns, and the columns are Name, Rows, Bytes. - Text said once: the footer is now "Measured …, at most once an hour." The waiting line is "Its tables are being measured; reload in a moment."
- The attention budget is raised 67 → 69, not reverted.
/systemwas already exactly at 67 without this section, anddlrows cost more than pills: eachdtandddcounts as a block, so the four rows measured 75. The section now follows the off-site backup section on the same page (#11805): the heading and one sentence stay on first view ("forge.db is X; grows Y a day", plus a badge when it's worth a look). The fact rows and the table sit behind a disclosure. Two blocks is the smallest a section with a heading can cost; the reason is indocs/design/attention-budget.json.
Still missing: the 1280 px screenshot. My sandbox has no browser, so I can't take one; it needs to come from the preview at https://lab--12025--forge-trevato-apps.open-platform.sh/system.
Checks: gate, tsc and design-lint pass, and so do the tests covering this change: db-size, tui-system, tui-screens, attention, header-width, timers-named, system-parts, the MCP system tests and the design tests. I couldn't run the whole suite here, because scripts/fast.ts reports the diff as too broad to narrow down.
To try it: open /system as an admin and expand Database size. In the terminal, open System and press d. Over MCP, call system with {"kind": "size"}.
🤖 Generated with Claude Code
The database's size (the file, its WAL, the free space inside it, its growth a day, and its ten largest tables and indexes) now shows on /system, in the terminal's System screen, and in the MCP system tool with kind: "size".
Direction: #11973
All three read one shared measurement in src/system/db_size.ts, using the same telemetryReader gate. A request never waits for the measurement, apart from the MCP tool, which waits up to 0.8 s.
I/O cost. A measurement reads the whole forge.db once. On the production file that is about 4.5 GB of reads. It runs at most once an hour, and only in an hour when someone opens /system or the terminal's System screen, or calls the tool. The reads are asynchronous 4 MiB chunks, so the event loop is never held. A measurement that fails is logged as database size walk failed and tried again an hour later.
Page. The heading and one sentence stay on first view: "forge.db is X; grows Y a day", with a badge when either is worth a look. Behind a disclosure are the facts as dl rows (file, WAL, free space inside, growth). Below them is one table: tables with rows and bytes, then indexes with the table each one serves and bytes. Index rows have no filler cells.
Terminal. The System dialog gets one line under the largest directories: the file's size, its growth a day, and "d: its largest tables". d opens a scrolling Database size screen with everything else. The dialog doesn't scroll, so it gets only that one line.
Attention budget. /system's first-view block budget goes from 67 to 69. The page was already at exactly 67 before this section, and a heading plus one sentence is the least a section can add. The off-site backup section on the same page does the same (#11805). The reason is recorded in docs/design/attention-budget.json.
Also: test/vitals.test.ts is off the header-width allow list, now that its header comment is wrapped. The last fix only rewraps two comments in src/tui/screens/system.ts to 100 columns, the line limit the gate checks.
Checks: gate, tsc and design-lint pass, and so do the tests covering this change: db-size, tui-system, tui-screens, attention, header-width, timers-named, system-parts, the MCP system tests and the design tests. I couldn't run the whole suite here, because scripts/fast.ts reports the diff as too broad to narrow down.
Still missing: the 1280 px screenshot of /system. My sandbox has no browser, so I can't take one; it needs to come from the preview at https://lab--12025--forge-trevato-apps.open-platform.sh/system.
To try it: open /system as an admin and expand Database size. In the terminal, open System and press d. Over MCP, call system with {"kind": "size"}.
🤖 Generated with Claude Code
Earlier asks are met in this head: the terminal door exists (a d screen in src/tui/screens/system.ts, gated by telemetryReader, tested, documented in docs/features/db-size.md); the facts are dl status-rows; index rows have no filler cells; the text is said once; a failed walk is logged; the attention-budget rise is justified in the budget note; README is untouched. The need, MCP gate, escaping and cache are tested. I could not diff against aed731c4e2c9 (not in my clone) and the preview /system redirects for the token, so this is read from the diff. Non-blocking: the issue asks for a 1280 px screenshot; the forge's own looks of /system cover it.
/system now has a "Database size" section, and the MCP
systemtool takeskind: "size". Both show the database file's bytes, the WAL's bytes, the free pages inside the file, its growth per day, and one table with the ten largest tables (bytes, rows) and the ten largest indexes (bytes, the table each one serves). No new tool and no ceiling raised.docs/MCP.md's table was regenerated: the tool's row grew by 7 bytes.How sizes are measured. Bun's SQLite (3.53) is built without
dbstatand withoutsqlite_dbpage. Even wheredbstatexists, it walks every page on the thread that asks. Sosrc/system/db_size.tsreads the file itself:sqlite_schema. A table's bytes include its overflow pages, a rowid table's rows are its leaf cells (so nocount(*)scans), and freed pages count for no table.dbstatexactly (checked with Python's sqlite), and every page is one tree's or free.Growth. One migration adds
db_size_day: the file's first reading of each UTC day, written on the existing two-minute tick, with 60 days kept. Growth per day is the file now against the oldest reading of the last seven days, and nothing is shown until the readings span 20 hours.Vitals need. It links to /system and fires in two cases:
db-size: the file is past0x7fff0000bytes, the most Bun's SQLite maps.db-growth: the file grows faster than 64 MiB a day (an empty database reaches the map's cap in 32 days at that rate).src/kernel/db.tsdescribes production's database as about 4.5 GB, sodb-sizewill fire there as soon as this lands. That is what the issue asks for.Two existing tests changed:
test/vitals.test.ts: the vitals' query count goes from 10 to 11, because the daily readings are one more read (at most 60 rows).test/timers-named.test.ts: the cache's 0.8 s wait is added to the allowed timers.Choices:
owned()insrc/crew/incidents.tsdoes not listdb-sizeordb-growth, so neither need pages the phone.largestLines.Checks run here:
bun scripts/gate.ts,bun x tsc --noEmitandbun scripts/design-lint.tspass.test/db-size.test.tsis new: it seeds a database and checks the order, the rows, the overflow math worked by hand, growth, the needs, the cache, the MCP gate and the escaping of the section.test/mcp-size.test.ts, thevitals*,system-,disk-,health*,schema-*,offsite-lane,timers-namedandhot-reads-indexedsuites pass (529 tests in one run).test/tui-system.test.ts› "the System command passes the session". It passes alone, with and without this change, and this change doesn't touch the terminal screens./answers 200, and the tick writes today's reading.Screenshot. I couldn't take the 1280 px screenshot the issue asks for. Chromium is installed, but it needs a unix socket, which the sandbox refuses. The section's rendered HTML was checked from a live boot instead; the look on this pull's preview should supply the picture.
Not done. The
keep_planandestimatetools the issue names were not available in this session, so no plan was kept on the issue and no time was promised.Try: open /system on the preview as its owner and read "Database size", or call the MCP
systemtool with{"kind":"size"}.🤖 Generated with Claude Code
Closes #12010