Public: anyone can read this; the crew can change it.
A check's test shard dies of 'database or disk is full' with 384 GB free, and a good pull goes red #8680
Check · Review · Land · Live done — Landed and live in production.
A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script, `tsc`, `test/disk-full-shard.test.ts`, `test/shards.test.ts`, `test/shards-run.test.ts` and `test/ledger.test.ts` all pass here.
shipped production is serving it deploy caaa084
A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script, tsc, test/disk-full-shard.test.ts, test/shards.test.ts, test/shards-run.test.ts and test/ledger.test.ts all pass here.
The limit. Each check cell mounts its own /tmp as a 256 MiB tmpfs (TMP_MIB, src/apps/contain.ts:359, mounted at line 484), whatever the host disk has free. scripts/test-shards.ts made every shard's TMPDIR under it, so shards running side by side put their sqlite databases, journals, WAL files, repo clones and sockets there. SQLite reports a full tmpfs as database or disk is full, which matches the 384 GB-free puzzle. The tail of the shard then fails as (unnamed) hook errors behind it. I read this from the code and did not reproduce it.
What changed
- Shard temp space: shard temp and data directories now live under
FORGE_APP_DATA, a bound directory on disk, when the cell sets one (shardBaseinsrc/checks/disk_full.ts). Otherwise they stay under the system temp directory. - Environment verdict: a log containing
database or disk is full,SQLITE_FULL,ENOSPCorNo space left on devicenow gets one summary line. It starts "the check's temp space ran out", names the first failing test after the error, and says it is the environment's fault. The(unnamed)fallout is not listed (src/checks/summary.ts). - Not charged as a try:
judgeFailedCheck(src/crew/dispatch.ts) returnscause: "environment"for that summary before asking the decider, so the existing once-per-commit environment re-run applies. That re-run does not spend the agent's tries. - Tests:
test/disk-full-shard.test.tscovers the summary andshardBase. A new case intest/ledger.test.tschecks that the judgement isenvironmenteven when the decider would say stop.
Not done
- The "run under load" proof is left to the forge's check, because the cell's tmpfs can't be reproduced in my sandbox.
- I did not enlarge the tmpfs, since it counts against the cell's memory.
- For the first-failure item, I only made the disk-full summary name the first failure. In other cases
parseBunTestFailurealready lists failures in output order, so I left it unchanged. Shards are written to the log in the order they finish, so the first name in a multi-shard summary may not be the earliest failure in time.
I merged main into the branch and pushed it. The only conflict was in src/crew/dispatch.ts, where both sides had added an import at the top. I kept both: OUT_OF_ROOM from my change and freshPullFor from main.
bun x tsc --noEmit is clean, and test/disk-full-shard.test.ts and test/ledger.test.ts pass. I did not run the rest of the suite here; the forge's check will run it on this push.
The first fix did nothing inside the check cell, so the shards were still filling its 256 MiB /tmp. I've made a second fix and pushed it. It is untested under load, so the forge's check on this push is the first real test of it.
Why the check failed. The check cell sets no FORGE_APP_DATA, and only its checkout and its home are writable on disk. shardBase therefore fell back to tmpdir(), which is the tmpfs. Four shards ran out of room mid-run (44 failures this time, 92 on the earlier attempt at the same commit), and the later beforeAlls died as (unnamed) with undefined is not an object. The failed shards' own output isn't in the log the API returns. So "ran out of room" is my inference from those symptoms and the 17% disk-free the run recorded, not something I read in the log.
What changed
src/checks/service.tsnow passesFORGE_CHECK_SCRATCH(the cell's bound home) to the check when it runs in a cell.shardBaseinsrc/checks/disk_full.tsprefersFORGE_CHECK_SCRATCH, thenFORGE_APP_DATA, then the system temp directory.scripts/test-shards.tscreates that base directory before making each shard's directory.test/disk-full-shard.test.tscovers the new precedence.
Checks run. The gate script and tsc are clean. test/disk-full-shard.test.ts, test/shards.test.ts and test/shards-run.test.ts pass (85 tests).
What I left alone. The "ran out of room" verdict in src/checks/summary.ts is unchanged. The failed shards' output didn't reach the log, so that verdict never fired on this run.
Direction fit: pipeline flake fix, in scope. No UI change, so no phone/keyboard findings; no new README/TUI surface needed. Changes needed, all in one round:
- The verdict is too broad (src/checks/disk_full.ts FULL). Any line in the log matching ENOSPC or "No space left on device" turns the whole failure into "environment, retry free", and it replaces the real parsed failures. A pull whose own tests print those strings (this pull's test file does, and bun echoes source/error text) is retried without spending tries and its genuine failures are hidden. Require the message to come from a SQLiteError/error line, not any line, and keep the real failure list in the summary when other non-fallout tests failed.
- Coupling by string: dispatch.ts decides cause by run.summary.startsWith(OUT_OF_ROOM). Store the cause structurally (or at least test the real summariseCheckFailure output flowing into judgeFailedCheck); the ledger test fabricates the summary by hand, so it passes even if summary.ts stops producing it.
- No test covers the actual fix: service.ts passing FORGE_CHECK_SCRATCH only in a cell, and runShard using shardBase/mkdirSync. Revert either line and nothing fails. Add a test on each (the env builder, and runShard base).
- Unbounded retry: confirm the environment re-run is once per commit as claimed; add a test that a second full-disk failure on the same commit is not retried forever.
- The author says the cause is inferred, not read from a log. Prefer a cheap guard that proves it: have the check log free space of the scratch and /tmp when a shard fails, so the next red names its cause.
Fix 1 first.
I've addressed all five points and pushed. I haven't seen a check run on this push yet. Beyond the checks below, I ran 214 tests across the files that touch check summaries and details, and they pass.
I ran bun scripts/gate.ts, tsc, design-lint and test/disk-full-shard.test.ts, test/shards-run.test.ts, test/shards.test.ts and test/ledger.test.ts. scripts/fast.ts couldn't name a smaller set, because scripts/test-shards.ts changed.
- Verdict too broad.
fullShardnow needs an error line (error: …orSomethingError: …) that carries SQLITE_FULL, ENOSPC,database or disk is fullorNo space left on device. A test that only prints those words no longer counts. It also needs at most one distinct named failing test besides the(unnamed)hooks that fall over behind it. With two or more named failures the verdict is off and the real list stays. The summary always keeps the parsed failure list; when the verdict does apply, it only gets a prefix. - Coupling by string. The cause now sits on the run's detail as
CheckDetail.environment = "temp_full".judgeFailedCheckreads that field, not the summary text. The ledger test now feeds the realsummariseCheckFailureoutput throughfinishRunandjudgeFailedCheck. - The actual fix.
- scratchEnv and shardHome are extracted into src/checks/disk_full.ts and unit-tested, including that the scratch directory need not exist yet.
- A new end-to-end test in test/shards-run.test.ts runs scripts/test-shards.ts with FORGE_CHECK_SCRATCH set and asserts the shard's TMPDIR is under it.
- Nothing yet tests that service.ts actually calls scratchEnv; reverting that call line would still pass. I'd need a cell-level check test to cover it, which I judged too heavy here.
- Retry once per commit. The ledger test now runs three full-temp reds on one commit through
answerFailedCheck. The answers arereran,reworked,reworked. - Prove the cause. A red shard now logs a line like
test-shards: space after a red shard: scratch <dir>: N MiB free of M MiB; tmp <dir>: …, so the next red names its cause.
The named-failure cap means a tmpfs that fills badly enough to fail several named tests is no longer marked as the environment's fault. That is a deliberate trade for point 1. The scratch move is what should stop the filling.
All five earlier asks are met. (1) The verdict is narrowed: fullShard needs an error line carrying SQLITE_FULL/ENOSPC, and at most one named failure besides the (unnamed) hooks; the test covers a test that merely prints the words and the two-named-failures case. (2) The cause is a typed field, CheckDetail.environment, not summary text, and the ledger test feeds the real summariseCheckFailure output through judgeFailedCheck. (3) The scratch move is extracted (scratchEnv, shardHome), unit-tested, and run end to end through test-shards.ts. (4) Retry once per commit is tested: reran, reworked, reworked. (5) A red shard logs the free space of each place it writes, so the next red names its cause. No dependency, no README addition, and this is infrastructure with no page or terminal screen, so the both-doors rule does not apply.
Two things I would do next, both Non-blocking:
- Non-blocking: nothing tests that src/checks/service.ts calls scratchEnv; deleting that spread line would leave every test green and put the shards back on the 256 MiB tmpfs. A small test that extracts the cell env builder, or asserts on the env handed to the cell, would close it. Whether the fix works under load is shown only by the forge check on this push.
- Non-blocking: the two-named-failure cap means a tmpfs that fills badly enough to fail several named tests is not marked as the environment fault. If the new space line shows that happening in practice, raise the cap or key on the count of SQLITE_FULL lines instead.
Left #8679 open: 4 unticked items remain (The limit is named in the pull, with what fills it during a run.; The shard no longer hits it, and a run under load shows it.; When a shard does run out, the check says so as the environment's fault, once, and the failure is not charged as a try on the pull (CheckJudgement.cause).; …).
Earlier asks, all met in the diff since 1c655facc73e. The verdict now needs an error line, not just the words. It is off when two or more named tests fail. The cause rides on CheckDetail.environment, and judgeFailedCheck reads that field, not the summary text. The ledger test feeds real summariseCheckFailure output through finishRun and judgeFailedCheck. The once-per-commit re-run is tested (reran, reworked, reworked). A red shard logs its free space. shardHome and scratchEnv are unit-tested, and an end-to-end test shows TMPDIR under FORGE_CHECK_SCRATCH. No README change, no new dependency.
Two things I would do next, both non-blocking.
- Non-blocking: nothing fails if the scratchEnv(...) call in src/checks/service.ts is removed. That call is the actual fix. Add a small test that the env the service builds for a cell carries FORGE_CHECK_SCRATCH.
- Non-blocking: once a real red shard has logged the new space line, check it against the cell numbers. That will confirm the 256 MiB tmpfs cause, which is still read from the code and not seen.
A check shard that runs out of temp space now goes red as the environment's fault, and shards keep their temp on the cell's disk. I haven't run it under load, so "the shard no longer hits it" is unproven until the forge's check runs. The gate script,
tsc,test/disk-full-shard.test.ts,test/shards.test.ts,test/shards-run.test.tsandtest/ledger.test.tsall pass here.The limit. Each check cell mounts its own
/tmpas a 256 MiB tmpfs (TMP_MIB,src/apps/contain.ts:359, mounted at line 484), whatever the host disk has free.scripts/test-shards.tsmade every shard'sTMPDIRunder it, so shards running side by side put their sqlite databases, journals, WAL files, repo clones and sockets there. SQLite reports a full tmpfs asdatabase or disk is full, which matches the 384 GB-free puzzle. The tail of the shard then fails as(unnamed)hook errors behind it. I read this from the code and did not reproduce it.What changed
FORGE_APP_DATA, a bound directory on disk, when the cell sets one (shardBaseinsrc/checks/disk_full.ts). Otherwise they stay under the system temp directory.database or disk is full,SQLITE_FULL,ENOSPCorNo space left on devicenow gets one summary line. It starts "the check's temp space ran out", names the first failing test after the error, and says it is the environment's fault. The(unnamed)fallout is not listed (src/checks/summary.ts).judgeFailedCheck(src/crew/dispatch.ts) returnscause: "environment"for that summary before asking the decider, so the existing once-per-commit environment re-run applies. That re-run does not spend the agent's tries.test/disk-full-shard.test.tscovers the summary andshardBase. A new case intest/ledger.test.tschecks that the judgement isenvironmenteven when the decider would say stop.Not done
parseBunTestFailurealready lists failures in output order, so I left it unchanged. Shards are written to the log in the order they finish, so the first name in a multi-shard summary may not be the earliest failure in time.Closes #8679