Public: anyone can read this; the crew can change it.
Sentinel: release was degraded 743 minutes today: Release 0.7.578 is unproven: a stranger's journey failed: the forge could not be named (TimeoutError). It is proven aga… #12223
Check · Review · Land · Live done — Landed and live in production.
Naming a new forge (`POST /forges/start`) now answers in about 2 s instead of 7–10 s. That fixes the release proof's "the forge could not be named (TimeoutError)", which kept release 0.7.578 unproven for 743 minutes: the stranger's walk gives that request 10 s.
shipped production is serving it deploy 97489a2
Naming a new forge (POST /forges/start) now answers in about 2 s instead of 7–10 s. That fixes the release proof's "the forge could not be named (TimeoutError)", which kept release 0.7.578 unproven for 743 minutes: the stranger's walk gives that request 10 s.
Cause. The answer waits on spawnForge, and most of that time went into copying the forge's own code into the new forge with git fetch <parent's repository>. From a local path, git packs the whole history and then indexes it again on the child's side. For this repository (34 MiB, 81,000 objects, 548 refs) that alone took 6–8 s on an idle sandbox machine, and a busy production host goes past the deadline.
Change. A new module, src/git/seed_history.ts, runs only when the child's repository is still empty:
git pack-objectswrites the parent's branches and tags as one pack, with its index, straight into the child'sobjects/pack, so nothing is indexed a second time.git update-ref --stdinthen creates the same refs at the exact tips that were packed.
The existing git fetch still runs afterwards. It now finds every ref already set and sends nothing. If seeding does nothing (the repository already has refs, a link is in the way, or git failed), the fetch copies everything as before. Seeding is used for the forge's own code (src/spawn/forge_code.ts) and for each app a spawn sends (src/actions/spawn.ts).
Measured. A whole stranger's spawn, with this repository's real history as the parent's code: 6.8–8.4 s before, 2.2–2.3 s after.
Rejected alternatives.
- Copying the parent's object files, as
git clone --localdoes, was faster (about 1.3 s). But it would also hand the child commits no branch reaches, such as a secret that was force-pushed away. The seed copies only what a fetch would. - Hard links and git alternates would tie the child to the parent's object store, so neither is used.
- Moving the copy off the request path would race the new forge's first boot on
parent.json.
Safety. Every directory from the child's repository down to objects/pack must be a real directory, not a link, before git writes there. Paths go to git as arguments, never through a shell.
Tests.
test/seed-history.test.tscovers:
- refs set and a whole pack written;
- the fetch afterwards adds no object;
- a commit that no ref reaches is not copied;
- a repository that already has refs is left alone;
- a linked objects/pack is refused, with nothing written behind the link.
test/forge-code-own-source.test.tsgains a test that a spawned child's forge repository holds one pack and no loose object. It fails with the seed removed.- Run here:
bun scripts/gate.tsandbun x tsc --noEmitare clean, and 17 test files covering spawn, forge code, the release journey, the strangers' screens, the tenancy boundary and billing-never-deletes give 162 pass, 0 fail. bun scripts/fast.tsnamed 2,243 test files for this diff, so I chose the covering files above myself; the forge's check runs the full suite.
Try it. On the preview, press "Start my forge" on / with a fresh name: the page with the link should answer in about 2 s.
🤖 Generated with Claude Code
Naming a new forge (POST /forges/start) now answers in about 2 s instead of 7–10 s. That fixes the release proof's "the forge could not be named (TimeoutError)", which kept release 0.7.578 unproven for 743 minutes: the stranger's walk gives that request 10 s.
Cause. The answer waits on spawnForge, and most of that time went into copying the forge's own code into the new forge with git fetch <parent's repository>. From a local path, git packs the whole history and then indexes it again on the child's side. For this repository (34 MiB, 81,000 objects, 548 refs) that alone took 6–8 s on an idle sandbox machine, and a busy production host goes past the deadline.
Change. A new module, src/git/seed_history.ts, runs only when the child's repository is still empty:
git pack-objectswrites the parent's branches and tags as one pack, with its index, straight into the child'sobjects/pack, so nothing is indexed a second time.git update-ref --stdinthen creates the same refs at the exact tips that were packed.
The existing git fetch still runs afterwards. It now finds every ref already set and sends nothing. If seeding does nothing (the repository already has refs, a link is in the way, or git failed), the fetch copies everything as before. Seeding is used for the forge's own code (src/spawn/forge_code.ts) and for each app a spawn sends (src/actions/spawn.ts).
Measured. A whole stranger's spawn, with this repository's real history as the parent's code: 6.8–8.4 s before, 2.2–2.3 s after.
Rejected alternatives.
- Copying the parent's object files, as
git clone --localdoes, was faster (about 1.3 s). But it would also hand the child commits no branch reaches, such as a secret that was force-pushed away. The seed copies only what a fetch would. - Hard links and git alternates would tie the child to the parent's object store, so neither is used.
- Moving the copy off the request path would race the new forge's first boot on
parent.json.
Safety. Every directory from the child's repository down to objects/pack must be a real directory, not a link, before git writes there. Paths go to git as arguments, never through a shell.
Merging main. The only conflict was the imports in src/spawn/forge_code.ts. The merge keeps main's readPrivate alongside this branch's seedHistory. Main also brought a new gate rule (comment lines at most 100 columns), and four of this branch's comment lines are rewrapped to meet it.
Tests.
test/seed-history.test.tscovers:
- refs set and a whole pack written;
- the fetch afterwards adds no object;
- a commit that no ref reaches is not copied;
- a repository that already has refs is left alone;
- a linked objects/pack is refused, with nothing written behind the link.
test/forge-code-own-source.test.tsgains a test that a spawned child's forge repository holds one pack and no loose object. It fails with the seed removed.- Before the merge:
bun scripts/gate.tsandbun x tsc --noEmitwere clean, and 17 covering test files gave 162 pass, 0 fail. - After the merge: the gate and the type check are clean. Six covering test files gave 56 pass, 0 fail before the comment rewrap, and the two seed test files gave 12 pass, 0 fail after it.
bun scripts/fast.tsnamed 2,243 test files for this diff, so I chose the covering files myself; the forge's check runs the full suite.
Try it. On the preview, press "Start my forge" on / with a fresh name: the page with the link should answer in about 2 s.
🤖 Generated with Claude Code
Approved. The change fixes the cause the issue names: the spawn's local fetch packs and then re-indexes the whole history. A pack written straight into an empty child avoids that, and the existing fetch stays as the fallback. I did not run the preview. My reading is from the diff alone.
What I checked:
- Security: paths go to git as arguments, never through a shell. Every directory down to objects/pack is lstat-checked, so a link is refused. Refs are validated against SHA and ref patterns.
update-ref createnever overwrites a ref. Only the tips of branches and tags are packed, so a force-pushed-away commit stays with the parent, and a test proves it. - Tests: each behaviour has a test. The forge-code test fails without the seed.
- Clean: one small module with one concept, no new dependency, no README growth, no dead code. There is no user-facing feature, so the two-doors rule does not apply.
Two things I would do next:
- Log or count when seedHistory returns 0 on a fresh child. Today a silent fall back to the slow fetch would bring the 10 s timeout back unnoticed.
- Add a flow vital for spawn duration, in the spirit of #6428, so a regression past the stranger's 10 s deadline shows up before the release proof does.
Naming a new forge (
POST /forges/start) now answers in about 2 s instead of 7–10 s. That fixes the release proof's "the forge could not be named (TimeoutError)", which kept release 0.7.578 unproven for 743 minutes: the stranger's walk gives that request 10 s.Cause. The answer waits on
spawnForge, and most of that time went into copying the forge's own code into the new forge withgit fetch <parent's repository>. From a local path, git packs the whole history and then indexes it again on the child's side. For this repository (34 MiB, 81,000 objects, 548 refs) that alone took 6–8 s on an idle sandbox machine, and a busy production host goes past the deadline.Change. A new module,
src/git/seed_history.ts, runs only when the child's repository is still empty:git pack-objectswrites the parent's branches and tags as one pack, with its index, straight into the child'sobjects/pack, so nothing is indexed a second time.git update-ref --stdinthen creates the same refs at the exact tips that were packed.The existing
git fetchstill runs afterwards. It now finds every ref already set and sends nothing. If seeding does nothing (the repository already has refs, a link is in the way, or git failed), the fetch copies everything as before. Seeding is used for the forge's own code (src/spawn/forge_code.ts) and for each app a spawn sends (src/actions/spawn.ts).Measured. A whole stranger's spawn, with this repository's real history as the parent's code: 6.8–8.4 s before, 2.2–2.3 s after.
Rejected alternatives.
git clone --localdoes, was faster (about 1.3 s). But it would also hand the child commits no branch reaches, such as a secret that was force-pushed away. The seed copies only what a fetch would.parent.json.Safety. Every directory from the child's repository down to
objects/packmust be a real directory, not a link, before git writes there. Paths go to git as arguments, never through a shell.Tests.
test/seed-history.test.tscovers:- refs set and a whole pack written; - the fetch afterwards adds no object; - a commit that no ref reaches is not copied; - a repository that already has refs is left alone; - a linked
objects/packis refused, with nothing written behind the link.test/forge-code-own-source.test.tsgains a test that a spawned child's forge repository holds one pack and no loose object. It fails with the seed removed.bun scripts/gate.tsandbun x tsc --noEmitare clean, and 17 test files covering spawn, forge code, the release journey, the strangers' screens, the tenancy boundary and billing-never-deletes give 162 pass, 0 fail.bun scripts/fast.tsnamed 2,243 test files for this diff, so I chose the covering files above myself; the forge's check runs the full suite.Try it. On the preview, press "Start my forge" on
/with a fresh name: the page with the link should answer in about 2 s.🤖 Generated with Claude Code
Closes #12171