Public: anyone can read this; the crew can change it.
The forge calls each running adapter's /pull on its schedule and records the run #10060
Check · Review · Land · Live done — Landed and live in production.
The forge now calls each running adapter's `POST /pull` on its own schedule and records every pull as a run. The timer is started in `src/http/server.ts` next to `startFeatureSchedule` and stopped with it. It ticks once a minute and is off under `bun test`.
shipped production is serving it deploy a168196
The forge now calls each running adapter's POST /pull on its own schedule and records every pull as a run. The timer is started in src/http/server.ts next to startFeatureSchedule and stopped with it. It ticks once a minute and is off under bun test.
What it does (src/apps/pull_schedule.ts, new):
- Which apps: an app is pulled only if all three hold:
- Its production is running. A sleeping app is never woken, and its manifest isn't even read.
- Its forge.json on the default branch has a valid adapter block, checked by parseAdapter. The result is cached per commit, so a tick reads git only when the branch moves.
- Its last pull, good or failed, is at least schedule.every seconds old.
- The request: a loopback POST that waits at most 10 s and reads at most 64 KB, the same limits the scheduled tools use. A body over 64 KB or no answer becomes a failed pull with a reason that names the problem.
- The answer: goes through
parsePullAnswerandrecordPull. - No overlap: two pulls of the same app never run at once. Apps are swept one after another, and the timer skips a tick while the previous sweep is still running.
- "Last pull" time: the new
lastPullAtinsrc/core/runs.tsreads it frompull_run.created_at, so the schedule survives a restart.
Tests (test/pull-schedule.test.ts) use a fake reach and a controlled clock. They prove that:
- a due adapter is pulled once;
- the next tick inside the interval does nothing, and one a full interval later pulls again;
- an adapter pulled less than
everyseconds ago is not pulled; - a 500 or no answer is recorded as failed with its reason;
- a sleeping app or a non-adapter is never reached;
- a second pull while one is in flight does not overlap it;
- the sweep survives one app throwing;
- the timer is off under test.
postPull is also tested against a real loopback server for the path, the 64 KB cap and the timeout.
docs/ADAPTERS.md now has a paragraph on the schedule, and the header comment in src/apps/pull.ts names the new caller.
Not done:
- A failed pull waits for the next scheduled time like any other; there is no faster retry. That matches docs/ADAPTERS.md.
- I didn't post a plan with
keep_plan, because that tool isn't available in this session.
Checks: bun scripts/gate.ts and bun x tsc --noEmit are clean. Because the diff touches server.ts, scripts/fast.ts named almost the whole suite, so I picked the covering files myself: the pull-schedule, apps-pull, feature-schedule, feature-schedule-fire, boot-no-stall, server-timing-sweep and apps-adapter tests, 55 pass and 0 fail.
To try it: deploy an app whose forge.json has "adapter": { "schedule": { "every": 60 } } and that answers POST /pull. Within about two minutes of production running, a pull_run row should appear.
🤖 Generated with Claude Code
The forge now calls each running adapter's POST /pull on its own schedule and records every pull as a run. A timer in the new src/apps/pull_schedule.ts is started and stopped in src/http/server.ts next to startFeatureSchedule. It ticks once a minute and is off under bun test.
What it does:
- Which apps: an app is pulled only if all three hold:
- Its production is running. A sleeping app is never woken, and its manifest isn't read.
- Its forge.json on the default branch has a valid adapter block, checked by parseAdapter. The result is cached per commit, so a tick reads git only when the branch moves.
- Its last pull, good or failed, is at least schedule.every seconds old.
- The request: goes over loopback, waits at most 10 s and reads at most 64 KB, the same limits the scheduled tools use. A body over 64 KB, or no answer at all, is recorded as a failed pull with a reason that says what went wrong.
- The answer: goes through
parsePullAnswerandrecordPull. - No overlap: two pulls of the same app never run at once. Apps are pulled one after another, and the timer skips a tick while the previous one is still running.
- "Last pull" time: the new
lastPullAtinsrc/core/runs.tsreads it frompull_run.created_at, so the schedule survives a restart.
Tests: test/pull-schedule.test.ts uses a fake reach and a controlled clock. It checks that:
- a due adapter is pulled once;
- the next tick inside the interval does nothing, and one a full interval later pulls again;
- a 500 or no answer is recorded as failed with its reason;
- a sleeping app or an app without an
adapterblock is never reached; - a second pull while one is in flight is refused;
- the timer is off under test.
postPull is also tested against a real loopback server for the path, the 64 KB cap and the timeout.
Docs: docs/ADAPTERS.md now describes the schedule, and the header comment in src/apps/pull.ts names the new caller.
Not done:
- A failed pull waits for the next scheduled time like any other; there is no faster retry, which matches docs/ADAPTERS.md.
- I didn't post a plan with
keep_plan, because that tool wasn't available in this session.
Fix for the failed check: the gate wants a Try: line in a commit message whenever src/http/server.ts changes. Force pushes are refused, so I added it in a new empty commit. It passed locally the first time only because the clone has no origin/main to compare against.
Checks:
- With
FORGE_CHECK_BASEset to the base commit,bun scripts/gate.tsandbun x tsc --noEmitboth pass. scripts/fast.tsnamed nearly the whole suite becauseserver.tschanged, so I picked the covering tests myself. Before the first push, seven files passed (55 tests). After theTry:commit, which changes no code, I re-ran three of them (31 tests); all pass.
To try it on the preview: open / to see the forge start with the pull timer running. Then deploy an app whose forge.json has "adapter": { "schedule": { "every": 60 } }, such as the example adapter from #10059. Within about two minutes of its production running, a pull run is recorded for it.
🤖 Generated with Claude Code
Clean. Pulls only a running production (port from envStatus, never woken), reads the default-branch adapter block cached by tip, checks due against the last pull's created_at (ISO Z, survives restarts), caps the body by bytes read (64 KB) and the wait at 10 s, routes the answer through parsePullAnswer/recordPull, guards overlap per app and per sweep, and the timer is off under test. No new input surface, no dependency, no secret; the loopback target is a port the forge itself holds. Tests cover due, not due, failed, no answer, sleeping, non-adapter, overlap, sweep isolation, and the real postPull cap and timeout. Docs updated in ADAPTERS.md, not README. Not a user-facing feature, so no TUI screen is owed.
The forge now calls each running adapter's
POST /pullon its own schedule and records every pull as a run. The timer is started insrc/http/server.tsnext tostartFeatureScheduleand stopped with it. It ticks once a minute and is off underbun test.What it does (
src/apps/pull_schedule.ts, new):- Its production is running. A sleeping app is never woken, and its manifest isn't even read. - Its
forge.jsonon the default branch has a validadapterblock, checked byparseAdapter. The result is cached per commit, so a tick reads git only when the branch moves. - Its last pull, good or failed, is at leastschedule.everyseconds old.parsePullAnswerandrecordPull.lastPullAtinsrc/core/runs.tsreads it frompull_run.created_at, so the schedule survives a restart.Tests (
test/pull-schedule.test.ts) use a fake reach and a controlled clock. They prove that:everyseconds ago is not pulled;postPullis also tested against a real loopback server for the path, the 64 KB cap and the timeout.docs/ADAPTERS.md now has a paragraph on the schedule, and the header comment in
src/apps/pull.tsnames the new caller.Not done:
keep_plan, because that tool isn't available in this session.Checks:
bun scripts/gate.tsandbun x tsc --noEmitare clean. Because the diff touchesserver.ts,scripts/fast.tsnamed almost the whole suite, so I picked the covering files myself: the pull-schedule, apps-pull, feature-schedule, feature-schedule-fire, boot-no-stall, server-timing-sweep and apps-adapter tests, 55 pass and 0 fail.To try it: deploy an app whose
forge.jsonhas"adapter": { "schedule": { "every": 60 } }and that answersPOST /pull. Within about two minutes of production running, apull_runrow should appear.🤖 Generated with Claude Code
Closes #10043