Public: anyone can read this; the crew can change it.
Hosted forges per person are limited by count and storage, each refused in plain words #9602
Check · Review · Land · Live done — Landed and live in production.
A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence. `tsc`, `scripts/gate.ts` and the tests that cover the change pass (the new file, `forge-sleep`, `mcp-size`, `limits`, `mcp-limits`, `tui-limits`, `spawn-api`); I did not run the full suite. I did not call `keep_pla…
shipped production is serving it deploy ef7edf1
A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence. tsc, scripts/gate.ts and the tests that cover the change pass (the new file, forge-sleep, mcp-size, limits, mcp-limits, tui-limits, spawn-api); I did not run the full suite. I did not call keep_plan; I didn't have that tool.
What changed
- Admin limits:
/admin/limitsnow listsforges.per_person(default 3, from 1 to 1000) andforges.storage_mb(default 1000 MB). They show up on the web page, the API and the MCP limits list, and a change applies to the next request. - Count limit moved: the count used to come from the
FORGE_FORGES_PER_PERSONenvironment variable. That variable is gone, so any host that set it falls back to the default of 3 until an admin sets the limit. - Refusals: the web form,
POST /api/v1/forgesand the MCPspawntool all go throughspawnForge, so one check insrc/spawn/sleep.ts(personRefusal) covers all three. The API answers 409, and the web form answers 409 with the sentence on the page. Examples:
- "you already have 3 forges, the most one person may have (3); remove one first" - "your forges already hold 4 MB, the most one person's forges may hold (1000 MB); remove one first"
- Storage use: the number is the files on disk under the person's forges' data directories.
- Tests: the new
test/spawn-person-limits.test.tschecks the defaults. It checks each refusal on web, API and MCP, that a site admin is not limited, and that raising the limit makes room again.test/forge-sleep.test.tsnow uses the admin limit instead of the removed variable. - MCP: no new tool and no change to the MCP size ceilings.
- Docs:
docs/features/forge-sleep.mdnow names the two admin limits.
What I left out
- Site admins stay exempt from both limits, as the old count limit already did.
- Storage is checked only when a forge is made. A forge that grows past the limit afterwards is not stopped, and the next new forge is the one refused.
- A real child forge is a few MB on its own, because of its database and repositories. Keep that in mind when setting
forges.storage_mb(its minimum is 1 MB).
To try it
Sign in as a non-admin, set forges.per_person to 1 at /admin/limits, make one forge, then try a second at /forges/new. bun test test/spawn-person-limits.test.ts runs the same flow on all three surfaces.
🤖 Generated with Claude Code
The check failed only because no commit message had a "Try:" line. I added an empty commit with one and pushed it. bun scripts/gate.ts now passes locally, and I haven't seen the forge's check re-run yet.
The code is unchanged from the first push:
- Limits:
forges.per_person(default 3) andforges.storage_mb(default 1000 MB) are admin settings at/admin/limits. - Refusals: web, API and MCP all refuse a new forge past either limit with one sentence naming the limit and the person's use. Site admins are not limited.
- Tests:
test/spawn-person-limits.test.tscovers each refusal on each surface.
To try it on the preview: sign in as a non-admin and set forges.per_person to 1 at /admin/limits. Make one forge, then open /forges/new and make a second to see the refusal.
🤖 Generated with Claude Code
The failing test was test/settings-visible.test.ts, and I fixed it and pushed. I haven't seen the forge's check re-run on this push.
The test looks for each limit's label in the rendered /admin/limits page. My storage label, "Storage one person's hosted forges may hold together", has an apostrophe, and the page escapes it to ', so the label wasn't found. I renamed it to "Storage a person can hold in hosted forges". The limit itself is unchanged.
test/settings-visible.test.ts, test/spawn-person-limits.test.ts, test/limits.test.ts and test/mcp-limits.test.ts pass locally, and tsc is clean.
The change adds two per-person limits, set in admin limits. A new hosted forge is refused through the web, API and MCP with one plain sentence once a person is past either limit.
- Count:
forges.per_person, default 3. - Storage:
forges.storage_mb, default 1000 MB. - Admins: site admins are not limited.
- Storage check: it runs only when a forge is made, so a forge that grows later isn't stopped.
To try it on the preview: open /admin/limits and set the hosted-forges count to 1. Then, signed in as a non-admin, make one forge at /forges/new and try a second to see the refusal.
🤖 Generated with Claude Code
The limits work and the refusals are plain on web, API and MCP, but two things need fixing in one round.
- test/forge-sleep.test.ts: "a person's forge limit holds through spawnForge" no longer sets any limit. It only passes because the earlier test set forges.per_person=1 and never reset it, since the reset sits in the later test's finally. Set the limit to 1 inside that test (try/finally), and reset the earlier one too, so each test stands alone and survives reordering or --only.
- The refusal grammar reads "you already have 1 forges". Say "1 forge" when the count is 1. Update the exact-string assertion in test/spawn-person-limits.test.ts to match.
Smaller points:
- heldByPerson walks and stats every file of every child forge synchronously on the spawn request path. Fine for now, but say so in the pull, or compute it only after the count check passes (it already is).
- docs/features/forge-sleep.md: "the admin limits" should start with a capital after the full stop. Also name where they are set (/admin/limits, and the same list in the terminal limits screen).
- The sleep.ts header comment got a long run-on line; reflow it.
Ticks and lefts are not given because this is not an approval.
I addressed both requested changes and the smaller points, and pushed. tsc, scripts/gate.ts and the two test files (test/forge-sleep.test.ts, test/spawn-person-limits.test.ts) pass locally. I haven't seen the forge's check re-run on this push.
- Tests stand alone: "a person's forge limit holds through spawnForge" now sets
forges.per_personto 1 itself. The other limit test intest/forge-sleep.test.tsalso resets it in afinally. Neither depends on the other's leftover state any more. - Grammar: the refusal now says "you already have 1 forge" and "3 forges" for larger counts. The exact-string assertion in
test/spawn-person-limits.test.tsmatches. - Storage walk:
heldByPersonwalks and stats every file of the person's forges synchronously on the spawn request. It only runs after the count check passes, and spawning is rare. That is now stated in thesrc/spawn/sleep.tsheader comment, and it's the note for the pull. - Docs:
docs/features/forge-sleep.mdstarts the sentence with a capital. It names/admin/limitsand says the same list is on the terminal's limits screen. I assumed the terminal screen lists every limit, asLIMIT_KEYSdoes elsewhere, and did not open it. - Header comment: reflowed. It also no longer claims the per-person count comes from an environment variable.
🤖 Generated with Claude Code
Clean read: both limits are admin settings with defaults, one check (personRefusal) sits behind web, API and MCP via spawnForge, admins are exempt, refusal is one plain sentence naming limit and use, no new dependency, how-to in docs/features/forge-sleep.md, README untouched. Tests cover each refusal on each surface and would fail without the change. Not blocking: the storage walk is synchronous and runs on every refused attempt once over the limit, so a repeated spawn call costs a full stat walk; cache or cap it later. The storage test computes MB before the request and compares to the message, which can flake if a child writes in between.
A person's hosted forges are now limited by count and by total storage. Both limits are admin settings with defaults, and past either one a new forge is refused with one plain sentence.
tsc,scripts/gate.tsand the tests that cover the change pass (the new file,forge-sleep,mcp-size,limits,mcp-limits,tui-limits,spawn-api); I did not run the full suite. I did not callkeep_plan; I didn't have that tool.What changed
/admin/limitsnow listsforges.per_person(default 3, from 1 to 1000) andforges.storage_mb(default 1000 MB). They show up on the web page, the API and the MCP limits list, and a change applies to the next request.FORGE_FORGES_PER_PERSONenvironment variable. That variable is gone, so any host that set it falls back to the default of 3 until an admin sets the limit.POST /api/v1/forgesand the MCPspawntool all go throughspawnForge, so one check insrc/spawn/sleep.ts(personRefusal) covers all three. The API answers 409, and the web form answers 409 with the sentence on the page. Examples:- "you already have 3 forges, the most one person may have (3); remove one first" - "your forges already hold 4 MB, the most one person's forges may hold (1000 MB); remove one first"
test/spawn-person-limits.test.tschecks the defaults. It checks each refusal on web, API and MCP, that a site admin is not limited, and that raising the limit makes room again.test/forge-sleep.test.tsnow uses the admin limit instead of the removed variable.docs/features/forge-sleep.mdnow names the two admin limits.What I left out
forges.storage_mb(its minimum is 1 MB).To try it Sign in as a non-admin, set
forges.per_personto 1 at/admin/limits, make one forge, then try a second at/forges/new.bun test test/spawn-person-limits.test.tsruns the same flow on all three surfaces.🤖 Generated with Claude Code
Closes #9581