Operator Add-Path-At-Any-Time Surface (NWTUM3)¶
An operator can add a specific path, or trigger a bounded on-demand discovery sweep, on a live, running bot — without restarting it and without stalling its update/solve/dispatch. This page documents the three reusable seams that make that possible, from the innermost (the registration pipeline) to the wire-facing operator channel.
It complements the verify-lifecycle shared decisions in the NWTUM3 ergo task
(Python driver; the Rust-owned bot_core/registration_lifecycle.rs is the
middle/hosted form; D-B one shared provider, D-C no-config fail-fast,
D-A tripwire = verification mismatch).
1. The long-lived registration pipeline¶
examples/eth_settlement_arbitrage_v2_v3_v4_rust.py::PathRegistrationPipeline is the
single place that turns a path’s hop steps into registered, verified,
released pools. Both the built-in discovery source AND an operator add route
through the same _consume body (build → register + verify → per-path
release → dedup → register_path), so an operator-added path is treated
identically to a discovered one.
The pipeline is long-lived and owned by the session (BotRunner),
not created and dropped inside build_paths. That is the key structural
change that makes mid-run adds possible:
run()builds aConstructionContextonce and holds it, and creates thePathRegistrationPipelineon the session (self._pipeline).build_pathsreuses the caller-supplied pipeline (falls back to building its own only when none is supplied).Because the pipeline retains its own
ConstructionContext, it keeps constructing through the RustPoolBuildereven after the main-loop trim (self.bot = None) drops the Python bot — adding a path mid-run does not resurrect Python construction state, satisfying the trimmed-state AC.
Programmatic API¶
BotRunner exposes two coroutines that route into the live pipeline. Both
never await the pump, so a mid-run command cannot stall solve/dispatch:
await session.enqueue_path(path_steps, directions=None) # add ONE path
n = await session.trigger_discovery(bound=None) # bounded one-shot sweep
enqueue_path takes the same path_steps list the pipeline’s discovery uses
(step objects with a .type pool-table base class, .address, and .hash for
V4) plus optional per-hop directions. trigger_discovery runs a bounded
sweep distinct from the unbounded background producer. Both raise
RuntimeError loudly if no live pipeline exists (e.g. an injected/fake run).
2. The operator command channel (wire)¶
src/degenbot/operator/operator_channel.py hosts a Unix-domain-socket,
JSON-lines protocol so an operator can steer the bot from a separate
process. The bot runs an OperatorServer asyncio task; a client writes one
JSON command line and reads one JSON response line.
Requests / responses (one JSON object per line):
{"op": "add_path",
"steps": [{"family": "V2|V3|V4", "address": "0x..",
"hash": "0x..(v4 pool_id, v4 only)"}, ...],
"directions": [true, false]} # optional; auto-resolved if absent
{"op": "discover", "bound": 5}
{"ok": true, "detail": "..."}
{"ok": false, "error": "..."}
Key properties:
No Python class crosses the wire.
step_from_wiremaps afamilystring (V2/V3/V4) to the matching pool-table base class, the single source of the classification_consumeneeds.Wire hygiene.
wrap_handlerturns any exception into{"ok": false, "error": ...}so a malformed or failing command never crashes the host, and the server guards JSON decode / op dispatch the same way.Self-sufficient
close(). It cancels the in-flightserve_foreverloop beforewait_closed(), so shutdown never blocks on a pending accept loop, and unlinks the socket file.
The example bot hosts the server when launched with --operator-socket <path>,
routing commands into session.enqueue_path / session.trigger_discovery.
3. The CLI client¶
degenbot path in src/degenbot/cli/path.py is a thin client over
send_command (no local pool/db work):
degenbot path add --socket /tmp/bot.sock \
--hop v3:0x<weth/usdt> --hop v4:0x<manager>:0x<pool_id>:[--direction zfo]
degenbot path discover --socket /tmp/bot.sock [--bound 5]
Hop spelling is FAMILY:ADDRESS (V2/V3) or V4:ADDRESS:HASH (HASH = the 0x+64
pool id). --direction zfo/ozf applies a per-hop True/False list; omitting it
lets the bot auto-resolve. A rejecting or unreachable host surfaces as a
click.ClickException, so a failed command exits non-zero rather than silently.
Why no in-process CLI hack¶
The degenbot CLI cannot add a path “into” a running bot by importing the
session — the bot lives in a different process. The socket channel is the honest
boundary: the CLI is a driver, the bot’s process owns the state, and the wire
is a tiny versioned protocol a client of any vintage can talk to.
Test coverage¶
tests/operator/test_operator_channel.py— wire round-trip (add_path,discover), family mapping, error →ok:falsehost-survival, malformed request,wrap_handlernormalization.tests/cli/test_path_cli.py— hop parsing,--directionmapping, live round-trip + error exit against a threadedOperatorServer.tests/arbitrage/test_arbitrage_session.py— programmatic surface raises without a live pipeline; a mid-run add during an advancing consumer does not stall or abort dispatch.