Repository navigation
feat(packets): show hop ambiguity, resolved per observer (port of upstream 2099) - #185
Conversation
…#1153 check (#165) The full-chain assertion derived the hop count from chain.children.length minus buttons. Once a hop and its badge are wrapped together (the ambiguity UI ported for #165), a wrapped hop is one child holding a pill and a button, so the count drops even though every hop is rendered. Count the hop pills themselves (links and spans, excluding the wrapper and anything inside a badge button). Green on master: 11 passed against a local fixture server. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
Port of upstream PR 2099 (Kpa-clawbot/CoreScope, head 3532ced): - resolveHops() resolves with the observer that heard the packet and its position, and caches under hop:observer; resolveHopsForPackets() groups multi-packet call sites by observer. - HopDisplay wraps a pill and its badge in .hop-group, and opts.badge === false drops the per-hop badge for the packets list, which shows one .hop-path-warn summary per path instead. - The new styles use CSS variables only (no hex fallbacks). Fork-specific adjustments: - The unit test lives in the repo root as test-issue-165-hop-ambiguity-badge.js and is registered in test-all.sh and the deploy.yml JS unit step. - HopDisplay.renderHop takes opts.className, and nodes.js uses it for hop-current. nodes.js patched the first class="" of the returned HTML, which is the .hop-group wrapper once a hop has a badge, so the Kpa-clawbot#1153 selected-hop marker was lost (4 E2E failures in test-issue-1146-path-link-contrast-e2e.js before this change). - The incremental (WS/poll) hop resolution is extracted unchanged into resolveIncomingHops(), and the resolve helpers are exposed on _packetsTestAPI for vm-harness tests. The defects listed in #165 (null coordinates, client overriding the server's resolved_path, the incremental path, an unbounded cache) are carried over as-is here and are fixed test-first in the following commits. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…#165) observerPosition() runs Number() on lat/lon, and Number(null) is 0, which is finite. An observer that /api/observers returns with lat: null, lon: null therefore anchors hop resolution at (0, 0), and a same-prefix candidate near Null Island wins over the candidate order. The new vm-harness test loads the real packets.js, hop-resolver.js and hop-display.js and asserts: - null, half-null or empty coordinates give [null, null]; - (0, 0) counts as no fix, as on the server (routes.go hasRealFix) and in HopResolver; - real coordinates still pass through; - an observer without coordinates does not change the pick. Red on this commit: 4 failed, 1 passed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
observerPosition() now accepts a coordinate only when it is a real number, so null, undefined, '' and non-numeric values all count as missing. Both lat and lon must be present, and (0, 0) is treated as no fix, as the server (routes.go hasRealFix) and HopResolver already do. Anything else gives [null, null], so hop resolution runs without an observer anchor instead of anchoring at Null Island. test-issue-165-hop-resolution-per-observer.js: 5 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…#165) cacheResolvedPaths() writes the server's resolved_path entries under bare hop keys only. resolveHopsForPackets() then finds no hop:observer key, re-resolves the same hop on the client and writes that key, and renderHop() prefers it. The heuristic's pick (next to the observer) replaces the node the server confirmed. The incremental path writes the server's entries bare in the same way. Three tests: - the initial-load sequence keeps the server node, with a precondition showing that the heuristic would pick another; - a hop the server left null is still client-resolved, and the list summary counts only that hop as uncertain; - resolveIncomingHops() keeps the server node. Red on this commit: 3 failed, 5 passed. Section headers now print in order with their tests. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…ic (#165) Chosen shape (option 1 in the #165 review, point D): the server's entries are written under hop:observer as well as the bare key, by one helper, cacheServerResolvedPath(), which cacheResolvedPaths() and the incremental resolveIncomingHops() now share. Why this option and not a skip in resolveHops(): renderHop(), renderPath()'s summary and resolveHops() all read the per-observer key first. Storing the server's answer where those readers look makes it win everywhere at once. resolveHops() works on a set of prefixes per observer and does not know which packet a prefix came from, so it could not tell which hops to skip. renderDetail() already wrote both keys the same way. Hops the server leaves null are not written. The client still resolves them, so a genuinely unresolved hop keeps its ambiguity badge. test-issue-165-hop-resolution-per-observer.js: 8 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…op (#165) resolveIncomingHops() decides whether there is anything to resolve by looking only at the bare hop key. Once observer A has resolved a prefix, a packet from observer B with the same prefix finds the bare key, resolves nothing, and renders A's pick through the bare-key fallback. Two tests: - A, then B, in separate incremental batches; - an initial load for A, then one incremental batch holding both A and B. Red on this commit: 2 failed, 8 passed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
resolveIncomingHops() now checks the hop:observer key for each packet's own observer before deciding whether anything needs resolving. It no longer checks the bare key that any observer may have filled. resolveHopsForPackets() already groups by observer and skips keys that are present, so a known (hop, observer) pair still costs nothing. test-issue-165-hop-resolution-per-observer.js: 10 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
hopNameCache gains a key per (prefix, observer) pair and is only cleared in destroy(). AGENTS.md requires a size limit or eviction for every map. The tests require: - an exposed HOP_CACHE_MAX that the cache never exceeds; - eviction that drops the oldest entries and keeps the newest; - destroy(), the existing reset point, emptying the cache. Red on this commit: 2 failed (no cap exists), 11 passed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
hopNameCache becomes a Map read and written only through hopCacheHas(), hopCacheGet() and hopCacheSet(). - Past HOP_CACHE_MAX (20000 entries) the oldest entry is evicted. A Map keeps insertion order, so this costs O(1) per write, and a rewrite moves the key to the newest end. - destroy(), the existing reset point, still replaces the whole cache. - An evicted hop:observer entry falls back to the bare key in renderHop(), and the next packet from that observer re-resolves it. 20000 is above the per-observer working set seen here (prefixes x ~42 observers), and its worst-case footprint stays in the tens of MB. It is hardcoded for now and should move to the customizer later (AGENTS.md rule 8). The ported structural assertion in test-issue-165-hop-ambiguity-badge.js now looks for the hopCacheSet(hopCacheKey(...)) write. test-issue-165-hop-resolution-per-observer.js: 13 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
The previous commit set HOP_CACHE_MAX to 20000 and claimed that this was above the working set. That claim was not measured, and it is wrong. Measured with the real packets.js in a vm sandbox, on a synthetic 30K packet load: - 2000 repeaters in three regions, 42 observers (27 with coordinates) and 0-8 hops per packet (85% 1-byte, 30% with resolved_path); - the initial load leaves 28962 entries (bare plus hop:observer), using 9.6 MB of heap; - at 20000 the cache evicts during the initial load itself, which costs about 80 ms extra (309 ms vs 229 ms median of 7 runs). 50000 holds that load with headroom. At the cap the heap stays under about 17 MB at the same per-entry size. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…rs (#165) Resolving per observer does more work than master's single resolve. On the synthetic 30K load (see the previous commit), the initial hop resolution takes 217 ms against master's 67 ms, all in one main-thread block, because the observer groups run back to back on microtasks. The test schedules a timer and requires it to fire after the first observer group and before the last. Red on this commit: 1 failed, 13 passed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
resolveHopsForPackets() now awaits a macrotask (setTimeout 0) before every observer group after the first. Input and paint can then get in during the initial load, instead of waiting for the whole per-observer resolve. Synthetic 30K load (2000 repeaters, 42 observers), real packets.js in vm, median of 7 runs: | measure | master | before (per observer) | after (with yield) | |------------------------------|--------|-----------------------|--------------------| | total hop resolution | 67 ms | 217 ms | 271 ms | | longest main-thread block | 70 ms | 220 ms | 92 ms | The remaining longest block is cacheResolvedPaths() over the whole load (~40 ms) plus grouping and the first observer group. The job still runs off the critical path: rows render with hex hops first and are upgraded when it finishes (Kpa-clawbot#1693). Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…165) scripts/bench-hop-resolution.js times the initial-load hop resolution of the real packets.js on a deterministic synthetic 30K packet load: - 2000 repeaters in three regions; - 42 observers, 27 of them with coordinates; - 0-8 hops per packet, 30% with resolved_path. It reports, as medians of 7 runs, the total time, the longest main-thread block and the cache size. Point it at another checkout's public/ for a baseline: packets.js from before #165 runs the old single resolveHops() sequence. | public/ from | total | longest block | cache entries | |-------------------------------|--------|---------------|---------------| | origin/master | 60 ms | 61 ms | 2236 | | upstream port as-is (18afd8d) | 396 ms | 396 ms | 28962 | | this branch | 251 ms | 87 ms | 28962 | Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
… path (#165) Browser check of the port: the path cell in the packets list is narrow (overflow: hidden). The .hop-path-warn summary is appended after the last hop, so on any path that overflows it is clipped. It is exactly the long paths where the warning matters. It is also a child of .path-hops, so _finalizePathOverflow() counts it as a hidden hop and the "+N more hops" pill reads one too many. The tests require: - the summary precedes the first hop in list form; - the detail form keeps per-hop badges and no summary; - the overflow pill counts hops only (fake DOM: one clipped hop and a clipped summary give "+1"). _finalizePathOverflow is exposed on _packetsTestAPI for this. Red on this commit: 2 failed, 14 passed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
) renderPath(..., { summary: true }) now puts .hop-path-warn before the hops instead of after them, and the overflow pill skips it when counting hidden hops. The list's path cell clips overflow (Kpa-clawbot#1122). With the summary last, a path long enough to overflow hid it, so the warning vanished on exactly the paths with the most hops. In the browser (desktop 1440, fixture packet with three genuinely ambiguous, server-null hops) the "warning 3" indicator was clipped before this change and is visible after it. The overflow pill stays a count of hops. Test fixes in the same commit, the test intent unchanged: - the first-hop search matched class="hop-path-warn" itself; it now matches the hop class exactly; - the detail-form check asserted a conflict badge, which needs an IATA region this sandbox does not set; it now asserts no summary plus hop-ambiguous. Badge rendering is covered in test-issue-165-hop-ambiguity-badge.js. Both tests are still red on the previous commit. test-issue-165-hop-resolution-per-observer.js: 16 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
Conflicts were only in test registration: - .github/workflows/deploy.yml: master's version. The JS unit step now runs test-all.sh (#174), so the two explicit test-issue-165 lines are no longer needed there. - test-all.sh: master's version plus the two #165 files, in its new `run` format. public/packets.js (including #167) merged without conflicts. deploy.yml keeps 9 fork guards. sh test-all.sh: 202 passed, 0 failed. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
…557894) (#165) Ports upstream commit 2557894 ("lead the path with its warning so the clip edge never hides it"), from upstream PR 2099's final head. Most of it was already on this branch: 37dd287 puts .hop-path-warn before the hops, with flex: 0 0 auto, and leaves it out of the +N count. What remained: - the +N popover still listed the summary as a path item. Its body is now built by _pathPopoverHtml(host), which skips the pill and .hop-path-warn (upstream does the same in _pathHopSegments, a helper this fork does not have); - renderPath() returns `warn + body`, upstream's shape, so the ported structural assertion matches; - margin-right 4px, as upstream. Tests (written first, red before this change, 1 + 2 failures): - test-issue-165-hop-resolution-per-observer.js: the popover lists hops and arrows, not the summary or the pill (fake host). 17/17. - test-issue-165-hop-ambiguity-badge.js: upstream's two assertions, adapted to _pathPopoverHtml. 34/34. sh test-all.sh: 202/202 files. The packets E2Es are green locally: Kpa-clawbot#1128 layout 5/5, Kpa-clawbot#1128 multi-viewport 15/15, Kpa-clawbot#1146 11/11. Relates to #165 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANjbSkzh7MxgT51Dzd7xeZ
Rapport — CS-pve-agent2 PR#185 hop-ambiguity — head 8310f8aStatus: Draft, mergeable against What is portedUpstream PR 2099 (
Tests and CI
Known remaining items
|
Review — CS-Macmini PR#185 hop-ambiguity — head 8310f8aVerdict: REQUEST CHANGES. The port is faithful and well tested where it is tested, but in the default (grouped) packets view the new list summary warns about hops that the server has resolved: 23 of 60 rendered rows on the CI fixture. That breaks #165's first acceptance point. Everything else is nits. The review was read-only. It used Evidence tags: [T] test, CI or browser run here, [A] analysis, [K] taken from the author's report and not rerun. Findings
1. Faithfulness to upstream (final
|
| Deviation | Verdict |
|---|---|
opts.className instead of patching class="hop in nodes.js (upstream 4ca1ef87) |
Better: it cannot hit the wrapper. Browser: 364 .hop-current on 60 node pages, 0 on a .hop-group [T]. The fixture has no node path with a badged hop, so the in-group case is covered by the unit test only. |
coordOrNull and (0, 0) treated as no fix (defect 1) |
Correct; it matches the server's hasRealFix. |
cacheServerResolvedPath also writes hop:observer (defect 2) |
Correct where resolved_path exists; see F1 for where it does not. |
resolveIncomingHops with a per-observer check (defect 3) |
Correct. The old rp.length === hops.length guard is not lost: resolveFromServer returns {} on a length mismatch. |
Bounded Map cache plus a yield between observer groups (B, C) |
Fine. |
CSS uses var(--status-yellow) / var(--palette-gray-900) without hex fallbacks; hop-path-warn excluded from the +N hidden count |
Fine. The +N exclusion is harmless, since the summary leads. |
Test paths in the root; the #1153 E2E counts hop elements |
Fine. |
2. Defects 1–3 and points A–D from #165
| Item | Fixed | Tested | Notes |
|---|---|---|---|
| Defect 1, null and (0, 0) coordinates | yes | 5 tests [T] | |
| Defect 2, server over heuristic | partly | 3 tests, flat/children only [T] | not in the grouped default view (F1) |
| Defect 3, live path per observer | yes | 2 tests [T] | |
A, root tests in test-all.sh |
yes | [T] | deploy.yml unchanged |
B, cap 50000, oldest-first eviction, destroy() reset |
yes | 3 tests [T] | cap value unpinned (F3) |
| C, yield and bench | yes | 1 test plus bench [T] | |
| D, test that the server wins | yes for flat | [T] | none for grouped (F1) |
3. Performance
-
[A] No O(n²): grouping is O(packets × hops), and each group resolves its distinct hops. No per-item API calls:
ensureHopResolverfetches once. No new full DOM rebuild: the existingrenderTableRows()runs after the job. Cache writes are O(1). -
[T]
scripts/bench-hop-resolution.js, 3 interleaved rounds (median of 7 runs each). Apple M4, Node 22.22.1, load average about 4.5:public/total initial hop resolution longest main-thread block cache entries master 8e4b13d042.5–53.0 ms 43.9–60.3 ms 2236 head 8310f8a1225.9–230.7 ms 64.2–65.7 ms 28962 merged 220.8–224.7 ms 62.3–63.0 ms 28962 This reproduces the author's shape (about 4.5× total, longest block about +15 ms, which is acceptable after first render).
-
The 50000 cap is reasonable: about 1.7× the synthetic worst case of 28962. At the author's [K] 9.6 MB for 29K entries it scales to about 16 MB at the cap [A]. It is fine if F3 pins it.
4. Tests and mutants
-
[T] On the merged tree, Node 22:
test-issue-165-hop-resolution-per-observer.js: 17/17;test-issue-165-hop-ambiguity-badge.js: 34/34;sh test-all.sh: 203/203 files.
On the merge with the newer master
6f7a5e7c: 203/203. -
[T] CI on head: Go Build & Test, Playwright E2E and Docker build passed.
Own mutants, each in a copy of the merged tree with the full test-all.sh:
| Mutant | Result |
|---|---|
MA hopCacheSet without the delete-before-set (a rewrite does not refresh eviction order) |
survives |
MB HOP_CACHE_MAX = 100 |
survives (F3) |
MC resolveHops always overwrites the bare key |
survives; red with probe 2 below |
| MD list summary counts via the bare key only | survives; red with probe 1 below |
Probe tests, appended in scratch to the PR's own harness (green on the merged tree, red under the mutant):
observer A has the server's answer for "ef", observer B's "ef" is client-resolved and ambiguous → renderPath(['ef'], 'OBS-B', {summary:true}) shows .hop-path-warn(red under MD);…same setup → the bare "ef" stays the server node (FAR-AWAY)(red under MC:NEAR-ONE).
5. Browser
Local Go server built from the merged tree on the CI fixture: freshen, deploy.yml seed SQL, corescope-migrate, seed-2073. I confirmed it served the merged tree: analytics.js matches merged, not head. It was stopped by its listening pid.
| E2E (merged tree) | Result |
|---|---|
test-issue-1146-path-link-contrast-e2e.js (#1153/#1146) |
11/11 |
test-issue-147-packets-url-obs-e2e.js (#167) |
12/12 |
test-issue-1128-packets-layout-e2e.js / -multi-viewport- |
5/5, 15/15 |
test-issue-1122-details-row-clamp-e2e.js / -packets-filter-ux- |
18/18, 6/6 |
test-slideover-1056-e2e.js |
27/27 |
test-issue-1692-…, test-path-inspector-coverage-e2e.js, test-packet-trace-alignment-e2e.js, test-issue-1486-… |
1/1, 10/10, 17/17, 4/4 |
test-e2e-playwright.js |
Fail-fast stops at "Version info lives on Perf dashboard" on master's public/ too, the same binary and fixture (a local environment baseline; CI is green). In a scratch copy with fail-fast disabled: merged 131 passed / 1 failed / 7 skipped, master 131 / 1 / 7, with the identical single failure. |
Manual checks in Chromium (Playwright scripts plus the in-app browser at 1440×900):
- The summary is the first child of
.path-hopsand inside the cell on all checked rows. - At 390 px the path column is
col-hiddenon master and merged alike, so nothing is clipped. - The +N popover contains the 9 hops and no
.hop-path-warn. Its title says 19 items (F5). hop-currentsits on the pill, never on the wrapper.- No page errors. The only console errors are
404 /api/nodes/<id>/clock-skew, the same on master. - Deep links
#/packets/<hash>and?timeWindow=work (and the fix(packets): keep ?obs=/?viewPath= on packet detail URLs (#147) #167 E2E passed). - F1 was reproduced visually: the list shows ⚠1 for
e8b09a35…, and the detail pane showsHomestead MCwithout a badge.
6. Rules
- [T] Added lines contain no hex colours, and
check-css-vars.jsreports 0 undefined. - [T]
scripts/check-xss-sinks.sh --diff origin/masterpasses (rc 0, in a shared scratch clone at head). - [T] Fork guards: 9 on master, head and merged, and
deploy.ymlis unchanged. - [T] The PR text has no closing keywords, and its
deploy.ymlstatement is correct; the stale parts are F4.
Suggested fixes for F1 (pick one)
- Server: include the header observation's
resolved_pathingroupByHashrows (cmd/server, read-only side). This changes the API shape, so it needs a JSON-size and perf check on a 30K grouped load. - Client, minimal: render the list summary only for rows that carry
resolved_path(flat rows and expanded children), and leave grouped header rows without it until the server data is known. Add a grouped-row test.
Either way, add a test with a grouped packet (no resolved_path) whose prefix the server has resolved.
Not verified
- Real 30K data, staging or production, or a browser performance profile. The bench is the author's synthetic vm load.
- The heap size of the cache [K].
- A badged hop on a node detail page in the browser: the fixture has none, so only the unit test covers it.
- Firefox and WebKit.
- How many of the 23 grouped warnings would remain with server data (flat shows 0, so I infer none) [A].
Status — parked until #190Decision: #185 is parked. It is not part of the next staging round (v0.2.1). The branch Why
Plan when this is picked up again (after #190 is merged)
|
PR #185 review F1: /api/packets?groupByHash=true sends no resolved_path, so the packets page's default view guesses every hop on the client and its list summary flags hops the server resolved (23 of 60 rows on the CI fixture). The tests seed a transmission heard by two observers with different paths and resolved_paths, and require the grouped row to carry the resolved_path of the observation it displays (same observer_id and path_json), from the in-memory store and from the SQL fallback. A row whose observation has none omits the key. BenchmarkGroupedPackets165 measures the desktop page (limit 50000) on 30K transmissions and reports its JSON and gzip size, for the cost check #165 asked for. Red on the branch: both tests fail, the grouped rows have no resolved_path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d rows (#165) PR #185 review F1. The packets page resolves hop names from the grouped row, under hop:observer. Without the server's answer there, the client heuristic guessed every hop of the default view, and the list summary flagged hops the server had resolved. - In-memory store: groupedPageWithRP adds, per page row, the resolved_path of the observation pickBestObservation copied onto the tx (same observer and path, headerObservationID). The ids are read under s.mu; the SQL runs after it, as ONE query for the whole page (ids as a json_each parameter, no bound-variable limit). It bypasses the resolved-path LRU, which a 50K page would flush. - SQL fallback: the grouped query selects o.resolved_path of the row it already joins, or NULL on a schema without the column (v2). - The stored JSON is passed through as json.RawMessage (checked with json.Valid), not parsed and re-encoded. Cost, BenchmarkGroupedPackets165 (30K tx, 2 observations each, 3-hop paths, random-looking pubkeys; 3 runs, 4-core cloud VM): per request 28.2-29.8 ms -> 90.1-95.6 ms JSON 12.24 MB -> 16.95 MB (+38 %) gzip 0.25 MB -> 2.56 MB (synthetic raw_hex/decoded_json compress far better than real ones) Most of the added time is modernc/sqlite stepping 30K rows (~2 us per row); a statement per row, or 500-id batches, measured 117-126 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit tests (test-issue-165-hop-resolution-per-observer.js, test-issue-165-hop-ambiguity-badge.js), against the real modules: - F1: a grouped row as the server now sends it is not flagged for its server-resolved hop; the summary does not count another observer's bare-key entry; a server answer replaces an ambiguous client pick of the same node; cacheResolvedPaths yields on a large page and skips a repeated answer, and a different answer still replaces it. - F2: the review's two probes (MD: the summary counts per observer; MC: the heuristic does not overwrite the server's bare key). - F3: a bench-sized working set (42 observers x 690 hops) is cached without eviction; a rewritten entry is refreshed for eviction order. - F5: the +N popover title counts the hops it lists. - F6: the list counts exactly the hops the detail pane badges (HopDisplay.conflictBadgeCount), and marks them hop-uncertain. - F7: destroy() during resolveHopsForPackets or cacheResolvedPaths leaves the new cache empty. Two existing tests now give their observer an IATA code, so the ambiguous hop they expect in the summary is badged. E2E test-issue-165-grouped-hop-warn-e2e.js (registered in deploy.yml): on a cold grouped load of the fixture, every grouped row carries its observation's resolved_path, and every hop a row's summary counts is one that resolved_path leaves null; a control with resolved_path stripped still renders the summary. Red on the branch: 8 of 32 and 2 of 36 unit tests. The E2E is red against the unfixed server (24 warned rows, 26 hops the server resolved). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn entry (#165) PR #185 review F1 and F6. The list summary counted entry.ambiguous and fell back to the bare key: - The bare key is another observer's answer (or the server's for another observation), so the warning depended on what had been resolved before. The summary now reads only the row observer's own entry; the bare key stays the name fallback. - entry.ambiguous is set for any hop with two candidates, also where the detail pane shows no badge (no regional conflicts and no global fallback, e.g. an observer without an IATA code). The badge's count is now HopDisplay.conflictBadgeCount(entry), used by renderHop and by the summary, so the list flags exactly the hops the detail badges. Counted pills carry the class hop-uncertain, so which hops a summary counts can be checked (the E2E does). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #185 review F5. "Full path (N items)" counted host.children, which holds the pill and, since this PR, the path summary as well as the hops and arrows: 19 items for 9 hops on the fixture. _pathPopoverHtml now counts the hops it lists and says "Full path (9 hops)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #185 review F7. resolveHopsForPackets yields between observer groups, so the initial hop job could keep running after the page was left and write into the next page's cache. destroy() now bumps hopCacheGen; resolveHops() and resolveHopsForPackets() stop when it changed while they waited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dPaths (#165) Since grouped rows carry resolved_path (PR #185 F1), cacheResolvedPaths walks the whole page, up to 50K rows, and wrote two cache entries per hop per row in one synchronous loop. With 95 % of rows carrying a resolved_path, scripts/bench-hop-resolution.js measured the longest main-thread block at 227-249 ms. - cacheServerResolvedPath skips a row whose answers are already cached (holdsServerAnswer: same pubkey, and not a flagged client pick, which the server answer must still replace). - cacheResolvedPaths yields every 2000 rows and stops if destroy() ran meanwhile (hopCacheGen, as resolveHopsForPackets does). - The bench takes RP_SHARE (default 0.3, as before). Bench, 30K packets, 42 observers, median of 7 runs, 3 rounds each, 4-core cloud VM, Node 22: RP_SHARE total ms (before -> after) longest block ms 0.3 266-284 -> 279-283 91-99 -> 30-33 0.95 315-343 -> 234-240 227-249 -> 24-25 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent1 PR#185 runde 2 — head 0874df4Review feedback addressed (commit Evidence tags: [T] test, CI or browser run in this session, [A] analysis, [K] taken from an earlier report and not rerun. Branch:
No disagreements with the findings. Tests (red first)
MutantsAll 20 are caught [T]. Each was run in a copy of the tree. The client mutants were run with the full
Perf
CI on
|
| Job | Result |
|---|---|
| Go Build & Test | success; server ok, 1164 s, coverage 90.5 % |
| Playwright E2E Tests | success; the new E2E steps are ✓ in the log |
| Build & Publish Docker Image | success |
| Release Artifacts | skipped |
| Deploy Staging | skipped |
| Publish Badges & Summary | skipped |
Not verified / remaining
- Staging and production data, and a real-browser performance profile. All numbers come from synthetic loads and the CI fixture. The share of grouped rows that carry
resolved_pathafter feat(ingestor): resolve the last hop from the observer (#188) #190 is taken from the parking comment [K]. - The default view's response grows by about a third, roughly +1.9 MB gzipped at 30K rows [A].
- Total client hop-resolution time stays 2–4× master's (per observer, after first render, in short blocks).
- The hex-breakdown hop rows still read only the bare key. The customizer does not expose
HOP_CACHE_MAXor the summary yet. - Erratum: the message of
6649f867says "a statement per row, or 500-id batches, measured 117-126 ms". Only the 500-id batch variant, with JSON parsing, was measured.
Review — CS-Macmini PR#185 — head 0874df4Dom: APPROVE med nits Round-2 re-review of my own round-1 REQUEST CHANGES. The blocking finding is fixed at the source, and I reproduced both states myself: on the CI fixture the grouped first load went from 23 warned rows of 60 (25 flagged hops, all 25 of them hops the server had resolved for that row's observer) to 0 of 60, with no console errors. The other six round-1 findings are resolved. What is left is test gaps around the new server code and wording, none of which blocks. Read-only. I used Evidence tags: [T] test, CI or browser run here, [A] analysis, [K] taken from the author's report and not rerun. Findings
1. The blocking finding (F1)Fixed, on the server side as the parking comment planned. I measured all three states myself on the CI fixture, desktop 1440, 24 h window, after letting the warning count settle over three consecutive samples:
[T] All three. The second row is the true "before": The history dependence I reported in round 1 is gone. Grouped → flat → grouped: head is 0 / 0 / 0, the stripped shape is 23 / 0 / 0 [T], which is the round-1 symptom exactly. Red before, green after, independently reproduced:
2. My other round-1 findings
No disagreement with the author's handling of any of them. 3. The master mergeClean, and nothing from master is lost.
4. BrowserLocal Go server built from head on the CI fixture, prepared as in CI (freshen, the
[T] Node detail pages, 60 of them (45 with path chains): 332 5. Port fidelity, XSS, colours
TestsAll on head, which equals the merged tree against
E2E against the local server on
CI on MutantsSix of my own, each in a separate copy of the head tree.
Re-run of my four round-1 survivors on the round-2 tree: MA, MB, MC and MD all die [T], each on a named test (quoted in section 2). Performance, measured hereServer —
[T] The JSON figures match the author's to the byte; the time ratio (3.3×) matches their 3.2× on slower hardware. On the CI fixture the grouped response goes 432 KB → 589 KB (+36 %) [T], also matching. The batched read's plan is Client —
[T] This reproduces the author's shape and resolves the round-1 concern from the other direction: total stays 2.4–5.5× master's, but the longest block is now below master's at both shares, where the previous head was at 101–106 ms and 233–240 ms. Cache entries match their numbers exactly and stay well under Not verified
|
Relates to #165
Port of upstream
Kpa-clawbot/CoreScope#2099("resolve path hops with the observer that heard them"), up to its final head2557894d(merged upstream; tree-identical to415362c5). The first port was taken from3532cede; upstream4ca1ef87and2557894dare covered too (see below). The port includes the fixes the #165 review asks for (defects 1–3, points A–D) and the fixes for the PR #185 review (F1–F7).Head:
0874df4e. Branch is up to date withmaster(6444294d, merge96f91f2d).What the PR does
public/packets.js): every hop is resolved with the observer that heard it (resolveHops(hops, observerId),resolveHopsForPackets), and cached underhop:observer. The bare key stays the name fallback.resolved_path) is cached under the bare key andhop:observer, so the client heuristic never replaces it. Hops the server leftnullare resolved on the client and may show as uncertain.resolved_path(cmd/server, PR feat(packets): show hop ambiguity, resolved per observer (port of upstream 2099) #185 F1):/api/packets?groupByHash=truenow sends theresolved_pathof the observation each row displays (sameobserver_idandpath_json). Without it the default view guessed every hop..hop-path-warnsummary that leads the path cell ("N of M hops have more than one candidate"). Upstream's final version (2557894d) leads with it as well.HopDisplay.conflictBadgeCount), from the row observer's own entry only. Counted pills carryhop-uncertain..hop-group.Fixes from the #165 review
[null, null](observerPosition,coordOrNull)resolved_pathis written underhop:observertoo, so it wins over the heuristicresolveIncomingHops) checks the per-observer keytest-all.sh; the Kpa-clawbot#1153 E2E counts hop elementsMapcapped at 50000 with oldest-first eviction;destroy()resets itscripts/bench-hop-resolution.jsUpstream
4ca1ef87(hop-current on the pill) is covered byHopDisplay.renderHop(..., { className })instead of patchingclass="hop. Upstream2557894d(the +N popover skips the summary) is ported in8310f8a1.Fixes from the PR #185 review (round 2)
resolved_path(in-memory store and SQL fallback); the list counts only the observer's own entry; a server answer replaces an ambiguous client pick of the same node50e462be(test),6649f867,393b39f9(test),8d46c5fe,0874df4e393b39f9393b39f9441cd76bHopDisplay.conflictBadgeCount)8d46c5fedestroy()bumps a generation; hop jobs from before it stop writingae70759e,0874df4ePerf
Server,
/api/packets?groupByHash=true(default view):BenchmarkGroupedPackets165, 30K transmissions, 2 observations each, 3-hop paths, desktop limit 50000, 3 runs, 4-core cloud VM.resolved_paths of a page are read in one query (ids as ajson_eachparameter) afters.muis released, and passed through as stored JSON. Most of the added time is SQLite stepping 30K rows (~2 µs per row). 500-id batches with JSON parsing measured 117–126 ms.Client, initial hop resolution:
RP_SHARE=<share> node scripts/bench-hop-resolution.js [publicDir], 30K packets, 2000 repeaters, 42 observers (27 with coordinates), 0–8 hops, median of 7 runs, range over 3 interleaved rounds.RP_SHAREis the share of packets with aresolved_path(0.3 as before; 0.95 is about what grouped rows carry after #190).public/origin/master8310f8a1origin/master8310f8a1cacheResolvedPathsskips rows whose answers are already cached and yields every 2000 rows, so the longest block is now below master's.Tests
Unit (vm harness, real
packets.js,hop-resolver.js,hop-display.js):test-issue-165-hop-resolution-per-observer.js: 32/32.test-issue-165-hop-ambiguity-badge.js: 36/36.sh test-all.sh: 229/229 files;test-frontend-helpers.js: 709/709.Go:
cmd/serverandcmd/ingestorsuites pass (-timeout 30m). New:TestGroupedPacketsCarryHeaderResolvedPath165,TestGroupedPacketsSQLCarryHeaderResolvedPath165,BenchmarkGroupedPackets165.E2E against a local Go server on
e2e-fixture.db, prepared as in CI:test-issue-165-grouped-hop-warn-e2e.js(new, indeploy.yml)test-e2e-playwright.jstest-issue-1146-path-link-contrast-e2e.jstest-issue-147-packets-url-obs-e2e.jstest-issue-180-packets-url-modal-e2e.jstest-issue-1128-packets-layout-e2e.js/-multi-viewport-test-issue-1122-details-row-clamp-e2e.js/-packets-filter-ux-test-slideover-1056-e2e.js/test-slideover-1168-munger-e2e.jstest-issue-1692-…,test-path-inspector-coverage-e2e.js,test-packet-trace-alignment-e2e.jstest-issue-1486-…,test-issue-259-…,test-issue-258-…,test-packet-detail-sender-hash-size-obs-e2e.jsBrowser (Chromium, fixture, 24 h window): grouped first load 0 warnings on 61 rows (was 24); flat 0; back to grouped 0. With
resolved_pathstripped from the grouped response, 24 rows show the summary. The +N popover reads "Full path (6 hops)" for 6 hops. At 390 px the path column is hidden, as on master. No page errors.Mutants: 20 new ones, all caught (listed in the round-2 report on this PR), plus the 8 from round 1.
Static:
check-xss-sinks.sh --diff origin/masterclean,check-css-vars.js0 undefined, eslint@8 0 errors. Fork guards indeploy.yml: 9 before and after.Not verified / known limitations
HopDisplay.renderHop(hex, hopCacheGet(hex))) still read only the bare key.HOP_CACHE_MAXand the list summary are hardcoded.6649f867says "a statement per row, or 500-id batches, measured 117-126 ms"; only the 500-id batch variant was measured. The bench commit of round 1 names the port commit18afd8d; it is3c9976d8.🤖 Generated with Claude Code