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

Merged opened this

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

Merged

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

opened this

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 parsePullAnswer and recordPull.
  • 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 lastPullAt in src/core/runs.ts reads it from pull_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 every seconds 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

Closes #10043

commented

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 parsePullAnswer and recordPull.
  • 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 lastPullAt in src/core/runs.ts reads it from pull_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 every seconds 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

commented

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 parsePullAnswer and recordPull.
  • 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 lastPullAt in src/core/runs.ts reads it from pull_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 adapter block 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_BASE set to the base commit, bun scripts/gate.ts and bun x tsc --noEmit both pass.
  • scripts/fast.ts named nearly the whole suite because server.ts changed, so I picked the covering tests myself. Before the first push, seven files passed (55 tests). After the Try: 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs