seller: boot the buzz persona from the production daemon (#199) - #207
Open
orveth wants to merge 4 commits into
Open
seller: boot the buzz persona from the production daemon (#199)#207orveth wants to merge 4 commits into
orveth wants to merge 4 commits into
Conversation
added 4 commits
July 27, 2026 16:56
The seller daemon's boot now brings up the buzz persona when `[buzz]` is configured and clears its presence when the node stops cleanly, so "this seller has a persona on buzz" is true of the path `mobee sell` runs — not only of the capability behind it. Bounded and degrading by construction: the bring-up cannot outrun BUZZ_START_TIMEOUT, and no buzz outcome can keep the node from selling — a relay it cannot reach logs BUZZ DEGRADE and boot continues. With no `[buzz]` section nothing is opened, published, or logged. A stop request (SIGTERM/SIGINT) ends the loop so presence clears at exit rather than expiring on the relay's TTL. The receivers are installed ONLY for a daemon carrying a live persona: registering one replaces the process default, so a buzz-inert daemon installs nothing and dies exactly as it does today. Teeth (seller_node::buzz_wire_it, in-process relay): - configured vs absent as one differential, one observer, both keys: the configured node is the positive control for the other's silence - a dead buzz relay boots anyway, bounded - the signal receivers stay uninstalled without a live persona
The new test file is clean outright under the workspace edition; run.rs keeps its prevailing style, with only the two new constructs formatted so the diff adds no formatting divergence of its own.
The bound was a backstop asserted only by construction, and a relay that hangs mid-conversation — handshake complete, then silence — is the failure it exists for. That case is now driven by a local relay whose query and write policies never resolve. What it asserts is the arm that ran, not a duration: the bring-up's own bounds refuse the hang (kind-0 fetch 8s + the SDK's 10s OK-wait = 18s) while the 25s bound stays a backstop. The call has returned, so there is no window to be wrong about and nothing that rots when a machine gets slower. A dependency bump that pushes that sum past the bound flips this to TimedOut and turns it red — which is the day the backstop starts bearing load and someone needs to know. The arms had to become nameable for that, so the bring-up now reports a BuzzStartOutcome and the boot path keeps the logging.
clippy's large_enum_variant fires on `Live`, and boxing it would trade a one-off few hundred stack bytes for an allocation on every boot. The exemption carries its reason, matching the four other allows in this crate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
seller: boot the buzz persona from the production daemon (#199)
SellerNode::start_buzzwas merged, fenced and live-verified (#158) — and nothing in the shipping daemon ever called it. "This seller has a persona on buzz" was true of the capability and false of the pathmobee sellruns. This wires it, and nothing else.What changed
SellerNodeRunner::bootbrings the persona up when[buzz]is configured, as the last step of boot — after the marketplace relay is connected and authenticated, so buzz never sits in front of the money path's connect. The handle is held for the node's lifetime.The bring-up is bounded and degrades.
BUZZ_START_TIMEOUT(25s) caps it — the legs insidebuzz::startare individually bounded but the publish is not — and no buzz outcome can stop the node from selling (sell: startup doctor readiness gate — refuse to boot a box that can't sell (closes #107) #149's brick class): a relay it cannot reach logsBUZZ DEGRADEand boot continues with no persona. With no[buzz]section the call is inert: no connection, no publish, no log line.A clean stop clears presence instead of leaving the persona advertised as online until the relay's ~60s TTL expires it. Once the run loop is entered its only exits are the two
breaks, and both fall through to the clear. A crash still falls back to TTL expiry — that is the crash path, unchanged.Process semantics change, for buzz-configured daemons only:
mobee sellnow exits cleanly on SIGTERM/SIGINT. This is the point pr-feedback should look at hardest, so it is stated rather than buried. Registering a tokio signal receiver REPLACES the process default for that signal — installing one unconditionally would silently stop every seller daemon from dying onkill -TERM. So the receivers are installed only when a persona is live, the loop's arm is guarded the way it already guardsheartbeat_enabled, and a buzz-inert daemon installs nothing and dies exactly as it does today. Both halves are toothed, and both are proven on the real binary below. Blast radius at merge: zero — no production seller runs a[buzz]config today."tokio/signal"joins thewalletfeature onmobee-core(the feature that gatesseller_node). Unix only; other platforms keep the platform default.What it does NOT do
No mention handling, no bridge, no new capability. Zero changes inside
seller_node/buzz.rsor the signer actor — the signing allowlist{0, 20001, 22242}is untouched. This slice adds a CALLER. No wallet, budget or payment code is touched.Live proof — the real
mobee sellrelease binary, deployed buzzrelayRe-run after the rebase onto
8debf43, because #201 changed the boot path this slice wires into (it resolves anAgentRegistryat boot). Binarysha256 6ee9c8e1a47fb91712d83f2e5f77f3651b6969317407a29847b5c59cfa52bc71, built from this branch's tip. Marketplace leg pointed at a local in-memory relay, fresh throwaway key admitted for the run and revoked after; no production infrastructure, no money.Boot ⇒ persona live (2026-07-28T00:03:26Z)
The marketplace line precedes the persona line — the ordering claim in item 1, read off the log rather than argued.
The persona kind-0 is on the relay, with its rate card — fetched back afterwards through the same
/query, filter[{"kinds":[0],"authors":["d062f0a6…"]}]:Online in the relay's snapshot (00:03:48Z) —
POST /query, NIP-98, filter array[{"kinds":[20001],"authors":["d062f0a6…"]}]:SIGTERM ⇒ clean exit, presence gone (00:03:48Z), pid identity-checked through
/proc/<pid>/exe:/queryat 00:03:55Z →HTTP 200 []. That is 7s after the kill and ~29s after the last beat, so the 60s TTL cannot explain the absence: the clear is attributable to the shutdown path. The same query returned[]before boot, so both transitions are witnessed by the same instrument.Control — the inert half, same binary, no
[buzz](00:04:29Z):That is item 4's "unchanged" claim proven on a live process. (One line of that log matches "buzz" case-insensitively: the test's own
git_remote=https://example.invalid/buzzwire-live.git. Zero buzz-path lines.)Teeth —
seller_node::buzz_wire_it, in-process relay, 4 tests, 4 red-on-revert bites at rc=101They live in a NEW file so a four-deep merge train has one line of shared surface (the
moddeclaration).1. Configured vs absent, as one differential. Two nodes boot concurrently against one relay, one observer watches both keys: wired
kind0=1 presence=1, inert0/0. The configured node is the positive control, so the inert node's silence is evidence rather than an untested path. Bite — boot no longer calls the bring-up:2. A dead buzz relay never keeps the node from booting (invariant 2). A bound-then-released port: boot returns Ok bounded, with the DEGRADE line. Bite — propagate instead of degrade:
3. The signal receivers stay uninstalled without a live persona. Bite — drop the
persona_livegate:4. A relay that hangs is caught by the bring-up's INNER bounds, not by
BUZZ_START_TIMEOUT. A local relay whose query and write policies never resolve — handshake complete, then silence, which is the failure the bound exists for. It asserts the returned variant (Failed, notTimedOut), never elapsed time: the call has returned, so there is no window to be wrong about and nothing that rots when a machine gets slower. The arithmetic is in the test's docs — kind-0 fetch 8s + the SDK'sWAIT_FOR_OK_TIMEOUT10s = 18s against a 25s bound, and measured at 18.005s against an 18s prediction read off the constants. A red here means a dependency bump pushed that sum past the bound and the backstop has started bearing load. Bite — shrink the window to 15s:Gates
Branch is rebased onto
8debf43(#201). Every figure below was re-measured on that base, on both sides — nothing is carried from the pre-rebase run, because the base moved and a comparison is only valid against the tree it was taken on.cargo test --workspace→ rc=0, 665 passed, 0 failed on this branch; pristine8debf43→ rc=0, 661 passed. Delta = exactly the 4 teeth.--no-default-features·gateway·git-delivery·wallet·acp· all four together · bin default · bin+acp· bin+stub-pay·cargo check --workspace --all-targets. Release binary builds.--features gateway,git-delivery,wallet --all-targets -D warnings): 47 errors on this branch, 47 on pristine8debf43, per-file counts identical. Zero in the new test file;run.rscarries one on both sides. (The old base yielded 57 — seller recovery: order the resubscribe after NIP-42 (#189 client half), and give the open-pool re-arm an owned schedule (#190) #198/seller: multi-harness registry, wire advertisement, and harness-aware awarding #201 removed ten, which is exactly why this was re-measured rather than restated.)run.rsis NOT whole-tree formatted (pristine8debf43already yields 90 hunks under this toolchain, this branch also 90); the normalized hunk set-difference pristine-vs-branch is a singleuse super::{…}line that is already a pristine hunk — this diff adds one name to it.#[allow(clippy::large_enum_variant)]onBuzzStartOutcomecarries its reason: exactly one exists per process boot and it is destructured immediately, so boxing would trade a one-off few hundred stack bytes for a per-boot allocation. Four other allows in this crate, one already inrun.rs.cargo check -p mobee --no-default-features(sell.rsimports the feature-gatedmobee_core::profile).mobeeshipsdefault = ["wallet"], so no shipped configuration is affected — it is outside the all-combos gate, found while widening the matrix.ShutdownSignals(no Windows target installed on this box). It is written to keep the platform default —installanswersNone— matching the crate's existing posture, wherewalletalready carries#[cfg(unix)]paths.Found while wiring — filed, not fixed here
#202: the kind-0 clobber guard fail-closes on
Errbut readsOk(empty)as "no persona on this key".fetch_eventsreturnsOk(<empty>)when a relay never answers, so a dead socket — or auth-pending, or #189'srestricted:path — makes the node clobber a foreign kind-0 it merely could not see. The dead-relay tooth's failure lands at the kind-0 publish, which is only reachable past the guard. That is persona logic insidebuzz.rs, which this slice does not touch.