BeeverForge core architecture
Runtime and durability
The engine is Temporal. Every job is a workflow, every side effect is an activity, and every wait, from a CI poll to a production approval, is a durable timer or condition. A worker restart therefore does not lose the job: its waits continue where they were, and a job whose worker is lost during the agent pass parks for a person to resume or skip, because that pass is not retried.
Workflows, activities and the task queue#
One worker process registers every workflow and every activity on a single task queue, beeverforge-jobs. The main workflows are:
JobWorkflow- One per ticket. Sizing, the run in the box, the watch to merge, the promotion and every park happen inside it.
PollWorkflow- One board scan per tick, starting jobs into the free slots.
DeployWatchWorkflow- A detached child that observes the deployment after a merge.
PreviewWorkflow- A running preview of an unmerged change, when a preview runtime is configured.
ConversationWorkflow- A conversation with the factory, admitted and progressed by signals.
Other workflows cover the tech-lead watch, the knowledge refresh, the product role and the sweeps listed below. The heavy work, such as running the job in its box, is the run_job activity.
Schedules#
Recurring work is a Temporal schedule, reconciled by the worker when it starts.
| Schedule | Interval | Purpose |
|---|---|---|
beeverforge-poller | 3 minutes | Scan the boards; a tick that would overlap is skipped. |
beeverforge-techlead-watch-* | 1 hour | The tech-lead’s rounds over parked work. |
beeverforge-card-questions-* | 1 hour | Read the answers to questions asked on cards. |
beeverforge-okf-refresh-* | 6 hours | Bring the knowledge map current against the base branch. |
beeverforge-product-sweep-* | 1 week | The product role’s sweep for work that cannot finish. |
beeverforge-preview-reaper | 10 minutes | End previews whose pull request has merged. |
Workflow histories are retained by the engine for 30 days by default (BEEVERFORGE_ENGINE_RETENTION_DAYS).
Signals and durable timers#
A person’s answer reaches a running job as a signal: approve_prod, not_yet, human_merge_gate, act_on_impediment and advise_decision. Waits are workflow.sleep and workflow.wait_condition with a timeout, so a wait of minutes or days holds no worker and survives a restart. Examples are the CI poll (2 minutes, then 15), the deploy poll (1 minute), the production approval (3 days), the merge watch (14 days) and a parked impediment (3 days).
Idempotent side effects#
An activity may run again after a failure, so each external write is written to be safe to repeat or is not retried:
- Pull requests are find-then-update: an open pull request for the branch is reused.
- Release tags are find-or-create.
- Card transitions and comments carry the event id of the activity that caused them; the card’s door returns the transition already recorded for that id instead of writing it again.
- Merges are read back from the forge after the merge call, instead of trusting the call’s result.
- The job itself is retried only when its box kind declares that a retry re-attaches to the running job. The
containerandworktreeboxes do not: they run their job activity once, because a retry would be a second agent pass that repeats every comment. Thefargatedescription declares it; its runner is supplied by an add-on.
Heartbeats#
Long activities heartbeat. Job activities run with a heartbeat timeout of 120 seconds and beat about every 30 seconds; work that continues after the last beat is bounded to fit inside that window. A worker that dies mid-activity is therefore noticed within the timeout rather than at the end of the activity’s full duration.
Replay safety#
Temporal replays a workflow’s history against the current code, so a change to workflow logic must not change the commands an in-flight workflow already recorded.
- Every such change is gated by
workflow.patched()with an id that names the behaviour, for examplesigned-gatesora-self-merge-is-read-back. Old histories take the old branch; new runs take the new one. - Replayer tests run a workflow against an ephemeral Temporal server, fetch the recorded history and replay it with
Replayeragainst the current code. Some first run a twin of the workflow from before the patch. The histories are recorded inside the tests; none are committed as files. - Code that must stay out of the workflow body is kept in activities. The box-proof check at pickup lives in the board-scan activity for exactly that reason.
One engine client per process#
A process holds one engine client. runtime/temporal/connection.py keeps a single client keyed by the connection’s settings; concurrent first callers share one connection attempt, and reconnection backs off for at most 60 seconds. Guard tests assert that concurrent first calls, and repeated approvals, open one client.
Bounded structures#
The worker and the panel are long-lived processes, so in-memory caches are bounded by construction.
util/bounded.pyprovidesBoundedDict: thread-safe, with a required maximum size, evicting the oldest entry first, counting what it evicted. It is deliberately not adictsubclass, so nothing can grow it around its bound.util/single_flight.pylets readers that arrive together share one read. Each waiter is shielded from another caller’s cancellation, at most one read is in flight, and a failure is shared with the current waiters and not stored.- A structural test,
tests/test_no_unbounded_growth.py, requires every site that allocates or memoises in a long-lived process to state its bound.
Journal, metrics and observability#
- The job journal records typed events (
state,agent_action,validation,review,pr,note,warning,error). The default sink is a file,<issue>-events.jsonlin the project’s log directory;BEEVERFORGE_EVENT_SINKselects another. - Metrics record cost and effort per agent invocation and per job. The built-in sinks are
null,sqliteandmemory. Without configuration the sink isnulland nothing is stored, unlessBEEVERFORGE_METRICS_TABLEnames a table, which selectsdynamodb, a kind supplied by an add-on. The panel serves them atGET /api/metrics. - Health has its own routes, and the Temporal web UI shows each workflow’s history.
- The core has no Prometheus or OpenTelemetry exporter.
Panel API#
The panel is a FastAPI application. Typed reads are served under /api/v1/, and their unversioned paths remain, marked deprecated. The OpenAPI description is committed at docs/reference/openapi.json; a test asserts that the committed file equals what the application generates and prints the command that regenerates it, python -m beeverforge.api.openapi.
Sources#
beeverforge/runtime/temporal/__init__.pybeeverforge/runtime/temporal/worker.pybeeverforge/runtime/temporal/workflow.pybeeverforge/runtime/temporal/activities.pybeeverforge/runtime/temporal/schedule.pybeeverforge/runtime/temporal/connection.pybeeverforge/adapters/sandbox/registry.pybeeverforge/lifecycle/card.pybeeverforge/util/bounded.pybeeverforge/util/single_flight.pybeeverforge/observability/events.pybeeverforge/observability/registry.pybeeverforge/api/openapi.pydocs/reference/openapi.jsontests/test_the_merge_gate_is_heard_on_every_path.pytests/test_the_panel_holds_one_engine_client.pytests/test_no_unbounded_growth.pytests/test_the_api_is_versioned.py- ADR-0009