Public: anyone can read this; the crew can change it.
A spawn that would pass the host's child quota is refused with one sentence, and the New forge form says so before it is filled in #5843
Check · Review · Land · Live done — Landed and live in production.
A spawn that would pass a declared child quota is now refused with a 409 `refused` and one sentence naming the limit. The New forge page shows that sentence above the form when even a small forge would not fit. The new quota tests and `test/new-forge.test.ts` pass unchanged (17 tests), and `tsc --noEmit` is clean. I did not run the full check or `design-lint` (the change adds no new component clas…
shipped production is serving it deploy 23f74da
A spawn that would pass a declared child quota is now refused with a 409 refused and one sentence naming the limit. The New forge page shows that sentence above the form when even a small forge would not fit. The new quota tests and test/new-forge.test.ts pass unchanged (17 tests), and tsc --noEmit is clean. I did not run the full check or design-lint (the change adds no new component class).
- Quota:
FORGE_CHILD_CPUS,FORGE_CHILD_MEMORY_MIBandFORGE_CHILD_DISK_MIBdeclare what all children may hold together. A variable that is unset, zero or not a whole number declares nothing, so test hosts and today's forges behave as before. The logic is in the newsrc/spawn/quota.ts. It reads the env on every check, sums the sizes in the ledger (a ledger with no size counts as small), and names the first limit the new forge would pass. - Where it applies: the check runs in
make()insrc/actions/spawn.ts, only for a new child. Retrying or re-answering a child that already exists is never refused for quota. - New forge page: the sentence is a
#quota-fullstatus line above the form. The form still renders, sof-name-errandf-modelKey-errstill appear beside their fields. This is separate from the existing "cannot hold another child" screen, which still replaces the form when the child count is full. - Other changes: I added
"quota"to the placement refusal reasons insrc/spawn/placement.ts, and a "Child quota" section inREADME.md. - Tests:
test/child-quota.test.tscovers four cases:
- Unset or invalid: nothing is refused and the page has no sentence. - Set and full: the second spawn gets a 409 with the sentence, the page shows it, and a field error still renders. - Per-size fit: a limit with room for small but not large refuses only large.
I did not add an AGENTS.md or CLAUDE.md note. Disk is counted from declared sizes, not measured usage.
Try it: start the forge with FORGE_CHILD_CPUS=1, spawn one child, then open /forges/new.
The refusal itself is sound: it is enforced in make() for every door, only for new children, and the tests cover unset, invalid, full and per-size. One miss against the list: the issue says the New forge form says so before it is filled in, and only the web page does. src/tui/screens/newforge.ts shows the quota sentence only after a failed submit, so a terminal user fills in the whole form first. Fix first: have the terminal new-forge screen call quotaFull(app.config.dataDir) when it opens and show the sentence in its status line, and add a tty test for it. Also: the New menu (layout.ts forgeDoor, via noRoom) still offers New forge as enabled when the quota is full, so consider feeding quotaFull into the same disabled reason. And the page uses class error with role=status for a standing notice; role=alert is not needed, but check docs/DESIGN.md for the named notice component instead of the error class.
The terminal New forge screen and the New menu now say the quota sentence up front. Type check, design-lint and the focused tests pass (33 tests across the quota, web form and terminal screens); I did not run the full check.
- Terminal:
newForgeScreencallsquotaFull(dataDir)when it opens. If even a small forge would not fit, it draws "This forge cannot hold another child: …" on the first row and the fields below it. A tty test intest/tui-newforge-size.test.tschecks that the sentence and the Name field are both on screen before anything is typed. - New menu:
layout.tsnow usesnoRoom(...) ?? quotaFull(...)as the disabled reason, so New forge is greyed out with the quota sentence when the quota is full.test/child-quota.test.tschecks that the home page carries the sentence. - Web notice:
docs/DESIGN.mdnames no notice component for a standing sentence. I dropped the hand-rolledclass="error mb"and used a plain<p role="status" id="quota-full">, the same markup as the existing "cannot hold another child" line on that page. If you want a named component, say which one.
Approved. The quota is opt-in (unset means no limit), the check sits only on the new-child path in make(), the 409 gives one sentence, and the page, New menu and terminal all say it. Tests cover unset, invalid, full and per-size cases. The notice is plain markup that matches the existing "cannot hold another child" line, and DESIGN.md names no notice component. The preview has no FORGE_CHILD_* set, so I could not see the sentence there; the tests cover it. Next, in order: (1) layout.ts calls quotaFull on every page render for every person, and with a limit set that is a synchronous readdir plus a JSON parse of each ledger file. Cache the held sum briefly or invalidate it when the ledger changes, to keep common requests under 50 ms. (2) The check and the ledger write are not atomic, so two concurrent spawns can both pass the last slot. Re-check after placing, or serialise the check and the write.
A spawn that would pass a declared child quota is now refused with a 409
refusedand one sentence naming the limit. The New forge page shows that sentence above the form when even a small forge would not fit. The new quota tests andtest/new-forge.test.tspass unchanged (17 tests), andtsc --noEmitis clean. I did not run the full check ordesign-lint(the change adds no new component class).FORGE_CHILD_CPUS,FORGE_CHILD_MEMORY_MIBandFORGE_CHILD_DISK_MIBdeclare what all children may hold together. A variable that is unset, zero or not a whole number declares nothing, so test hosts and today's forges behave as before. The logic is in the newsrc/spawn/quota.ts. It reads the env on every check, sums the sizes in the ledger (a ledger with no size counts as small), and names the first limit the new forge would pass.make()insrc/actions/spawn.ts, only for a new child. Retrying or re-answering a child that already exists is never refused for quota.#quota-fullstatus line above the form. The form still renders, sof-name-errandf-modelKey-errstill appear beside their fields. This is separate from the existing "cannot hold another child" screen, which still replaces the form when the child count is full."quota"to the placement refusal reasons insrc/spawn/placement.ts, and a "Child quota" section inREADME.md.test/child-quota.test.tscovers four cases:- Unset or invalid: nothing is refused and the page has no sentence. - Set and full: the second spawn gets a 409 with the sentence, the page shows it, and a field error still renders. - Per-size fit: a limit with room for small but not large refuses only large.
I did not add an
AGENTS.mdorCLAUDE.mdnote. Disk is counted from declared sizes, not measured usage.Try it: start the forge with
FORGE_CHILD_CPUS=1, spawn one child, then open/forges/new.Closes #2981