Skip to content

Port upstream #2099 hop ambiguity UI with observer/cache correctness fixes #165

Description

@dborup

Track the proposed port of upstream Kpa-clawbot/CoreScope#2099 to this fork. Review was performed on upstream head 3532cede0e3ad242dee8d348f9ee80c1a78b8d9f. These are findings in the upstream PR, not claims that the same code or defects already exist in our fork. Compare against our current master before implementing; carry over the useful ambiguity UI only with the cases below addressed and tested against our code.

1. Missing observer coordinates become a real (0, 0) anchor

observerPosition() accepts Number(null) as finite, so an observer returned with lat: null, lon: null produces [0, 0], not [null, null]. The default /api/observers response uses nullable coordinates when no node location is known (response construction). HopResolver.resolve() then treats (0, 0) as a geographic anchor and can select the candidate closest to that false location. The PR description says only 27 of 42 observers report a location, so this is a normal input, not just a malformed response.

Reproduction: with observerMap containing {id: 'obs', lat: null, lon: null}, observerPosition('obs') returns [0, 0]. A test should verify that this returns [null, null] and does not influence an ambiguous hop's candidate selection.

2. Client resolution can replace a server-resolved path

The initial packet load calls cacheResolvedPaths(packets) followed by resolveHopsForPackets(packets). The first writes server resolved_path entries under bare hop keys; the second looks for observer-specific keys, finds them absent, recomputes the same hops on the client, and writes those keys. renderHop() prefers the observer-specific entry. This can replace the server-confirmed pubkey with a different heuristic candidate, contrary to cacheResolvedPaths()'s documented precedence. The same sequence occurs when expanding grouped packets.

Reproduction: provide a packet whose resolved_path names candidate A and whose ambiguous prefix makes HopResolver.resolve() select candidate B for its observer. The rendered hop should keep A; the current cache ordering renders B.

3. Incremental packets can skip observer-specific resolution

The incremental update tests only whether a bare hop key is cached before deciding to call resolveHopsForPackets() (packets.js lines 1424–1434). After observer A has populated that bare key, a packet from observer B with the same prefix yields newHops.size === 0, even though B's observer-specific key is absent. Rendering then falls back to A's cached result. This defeats the per-observer cache introduced by this PR on the live update path.

Reproduction: process the same ambiguous prefix first for observer A, then for B via the incremental packet path. Verify that B receives its own resolved entry and does not inherit A's name.

Upstream CI gate

PR run 36783862462 failed in Playwright on the same head: four #1153 full-chain checks report “Expected all 18 hops in the rendered chain.” The new .hop-group wrapper changes the child structure, while the existing test calculates chain.children.length - chain.querySelectorAll('button').length (test line 202). Count the actual hop links and verify visually that the full chain remains present. The new unit test passes 30/30 locally, but it does not cover the cases above.

Acceptance for a fork port

  • Preserve server resolved_path precedence while showing uncertainty when it is genuinely unresolved.
  • Use observer coordinates only when both are present and valid; never convert missing values into (0, 0).
  • Resolve the same prefix independently for different observers, including incremental packet updates.
  • Confirm the full route is still visible in the browser at desktop and mobile widths, and update any DOM-count assertion to count hop elements rather than wrapper structure.
  • Run relevant unit and Playwright tests on our fork. Keep any IATA-coordinate-table repair as a separate issue unless its behavior is needed for this port.

Activity

  1. dborup commented on Oct 1, 2026

    @dborup
    OwnerAuthor

    Review notes (verified against upstream head 3532cede and our master 727efca0)

    The three defects are confirmed in the upstream code (read-only analysis):

    1. observerPosition() turns lat: null / lon: null into [0, 0], because Number(null) is 0 and therefore finite. Upstream /api/observers returns these coordinates as interface{}, so they come out as JSON null when no node location is known.
    2. cacheResolvedPaths() only writes the bare hop keys. resolveHops() writes hop:observer, and renderHop() prefers that key, so the client's heuristic overrides the server's resolved_path.
    3. The incremental path (packets.js ~1424–1434) only checks whether the bare key exists.

    Upstream PR run 36783862462 concluded failure on the same head. I have not checked which individual tests failed.

    Our fork today: renderHop() (public/packets.js ~1028) already looks up hop:observer, but resolveHops(hops) never writes that key. None of the three defects exists here yet. A port would introduce them unless it fixes them too.

    Additions for the port

    A. Test paths differ in this fork.

    • The #1153 full-chain check lives in test-issue-1146-path-link-contrast-e2e.js in the repo root, not under tests/e2e/.
    • The new unit test should follow our convention: test-*.js in the repo root, registered in test-all.sh and in the relevant deploy.yml test step. The fork guards must stay unchanged (9 × github.repository == 'Kpa-clawbot/CoreScope').

    B. Bound the per-observer cache.

    • hopNameCache keyed by hop:observer grows with prefixes × observers (~42 observers here).
    • It is reset in only one place today (packets.js ~1401).
    • AGENTS.md requires a size limit or eviction for every map. Add a cap, or a reset tied to the existing reset points, and test it.

    C. Perf proof for the packets page.

    • The initial load now resolves hops once per observer group instead of once overall. This is probably cheap, since there are at most ~42 groups and the work is local, but this is a hot path.
    • Include before/after timing of resolveHopsForPackets (or of the full initial load) on ~30K packets, as AGENTS.md rule 0 requires.

    D. A possible shape for defect 2.

    • When a packet has observer_id, cacheResolvedPaths() can also write hop:observer.
    • Alternatively, resolveHops() can skip hops that already have a server-resolved entry for that packet.
    • Either way, a test must show the server's candidate wins when the client heuristic would pick another.

    Priority: low. This is a new feature, a UI for ambiguous hops, not a fix for a defect in our fork.

    Relates to upstream Kpa-clawbot/CoreScope#2099 (referenced in code format on purpose, to avoid cross-references on the upstream PR).

  2. dborup commented on Oct 3, 2026

    @dborup
    OwnerAuthor

    Parked with #185 until #190 is merged; see the status comment on #185 for the reasons and the plan.

  3. dborup commented on Oct 4, 2026

    @dborup
    OwnerAuthor

    Deferred upstream assessment — 2026-10-04

    Reusing this issue for the weekly upstream backlog rather than opening a duplicate. Upstream PR 2099 is now merged.

    What it adds: observer-aware hop resolution and visible ambiguity information, so a short prefix does not look like a certain node identity merely because the UI chose one candidate. Useful for interpreting routes, but not proof of an individual sender, physical location or radio link.

    What we already have: this issue's correctness requirements and open #185 provide the adaptation candidate. Backend observer-based last-hop resolution in #190 is now merged; it is complementary, not a substitute for correct UI presentation.

    Remaining decision: #185 was explicitly parked pending #190. The prerequisite has landed, but that does not automatically unpark or approve it. Its review still records the grouped-row problem (missing resolved_path causes misleading client warnings), cache/test gaps, popup count issues and increased client CPU cost. Prefer using persisted server evidence, with heuristics only where necessary, if this is resumed.

    Later checklist: recheck the grouped API shape and server precedence; reproduce review findings on the latest branch; measure JSON size and real-browser resolution/rendering cost; cover null coordinates, different observers, cache pressure and grouped/flat navigation; coordinate packet layout/popup work from upstream Kpa-clawbot#2090.

    Decision remains deferred: improve the existing PR, adopt only the useful UI subset, or retain current behavior. No new port or implementation is started by this update.

  4. dborup commented on Oct 7, 2026

    @dborup
    OwnerAuthor

    Fixed by #185 (merged as 461803d95505d9d919f9a38784025ee4c4e0b9c6).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions