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.

From a poll tick to a releaseThe schedule starts the poll workflow, whose activities read the boards, count the free slots and start one job workflow per free slot. The job workflow runs sizing and the job as activities, the job runs in its box, and the workflow then waits on durable timers until the merge, and on signals for a person’s answers.BoxActivitiesJobWorkflowPollWorkflowStarted by the scheduleevery 3 minutesloop[Until it merges, at most 14days]opt[Promotion enabled]A personscan_projects, available_slotsscan_todochecks the box proofstart_jobsStart, one per free slotpreflight_checksizingrun_jobheartbeat about every 30 ssetup:, agent pass,validate: gates, reviewThe pull request is openDurable timer: 2 minutes, then 15check_pr_status, CI checks,repair_ci when redhuman_merge_gatesealed, from the panelpromote_stagingapprove_prodsealed, within 3 daysverify_gate_seal,then release_prod
One job from the poll to a release. Calls to Activities run on the worker; a person’s answers arrive as signals. Every wait is a durable timer or condition, so none of them holds a worker.

Schedules#

Recurring work is a Temporal schedule, reconciled by the worker when it starts.

ScheduleIntervalPurpose
beeverforge-poller3 minutesScan the boards; a tick that would overlap is skipped.
beeverforge-techlead-watch-*1 hourThe tech-lead’s rounds over parked work.
beeverforge-card-questions-*1 hourRead the answers to questions asked on cards.
beeverforge-okf-refresh-*6 hoursBring the knowledge map current against the base branch.
beeverforge-product-sweep-*1 weekThe product role’s sweep for work that cannot finish.
beeverforge-preview-reaper10 minutesEnd 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 container and worktree boxes do not: they run their job activity once, because a retry would be a second agent pass that repeats every comment. The fargate description 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 example signed-gates or a-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 Replayer against 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.py provides BoundedDict: thread-safe, with a required maximum size, evicting the oldest entry first, counting what it evicted. It is deliberately not a dict subclass, so nothing can grow it around its bound.
  • util/single_flight.py lets 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.jsonl in the project’s log directory; BEEVERFORGE_EVENT_SINK selects another.
  • Metrics record cost and effort per agent invocation and per job. The built-in sinks are null, sqlite and memory. Without configuration the sink is null and nothing is stored, unless BEEVERFORGE_METRICS_TABLE names a table, which selects dynamodb, a kind supplied by an add-on. The panel serves them at GET /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#