Summary
We are asking for LXMF plus a listener mode owned by the substrate, and — the reason this is worth doing now — we surveyed what the agent currently carries to make edge work. It is ~1,000 lines of Python orchestrating and observing a transport we do not own, none of which is agent-domain logic. Every line of it is a candidate to delete once the Rust owns the shape.
This follows CEG §0.0 layering and Edge OQ-01's library, not sidecar: the agent surfaces, the fabric produces.
The survey — what the agent carries today
ciris_engine/logic/runtime/edge_runtime.py — 1,224 lines, 42 substrate calls:
| lines |
function |
what it actually is |
| 525 |
initialize_edge_runtime |
node-key provisioning, alias reconciliation, Reticulum transport enable, canonical TCP auto-seed |
| 487 |
_spawn_delivery_rooting_probe |
pure observability — polls delivery_status() / compose_status() and re-emits them as log lines |
| 19 |
_seed_bootstrap_peers_into_edge |
peer seeding |
| 16 |
should_reprime |
federation retry policy |
plus, as module constants:
REPRIME_SCHEDULE = (45, 120, 300)
REPRIME_CADENCE = 180
SILENT_PEER_AFTER_REPRIMES = 2
Alongside it: node_fold.py (660 lines — composing the node around a running brain, port-bind checks, identity resolution), brain_adapter.py (179), substrate_logging.py (179), substrate_caps.py (80).
Why each of these is yours, not ours
The 487-line probe exists only because the accessors are in-process. Its own docstring says it: "the accessor is in-process to the server, so it MUST be logged here, not called from the runner's own process." The substrate already knows rooting state, KEX resolvability, envelope counts — we re-derive and re-log them in Python purely to make them visible. If the substrate emitted structured delivery/rooting events, this file section deletes.
The reprime schedule is transport policy living at the wrong layer. reprime_federation_delivery() is a substrate call, but when to call it is a tuple in our Python, tuned from a QA observation ("141 rounds, 21 Key envelopes sent, zero inbound, KEX never landed, window torn down before a second nudge"). Retry cadence belongs next to the transport that knows whether a peer is healing.
Node-key provisioning is already half yours and currently broken. See CIRISServer#528 — the substrate mints a node key when handed an actor key and the owner-binding cannot follow, so owner_of(node) never resolves. We carry the provisioning dance in Python around that.
The ask
1. LXMF, first-class
ciris_server already exports LxmfPropagationClient, LxmfPropagationNode, StoreAndForward, RelayNode, RetRelay, install_relay, rns_destination_hash, NetworkEventSubscription. What is missing is a supported way to run an inbox without a node — subscribe, hold, and signal, with no cognitive runtime attached.
2. A listener mode
A mode that boots edge + persist + the LXMF inbox and nothing else, identified by the node ID.
The identity question is already settled on our side: first run always ends with a node ID (the only variable is whether the user opted out of advertising to the federation directory), and provision_node_identity() already runs early. So the listener's identity is the node ID — which is exactly why #528 is a prerequisite, not a parallel concern: a listener that is the node ID inherits an unresolved owner-binding.
What it needs to expose:
- boot to inbox-ready without composing a node or a brain
- a heartbeat/readiness signal the host can poll (today the client polls
/health on a server that may not have bound — see CIRISClient#28)
- a wake signal on inbound LXMF
- teardown that hands the inbox back cleanly
3. Own the delivery observability and the retry policy
Emit rooting/KEX/envelope state as structured events, and own the reprime cadence. Both currently live in our Python because they had nowhere else to live.
What the agent will do
Our side of this is smaller than it looks, and mostly already present:
init_edge_runtime is registered at InitializationPhase.DATABASE — before MEMORY/IDENTITY/SECURITY/SERVICES. Edge and the node identity are up ~4s into boot, well before the 22 services. The listener boundary is "stop after DATABASE", not a new split.
BrainAdapter(upstream, runtime_boot=...) already implements the inverse topology and is passed by nothing. Its docstring: "runtime_boot remains for the inverse topology — a node that starts its own brain — which is not how the agent boots today." Wiring it is the wake path.
- A staged-startup mechanism already exists (
FIRST_RUN_SERVICES = 10 / RESUME_SERVICES = 12), though those ten are infrastructure and graph services with no transport — it proves the machinery, not the set.
So we can meet a listener mode quickly. What we cannot do is define it: the inbox, the identity, and the transport all live below us, and building them here would reintroduce exactly the sidecar key-leak surface OQ-01 rejected.
Platform note
iOS cannot hold a background listener at all — every Reticulum iOS client reports this. The working model there is store-and-forward plus a push (propagation node holds, APNs notifies, app wakes and fetches), which StoreAndForward + LxmfPropagationClient already suggest is on-architecture. Worth confirming that the listener mode's contract covers "wake, drain, sleep" and not only "stay resident", so one design serves both platforms.
Context: CIRISAgent#1131 (client-side process model and the Sideband reference), CIRISServer#528 (owner-binding prerequisite), CIRISClient#28 (test-server readiness).
Summary
We are asking for LXMF plus a listener mode owned by the substrate, and — the reason this is worth doing now — we surveyed what the agent currently carries to make edge work. It is ~1,000 lines of Python orchestrating and observing a transport we do not own, none of which is agent-domain logic. Every line of it is a candidate to delete once the Rust owns the shape.
This follows CEG §0.0 layering and Edge OQ-01's library, not sidecar: the agent surfaces, the fabric produces.
The survey — what the agent carries today
ciris_engine/logic/runtime/edge_runtime.py— 1,224 lines, 42 substrate calls:initialize_edge_runtime_spawn_delivery_rooting_probedelivery_status()/compose_status()and re-emits them as log lines_seed_bootstrap_peers_into_edgeshould_reprimeplus, as module constants:
Alongside it:
node_fold.py(660 lines — composing the node around a running brain, port-bind checks, identity resolution),brain_adapter.py(179),substrate_logging.py(179),substrate_caps.py(80).Why each of these is yours, not ours
The 487-line probe exists only because the accessors are in-process. Its own docstring says it: "the accessor is in-process to the server, so it MUST be logged here, not called from the runner's own process." The substrate already knows rooting state, KEX resolvability, envelope counts — we re-derive and re-log them in Python purely to make them visible. If the substrate emitted structured delivery/rooting events, this file section deletes.
The reprime schedule is transport policy living at the wrong layer.
reprime_federation_delivery()is a substrate call, but when to call it is a tuple in our Python, tuned from a QA observation ("141 rounds, 21 Key envelopes sent, zero inbound, KEX never landed, window torn down before a second nudge"). Retry cadence belongs next to the transport that knows whether a peer is healing.Node-key provisioning is already half yours and currently broken. See CIRISServer#528 — the substrate mints a node key when handed an actor key and the owner-binding cannot follow, so
owner_of(node)never resolves. We carry the provisioning dance in Python around that.The ask
1. LXMF, first-class
ciris_serveralready exportsLxmfPropagationClient,LxmfPropagationNode,StoreAndForward,RelayNode,RetRelay,install_relay,rns_destination_hash,NetworkEventSubscription. What is missing is a supported way to run an inbox without a node — subscribe, hold, and signal, with no cognitive runtime attached.2. A listener mode
A mode that boots edge + persist + the LXMF inbox and nothing else, identified by the node ID.
The identity question is already settled on our side: first run always ends with a node ID (the only variable is whether the user opted out of advertising to the federation directory), and
provision_node_identity()already runs early. So the listener's identity is the node ID — which is exactly why #528 is a prerequisite, not a parallel concern: a listener that is the node ID inherits an unresolved owner-binding.What it needs to expose:
/healthon a server that may not have bound — see CIRISClient#28)3. Own the delivery observability and the retry policy
Emit rooting/KEX/envelope state as structured events, and own the reprime cadence. Both currently live in our Python because they had nowhere else to live.
What the agent will do
Our side of this is smaller than it looks, and mostly already present:
init_edge_runtimeis registered atInitializationPhase.DATABASE— before MEMORY/IDENTITY/SECURITY/SERVICES. Edge and the node identity are up ~4s into boot, well before the 22 services. The listener boundary is "stop after DATABASE", not a new split.BrainAdapter(upstream, runtime_boot=...)already implements the inverse topology and is passed by nothing. Its docstring: "runtime_bootremains for the inverse topology — a node that starts its own brain — which is not how the agent boots today." Wiring it is the wake path.FIRST_RUN_SERVICES = 10/RESUME_SERVICES = 12), though those ten are infrastructure and graph services with no transport — it proves the machinery, not the set.So we can meet a listener mode quickly. What we cannot do is define it: the inbox, the identity, and the transport all live below us, and building them here would reintroduce exactly the sidecar key-leak surface OQ-01 rejected.
Platform note
iOS cannot hold a background listener at all — every Reticulum iOS client reports this. The working model there is store-and-forward plus a push (propagation node holds, APNs notifies, app wakes and fetches), which
StoreAndForward+LxmfPropagationClientalready suggest is on-architecture. Worth confirming that the listener mode's contract covers "wake, drain, sleep" and not only "stay resident", so one design serves both platforms.Context: CIRISAgent#1131 (client-side process model and the Sideband reference), CIRISServer#528 (owner-binding prerequisite), CIRISClient#28 (test-server readiness).