Skip to content

feat: computed-upstream demand-driven subscription chains (piece D)#4661

Draft
sanity wants to merge 12 commits into
mainfrom
feat/computed-upstream-chains
Draft

feat: computed-upstream demand-driven subscription chains (piece D)#4661
sanity wants to merge 12 commits into
mainfrom
feat/computed-upstream-chains

Conversation

@sanity

@sanity sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

⚠️ PARKED — piece D (computed-upstream host-chains), the keystone

Highest-risk piece: it deletes the hollow-relay firefight and re-enables the very path #3763 disabled. REQUIRES Ian's explicit sign-off + the passing sim proofs before merge. Do NOT merge; do NOT enable auto-merge. Targets 0.2.91+, and B/E/F build on it, so it lands only after Ian's morning review. Stacked on #4657 (sim-harness); merge that first.

Problem

Hosting was holding-driven, not demand-driven, and subscription chains were hollow: a peer forwarding a SUBSCRIBE registered downstream interest but held no state and wasn't advertised as a host. Hollow chains create findability dead-ends (a GET routing toward a key lands on a non-hosting relay with nothing to serve), which the SubscribeHint placement migration papered over — and that migration became the 0.2.87 storm. The blunt anti-#3763 rule ("a relay is never a subscriber") is what created the hollow chains in the first place.

Solution

Implements the core of the (now-merged) design docs/design/demand-driven-hosting.md (§3–§6). There is no "relay" category: a peer on a live subscription chain is a real subscribed host.

  • §5a interest-gated renewalcontracts_needing_renewal §1 is gated on contract_in_use (a local client OR a downstream subscriber). Un-demanded subscriptions lapse; the chain collapses inward. This is the fix: include hosted contracts in subscription renewal #3763 storm bound (renewal ∝ active demand, not accumulated cache). The eviction budget (piece A) cannot bound it: an in-use subscription is eviction-exempt AND self-renewing, so the storm set is exactly the set the budget may not touch.
  • §4 computed upstreamRing::most_keyward_hosting_neighbor derives the upstream on demand from live neighbor-hosting advertisements + ring distance (the connected host closest to the key that is strictly closer than self), never a stored formation flag. Strictly-closer makes the upstream relation a strict descent → acyclic by construction; a deterministic address tiebreak gives a total order.
  • §5b/§5d renewal to the computed upstreamrun_renewal_subscribe targets the computed upstream directly (first_hop), so re-rooting is automatic: when a closer host appears or the upstream drops, the next renewal follows the new computation, with no dedicated migration path. A terminus (no closer neighbor) refreshes locally; a stranded host (closer non-hosting neighbors) routes a fresh keyward subscribe to re-root.
  • §3 real chain hosts — each subscribe hop becomes a real host after bubbling the reply upstream (fetch state, ring.subscribe, announce_contract_hosted), via the shared finalize_host_subscribe. Extended to piece C's terminal-advertisement consult path (a peer that subscribes via consult also hosts). The hollow-relay concept + its pin tests are removed and replaced with new-model pins.
  • §6 strict-farther downstream — a downstream is registered only when the peer is strictly farther from the key than us (our upstream is excluded). This is the collapse-side dual of the strictly-closer upstream rule: together they keep the chain acyclic and its collapse terminating, closing mutual self-perpetuation (two co-hosts propping each other up forever).

Testing

Simulation proofs (the gate) — all pass across ≥3 seeds, using the sim-harness's deterministic AdvanceHostingClock + node_*_count accessors:

  • test_subscription_count_tracks_demand_not_cache — no-storm: leases track active_demand_count, never hosting_count (cache); max_cycle_batch ≤ 10.
  • test_subscription_chain_collapses_on_client_leave — mesh forms, then collapses to 0 survivors after disconnect + clock jump.
  • test_chain_collapse_reaches_root_no_cyclesurvivors == 0 (no lone survivor = no self-perpetuating lease; no pair = no mutual-upstream cycle).
  • test_popular_contract_fanout_is_measured — fan-out is measured only (bounded by piece B admission, out of scope here).
  • test_relay_become_host_closes_deadendforces the relay-become-host path (chain_hosts_formed = 5–7) with a single distant SeedHostedContract holder + piece-C consult, and asserts a fresh mid-chain host holds state — a findability dead-end closed by a chain of real hosts (D's core value).

Plus unit tests: the interest-gated renewal bound (test_contracts_needing_renewal_bounded_by_active_interest adapted + a no-interest-lapses control), the new-model subscribe-driver pins, and most_keyward_hosting_neighbor.

Findings surfaced during validation

  1. Relay-become-host path — in a PUT-seeded topology the contract pre-replicates to ~half the peers, so subscribes resolve at hop 1 and the relay-become-host counter reads 0; the mesh still forms (via the originator path) and the safety proofs hold regardless. test_relay_become_host_closes_deadend exercises the relay path directly with a single distant SeedHostedContract holder + consult, so D's core value is demonstrated, not just documented.
  2. Demand-source divergence (cosmetic, pre-existing) — the InterestManager's local_client_count/downstream_subscriber_count can diverge from HostingManager's client_subscriptions/downstream_subscribers. Investigated + confirmed pre-existing (from feat: add delta-based state synchronization #2667) and non-blocking: every hosting/renewal/eviction/collapse decision — including this PR's gate — reads the HostingManager source (contract_in_use); the InterestManager counter only feeds broadcast-targeting + telemetry, never a hosting decision. This PR fixes only the misleading sim accessor Ring::active_demand_count to read the gate's own source. Reconciling the two managers is a separate follow-up (Reconcile drift between InterestManager LocalInterest counters and HostingManager subscription state #4660), deliberately kept out of this PR to avoid bloat/re-stale.

Refs #4642

🤖 Generated with Claude Code

https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR

[AI-assisted - Claude]

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Validation status (parked for @sanity's morning review)

All sim proofs pass, incl. §6-regression re-check. After the §6 strict-farther-downstream guard was added, I re-ran the safety proofs to confirm it didn't regress mesh formation/collapse — both pass across 3 seeds with survivors=0 (total collapse, no self-perpetuating lease, no mutual-upstream cycle):

  • test_subscription_chain_collapses_on_client_leave — mesh 6/4/6 → survivors 0/0/0
  • test_chain_collapse_reaches_root_no_cycle — formed_mesh 5/6/5 → survivors 0/0/0 (shape="none (total collapse)")
  • test_relay_become_host_closes_deadendchain_hosts_formed=5/7, mid-chain hosts hold state (dead-end closed)
  • test_subscription_count_tracks_demand_not_cache + test_popular_contract_fanout_is_measured — pass
  • Unit: most_keyward_among (computed-upstream selection: strict-closer, farther-or-equal excluded, tiebreak), interest-gated renewal bound, new-model subscribe pins.

Lint/format: piece-D code is cargo fmt + clippy clean. clippy --all-targets -- -D warnings surfaces 3 errors in files this PR does NOT touch (secrets_store/store.rs, bin/commands/update.rs, client_events/user_op_rate_limit.rs) — pre-existing base lints (clippy 1.94.0 version drift), not piece D; flagging for a base cleanup, not fixed here to keep scope tight.

Reviews: external codex review is being run and will be posted here. The adversarial Claude pass was interrupted twice by transient Anthropic API 529 Overloaded errors (recording the substitution per the multi-model-review rule); it will be re-run and posted before this leaves draft. Do NOT mark ready / merge until both reviews are posted, addressed, and you've signed off.

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review (external, non-Claude) — clean

Ran codex review --base feat/sim-harness-hosting (OpenAI codex-cli 0.141.0), which was unaffected by the Anthropic overload. Verdict:

I did not identify any discrete, actionable correctness issues in the diff.

(Codex could not run cargo in its sandbox — no network for dep resolution — so this is a diff-read review; the build/tests/clippy are verified locally, see the validation-status comment above.)

Adversarial Claude pass re-running now; will post when it lands.

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Adversarial Claude pass — blocked by repeated Anthropic 529 overload

The adversarial Claude reviewer was re-attempted and hit a transient 529 Overloaded again (third time). It reached the high-risk-surface areas and flagged them clean before dying, but did not produce a final structured report. Recording the substitution per the multi-model-review rule.

Review posture for merge: the external non-Claude pass (codex — clean) is the load-bearing independent signal and it passed; the design was Ian-approved and merged (demand-driven-hosting.md); all 5 sim proofs + unit tests pass; codex found nothing. Before this leaves draft, please re-run the adversarial Claude pass once the overload clears (coordinator is pacing retries) so the Full-tier review is complete. No blocking findings from any pass so far.

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Adversarial Claude review — completed, NO BLOCKERS

The adversarial pass finally completed (after three 529s). Verdict: no blockers. The acyclicity core, forward-before-contract-handling ordering, double-host prevention, and lock-safety are all confirmed sound. One MAJOR (verify) + MINORs. Full findings + my triage:

MAJOR (verify before merge) — §1 renewal now depends on HostingManager.client_subscriptions

contract_in_use reads client_subscriptions/downstream_subscribers. Pre-PR, §1 renewed all active leases; now it gates on contract_in_use, so any subscribe path that installs a lease but registers demand only in InterestManager (not add_client_subscription) would silently stop being renewed.

  • Triage / partial verification: per design §5a the gate is correct (a demand-less lease should lapse). Verified the common paths are covered: WS-client and GET-with-subscribe:true register client_subscriptions via the client_events listener path (client_events.rs:98/1194), separate from finalize's add_local_client; chain hosts have a downstream_subscriber. Residual to confirm before merge: a purely-internal run_executor_subscribe with no client/downstream — does it represent real demand that must be renewed, or is lapsing correct? Action: add a regression test asserting every legitimate client-subscribe entry point makes contract_in_use true. Not a confirmed bug; a required verification.

MINORs (my triage)

  • Directed-renewal no within-cycle fallback (op_ctx_task.rs:460): if the computed upstream dead-ends, the renewal fails that cycle, self-heals in ≤30s. Accept (bounded, self-healing); optionally add a greedy fallback later.
  • feat: track local client interest to enable safe subscription renewal and fast cache serving #3769 local-access excluded from §1 (hosting.rs): a local-GET-without-subscription contract gets a ≤30s update gap per ~8-min lease period (§3 re-subscribes reactively). Accept + document as deliberate (local-access is a reactive-only signal); narrow + self-healing.
  • §6 early-return skips PUT of a fresh contract id gives up broadcast before subscriptions resolve → state lands locally-only, remote nodes get NotFound (and fdev reports success) #4359 flush for a closer new subscriber (subscribe.rs:658): the finalize_host_subscribe upstream-registration path also flushes toward closer peers, so not generally lost. Follow-up test that a mis-rooted subscriber doesn't strand a deferred PUT.
  • Hint/fallback-routed closer previous-hop declined (subscribe.rs:653): a behavior change vs pre-PR for low-connectivity/hint cases; directed renewals are safe by construction. Accept per §6 intent.
  • Tiebreak doc "addressless peers sort last" is inaccurate (None sorts first; currently dead because the caller pre-filters on location()). Will fix the comment (trivial).
  • Distance PartialEq(epsilon) vs Ord(exact) inconsistency (location.rs, pre-existing): not triggered here; flag for a base cleanup.

INFO

  • Per-hop body-fetch/storage amplification is by design (piece D), bounded by interest-gated collapse — noted for capacity planning.

Merge posture: codex (external) clean + adversarial clean-of-blockers. The MAJOR verification + its regression test, and the two follow-up tests, should land before this leaves draft. @sanity to sign off.

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Review findings addressed

MAJOR (resolved — verified NOT a false-lapse bug, 4900a898). The §1 renewal gate depends on HostingManager.client_subscriptions/downstream_subscribers. Traced every subscribe entry point:

  • WS-client SUBSCRIBE and GET-with-subscribe:true both register client_subscriptions via the RegisterSubscriberListenerResponse handler (client_events.rs:98/:1194ring.add_client_subscription) — exactly the map contract_in_use reads. So a genuinely-subscribed contract is always renewed. ✅
  • A chain host renews via its downstream subscriber. ✅
  • run_executor_subscribe installs a lease but registers interest only in the InterestManager (add_local_client), not client_subscriptions. With no accompanying WS client / downstream (a PUT/seed with no ongoing interest), the lease correctly lapses — per design §1 a PUT only seeds and is not demand; idle contracts evaporate. This is intended piece-D behavior, not a false-lapse. The executor subscribe's real demand, if any, is the WS client that PUT/subscribed, tracked separately in client_subscriptions.

New regression test test_active_lease_renewed_iff_contract_in_use pins all four cases (seed lapses; client-sub renews; downstream renews; client-sub removal → lapse).

Doc fix (done, 4900a898). Corrected the most_keyward_among tiebreak comment (addressless peers are pre-filtered by the caller via location()? and never reach the helper).

MINORs (tracked — #4662). The three self-healing edges (§6 skips #4359 flush for a closer subscriber; directed-renewal within-cycle fallback; #3769 local-access §1/§3 gap) plus the pre-existing Distance Eq/Ord inconsistency are filed as #4662 rather than bloating this PR (one logical change). None are correctness bugs.

Review complete: codex (external) clean + adversarial Claude no-blockers + the one MAJOR verified/pinned. Ready for @sanity's sign-off.

@sanity

sanity commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Chain-host state-acquisition hardening (05ff1df) + relay-caching-masking note

Piece E's relay-caching removal surfaced that the piece-D sim proofs were passing on relay-caching contamination: with relay-caching present the contract body was already local (has_contract hit instantly), so finalize_host_subscribe's one-shot best-effort 2s fetch always succeeded. Without relay-caching a just-subscribed chain host is genuinely body-less, and the deferred-announce "UPDATE will fill the gap" fallback is broken (an UPDATE to a body-less node fails "missing contract").

Hardening added: on inline-fetch timeout, now spawns a bounded background retry that keeps pulling the body (greedy GET + terminal-advertisement consult, robust to a still-forming body-less responder), then announces once it lands — the §5d proximity/summary-sync, deferring announce/serve until synced (invariant 1). Bounded by body-present / demand-fade / attempt budget. It is a no-op with relay-caching present (inline fetch still succeeds), so it does not change this branch's passing proofs; it is the correctness prerequisite for E.

Not yet resolved (reported to Ian for direction): validating with relay-caching REMOVED (cherry-picked E's PUT-removal onto a throwaway branch), the state-acquisition fix is necessary but not sufficient — 3/5 proofs still fail because the dominant issue is subscribe reachability, not body-landing: with 8 concurrent subscribers and a single holder, ~6/8 subscribes dead-end (NotFound, no lease; chain_hosts_formed=1, max_upstreams=0; seed-dependent — some seeds form a mesh of 5). Removing relay-caching drops host/advertisement density, and subscribe routing + neighbor-scoped consult can't reliably bridge far subscribers to the single holder. That is a broader convergence/mesh-bootstrap question than the scoped state-acquisition fix. See report to coordinator.

sanity and others added 12 commits July 3, 2026 18:07
…4642)

The demand-driven hosting/subscription redesign (epic #4642) cannot be
validated with behavioral simulation tests because the harness is missing
several capabilities. Three separate agents hit concrete gaps:

- **Eviction (piece A):** `HostingManager` hardcoded `InstantTimeSrc`, so a
  sim could not fast-forward the 8-minute TTL gate; and there was no per-node
  hosting-budget knob to force cache pressure. Eviction work fell back to
  cache-level unit tests.
- **Re-subscribe (piece F):** no way to crash a specific peer at a scripted
  point in the controlled runner, and the subscription-tree upstream edge was
  not observable from a sim (the topology snapshot hardcodes empty edges).
- **Host-chains / no-storm proof (piece D):** no per-node measurement of
  subscription / hosting / active-demand counts to detect a #3763-style storm
  signature (subscriptions ∝ cache vs ∝ active interest).

Test-infrastructure only; production behavior is unchanged (all injection is
via test/sim config that is `None` in production, mirroring the existing
`governance_config_override` pattern).

- **Injectable hosting clock (piece A).** `HostingManager` now holds an
  `Arc<dyn TimeSource + Send + Sync>` (blanket `impl TimeSource for Arc<T>`
  added). `HostingManager::new` keeps the production wall clock;
  `with_time_source` injects a controllable one. Threaded through
  `NodeConfig::hosting_time_source_override` → `Ring::new`. `SharedMockTimeSource`
  is made available outside `#[cfg(test)]` so the harness can own one.
  `SimNetwork::enable_hosting_time_control()` + `SimOperation::AdvanceHostingClock`
  advance it in-order under the turmoil runner.
- **Per-node hosting-budget knob (piece A).** `SimNetwork::with_hosting_budget`.
- **Per-node measurement accessors (piece D).** `Ring::active_subscription_count`,
  `Ring::active_demand_count` (via new `InterestManager::active_demand_count`,
  excludes cache-only hosting), `Ring::upstream_interest_count`, exposed on
  `ControlledSimulationResult` as `node_hosting_count` / `node_subscription_count`
  / `node_active_demand_count` / `node_upstream_count` / `captured_node_labels`.
- **Scripted mid-run crash (piece F).** `SimOperation::CrashNode` crashes a
  named peer via the fault injector, in-order with other events.

- Unit: `test_with_time_source_lease_expiry_follows_injected_clock` proves the
  injected clock drives subscription-lease expiry deterministically (manager
  level).
- Sim: `test_hosting_clock_injection_and_measurement_end_to_end` exercises the
  controllable clock + budget knob + `AdvanceHostingClock` + measurement
  accessors end-to-end; `test_scripted_node_crash_mid_run` exercises
  `CrashNode`. Both deterministic and CI-runnable.
- `cargo fmt`, `cargo clippy --locked -p freenet -- -D warnings` (and
  `--features trace-ot`) clean; production build clean.

PARKED for Ian's review — DO NOT MERGE.

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
… fix

- Add ring::interest::tests::test_active_demand_count_excludes_cache_only_hosting
  directly asserting the filter (hosting-only excluded; client and downstream
  demand included; no double-count), per testing.md's edge-case requirement.
- Merge the SharedMockTimeSource doc note into the opening summary so it no
  longer renders under the pre-existing `# Example` heading.

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
The codex-review pass compiled a throwaway /tmp/coerce.rs with rustc, which
emitted a 'coerce' ELF into the worktree root; a subsequent git add -A picked
it up. Remove it — it is unrelated to the PR and must never land in the tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
…count test

Addresses rule-review findings:
- Add test_advance_hosting_clock_without_control_is_graceful_noop exercising the
  error path where AdvanceHostingClock is scheduled without enable_hosting_time_control
  (graceful no-op, sim completes).
- Replace the duplicate assertion in test_active_demand_count_excludes_cache_only_hosting
  with a distinct check that the hosting-only contract is tracked as interest yet
  excluded from active demand (the interest-vs-demand distinction).

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
Implements the core of docs/design/demand-driven-hosting.md (§3-§5) on top of
the parked sim-harness branch. There is no "relay" category: a peer on a live
subscription chain is a real subscribed host.

- Interest-gated renewal (§5a): gate `contracts_needing_renewal` section 1 on
  `contract_in_use` so a subscription is renewed only while it has active demand
  (local client or downstream subscriber). Un-demanded subscriptions lapse; the
  chain collapses inward. This is the #3763 storm bound (renewal ∝ active
  demand, not accumulated cache); the eviction budget can't bound it because
  in-use subscriptions are eviction-exempt AND self-renewing.

- Computed upstream (§4): `Ring::most_keyward_hosting_neighbor` derives the
  upstream on demand from live neighbor-hosting advertisements + ring distance
  (the connected host closest to the key that is strictly closer than self),
  never a stored formation flag. Strict-closer makes the relation acyclic by
  construction; a deterministic address tiebreak gives a total order.

- Renewal to the computed upstream (§5b/§5d): `run_renewal_subscribe` targets the
  computed upstream directly (`first_hop`), so re-rooting is automatic — when a
  closer host appears or the upstream drops, the next renewal follows the new
  computation. A terminus (no closer neighbor) refreshes locally; a stranded
  host (closer non-hosting neighbors) routes a fresh subscribe to re-root.

- Real chain hosts (§3): each subscribe hop becomes a real host after bubbling
  the reply upstream (fetch state, ring.subscribe, announce), via the shared
  `finalize_host_subscribe`. Extended to piece-C's terminal-advertisement
  consult path (a peer that subscribes via consult also hosts). Replaced the
  hollow-relay pin tests with pins that assert the new model + the
  forward-before-hosting ordering. `chain_hosts_formed` sim instrumentation.

WIP: sim proofs (no-storm / collapse-on-leave / no-cycle / fan-out) using the
AdvanceHostingClock + demand-count harness, a unit test for
most_keyward_hosting_neighbor, and the §6 strict-farther-downstream refinement
are the remaining validation before the PR opens. Parked bundle, targets
0.2.91+; NOT for merge without Ian's sign-off + passing sim proofs.

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
…ions

Add the load-bearing sim validation gate for piece D (#4642) in
crates/core/tests/simulation_integration.rs. Four #[test_log::test] proofs,
each looping >=3 seeds, all passing:

1. test_subscription_count_tracks_demand_not_cache (no-storm, design §7 / #3763)
   A hub subscribes to 2 contracts and GETs 8 more (cache-only), then the
   hosting clock jumps 20 min past the recent-access window. Result across 3
   seeds: hosting_count=10, subscription_count=2, and NETWORK-WIDE cache-only
   leases=0 while demand leases=2. Renewal tracks active demand, not cache;
   max_cycle_batch<=10.

2. test_subscription_chain_collapses_on_client_leave (§5a/§6)
   Two-phase (shared seed/topology): a formation run builds a real multi-node
   subscription mesh (size 6/4/6), a collapse run disconnects every client +
   jumps the clock 20 min and the mesh collapses to 0 survivors. Interest-gated
   renewal keeps no un-demanded lease alive.

3. test_chain_collapse_reaches_root_no_cycle (§6/§7)
   Same scenario, asserting EXACTLY zero survivors — a lone survivor would be a
   self-perpetuating lease, a pair a mutual-upstream cycle; strict-distance
   computed-upstream (acyclic) forbids both. formed_mesh 5/6/5 -> survivors 0.

4. test_popular_contract_fanout_is_measured (measure-only)
   15 nodes subscribe to one contract; mesh size 10/11/11 measured and
   reported. No upper bound asserted — fan-out is bounded only by admission
   (piece B), which is not in this piece.

Findings recorded in the test docs (harness characteristics, NOT piece-D
defects):
- chain_hosts_formed and max_upstreams stay 0 in every achievable topology: a
  PUT pre-replicates the contract to ~8/16 peers, so subscribes resolve at the
  first hop (relay_subscribe_local_hit) and never traverse a non-hosting relay.
  Confirmed by a single-subscriber sweep across all 15 nodes. The mesh forms via
  the ORIGINATOR path, so formation is proven by mesh SIZE, not that counter.
- active_demand_count (InterestManager local_client_count) reads a different map
  than the renewal gate contract_in_use (HostingManager client_subscriptions);
  it reports 0 for demand-backed leases in the sim, so proof 1 categorizes
  surviving leases demand-vs-cache network-wide rather than comparing to it.

Drive-by: fix a pre-existing needless_range_loop clippy warning in
test_placement_migration_at_scale_renewal_load_stays_bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
Two follow-ups to the computed-upstream subscription model:

- §6 strict-distance stop-hosting rule (demand-driven-hosting design §6/§7):
  `register_downstream_subscriber` now registers a downstream ONLY when the peer
  is STRICTLY FARTHER from the contract's key than us (resolvable-closer peers —
  our upstream — are skipped). This makes the downstream set strict-farther by
  construction, so `contract_in_use` (any-downstream) is effectively the §6
  predicate. It is the collapse-side dual of the computed upstream's "strictly
  closer" rule: together they keep the chain acyclic and its collapse
  terminating, closing the mutual-self-perpetuation (two co-hosts propping each
  other up) that the strict-distance total order forbids. Defense-in-depth: the
  computed-upstream renewal direction already makes downstreams strict-farther in
  practice (proven by the passing no-cycle sim), but this enforces it at the
  registration boundary too. Unresolvable-location peers are registered
  conservatively; the lease-expiry backstop bounds any mistake.

- Finding 2: `Ring::active_demand_count` (the #3763 no-storm denominator read by
  the sim harness) now reads `HostingManager::in_use_contract_count` — the SAME
  `client_subscriptions`/`downstream_subscribers` maps the renewal gate consults
  — instead of the InterestManager's separate `local_client_count`/
  `downstream_subscriber_count`, which can diverge and mis-report the leases the
  gate actually keeps alive (it read 0 demand for genuinely-demand-backed,
  renewed leases). A read-only investigation of whether the InterestManager
  source drives any real decision is separate; this fixes the misleading
  measurement accessor.

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
The prior subscription-mesh proofs (proofs 2-4) formed the mesh via the
ORIGINATOR path only: a PUT pre-replicated the contract to ~half the
peers, so every subscribe resolved at hop 1 on an already-hosting
neighbor and the relay-become-host counter `chain_hosts_formed` never
incremented. Those proofs therefore never exercised piece D's whole
reason to exist (design doc demand-driven-hosting.md §3): a findability
dead-end (a single distant holder unreachable by greedy routing) being
CLOSED by a relay that becomes a REAL host mid-chain.

This adds `test_relay_become_host_closes_deadend`, which seeds the
contract on EXACTLY ONE distant holder (off the greedy path) so a
subscribe MUST traverse a non-hosting cluster relay that then consults
the holder's advertisement and hosts the contract itself. It asserts
the piece-D outcome the sibling
`test_terminal_advertisement_consult_closes_subscribe_dead_end` does not:
`chain_hosts_formed > 0` AND a fresh mid-chain cluster host holding real
state (the discriminator against the pre-piece-D hollow relay, which
registered downstream interest but held no state).

Passes on both seeds (chain_hosts_formed = 5 and 7; every cluster peer
1..=6 ends up a real host with state). Seed ...0011 reaches 7 with
consult_resolved_found = 0, showing the chain also forms via the greedy
Subscribed-reply path, not only the terminal consult.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
Extract the strict-closer + deterministic-tiebreak core of
`Ring::most_keyward_hosting_neighbor` into a pure `most_keyward_among` helper
(mirroring `no_closer_routable_neighbor`) so the computed-upstream selection
(design §4/§6) has direct unit coverage without a heavyweight async Ring
fixture. Tests: picks the most-keyward strictly-closer neighbor; excludes
farther-or-equal (strict `<` = acyclicity); None when none closer
(terminus/stranded); deterministic socket-addr tiebreak on equal distance.

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
Resolves the adversarial-review MAJOR: the interest-gated §1 renewal now
depends on `HostingManager.client_subscriptions`/`downstream_subscribers`.
Verified + pinned that the gate does NOT false-lapse a demanded contract:

- Every WS-client subscribe entry point (plain SUBSCRIBE and
  GET-with-subscribe:true) registers `client_subscriptions` via the
  RegisterSubscriberListenerResponse handler (client_events.rs:98/:1194 →
  ring.add_client_subscription) — the exact map `contract_in_use` reads — so a
  genuinely-subscribed contract is always renewed. A chain host renews via its
  downstream subscriber.
- A bare `run_executor_subscribe` lease (registers only InterestManager
  add_local_client, no client_subscription/downstream — a PUT/seed with no
  ongoing interest) is NOT renewed and lapses. This is CORRECT per design §1
  (a PUT only seeds; idle contracts evaporate), not a false-lapse.

New `test_active_lease_renewed_iff_contract_in_use` covers all four cases
(seed lapses; client-sub renews; downstream renews; client-sub removal →
lapse). Also fixes an inaccurate `most_keyward_among` tiebreak doc-comment
(addressless peers are pre-filtered by the caller, never reach the helper).

Refs #4642

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnRCbtbjepaB5GWrb5U8SR
finalize_host_subscribe used a one-shot best-effort 2s fetch; on timeout it
deferred the announce and relied on "UPDATE propagation" to fill the gap. With
relay-caching present the body was already local (has_contract hit instantly),
so the fetch always succeeded and this was never exercised — the piece-D sim
proofs were passing on that contamination. With relay-caching removed (piece E)
a just-subscribed chain host is genuinely body-less until it syncs from a
connected co-host, and the deferred-announce fallback is broken (an UPDATE to a
body-less node fails "missing contract").

Fix (design §5d proximity/summary sync): on inline-fetch timeout, spawn a
bounded background retry that keeps pulling the body (greedy GET + terminal-
advertisement consult finds the announced holder, robust to a still-forming
body-less responder), THEN announces once it lands — so the node becomes a
findable/computable host and joins the update mesh. Announce stays deferred
until the body is present (invariant 1's "state in flight during resync"
transient). Does not block the originator's subscribe_success (#4223 bounded
path preserved). Bounded: stops on body-present, demand-fade (contract_in_use
false), or attempt budget.

No-op with relay-caching present (inline fetch still succeeds, retry never
spawns), so it does not change behavior on the piece-D branch; it makes chain
hosts converge once E removes relay-caching.

Refs #4642
…t lease

The 0.2.91 fallback ('ship D+B+F, keep relay-caching') was flagged for an
unverified interaction: does D's interest-gated subscription renewal lapse
relay-cached / GET-auto-subscribe copies out of the update mesh, making
D-minus-E a de-facto partial E?

Investigation (this branch) shows it does not:
 - Relay-caching stores (GET return-path cache_contract_locally, PUT-relay
   store) install NO active_subscriptions lease -- they are cache-only
   (PutQuery + record_get_access + register_local_hosting +
   announce_contract_hosted). Renewal never governed them; D's contract_in_use
   gate (contracts_needing_renewal §1) cannot touch them.
 - Update-mesh membership (get_broadcast_targets_update: proximity announce +
   interested_peers) is driven by LOCAL HOSTING interest advertised via the
   5-min interest_heartbeat (get_all_interest_hashes), which has no lease and
   no TTL. It persists until hosting-cache eviction (piece A), independent of
   the subscription lease D gates.
 - GET-auto-subscribe DOES install a lease that D lapses when contract_in_use
   is false, but the copy stays HOSTED and in the mesh (existing sim test
   test_subscription_count_tracks_demand_not_cache: cache-only contracts hold
   ZERO leases yet remain hosted). What lapses is the redundant #3763 storm
   lease, not the cached copy or its freshness.

These two unit tests pin the positive half (mesh advertisement survives the
lease it never depended on); hosting.rs::test_active_lease_renewed_iff_contract_in_use
already pins the negative half (a demand-less lease correctly lapses).

[AI-assisted - Claude]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant