degenbot.runner.bot_runner¶
Settlement-arbitrage runtime driver facade (BotRunner).
BotRunner is the
Python-companion cockpit over the Rust-owned engine: it owns the config and
the lifecycle, while the coordination state itself (the three actors
bot/engine_registry/async_w3 + the Dispatcher + the block
clock) is owned by the ONE _SessionState built in start().
It is the one place that enforces the phase ordering the engine’s state
machine requires.
- start(): subscribe -> stream snapshots -> backfill -> verify config
(
EngineRegistry.start, stops at Backfilled, pre-resume)- run(): the run ritual (
_run_ritual) drives consumer-attach ->watch-attach ->
resume()-> registration -> main loop
The driver is stays-python (asyncio loop, SIGINT, deployment policy): it
controls the Rust engine but owns no pool state (ADR-003: Bot is the
single state owner; ADR-006: Bot is the per-chain orchestrator).
Sequencing supersedure (ADR-050, 2026-09-14): the start/run sequencing
described here is Rust-owned in EngineDriver; only asyncio/SIGINT/
deployment policy stays stays-python — see
docs/architecture/rust-settlement-bot-parity.md. It
delegates path registration to build_paths and the
main loop to consume.
Testability seams (mirrors EngineRegistry’s engine= seam): bot,
engine_registry, async_w3, snapshots, path_builder, and
consumer are injectable.
Module Contents¶
- exception degenbot.runner.bot_runner.ActivationGateRefused(refusal: ValueError)¶
Bases:
RuntimeErrorStrategy readiness (the activation gate) refused a live session.
The gate refuses an activated facet that fails readiness, so a live boot either aborts here or degrades a broadcast to the public mempool; the gate’s ValueError text becomes the message so the operator keeps the gate’s own diagnostic unchanged.
- exception degenbot.runner.bot_runner.PhaseError¶
Bases:
RuntimeErrorCockpit phase violation: a lifecycle method ran in the wrong phase.
The session phase machine is
New -> Started -> Running -> Closed:start()builds once fromNewand is an idempotent no-op onStartedre-entry (Running/Closed re-entry raises);run()requiresStarted;enqueue_path/trigger_discoveryrequireRunning;shutdown()stays deliberately any-phase and idempotent (the SIGINT teardown ordering depends on it).The transition table itself is the Rust host’s
SessionPhase(strategy_host.rs), read throughdegenbot._ffi.session_phase_next; this exception is the Python surface of its refusals.
- class degenbot.runner.bot_runner.InjectedActors¶
Test/DI actor overrides for
BotRunner(None= build from cfg).- bot: degenbot.Bot | None = None¶
- engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry | None = None¶
- async_w3: degenbot.provider.AsyncAlloyProvider | None = None¶
- path_builder: Any = None¶
- consumer: Any = None¶
- pipeline_factory: collections.abc.Callable[[_SessionState], Any] | None = None¶
- readiness: collections.abc.Callable[[], degenbot.strategy.StrategyReadinessView] | None = None¶
- settlement_endpoints: collections.abc.Callable[[], list[str]] | None = None¶
- stop_engine: collections.abc.Callable[[], None] | None = None¶
- scheduler: collections.abc.Callable[[collections.abc.Coroutine[Any, Any, None]], asyncio.Task[Any]]¶
- class degenbot.runner.bot_runner.BotRunner(cfg: degenbot.runner.config.ArbitrageConfig, *, actors: InjectedActors | None = None, install_sigint: bool = True)¶
Orchestrator that collapses the settlement-arbitrage startup ritual behind one facade.
Owns the config and the lifecycle; the coordination state itself (the three actors
bot/engine_registry/async_w3, theDispatcher, the block clock, the sim context, the pipelines) lives on ONE_SessionStatebuilt instart()— the runner’s same-named attributes are a facade over that owner, not mirrors. The runner is the ONE place that enforces the phase ordering the engine’s state machine requires:- start(): subscribe → stream snapshots → backfill → verify config
(
EngineRegistry.start, stops at Backfilled, pre-resume)- run(): the run ritual (
_run_ritual.RunRitual) owns the startup ordering — attach consumer → watch →
resume()→ registration → main loop — as a state machine; the cross-task fail-fast channel surfaces a fatal registration error.
Usage (production):
cfg = ArbitrageConfig.build(live=not dry_run, permutation=args.permutation) async with BotRunner(cfg) as session: await session.run()
In production
run()schedules discovery+registration as a background task (through the injected scheduler) and enters the main loop immediately; the state-trim runs on registration completion (inside the scheduled hand-off), not on the main-loop entry path. A fatal verification error still crashes loudly through the cross-task channel. The hot loop keeps onlyengine_registry+async_w3+ dispatcher once trimmed — the Python pool/token caches are scaffolding once the Rust engine owns canonical state.Testability seams (mirrors
EngineRegistry’sengine=seam):bot,engine_registry,async_w3,snapshots,path_builder,consumer, and the registrationschedulerare injectable. When injected,start()/run()orchestrate the fakes and the phase ordering is verifiable offline; whenNone(production), the actors are built fromcfgand the real module functions are called.- cfg¶
- property bot: degenbot.Bot | None¶
The session’s Python-companion bot (
Noneonce the trim dropped it).Before
start(): the injected seam (a write is an injection).
- property engine_registry: degenbot.arbitrage.engine_registry.EngineRegistry | None¶
the injected seam.
- Type:
The session’s engine registry. Before
start()
- property async_w3: degenbot.provider.AsyncAlloyProvider | None¶
the injected seam.
- Type:
The session’s dispatch-path provider. Before
start()
- property dispatcher: degenbot.dispatch.Dispatcher | None¶
None(no session yet).- Type:
The session’s dispatcher. Before
start()
- property session_watch: degenbot.runner._session_watch.SessionWatch¶
The session watch the ritual attaches the run’s members to.
- property consumer: Any¶
The injected consumer (
None= the production block loop).
- property readiness: degenbot.strategy.StrategyReadinessView | None¶
The readiness view resolved once at the
start()boundary.
- property path_builder: Any¶
The injected path builder (
None= the realbuild_paths).
- property scheduler: collections.abc.Callable[[collections.abc.Coroutine[Any, Any, None]], asyncio.Task[Any]]¶
The registration hand-off scheduler (data; production
create_task).
- property trim: collections.abc.Callable[..., None]¶
The python-state trim (the registration hand-off’s completion duty).
- property pump_finished_watchdog: collections.abc.Callable[..., collections.abc.Coroutine[Any, Any, None]]¶
The pump-finished watchdog factory (the watch’s always-on member).
- async start() BotRunner¶
Build the actors, fetch block state, load snapshots, run
engine_registry.start().Stops at
Backfilled— BEFOREresume(). Zero result batches emit during this window (the pump isn’t running), sorun()can attach the consumer in the gap beforeresume()without a stale-backlog window. Idempotent via the phase alone: re-entry once Started is a no-op; Running/Closed re-entry raisesPhaseError.- Returns:
The started runner, stopped at
Backfilled.- Raises:
RuntimeError – If the latest-block fetch fails at session start.
- async run() degenbot.runner._session_watch.SessionEndVerdict¶
Run the cockpit main loop until the consumer task ends.
Requires the
Startedphase — the session phase machine (PhaseError, delegating to the Rust host’sSessionPhasetable) owns the lifecycle gate. The startup ordering itself is the run ritual’s (_run_ritual): this method is the thin phase-gated drive over that machine.- Returns:
The session’s end verdict (the session watch’s ranking over how the run ended). Consuming it is optional — callers that ignore it behave exactly as before this return existed.
- async enqueue_path(path_steps: Any, directions: list[bool] | None = None) None¶
Add ONE specific path at any time (the operator surface).
Delegates to the session’s live
PathRegistrationPipeline(created by the run ritual);path_steps+ optionaldirectionsare the same shapes asPathRegistrationPipeline.enqueue_path(). The path is built via the retainedConstructionContext(RustPoolBuilder), registered + verified, released toLive, and registered — without disturbing the pump’s update/solve/dispatch.- Raises:
RuntimeError – if no live pipeline exists (injected fake builders have no construction surface, or
run()has not run).
- async trigger_discovery(*, bound: int | None = None) int¶
Trigger a bounded one-shot discovery sweep (on-demand trigger).
Delegates to the session’s live pipeline.
- Returns:
The number of paths processed by the sweep.
- Raises:
RuntimeError – if no live pipeline exists (injected fake builders, or
run()has not run).
- async shutdown() None¶
Signal the Rust core to stop the pump (best-effort).
Safe to call at any point in the lifecycle — before
start()finished (engine_registrymay beNone), afterrun()exited, or from aSIGINT/KeyboardInterrupthandler. Mirrors the Ruststop()contract: idempotent, sets the shutdown flag + aborts the pump task so the WS stream’scombined.next().awaitunblocks immediately (60s cold-shutdown otherwise). Any exception is swallowed and logged so a partial-startup teardown can’t mask the original in-flight exception.This is the one place that closes the Rust core’s pump — the
KeyboardInterrupt-exits-slowly bug was the pump task (spawned on the shared tokio runtime, decoupled from the asyncio loop) blocking on a silent WS subscription, whichasyncio.run’s teardown did not reach until the OS closed the socket.