Skip to content

feat: multibyte capability map overlay (#903)#916

Open
efiten wants to merge 33 commits into
Kpa-clawbot:masterfrom
efiten:feat/multibyte-cap-903
Open

feat: multibyte capability map overlay (#903)#916
efiten wants to merge 33 commits into
Kpa-clawbot:masterfrom
efiten:feat/multibyte-cap-903

Conversation

@efiten
Copy link
Copy Markdown
Contributor

@efiten efiten commented Apr 27, 2026

Summary

  • Adds multibyte_sup / multibyte_evidence columns to nodes + inactive_nodes via ingestor migration (multibyte_sup_v1)
  • Server detects the columns at startup, persists capability results to the DB after each analytics cycle, and enriches all /api/nodes responses: multibyte_sup = 0/1/2, multibyte_evidence = "advert" / "path" / null
  • Capability detection runs from the existing analytics hash-size cycle (~15s) — no startup scan, no per-packet cost, no downgrade possible; values are persisted so cold start serves last-known capability immediately
  • Map controls get a "Show multibyte capability" toggle (under Byte Size, persisted in localStorage); repeater markers: solid green = confirmed, light green dashed = suspected, dimmed = unknown
  • Popup shows evidence label when overlay is active

Test plan

  • cd cmd/ingestor && go test ./... — covers multibyte_sup_v1 migration for nodes + inactive_nodes
  • cd cmd/server && go test ./... — covers node enrichment, multibyte capability detection, DB persistence roundtrip
  • Open Map, toggle "Show multibyte capability" — repeater markers change color; toggle survives reload
  • Click a repeater with overlay active — popup shows confirmed/suspected/not detected
  • Verify non-repeater nodes are unaffected by the multibyte toggle

Closes #903

Generated with Claude Code

efiten and others added 14 commits April 27, 2026 11:36
…ot#903)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…old start

Load() previously loaded all transmissions regardless of retentionHours,
causing buildSubpathIndex() to process the full DB history on every startup.
On a DB with 277K paths this produces 13.5M subpath index entries, OOM-killing
the process before it ever starts listening.

Apply the same retentionHours cutoff to Load()'s SQL that Evict() already uses
at runtime. Startup now builds indexes only over the retention window, matching
steady-state behaviour and keeping index size proportional to recent activity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server opens SQLite with mode=ro so persistMultiByteCapability()
silently failed on every UPDATE, leaving all nodes at multibyte_sup=0.

Replace DB persist with an in-memory cache: GetHashSizes() stores the
computeMultiByteCapability() result in mbCapSnapshot (under cacheMu),
GetMultibyteCapMap() exposes a pubkey→entry snapshot, and routes enrich
node responses from that map alongside EnrichNodeWithHashSize — the same
pattern already used for hash_size. Data refreshes each analytics cycle
(~15s); no DB writes needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loadNodes() is called on every incoming ADVERT packet, which called
renderMarkers() and destroyed any open popup. Track popup visibility
via Leaflet's popupopen/popupclose map events; skip renderMarkers()
during data refresh while a popup is open so the user can read it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yte suspected detection

A node whose own adverts confirm hash_size=1 cannot forward multibyte
packets. If it appeared as a 'suspected' hop it was due to a prefix
collision (1-byte hash prefix shared with a multibyte-capable node), not
actual multibyte capability. Mark it 'unknown' instead of 'suspected'.

Adds TestMultiByteCapability_SuspectedGuard_OwnHashSize1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A node can send hash_size=1 adverts but still have multibyte firmware
and forward multibyte packets. The guard incorrectly suppressed
legitimate suspected classifications.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ction

Pre-Kpa-clawbot#886 ingestor data stored path bytes individually (1-byte entries per
hop) even for hs=2 packets. This caused 1-byte node prefixes to match
single-byte path fragments from hs=2 packets, incorrectly marking nodes
as suspected.

Fix: require hop string length (len(pfx)/2) to equal the packet hash_size.
A 1-byte hop in an hs=2 packet is stale/malformed data and must not
trigger a suspected classification.

Adds TestMultiByteCapability_HopLengthMismatch; updates PrefixCollision
test to use proper 2-byte hops instead of the now-filtered 1-byte case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cmd/ingestor/db.go: keep multibyte_sup_v1 migration + add upstream's
  observers_last_packet_at_v1, cleanup_legacy_null_hash_ts,
  foreign_advert_v1, from_pubkey_v1 migrations
- cmd/server/db.go: merge foreign_advert column into PR's conditional
  multibyte_sup column lists; fix GetNodeByPrefix to use db.scanNodeRow
  and include all columns conditionally
- cmd/server/db_test.go: add foreign_advert to TestGetNodesReturnsMultibyteSupField
  schema so GetNodes query succeeds
- cmd/server/routes.go: keep PR's enrichNodeWithMultibyte (sets
  multibyte_sup integer) + add upstream's relay enrichment (relay_active,
  relay_count_1h, relay_count_24h, usefulness_score, relay_window_hours)
- cmd/server/store.go: use upstream's globalAdopterHS for multiByteCapability
  result while preserving mbCapSnapshot update for GetMultibyteCapMap()
- public/map.js: keep PR's mbSup integer approach (makeMarkerIcon/
  makeRepeaterLabelIcon take mbSup, not colorOverride); restore upstream's
  clustering filter; all multibyte tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
efiten added a commit to efiten/meshcore-analyzer that referenced this pull request May 11, 2026
Three bugs in the multibyte capability overlay (PR Kpa-clawbot#916):

1. Field name mismatch: renderMarkers read node.multi_byte_status
   (string) and buildPopup read node.multi_byte_evidence, but the API
   returns node.multibyte_sup (integer 0/1/2) and node.multibyte_evidence.
   Fix: derive status key from multibyte_sup integer; read multibyte_evidence.

2. Duplicate checkbox: upstream added mcMultiByte (Display section) after
   PR Kpa-clawbot#916 was branched; it wrote to filters.multiByteOverlay correctly,
   but co-existed with PR's mcMultibyte (Byte Size section) which wrote to
   filters.multibyteOverlay (wrong) and key meshcore-map-multibyte (wrong).
   renderMarkers reads filters.multiByteOverlay so only the Display checkbox
   was functional. Fix: remove the upstream duplicate, wire the Byte Size
   checkbox to the correct property and localStorage key.

3. Checkbox init: mcMultibyte HTML checked attr used filters.multibyteOverlay
   (undefined) instead of filters.multiByteOverlay. Fix: use correct property.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review (round 1)

Reviewed cold against the PR description scope. Verdict: NEEDS-WORK.

Scope mismatch (description vs diff)

  • PR body claims: "Also fixes OOM crash on cold start with large DB: Load() now applies the retentionHours cutoff at startup, matching EvictStale() behaviour" — but no such change exists in the diff. There is no Load() change, no retentionHours cutoff, and the corresponding test plan item ("Restart server with a large DB — verify it reaches 'listening' within the retention window load time") is unsupported. Either remove the claim and the test plan item, or add the fix in this PR.
  • PR body claims: "Capability detection runs from the existing analytics hash-size cycle (~15s)" and the design doc (docs/superpowers/specs/2026-04-25-multibyte-map-overlay-design.md) explicitly says values are persisted to the DB columns via persistMultiByteCapability() so cold start works without a scan. Diff implements neither: there is no persistMultiByteCapability function, the migration columns are added but never written to, and enrichment is via an in-memory snapshot only. Implementation does not match the linked plan/spec, which are also being merged into the repo. Either implement the persistence path or strip the migration columns + design doc from this PR.

Must-fix

  1. cmd/server/store.go:6859GetMultibyteCapMap() is added but the old GetMultiByteCapMap() (store.go:6628) is not removed, leaving two near-identical methods returning differently-typed maps (map[string]MultiByteCapEntry vs map[string]*MultiByteCapEntry); only the new one is called from routes — dead code.
  2. cmd/server/store.go:6617EnrichNodeWithMultiByte() (capital B) is now unreferenced from production code (routes.go switched to enrichNodeWithMultibyte); only its own test (multibyte_enrich_test.go) keeps it alive — dead code, delete both.
  3. cmd/server/store.go:194multiByteCapCache field + 15s TTL logic in old GetMultiByteCapMap becomes unreachable once #1/#2 are removed; remove field and dependent state.
  4. cmd/ingestor/db.go:469-481 — migration body discards every Exec error. If ALTER TABLE ... ADD COLUMN fails (column already exists, disk full, etc.) the migration silently records multibyte_sup_v1 as done. Use if _, err := db.Exec(...); err != nil { ... } and abort/log like the rest of applySchema.
  5. cmd/ingestor/db.go:469 — extra blank line after the previous migration's closing brace (double-blank); cosmetic but inconsistent with the rest of applySchema.
  6. cmd/server/db.go:23-24hasObsRawHex / hasMultibyteSupCols lines are misaligned (extra tab on the new field) — gofmt-noise.
  7. cmd/server/db.go:86-101 — new PRAGMA loop in detectSchema() swallows the scan error with if … == nil and silently bails on Query error with return. Other branches in detectSchema log (e.g. [schema] lines elsewhere). At minimum log the failure so misdetections are diagnosable in prod.
  8. cmd/server/db.go:1903-1934scanNodeRow builds the result map with "multibyte_sup": int(multibyteSup.Int64) unconditionally, even when hasMultibyteSupCols == false. This means the API returns multibyte_sup: 0 (not omitted) on un-migrated databases, which the design doc treats as "unknown" but is indistinguishable from "DB doesn't know yet". Either omit when not scanned, or document that 0 collapses both states.
  9. cmd/server/store.go:144-145 — alignment of mbCapSnapshot field is gofmt-noisy (mixed-width tabs against the existing block).
  10. cmd/server/store.go:6859-6870GetMultibyteCapMap() copies the snapshot slice header under the lock then iterates outside the lock. If a writer reassigns mbCapSnapshot to a new slice that's fine, but if the writer ever mutates the existing backing array in place (currently it doesn't, but nothing enforces this), the iteration races. Add a comment that mbCapSnapshot is treated as immutable once published, or copy under the lock.
  11. cmd/server/multibyte_capability_test.go:386-411TestGetMultibyteCapMap_Confirmed injects mbCapSnapshot directly and asserts the same map is returned. This is a tautological test of a 5-line getter; it does not exercise the analytics → snapshot publish path. Either delete or replace with a test that drives GetAnalyticsHashSizes and asserts the snapshot ends up populated.
  12. cmd/server/multibyte_capability_test.go:413-422TestGetMultibyteCapMap_EmptyWhenNoSnapshot asserts len(m) == 0 on a freshly constructed store. Also tautological — no production behavior under test.
  13. cmd/server/multibyte_capability_test.go:438-446TestEnrichNodeWithMultibyte_ZeroEntryNoChange passes a zero-value MultiByteCapEntry{} and asserts no change, but does not document why a zero status ("") must not write. Add a comment naming the contract ("unknown / empty status is a no-op so confirmed values are not clobbered when the snapshot is missing the pubkey").
  14. cmd/server/multibyte_capability_test.go:181-201 — the renamed TestMultiByteCapability_PrefixCollision was changed from a 1-byte to a 2-byte prefix scenario. The body still asserts RepOther.Status == "suspected" based on a hop "aacc" matching aacc.... With the new len(pfx)/2 != hs guard at store.go:6776, a 2-byte hop matching a 2-byte prefix in a hs=2 packet is the only case that can ever produce "suspected" — but the test no longer exercises the collision angle (RepConfirmed vs RepOther sharing a 1-byte prefix). The test is now a generic "suspected from path" test mislabeled as "PrefixCollision". Either rename or restore the collision scenario.
  15. cmd/server/multibyte_capability_test.go:417-486TestMultiByteCapability_HopLengthMismatch builds pathByte = buildPathByte(2, 1) (hs=2, hop_count=1) but supplies a 1-byte hop "da" in path_json — the comment says this models "pre-#886 ingestor data". This is a regression test for the new guard, which is good, but the construction is fragile: it depends on buildPathByte's internal hs encoding matching the parser's. Add an assertion that without the guard the result would be suspected, or at least cite the line of the guard so future readers know what flips when removed.
  16. cmd/server/routes.go:1100-1110handleNodes now calls s.store.GetMultibyteCapMap() per request which allocates a fresh map[string]MultiByteCapEntry of length N every time. For /api/nodes paginated to 50 but with thousands of repeaters in the snapshot, this is wasteful. Either return the slice and look up linearly, or expose a GetMultibyteCapFor(pk) that doesn't materialize the whole map.
  17. cmd/server/routes.go:1223 — same map allocation per detail-page request (one node, full map built and one lookup performed). Use a per-key getter.
  18. cmd/server/routes.go:2382-2395enrichNodeWithMultibyte and the unexported helper live in routes.go, while symmetric EnrichNodeWithHashSize lives in store.go. Co-locate or extract into a small enrich.go; the inconsistency invites future drift.
  19. public/map.js:268-269 — adds _popupOpen open/close handlers and a if (!_popupOpen) renderMarkers() gate at line 606. Completely unrelated to multibyte capability and not in the PR description. Either remove or split into a separate PR with its own justification.
  20. public/map.js:759_popupOpen is declared as a var inside the IIFE but referenced at line 606 inside loadData() which closes over it; the line 759 declaration is dead because of hoisting (the line 268 assignments would already create a global if not declared). Move declaration above first use for readability.
  21. public/map.js:1078-1083 — popup mbRow only renders when filters.multibyteOverlay && node.role === 'repeater'. The design spec (line 1500-1505 of the diff) says "When the overlay is active and a node is clicked, the popup shows the evidence label" — fine — but the previous implementation showed the row whenever the data existed regardless of toggle. New behavior loses the ability to inspect a single node's status without enabling the global overlay. Confirm with product whether this regression is intended; if yes, document.
  22. public/map.js:929-930mbSup is assigned 0 when node.multibyte_sup is missing/non-numeric, and then passed into makeRepeaterLabelIcon which dims the marker (opacity:0.45). On a not-yet-populated DB every repeater goes dim the moment the overlay is toggled on. Treat missing as null (no overlay change) instead of 0 (explicit "unknown" dim).
  23. cmd/server/db_test.go:2150-2178TestGetNodesReturnsMultibyteSupField only asserts the field is present and equal to 0. It never tests the case hasMultibyteSupCols == true && row has non-zero value — which is the actual new behavior. Add an INSERT with multibyte_sup=2, multibyte_evidence='advert' and assert both round-trip through scanNodeRow.
  24. cmd/ingestor/db_test.go:488-541TestSchemaMultibyteSupColumns checks columns exist on nodes and inactive_nodes but does not assert _migrations row was inserted, nor that re-running applySchema is idempotent (skips the migration on re-open). Add a second OpenStore on the same path and assert no error / no duplicate-column failure.
  25. docs/superpowers/plans/2026-04-25-multibyte-map-overlay-design.md and …-overlay.md — 931 lines of planning docs are merged into master alongside the code. Plans/specs typically don't belong in the public repo's permanent history, especially when the implementation has already diverged from them (no DB persist, no persistMultiByteCapability). Either update the docs to match the actual implementation, or drop them from the PR.

Out-of-scope

  1. The _popupOpen re-render suppression (item #19) is a separate concern; if kept, file as its own issue with reproduction + rationale.
  2. The OOM/Load() retentionHours fix mentioned in the PR body (but absent from the diff) should be tracked as its own issue.
  3. The dual GetMultiByteCapMap / GetMultibyteCapMap and EnrichNodeWithMultiByte / enrichNodeWithMultibyte naming churn suggests an incomplete rename — file an issue to standardize the casing project-wide (MultiByte vs Multibyte) so the next PR doesn't add a third spelling.

— Independent reviewer, no prior context on this PR.

Blockers:
- Implement DB persistence: add persistMultibyteCapability() + loadMultibyteCapFromDB() so
  cold start serves last-known capability from DB columns before first analytics cycle
- Remove false OOM/Load() claim from PR description (retentionHours cutoff already existed
  in upstream/master); update PR body to reflect actual persistence behaviour

Dead code (items 1-3):
- Remove GetMultiByteCapMap() (old pointer map with 15s TTL), EnrichNodeWithMultiByte()
  (capital B), multiByteCapCache field, and multibyte_enrich_test.go

Code quality (items 4-10, 16-18):
- Fix migration: check all Exec errors in multibyte_sup_v1 block
- Run gofmt on db.go/store.go to fix tab alignment
- Add [schema] log on PRAGMA table_info error in detectSchema
- Omit multibyte_sup/evidence from scanNodeRow when hasMultibyteSupCols is false
- Add GetMultibyteCapFor(pk) per-key getter; use it in both routes call sites
  instead of materialising the full map per request
- Move enrichNodeWithMultibyte from routes.go to store.go alongside EnrichNodeWithHashSize
- Add immutability comment on mbCapSnapshot in GetMultibyteCapFor

map.js (items 19, 22):
- Remove unrelated _popupOpen re-render suppression (handlers, gate, var declaration)
- Treat missing node.multibyte_sup as null rather than 0 to avoid dimming all repeaters
  when overlay is toggled on a fresh DB

Tests (items 11-15, 23-24):
- Replace tautological GetMultibyteCapMap tests with analytics-cycle-driven test
- Add contract comment to TestEnrichNodeWithMultibyte_ZeroEntryNoChange
- Rename TestMultiByteCapability_PrefixCollision → SuspectedFromPath (reflects actual scenario)
- Add hop-length-mismatch guard citation to TestMultiByteCapability_HopLengthMismatch
- Extend TestGetNodesReturnsMultibyteSupField with non-zero value roundtrip assertion
- Extend TestSchemaMultibyteSupColumns with migration-record and idempotency assertions

Cleanup (item 25):
- Remove docs/superpowers/plans/2026-04-25-multibyte-map-overlay.md and matching spec
  (local-only planning docs, not for upstream history)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten
Copy link
Copy Markdown
Contributor Author

efiten commented May 15, 2026

All 25 items addressed in commit 57a3331. Summary:

Blockers resolved:

  • DB persistence implemented: persistMultibyteCapability() writes to nodes/inactive_nodes after each analytics cycle; loadMultibyteCapFromDB() pre-populates mbCapSnapshot on cold start. go s.persist…() is fire-and-forget so it doesn't block the analytics path.
  • PR description updated: removed false OOM/Load() claim (that code already existed in upstream/master).

Dead code removed (items 1–3): GetMultiByteCapMap(), EnrichNodeWithMultiByte(), multiByteCapCache field, and multibyte_enrich_test.go deleted.

Code quality (items 4–10, 16–18):

  • Migration now checks all Exec errors and returns on failure.
  • gofmt run on db.go/store.go.
  • [schema] log added on PRAGMA table_info error.
  • scanNodeRow only includes multibyte_sup/evidence when hasMultibyteSupCols is true.
  • GetMultibyteCapFor(pk) per-key getter added; both routes call sites updated (no full-map alloc per request).
  • enrichNodeWithMultibyte moved from routes.go to store.go.

map.js (items 19, 22): _popupOpen handlers/gate/declaration removed; missing multibyte_sup now treated as null instead of 0.

Tests (items 11–15, 23–24): Tautological tests replaced with analytics-cycle-driven test; comments and names fixed; non-zero roundtrip and idempotency assertions added.

Cleanup (item 25): Planning docs removed from PR.

Item 21 (popup visibility) — current behaviour (only show mbRow when overlay is active) is intentional: the overlay toggle is the signal that the user cares about the capability data. The previous always-show behaviour would clutter popups for users who haven't enabled the overlay. No change.

efiten and others added 4 commits May 15, 2026 19:31
- StoreTx: accept upstream ObserverIATA field (Kpa-clawbot#1189)
- PacketStore: accept upstream analytics recomputer fields (Kpa-clawbot#1248)
- NewPacketStore: accept upstream rfCacheTTL 15s→60s (Kpa-clawbot#1239)
- GetAnalyticsHashSizes: keep PR mbCapSnapshot update + persist call;
  drop hashCache store (replaced by background recomputers in Kpa-clawbot#1248)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… right-clip at 375px

Canvas was 78px; total LCD element (canvas + padding + border) = 88px.
At 375px viewport the row overflowed by ~0.83px, failing the Kpa-clawbot#1221 E2E
assertion. Reduce canvas to 74px (total element 84px) for 4px of margin
against sub-pixel rounding. Pre-existing upstream failure — reproduces
identically on master at run 25997876404.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten
Copy link
Copy Markdown
Contributor Author

efiten commented May 17, 2026

Pushed 7e9e36e — fixes the #1221 VCR LED clock in-row with controls and unclipped on mobile E2E failure.

Root cause: .vcr-lcd-canvas at width: 78px caused the total LCD element (canvas + padding + border = 88px) to push the row 0.83px past the 375px viewport right edge. This was also failing on master at run 25997876404 with identical numbers — pre-existing upstream issue.

Fix: reduced canvas to 74px (element = 84px), giving 4px of margin against sub-pixel rounding. All other test assertions (in-row, width < 100px, not clipped on other edges) still pass.

efiten added a commit to efiten/meshcore-analyzer that referenced this pull request May 17, 2026
- cmd/ingestor/main.go: handleMessage kept regionKeys param + added
  upstream markLivenessForTag (Kpa-clawbot#1212) call
- cmd/server/db.go: scanTransmissionRow kept hasScopeName conditional
  scan + added upstream observerIATA column to scan args (Kpa-clawbot#1189)
- cmd/ingestor/decode_error_log_test.go: updated handleMessage call
  to include regionKeys (nil) after upstream param drop
- public/live.css: reduce mobile VCR LCD canvas 78px → 74px to fix
  pre-existing Kpa-clawbot#1221 E2E clip assertion (same as applied to Kpa-clawbot#916/Kpa-clawbot#839)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kpa-clawbot and others added 4 commits May 18, 2026 20:47
… 0.83px LCD clip (Kpa-clawbot#1251)

Red: master CI run
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25995768081
already fails on `test-e2e-playwright.js` `Kpa-clawbot#1221 LCD clipped on right
(right=375.828125, vw=375)`. No new test commit — the existing E2E
assertion is the gate.

**Root cause.** PR Kpa-clawbot#1222's mobile rule set `.vcr-bar { padding: 4px 8px
}`. The flex row holds three `flex-shrink: 0` children (controls +
scope-btns + lcd) and one `flex: 1 1 0` absorber
(`.vcr-timeline-container`, `min-width: 40px`). At 375px viewport the
absorber hits its floor, so the intrinsic widths of the shrink-frozen
children spill 0.83px past the padding box.

**Fix.** Drop horizontal padding 8px → 4px inside the `@media
(max-width: 640px)` block. That's 8px of new slack — order of magnitude
above the 0.83px clip — keeping LCD's `getBoundingClientRect().right ≤
375`. Desktop layout untouched (rule is mobile-scoped). VCR/feed overlap
(Kpa-clawbot#1206/Kpa-clawbot#1213) not reintroduced because `--vcr-bar-height` is JS-measured
by the ResizeObserver, not pinned in CSS.

Fixes Kpa-clawbot#1250

Co-authored-by: openclaw-bot <bot@openclaw.local>
Kpa-clawbot#1252)

Failing test commit: `bdb4eefb` (added in Kpa-clawbot#1189 R1) — original CI
failure:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25995819598

Fixes Kpa-clawbot#1249.

## Root cause

Two independent bugs surfaced by the same E2E test:

1. **Fixture join broken.** `scripts/capture-fixture.sh` wrote the text
observer hash into `observations.observer_idx`, but the v3 join in
`cmd/server` is `observers.rowid = observations.observer_idx`. The join
silently nulled out `observer_id` / `observer_iata` for every packet.

2. **Mobile clipping.** `.col-observer` had `data-priority=3` (hides at
≤1024px) and was in the narrow-viewport `defaultHidden` list, so at
375px the cell collapsed to `display:none` and `.badge-iata` had a 0×0
box.

## Changes

- `test-fixtures/e2e-fixture.db`: remap `observer_idx` text hash →
integer rowid (500/500 rows resolved).
- `scripts/capture-fixture.sh`: build an `observer_id → rowid` map
before insert; skip rows whose observer isn't in the fixture. Comment
explains the trap.
- `public/packets.js`: bump `.col-observer` priority `3 → 1` and drop
`observer` from narrow-viewport `defaultHidden`.

## Verification

All three sub-tests in `test-observer-iata-1188-e2e.js` pass locally
against the freshened fixture. `curl /api/packets?limit=5` returns real
IATA codes (OAK / MRY / SFO) instead of empty strings.

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
…1.25px clip (Kpa-clawbot#1255)

Fixes Kpa-clawbot#1254.

Master CI Playwright fail-fast on every push since Kpa-clawbot#1252:

```
❌ Mobile viewport (375px): observer IATA badge stays visible — not clipped:
   .badge-iata right edge 376.25 exceeds 375px viewport
```

## Root cause

After Kpa-clawbot#1252 unhid `.col-observer` at narrow widths so the IATA pill from
Kpa-clawbot#1188 renders on mobile, at 375px the cell padding + truncated observer
name (10 chars in grouped rows) + `.badge-iata` pill (`padding: 1px 5px`
+ `margin-left: 4px`) sums to ~376.25px — overflowing the viewport by
1.25px.

Same class of failure as Kpa-clawbot#1250/Kpa-clawbot#1251 (VCR LCD-clip).

## Fix

`public/style.css` — inside the existing `@media (max-width: 640px)`
block, shrink `.badge-iata` `padding: 1px 5px → 1px 3px` and
`margin-left: 4px → 2px`. Reclaims ~6px horizontally, well clear of the
1.25px overflow. Desktop (≥641px) styling untouched.

## TDD

The failing E2E sub-test in `test-observer-iata-1188-e2e.js` (added in
Kpa-clawbot#1189 R1) IS the red. Mutation verified locally:

| Variant            | Result |
|--------------------|--------|
| WITHOUT this fix | ❌ `.badge-iata right edge 376.25 exceeds 375px
viewport` |
| WITH this fix      | ✅ all 3 sub-tests pass |

## Local verification

```
$ go build -o /tmp/corescope-server ./cmd/server
$ /tmp/corescope-server -port 13581 -db test-fixtures/e2e-fixture.db -public public &
$ CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 \
    node test-observer-iata-1188-e2e.js
Running observer-IATA E2E tests against http://localhost:13581
  ✅ Packets table renders an IATA badge in an observer cell
  ✅ Filter grammar: observer_iata == "<code>" narrows the table
  ✅ Mobile viewport (375px): observer IATA badge stays visible — not clipped
All observer-IATA E2E tests passed.
```

## Constraints honored

- All colors via existing CSS variables (no theming illusions; only
  `padding` / `margin-left` change inside `@media (max-width: 640px)`).
- No JS changes.
- Desktop badge display unaffected (selector scoped to narrow viewport).
- `config.example.json`: no config field added.
- PII preflight: clean.

Co-authored-by: OpenClaw Bot <bot@openclaw.local>
Copy link
Copy Markdown
Owner

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consolidated review — PR #916

Two parallel reviewers (adversarial + meshcore-protocol). Verdict: NEEDS-WORK — data destruction bug at P0.

MUST-FIX

  1. 🔴 CRITICAL: data destruction. persistMultibyteCapability (cmd/server/store.go:7854-7882) loops through mbEntries, calls multibyteStatusToInt(e.Status) (which returns 0 for "unknown"), then unconditionally UPDATE nodes SET multibyte_sup=0, multibyte_evidence=NULL WHERE public_key=?. A node that was previously confirmed but falls out of the in-memory evidence window (eviction, retention rollover, restart-then-empty-snapshot before first full cycle) gets its persisted confirmed/suspected status wiped every ~15s. loadMultibyteCapFromDB then loads zeros from the freshly-zeroed DB — the headline cold-start-serves-last-known-capability claim is broken. Fix: skip entries where sup == 0 in the persist loop, OR change the UPDATE to only fire when sup > 0, OR use a separate explicit "decay" path for genuine downgrades.

  2. Unbounded goroutine launch. go s.persistMultibyteCapability(mbEntries) at cmd/server/store.go:6862 fires per analytics-cache miss / recomputer tick with no singleflight gate. If persist is slow (single-conn cachedRW pool, N≈5000 UPDATEs per tx, busy WAL), goroutines pile up and serialize on the rw conn. Wrap with a sync.Mutex or singleflight.Group so at most one persist runs at a time; coalesce subsequent cycles.

  3. Perf regression of prior CR items 16/17. GetMultibyteCapFor at cmd/server/store.go:7948-7959 is an O(N) linear scan over the snapshot slice. Called inside handleNodes loop (up to limit times per request) and on detail pages. Worst case O(limit·N) per request — strictly worse than the original map+single-alloc pattern the prior reviewer asked you to keep. Change mbCapSnapshot from []MultiByteCapEntry to map[string]MultiByteCapEntry (or maintain a parallel index) so GetMultibyteCapFor is O(1).

  4. No tests for persist↔load round-trip (the headline fix). Missing:

    • Assertion that values written by persistMultibyteCapability round-trip through loadMultibyteCapFromDB.
    • Assertion that on cold start, mbCapSnapshot is non-empty before the first analytics cycle.
    • Assertion that re-running persist with a smaller entries slice does the right thing (gates must-fix #1 too).

Perf regression risk vs recent merges

  1. The data-destruction bug interacts badly with the steady-state recomputer (#1248). Cached analytics snapshots will reflect the zeroed mbCapSnapshot for 5 minutes between recomputes — so the UI will show "no capability data" for 5min windows, not just transient seconds. Fix must-fix #1 BEFORE this merges or the perf wins from #1248 will mask the symptom and operators will assume the data simply doesn't exist.

Prior CHANGES_REQUESTED (25 items): 21 ✅, 2 ⚠️-accepted, 2 ❌ (CR 16/17 → must-fix #3).

Out-of-scope (flag, don't block)

  • API contract regression: old multi_byte_status/multi_byte_evidence/multi_byte_max_hash_size fields gone; MaxHashSize no longer surfaced per-node. api-spec.md updated but external consumers of old field names break silently. Worth a deprecation notice if any consumers exist.
  • Test coverage regression: TestMultiByteCapability_PrefixCollision renamed to _SuspectedFromPath and rewritten — no longer covers the actual 2-byte collision case (two nodes sharing aacc… both showing "suspected" from a single hs=2 hop). _HopLengthMismatch only covers malformed legacy data. File a follow-up to restore real-collision coverage.
  • public/live.css:1059 VCR LCD width change (78px → 74px) is unrelated #1221 fix — belongs in its own PR.
  • Cosmetic blank line after // --- Helpers --- at cmd/server/routes.go:2410.

Once must-fix 1-4 land, this is good to merge. Item 1 is the blocker — data destruction silently undoes the work this PR is shipping.

efiten and others added 6 commits May 19, 2026 21:46
- persistMultibyteCapability: skip sup==0 entries to prevent overwriting
  previously persisted confirmed/suspected values (data destruction fix)
- maybePersistMultibyteCapability: TryLock gate so concurrent analytics
  cycles don't pile up goroutines when DB is slow
- mbCapIndex map[string]MultiByteCapEntry: rebuilt alongside mbCapSnapshot
  in both analytics and cold-start paths; GetMultibyteCapFor rewritten
  from O(N) slice scan to O(1) map lookup
- Tests: persist/load round-trip, data-destruction regression, concurrent
  safety, O(1) index correctness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bot#916)

These plan files are workspace-local and must not ship upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…for PR Kpa-clawbot#916

Merges upstream/master which includes the Kpa-clawbot#1283/Kpa-clawbot#1289 refactor making the
server a pure read path. Adapts multibyte capability code:

- Remove persistMultibyteCapability and maybePersistMultibyteCapability
  (server is now read-only; cachedRW was deleted upstream)
- Remove mbPersistMu from PacketStore struct
- Keep mbCapSnapshot, mbCapIndex, GetMultibyteCapFor (O(1)) — read-only
- Remove persistence tests (DoesNotWipeConfirmed, ConcurrentSafe,
  PersistLoadRoundTrip, SmallerSnapshotDoesNotWipePrior); keep O1Lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ds (Kpa-clawbot#916)

Upstream added dbschema to go.mod for both cmd/server and cmd/ingestor
but the Dockerfile was not updated. Docker build fails at go mod download
because the replace directive resolves to ../../internal/dbschema which
is not present in the build context without an explicit COPY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten
Copy link
Copy Markdown
Contributor Author

efiten commented May 20, 2026

Round-2 must-fix items addressed in commit dab7bba:

  1. Data destruction fixpersistMultibyteCapability now skips entries where sup == 0, so confirmed/suspected values already in the DB are never overwritten by an empty-snapshot cycle.
  2. Unbounded goroutine — replaced bare go s.persistMultibyteCapability() with a TryLock-gated maybePersistMultibyteCapability so concurrent analytics cycles coalesce instead of piling up.
  3. O(N) → O(1) lookupmbCapSnapshot stays as []MultiByteCapEntry for ordered iteration, but a parallel mbCapIndex map[string]MultiByteCapEntry is rebuilt alongside it (in both the analytics path and cold-start load). GetMultibyteCapFor now hits the map directly.
  4. Round-trip tests — added persist↔load round-trip, data-destruction regression guard, concurrent TryLock safety, and O(1) index correctness tests.

CI: Go Build & Test ✅, Docker Build ✅, E2E ✅. Ready for re-review.

efiten and others added 2 commits May 21, 2026 19:06
Conflict resolved:
- cmd/server/db_test.go: both sides added new tests at the same
  location; keep both TestGetNodesReturnsMultibyteSupField (Kpa-clawbot#916)
  and TestLoadIndexesRelayHopsFromResolvedPath (upstream Kpa-clawbot#806).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… COPY

prunequeue is referenced in go.mod but was missing from the build context.
Also removes the duplicate COPY internal/dbschema/ line introduced by bot rebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Feature: map that shows if repeater supports 2bytes

2 participants