Skip to content

fix(regions): exclude planned paths from observed history - #507

Merged
dborup merged 6 commits into
masterfrom
codex/regions-observation-history
Oct 20, 2026
Merged

dborup merged 6 commits into
masterfrom
codex/regions-observation-history

Conversation

@dborup

@dborup dborup commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fix the Regions history limitation disclosed in #505: a planned DIRECT path must never acquire observed scope membership merely because another observation of the same content transmission was FLOOD.

This is a separate correctness follow-up to #505. #501–#505 are merged and this PR now targets master (merged in at c6574b46). No merge or deployment is included.

Changes

  • Use one own-observation provenance gate for Regions memberships and directed steps: an explicit scoped transport-FLOOD header, known matching transport bytes, and a nonzero canonical scope code. Missing/mismatched evidence fails closed; known DIRECT and unscoped observations are deliberately excluded.
  • Build a separate, private Regions membership snapshot. Latest, Timeline, Health and shared-history pairs no longer inherit the legacy canonical-route/display-path attribution.
  • Retain valid non-display or single-hop FLOOD observations, including when the canonical transmission is DIRECT. Count each transmission/node/scope once despite repeated observers, paths or loops.
  • Preserve Latest's rolling window, including the current hour; history and directed steps still exclude the unfinished UTC hour.
  • Classify every observation of a scoped transmission that supplies no membership as excluded (identified as not eligible: known DIRECT/unscoped header, or own transport codes that differ from the stored frame; forwarders never rewrite transport codes), unprovable (unknown or malformed own header, own codes or canonical scope code) or capped (observations or memberships omitted by safety limits). Counts are attributed to the transmission's stored scope and FirstSeen hour and published as coverage.evidence (named Go structs): totals, per-scope totals and, for history/routes, per-bucket cells. Latest sums the requested rolling window.
  • Withhold rates, chart values and comparisons only for intervals that include an unprovable or capped bucket of the selected scope (any scope for "all scopes"). Exclusions never make counts incomplete. A bounded 128-scope × 505-hour evidence budget sets coverage.evidence.truncated, which fails closed for every rate and comparison. Publication caps stay snapshot-wide.
  • stepEvidence splits excluded/unprovable/capped observations; directed-step, per-transmission pair and cell caps are counted separately as cappedStepAppearances and never reach coverage evidence. Unresolved hop tokens qualify directed steps (adjacency), not the memberships of uniquely resolved relays.
  • Update provenance text and tests. Existing data is not rewritten or deleted. Historical counts may decrease after removing unprovable planned paths, or increase when valid flood observations were previously hidden by canonical DIRECT metadata.

What "relay appearances" now means

A relay appearance is one transmission counted once for each distinct relay identified on any observer's own attributable scoped-flood path, the union across observers, not only the displayed path. Repeated observers, identical paths and loops count once. #503/#505 history was attributed through the canonical display path and its candidate index, so relays seen only on other observers' paths were not counted. Totals therefore rise where several observers hear the same transmission along different paths, and more observers can reveal more relays for the same traffic. This is the mechanism behind staging's 24 h Health total rising from ~28k to 104k while observed relay nodes went from 378 to 383. A regression test pins it: one transmission observed via [A,B], [C,D], a repeat of [A,B] and a loop [B,A,B] gives 4 appearances on this branch and 2 (A and B only) on c6574b46. The UI Health note, Timeline details and docs now state this definition.

The generic node liveness, public relay statistics and traffic-share algorithms are intentionally unchanged. Region names still reuse stored canonical scope attribution; matching frame evidence is not independent authentication of a region name or proof of a cross-scope forwarding event.

Bounded work and performance

Capture still occurs once per transmission under the existing snapshot lock. Resolution and membership aggregation run outside it; warm handlers do not scan packets or acquire the packet-store lock. The existing shared capture limits remain 250k path variants, 128 per transmission, 8,192 bytes per path and 64 tokens. Capture now includes the current hour for Latest. Membership has independent 1M-entry / 10k-identity budgets and retains 32 newest scopes per identity; directed-step/edge-cell caps cannot suppress valid singleton membership. Limits are disclosed, not used as a ranking of all evidence. No per-node API calls, schema/config changes or new dependencies.

Matched local synthetic benchmark, Apple M5 / Go 1.26.3, 30k transmissions / 2k relays, ten iterations per version:

Workload Base #505 This change
Whole background snapshot 101.95 ms; 102.45 MB allocated 100.26 ms; 132.99 MB allocated
Capture under read lock 7.95 ms; 13.08 MB allocated 9.95 ms; 13.08 MB allocated
Warm paired response 14.44 ms; 10,418,506-byte JSON 15.53 ms; 10,418,533-byte JSON

Round 2 (per-bucket evidence), local Linux x86-64 (4 vCPU), 5×5 iterations, median, decedf15 (before) → 18c6e3fd:

Workload Before After
Whole background snapshot 132.1 ms; 130.2 MB allocated 139.2 ms; 130.2 MB allocated
Same, realistic evidence mix (8 scopes; direct, mismatched and unknown observations) 137.3 ms; 137.4 MB 144.2 ms; 137.5 MB
Capture under read lock 11.7 ms; 13.1 MB 12.0 ms; 13.1 MB
Warm paired response, mixed 32.0 ms; 10,858,541-byte JSON 32.5 ms; 10,868,567-byte JSON

Evidence classification adds O(1) map updates per non-eligible observation inside the existing capture loop; per-bucket aggregation, sorting and publication run outside the lock. The worst-case evidence payload at the full budget is 3,673,684 bytes (64,640 cells); with 48–168 h retention only populated hours of active scopes are emitted.

These are noisy single-host measurements, not a speedup claim or production guarantee. Strict membership aggregation costs additional transient allocations (about 30.5 MB in the matched run); three-iteration repeats measured 109–167 ms / 140–161 MB allocated for the whole background snapshot. Published membership state retains bounded latest scope summaries, not the temporary per-transmission entries. This work runs on the existing background refresh, not per request. Future configurable display/budget limits remain tracked in the Regions documentation; protocol validity checks are not tunable thresholds.

Validation (round 2, head 18c6e3fd)

  • Test-first: 3e475793 added the red tests (per-bucket/scope evidence with a realistic mix of valid, excluded, unprovable and capped observations; excluded-only data is complete; step cap counted separately; unaffected buckets keep rates and comparisons in the client). They failed on 4c20a190, where excluded-only data was reported as evidenceIncomplete: true.
  • Mutants killed: global flag on any unprovable/capped (Go and JS); mismatch classified unprovable; rate ignoring evidence; step cap counted as coverage capped; step cap not counted; only the first observation path counted; per-transmission membership dedup removed.
  • Full cmd/server suite, -race on Region|Route|Relay|Evidence|412|470, full cmd/ingestor suite, sh test-all.sh (258/258), node test-frontend-helpers.js (719), Regions browser suite (34/34) and core browser regression (154 passed, 3 fixture-dependent skips) against a local Go server with the CI-prepared e2e-fixture.db. XSS diff preflight and Go shard assignment passed.
  • No staging or production access.
  • CI run 39033511696 on 18c6e3fd: every job passed on attempt 2. Attempt 1 failed once in the unrelated test-issue-76-heard-by-e2e.js Home card test, which passed 8/8 three times locally; see the round-2 report comment.

Validation (round 1, head 4c20a190)

  • Test-first backend regression reproduced planned-path admission, lost canonical-DIRECT/non-display flood evidence and unsafe scope attribution before the fix.
  • Full Go server suite passed (71.549 s); full ingestor suite passed (116.706 s). Parent-repeated Regions/Repeater race tests passed (7.707 s).
  • New regressions cover missing headers/codes, both transport-code mismatches, zero scope code, explicit DIRECT/unscoped exclusion, ambiguous prefixes, singleton membership, loops/observation deduplication, byNode-only candidates, current-hour boundaries, membership limits independent of edge limits, and unchanged generic relay output.
  • Full JavaScript suite passed all 257 files; repeated on the final source before push. Unit tests cover retention versus evidence completeness, disabled comparisons/rates and qualified observed counts.
  • Focused Regions browser suite passed all 37 checks, including a reviewer-discovered comparison-reason regression, desktop/mobile screenshots, XSS names, navigation, themes, keyed tables and request reuse.
  • Core browser regression passed 154 tests, with 3 existing fixture-dependent skips and no failures. UTC/DST-host checks passed in both America/New_York and Europe/Copenhagen.
  • A separate unmocked local database/API/browser test confirmed exclusion of two planned-only identities, retention of a canonical-DIRECT single-hop flood identity, current-hour Latest-only membership, a shared pair with two genuinely attributed identities, and the incomplete-evidence warning. No production/staging data was accessed or modified.
  • Lint, XSS file preflight and both shard manifests passed: 2,686 active Go entries and 167 E2E suites assigned exactly once.
  • Two independent reviews completed. A P3 wording issue (missing evidence falsely described as a reached safety cap) was reproduced red, fixed and regression-tested before the PR commit. No open actionable findings. The final remote commit was independently rechecked and reviewed on this PR.

Round 1 commit: 4c20a1900d26f0e3c16b31cc8aff645d69757cc4. The post-push unmocked API/browser regression passed on this source, and the worktree is clean. Full-stack CI run 39007001083 passed on this exact head: every Go/race, ingestor, library/CLI, script/container, JavaScript/lint, Docker, E2E shard, coverage aggregation and final required gate succeeded. The stacked branch needs an explicit manual CI run because automatic PR CI targets master. The actual workflow conditions were checked: publishing requires an upstream push, and deployment additionally requires upstream master; this dispatch on our fork's feature branch did not publish or deploy.

🤖 Generated with Claude Code

@dborup

dborup commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

Independent review

Reviewed commit 4c20a1900d26f0e3c16b31cc8aff645d69757cc4 against base 53fd65f5ab3bbe8d5cefe76e3ad20783bf6c1d61. The remote PR head was verified and its tree matches the reviewed immutable tree 3efae569e82e7ce7b1ddd467ab9239acfb67a59d.

No actionable findings remain.

The review covered:

  • Strict own-observation scoped-FLOOD attribution across Latest, Timeline/history, Health and shared memberships. Planned DIRECT paths cannot supply memberships; valid non-display FLOOD observations still work when the canonical transmission is DIRECT.
  • Candidate discovery, identity ambiguity, fail-closed missing or mismatched transport evidence, per-transmission deduplication and singleton memberships.
  • Current-hour evidence remaining available in Latest but excluded from completed-hour history and directed steps.
  • Separate membership and directed-step budgets, bounded aggregation, live visibility, and incomplete-evidence reporting distinct from retained time coverage.
  • Preservation of existing generic relay, node-liveness and traffic-share calculations.

The pre-PR UI review identified a P3 wording problem: incomplete observation attribution was described as a reached safety limit. The final commit corrects that reason and adds a browser regression test. I reviewed that follow-up; no open issue remains from it.

The parent agent reports passing full Go server/ingestor tests, focused race tests, JS tests and real-database/browser validation. I reviewed the regression coverage but did not independently rerun the full test suite. CI for this exact head is still running at the time of this review; this comment does not claim a green final CI result or authorize merge/deployment.

@dborup
dborup changed the base branch from codex/regions-observed-routes to master October 17, 2026 11:46
@dborup

dborup commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

Review — CS-Coordinator PR#507 — head 4c20a19

Verdict: REQUEST CHANGES. One blocking finding, from staging with real data.

Reviewed against master c6574b46 (#501–#505 are merged, and this PR is retargeted).

  • The merged tree passes the full cmd/server suite, -race on Region|Route|Relay|Evidence|412|470, and the Regions unit tests.
  • It was deployed to staging as master-c6574b46-pr507.
  • There are 0 panic, fatal, concurrent map or SQLITE_BUSY entries, RSS is 1.70 GiB, and /api/regions/routes takes 0.93 s at 5.47 MB raw.

The direction is right: membership now comes from each observation's own provenance, and planned DIRECT paths no longer gain scope membership.

Findings

# Severity Finding
1 Blocking evidenceIncomplete is global and trips on real data, so Health and Timeline rates and comparisons become permanently unavailable. eligibleRegionsObservation returns incomplete for any observation with an unknown header or a scoped flood frame whose transport bytes do not match the canonical code. regionsMembershipBuilder ORs that into one snapshot-wide flag. On staging, coverage.evidenceIncomplete is true for /api/regions/observed, /history and /routes. The Health view now shows "Rate unavailable: incomplete or limited history" and "Observation evidence is incomplete; some paths could not be safely attributed or were omitted by safety limits", although retained-time coverage is complete. On a live network with 100k+ observations there will always be at least one unprovable observation, so after #507 these features never show rates or comparisons on our instances (48 h and 168 h retention). With #503 alone, the same view was usable. Suggestion: track incompleteness per bucket and scope, plus a count, instead of one global flag. Report how many observations were excluded as unprovable versus capped, and only withhold a rate or comparison for the buckets and scopes actually affected. Alternatively, treat excluded-but-identified observations, such as a known direct header or a mismatched transport code, as excluded, not incomplete, and reserve incomplete for caps and truly unknown evidence. Add a test with a realistic mix of eligible, mismatched and unknown observations, asserting that rates stay available for the unaffected buckets.
2 Info (confirm intent) The relay-appearance totals change a lot. Health for the same 24 h interval showed ~28,100 appearances with #503 and 104,230 with this PR, while observed relay nodes went from 378 to 383. The PR says counts may increase where valid flood observations were hidden. A ~3.7× increase suggests that appearances now take the union of nodes over all observers' paths per transmission. If that is intended, say so in the PR and the UI text, because it changes what "appearances" means.
3 Info The step cap from #508 is still hit (steps: 5000, stepsTruncated: true). If the cap contributes to evidenceIncomplete, fixing finding 1 should separate capped from unprovable.

dborup and others added 5 commits October 17, 2026 12:10
…rates (#507)

Review F1/F3 for PR #507, red on 4c20a19:

- TestRegionsEvidenceIncompletenessIsPerBucketAndScope mixes valid,
  excluded (known TRANSPORT_DIRECT, unscoped FLOOD, mismatched transport
  code), unprovable (missing header/codes) and capped (oversized path)
  observations across hours and scopes. coverage.evidence must report
  excluded/unprovable/capped per hour bucket and stored scope for history,
  routes and each Latest window; stepEvidence must split the same counts.
- TestRegionsEvidenceExcludedOnlyIsComplete: known direct and mismatched
  observations alone are not incomplete evidence. Red: the snapshot-wide
  evidenceIncomplete flag is true.
- TestRegionsEvidenceStepCapCountedSeparately: the directed-step cap is
  counted as capped step appearances and never reaches coverage evidence.
- test-regions-map.js: rates, chart values and comparisons stay available
  for unaffected buckets/scopes, are withheld only where an included
  bucket/scope is unprovable or capped, and unknown placement fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et and scope (#507)

Review F1 (blocking) and F3 for PR #507. On staging the snapshot-wide
coverage.evidenceIncomplete flag was always true, so Health and Timeline
rates and comparisons were permanently unavailable.

- One provenance gate classifies each observation of a scoped
  transmission: eligible; excluded (known DIRECT/unscoped header, or own
  transport codes that differ from the stored frame - forwarders never
  rewrite them, firmware Mesh::sendFlood/sendZeroHop); unprovable
  (unknown/malformed own header, own codes or canonical scope code).
  Exclusions never make counts incomplete.
- Counts are attributed to the transmission's stored scope and FirstSeen
  hour, together with capped observations and capped memberships. They
  are published as coverage.evidence (named Go structs, no new
  map[string]interface{}): totals, per-scope totals and, in history and
  routes, per-bucket cells; Latest sums the requested window. A bounded
  128-scope x 505-hour budget sets evidence.truncated, which fails closed.
- The client withholds rates, chart values and comparisons only for
  intervals that include an unprovable/capped bucket of the selected scope
  (any scope for "all scopes"); other buckets and scopes stay available.
  Publication caps remain snapshot-wide.
- Unresolved hop tokens qualify directed steps (adjacency), not the
  memberships of uniquely resolved relays.
- F3: stepEvidence splits excluded/unprovable/capped observations, and the
  directed-step, pair and cell caps are counted once, in one helper, as
  cappedStepAppearances; they never reach coverage evidence.

Mutants killed: global flag on any unprovable/capped (Go and JS), mismatch
classified unprovable, rate ignoring evidence, step cap counted as coverage
capped, step cap not counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#507)

Review F2 for PR #507: Health 24h relay appearances on staging rose from
~28k (#503) to 104k with this PR. Since #507 memberships come from every
attributable scoped-flood observation's own path; #503/#505 history used
the canonical display path and its candidate index, so relays seen only
on other observers' paths were not counted.

- TestRegionsEvidenceAppearancesUnionAcrossObservers pins the meaning: one
  transmission observed via [A,B], [C,D], a repeat of [A,B] and a loop
  [B,A,B] gives A, B, C and D one appearance each (total 4). Run against
  c6574b4 (pre-#507 master) the same test yields only A and B (total 2).
  Mutants killed: only the first observation path counted; per-transmission
  membership dedup removed (total 7).
- Health note, Timeline details and docs state the union semantics, that
  repeated observers/paths/loops count once, and that more observers can
  reveal more relays for the same traffic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a mixed-evidence variant of the 30k transmission / 2k relay fixture
(8 scopes, one known-direct and one mismatched-code observation per
transmission, an unknown header on every 10th) for the background
snapshot and the warm paired response, plus the worst-case evidence
payload at the full 128-scope x 505-hour budget.

Local, Linux x86-64 (4 vCPU), Go toolchain of this repo, 5x5 iterations,
median, decedf1 (before) -> this branch:
- background snapshot: 132.1 -> 139.2 ms; mixed 137.3 -> 144.2 ms;
  allocations unchanged (130.2 / 137.5 MB)
- capture under the packet read lock: 11.7 -> 12.0 ms, 13.1 MB both
- warm paired response, mixed: 32.0 -> 32.5 ms; payload +10,026 bytes
- worst-case evidence payload: 3,673,684 bytes for 64,640 cells

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dborup

dborup commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

Review — CS-Coordinator PR#507 — head 18c6e3f

Verdict: APPROVE. F1–F3 are resolved. Two release-relevant issues remain outside this PR and are filed separately.

Staging (pr507-18c6e3fd, which contains master c6574b46) on real data shows unprovable: 0, excluded: 7, so the per-bucket and per-scope evidence works and known-direct or mismatched frames no longer withhold anything. There are 0 panic, fatal, concurrent map or SQLITE_BUSY entries, and RSS is 1.76 GiB.

Still observed on staging, not caused by this PR:

  1. capped: 1114 in #dk (17 of 20 hours with entries). This comes from the global distinct-path budget maxRegionsRoutePaths = 250000 (SourceTruncated), which is exhausted by about 805k observations in only 48 h of retention. On meshview-pve (168 h) it will be worse. Rates for #dk and for "all scopes" are therefore withheld. Added to regions: Connections step cap is hit on real data; measure route-snapshot memory (follow-ups to #505) #508 (cap redesign).
  2. Retained-history bounds are "unknown" after this restart. loadCoverageRatio is 0.9989, and retainedHistoryCoverageStartLocked requires exactly 1, so every scope shows "Rate unavailable". This is pre-existing behaviour and also affects node activity24h.complete (My Mesh). It is filed as a new issue.

The fix here is correct, honest and well tested (red-first tests, union-of-observers definition documented, step cap kept separate). Merging.

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