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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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-objects writes the parent's branches and tags as one pack, with its index, straight into the child's objects/pack, so nothing is indexed a second time.
  • git update-ref --stdin then 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 --local does, 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.ts covers:

- 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.ts gains 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.ts and bun x tsc --noEmit are 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.ts named 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

commented

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-objects writes the parent's branches and tags as one pack, with its index, straight into the child's objects/pack, so nothing is indexed a second time.
  • git update-ref --stdin then 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 --local does, 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.ts covers:

- 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.ts gains 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.ts and bun x tsc --noEmit are 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.ts named 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

commented

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-objects writes the parent's branches and tags as one pack, with its index, straight into the child's objects/pack, so nothing is indexed a second time.
  • git update-ref --stdin then 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 --local does, 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.ts covers:

- 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.ts gains 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.ts and bun x tsc --noEmit were 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.ts named 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

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 create never 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:

  1. 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.
  2. 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.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs