Skip to content

ES-4034 botmonAlpha: workflow edge times, trace icons, rogue detection, chart checkpoint line, humanized durations - #45

Open
Detroitiron wants to merge 11 commits into
masterfrom
feature/ES-4034-botmon-alpha-parity-fixes
Open

ES-4034 botmonAlpha: workflow edge times, trace icons, rogue detection, chart checkpoint line, humanized durations#45
Detroitiron wants to merge 11 commits into
masterfrom
feature/ES-4034-botmon-alpha-parity-fixes

Conversation

@Detroitiron

@Detroitiron Detroitiron commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Co-authored by Claude on 2026-08-27.

Summary

Six botmonAlpha defects found while dogfooding the new UI against legacy botmon. Legacy is the behavioral reference throughout; it is not changed here.

Three of the six looked cosmetic but were data-layer bugs:

  • The workflow view's "0 / N/A" on every edge was a stats-merge crash that 500'd the stats API, plus a link-key direction mismatch that made the downstream half of every graph unmatchable.
  • Rogue and blocked were structurally unreachable: the status code summed an errors field on read/write stats that the Leo stats table never writes, so window errors were always 0, the error-rate alarm never fired, and the catalog's Errors column read 0 for every bot.
  • A rogue bot showed rogue in the tree but healthy on its own dashboard, because the header read only the shared catalog, which is fetched fire-and-forget (10s+ on a large bus) and keyed by bare ids while page ids can carry a bot: prefix.

The rest are presentation: trace icons built from the wrong asset prefix, a red chart line marking wall-clock "now" instead of the read checkpoint, and raw milliseconds on duration charts.

Tier

Tier: 2

Jira

Scope

One concern: botmonAlpha/legacy-botmon parity for the six defects reported on ES-4033. All six are display-correctness bugs in the same UI reported together during one dogfooding pass, and three share a root cause in how bot status reads stats — splitting them would separate a fix from its own regression test. No schema changes, no refactors bundled, no backend/pipeline changes.

What changed

1 · Workflow edge times (the N/A defect)

  • mergeStatsResults threw Object.entries(undefined) on any record lacking read or write (write-only bots, queue records), which 500'd /api/workflow/stats and blanked every edge label. Guarded with ?? {}, matching legacy's || {}.
  • New queue entries were assigned the same shared default object, aliasing every queue of a bot to one record; now cloned per queue.
  • Link-stats lookups are direction-aware: the stats are keyed downstream-first, but the children tree looked them up upstream-first, so right-side edges could never match. Both lookup sites — the edge labels and the relationship-importance scoring — now build keys through one shared linkStatsKey() helper.
  • The zero-event fallback indexed checkpoint maps with prefix-stripped ids; they are keyed by full refId (queue:foo). Quiet edges now show the checkpoint time like legacy instead of N/A.
  • fetchBotStats checks res.ok and response shape instead of merging undefined and throwing inside an untracked async.

2 · Event-trace iconstrace-fanout-tree built PNG URLs from bare base. Static files are served from CloudFront via assets; base routes through API Gateway into the SvelteKit Lambda, which has no static routes → 404 → broken-image placeholders. Now uses the app-wide assets || base prefix that every other icon consumer already uses. Invisible in dev, where both prefixes are empty.

3 · Rogue / blocked detection

  • Error and execution counts now read stats.execution — the only place the bus records errors.
  • The persisted errorCount that drives rogue exists in two independent copies: the shared bot catalog, and the page's own settings record. Refresh timers now update both, so the count is no longer frozen at page-load state (legacy re-scanned the cron table every 10s). Refreshing only the catalog cannot reach the dashboard header, which prefers the page record.
  • The relationship tree rebuilds after status evaluation instead of snapshotting status by value forever.

4 · Chart line — legacy draws a red read-cutoff line only on the one chart that can lag, at the read checkpoint. The new charts had inverted this: a wall-clock line on every chart, with the checkpoint line only where a prop happened to be passed. Removed the "now" line from the bucket, line, and sparkline components. Where a checkpoint exists — Events In Queue and the read-side sparklines — the checkpoint line is now the only line, and a || 0 call site that would otherwise pin it at epoch 1970 is guarded. GenericLineChart gets no line at all, deliberately: it backs execution count, error count, execution time, events written and write lag, none of which can lag behind a read checkpoint, and legacy draws nothing on them either.

5 · Duration formatting — added dataIsTimeBased to GenericBucketLineChart (mirroring the prop GenericLineChart already had) so Execution Time and Read Lag axis ticks and tooltips go through humanize(), matching their already-humanized summary totals.

6 · Rogue visibility — the dashboard header derives rogue from the page's own strongly-consistent settings record (the same source the Paused badge already used), falling back to the catalog, with prefix-tolerant id matching. Shown as a solid red badge with the error count and a ring on the bot avatar. In the tree, error-state nodes get a tinted fill and heavier stroke — a rogue node was previously a dark shape with a dark-red outline on a dark canvas, reading as quieter than the healthy green rings around it.

Supporting

  • Badge logic extracted to resolveHeaderBotStatus / isSameBotId so it is unit-testable rather than living untestable in markup.
  • Removed the phantom ReadWriteStats.errors field from the type and from mock-bots fixtures, which had been encoding the wrong record shape and validating the bug.
  • AGENTS.md gains the six footguns this work surfaced (assets-vs-base, where errors live, absent read/write keys, refId-keyed checkpoints, rogue's count living in two places that both need re-fetching, and the downstream-first link-key convention).
  • webapp bumped to 4.3.4.

Review round 2 (webapp 4.3.4)

A second review pass found three defects, two of which meant a fix in this PR never reached production. All three are fixed above; the details are in decision-log AD-008 through AD-011.

  • The header rogue badge was still frozen at page load. The 4.3.1 fix refreshed the shared catalog, but the header prefers the page's own settings record, and a defined 0 from page load permanently shadowed the fresher catalog value. Worse, the unit test asserting "settings is fresher than the catalog" encoded a premise the runtime inverted — a green test over a condition the app never satisfied. Fixed by making the premise true: the refresh timer now re-reads the settings record.
  • checkPointValue on GenericLineChart was unreachable. The wrapper never forwarded it and no caller passed one, so removing the wall-clock line left those charts with nothing rather than a checkpoint line. Per the decision above that is the intended behavior, so the dead prop is gone and the component states why instead of implying a line that could never render.
  • The link-key direction fix was half-applied. calculateRelationshipImportance still built its key upstream-first in both directions, so every lookup missed once the edge-label fix landed: event counts fell to 0 and now - undefined made every importance score NaN, which emptied the relationship list whenever "include inactive" was off. The line predates this PR, but it is the same bug this PR claims to fix, so it is fixed here — through the shared helper, since two hand-built copies of the convention are what allowed the drift.

Two smaller items came along: the error-rate alarm compared a rate that is forced to 0 at zero executions, so a bot with errors and no successful runs never alarmed where legacy would — it now compares counts the way legacy does. And the lockfile's 108 peer: true flips are reverted; a plain npm install rewrites them, so they were version-dependent noise rather than a dependency change.

The review's remaining findings are deferred to follow-ups under ES-4142 rather than widening an open PR: the blocked status now being reachable with broader semantics than legacy's (a product call, not a code fix), the catalog Errors column mixing windowed and persisted counts, the cost of /api/workflow/relationships sitting on two timers, the tree timer's every-other-tick cadence, buildRelationShipTree() running where no tree is mounted, and mergeStatsResults' min_duration seed of 0.

Tests / Validation (Ran / Not Run)

Env / Path Ran? Evidence or testable rationale
Unit + component tests npm test — 192 passed (19 files), node + Storybook chromium projects
New regression tests Merge guards for absent read/write, per-queue aliasing, execution-sourced error counts, the zero-execution error alarm, and header rogue resolution (catalog-not-loaded, stale-catalog, threshold boundary, bot: prefix)
Link-key regression test link-stats-key.test.ts builds the map with the real writer and reads it back with the real key builder. Verified RED against the old key order: 7 of its 9 cases fail
Source-guard tests Fail if any chart reintroduces x={now}, any component builds a PNG URL from bare base, or the dashboard timer drops its settings re-read. These are literal source matches — deletion tripwires, not behavioral proofs; a rename or an equivalent rewrite walks past them
Type check svelte-check 38 errors vs 43 on origin/master (measured in a clean baseline worktree); net −5, unchanged by the 4.3.4 fixes, none in touched code
Prod data verification Read-only DynamoDB: rcs-service-prod-satori-attempt-emitter has LeoCron errorCount 29 vs threshold 10 — confirming the tree was right and the dashboard was the bug
Deployed to test-cup ./scripts/deploy.sh test-cuphttps://test-apps.dsco.io/botmonAlpha returns 200 on the deployed branch. Last deploy was webapp 4.3.3 on 2026-08-27; the 4.3.4 review fixes are not on the stage yet
Asset-prefix fix (defect 2) Verified live on test-cup: bot.png returns 200 via the CloudFront assets prefix the fix now uses, and 302 (auth redirect, not the image) via the bare-base path the old code built — confirming both the root cause and the fix on a real stage, which dev cannot show since both prefixes are empty there
Deployed-stage visual check Stage is deployed and awaiting a human side-by-side pass against legacy botmon (edge times, trace icons, rogue badge, chart line, humanized durations)
Original rogue incident replay The bot from the original report had recovered (errorCount back to 0) — rogue is a transient, reset-on-success counter and history is not stored. Verified against a second, currently-rogue bot instead.

Rollout

  • Feature flag: no
  • Backwards compatible: yes — UI-only display fixes; no API contract, schema, or payload changes
  • Data migration: no
  • Rollback path: revert the PR and redeploy; nothing persists state

AI Usage

  • Tool(s): Claude Code (Opus 5 / Fable 5), 2026-08-25 → 2026-08-28
  • Skills / sessions: create-task-from-jira, task-toml, worktree-for-task, ambiguity-and-decision-log, knowledge-capture, pr-from-task; four parallel code-archaeology agents, one per reported defect
  • The moment AI changed the outcome: the archaeology pass on the rogue defect found that calculateErrorCount summed errors off read/write stat entries — a field the Leo stats table never writes — by cross-reading legacy lib/stats.js, which records errors only under current.execution. That made blocked and the error-rate alarm structurally unreachable in production and pinned the catalog's Errors column to 0 for every bot, and it explained the reported symptom exactly: 35 errors visible on the bot's own Error Count chart (which reads execution.errors) while its status showed healthy. The same pass found the test fixtures encoded the same wrong shape, which is why the suite was green over a bug — so the fixtures were corrected alongside the code. Without that cross-read the likely "fix" was to lower the rogue threshold, which would have changed nothing.
  • Where it needed a second pass: the same rogue work then shipped a fix that could not reach the surface it was written for, and a unit test that passed while asserting a freshness ordering the runtime inverted. An AI review pass caught it, and the follow-up traced the mechanism (settingsErrorCount ?? catalogEntry?.errorCount against a settings record nothing refreshed) and the two related defects in round 2 above. The lesson generalizes past this PR: a test asserting that one data source is fresher than another pins a premise that lives in the callers, not in the helper under test — so it can stay green over a live bug. That is now written up in the workspace knowledge base.

Reviewers

Checklist

  • Brief + Codework JIRA tickets linked above
  • Brief written before code started; plan reviewed; brief re-edited where plan revealed gaps (defects 5 and 6 added to brief, codework, and plan mid-flight)
  • Tests present (or testable rationale for any "not run" row)
  • AI usage section is substantive — names the moment AI changed the outcome
  • Per-repo CLAUDE.md / AGENTS.md updated if a gap surfaced
  • One concern per PR (no bundled schema + behavior + refactor)
  • No ~/code/... paths in the body
  • feature/<CODEWORK-KEY>-<slug> branch; <CODEWORK-KEY> <description> title
  • Code follows project style; L1 then L2 self-review complete

Note

Medium Risk
Touches core bot status, stats merge, and workflow visualization paths used in production monitoring; changes are UI/display-focused with broad regression test coverage but high user visibility if edge cases remain.

Overview
Aligns botmonAlpha with legacy botmon for six dogfooding defects (ES-4034), mostly by fixing how stats are merged, keyed, and interpreted—not just UI polish.

Workflow graphmergeStatsResults no longer throws on records missing read/write and clones per-queue defaults so edges don’t 404 the stats API or show 0 / N/A everywhere. Link stats are keyed downstream-first via shared linkStatsKey(), with correct handling of queue vs bot LeoStats shapes, checkpoint catch-up, and getLinkLabel() read lag (event age vs run time, window compareTimestamp). Refresh paths also re-fetch bot settings and rebuild the relationship tree when status changes.

Rogue / blocked / alarms — Window errors and executions read current.execution only; error-rate alarms use legacy’s count comparison. Dashboard header rogue uses the page settings errorCount (with catalog fallback and bot: id matching) and timers refresh both settings and catalog. Tree nodes get clearer error styling.

Charts & trace — Red annotation marks read checkpoint only where lag applies (not wall-clock “now” on every chart). Duration bucket charts gain dataIsTimeBased humanization. Trace PNGs use assets || base.

Docs & testsAGENTS.md footguns, fixtures/types drop phantom read/write errors, plus unit, merge, link-key, link-record, and source-guard tests. Version 4.3.5.

Reviewed by Cursor Bugbot for commit 917b303. Bugbot is set up for automated code reviews on this repo. Configure here.

Detroitiron and others added 7 commits August 25, 2026 13:59
The trace fan-out tree built its PNG icon URLs from SvelteKit's bare
`base` path. In deployed stages that routes through API Gateway into the
Lambda, which has no static routes, so every node icon 404'd into a
broken-image placeholder. Static files are served from CloudFront via
the `assets` path; use the app-wide `assets || base` convention that
every other icon consumer already follows (utils.getNodeTypeLink,
bot-relationship-tree, dash-header). Invisible in dev where both
prefixes are empty.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
… now

Legacy botmon draws its red read-cutoff line only on the one chart that
can lag (Events In Queue), at the read checkpoint's event timestamp,
snapped to the last data bucket at or before it. The new charts inverted
that: an AnnotationLine at Date.now() on every chart (bucket charts,
line charts, and the sparkline fallback), with the checkpoint line only
where a prop happened to be passed.

- generic-bucket-line-chart-inner: drop the now-line; the checkpoint
  line (when checkPointValue is supplied and > 0) is now the only line,
  drawn solid red. The `|| 0` guard matters: chart-details-pane passes
  `last_read_event_timestamp || 0`, which previously slipped past the
  Number.isFinite check and would pin a line at epoch 1970.
- generic-line-chart-inner: same, via a new optional checkPointValue
  prop (no caller passes it yet — write-side/execution charts can't lag
  and now correctly show no line).
- sparkline-inner: remove the `return now` fallback; no checkpoint means
  no line (write-side sparklines can't lag). Drops the now-unused 30s
  clock tick.
- Update the help tooltip that described the "now" line.

A source-guard test fails if any chart reintroduces `x={now}`, and also
pins the assets-prefix fix for static PNGs.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
Workflow view showed "0 / N/A" on every edge, and heavily-erroring bots
(observed: invoice-prod-invoice-offload-ddb-invoice-entity, ~35 errors
in 45min on 2026-08-25) showed no rogue/blocked state anywhere. Six
stacked defects:

Edge stats (the N/A defect):
- mergeStatsResults threw Object.entries(undefined) on any LeoStats
  record lacking `read` or `write` (write-only bots, queue records),
  500ing /api/workflow/stats and blanking every edge label. Guard with
  `?? {}` like legacy lib/stats.js.
- New queue entries were assigned the SAME shared default object, so
  every queue of a bot aliased one record; clone it per queue.
- Link-stat keys are stored downstream-first, but the right (children)
  tree looked them up upstream-first, so the downstream half of the
  graph could never match; the lookup is now direction-aware.
- The zero-event fallback indexed checkpoint maps with prefix-stripped
  ids, but they are keyed by full refId (queue:foo); it now tries the
  refId forms, so quiet edges show the checkpoint time like legacy.
- fetchBotStats now checks res.ok and response shape instead of merging
  undefined and dying inside an untracked async.

Rogue/blocked (the missing-status defect):
- Window error/execution counts now read stats.execution — the only
  place the Leo stats table records errors. The old code summed an
  `errors` field off read/write entries that the bus never writes, so
  blocked and the error-rate alarm were structurally unreachable and
  the catalog Errors column was pinned to 0. (The bot's cron errorCount
  is 0 today — the incident passed — but the incident screenshot's
  Error Count chart reads execution.errors and showed 35, proving where
  the data lives.)
- Refresh timers (dashboard, relationship tree) now re-fetch bot
  settings alongside stats; the persisted errorCount that drives ROGUE
  was previously fetched once per page load and frozen (legacy
  re-scanned the cron table every 10s). fetchBotSettings' 30s stale
  guard keeps this cheap.
- The relationship tree rebuilds after status evaluation instead of
  snapshotting status by value forever.
- dash-header shows a Rogue (with tooltip) or Blocked badge; the bot
  dashboard previously had no error-state indicator at all.
- mock-bots fixtures now put errors under execution, matching the live
  record shape, with the phantom ReadWriteStats.errors field removed
  from the type; regression tests cover the merge guards, aliasing,
  and execution-sourced error counts.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
…TS.md

AGENTS.md gains the footguns this task surfaced: assets||base for static
PNGs, errors live only under current.execution, read/write can be absent
from stats records, checkpoint maps are keyed by full refId, and rogue's
persisted errorCount needs periodic re-fetch. Lockfile diff beyond the
version bump is peer-flag/formatting churn from the current npm, no
dependency changes.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
The Execution Time (bot dashboard) and Read Lag (queue dashboard)
bucket charts humanized their summary totals via formatTotal but drew
the y-axis ticks and tooltip values as raw milliseconds through
toLocaleString. Add a dataIsTimeBased prop to GenericBucketLineChart
(mirroring the one GenericLineChart already has — the details-pane
Execution Time chart was already correct through it) and set it at both
duration call sites so axis and tooltip values go through humanize().
Prop-contract test updated for the new optional prop.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
A rogue bot rendered as rogue in the workflow tree but its dashboard
header looked healthy. The header derived rogue solely from the shared
catalog entry (botSettings), which is populated by a fire-and-forget
fetch that takes 10s+ on a large bus and is keyed by bare bot ids while
ids reaching this page can carry a `bot:` prefix. Either miss left the
badge silently absent.

Derive rogue from the page's OWN settings record instead — a
strongly-consistent per-page LeoCron read that already carries
errorCount, and the same source the Paused badge uses — falling back to
the catalog entry. Verified against prod: rcs-service-prod-satori-
attempt-emitter has errorCount 29, so the tree was right and the
dashboard was wrong.

The rogue signal was also easy to miss:
- Dashboard badge is now solid red with an octagon-alert icon and the
  error count ("ROGUE · 29 errors"), and the bot avatar gets a red ring.
- Tree nodes in an error state get a tinted fill and a heavier stroke;
  previously a rogue node was a dark shape with a dark-red outline on a
  dark canvas, quieter than the healthy green rings around it.

Badge logic moved into resolveHeaderBotStatus/isSameBotId so it is unit
tested rather than living untestable in markup. Also tightened
getLowerText's optional-timestamp handling; svelte-check is now 38
errors, below master's 43 baseline.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
Marks the build carrying the rogue dashboard/visibility fixes so the
deployed version is distinguishable from the 4.3.1 build already under
test.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread webapp/src/lib/client/components/features/bot/bot-status.utils.ts
@ch-snyk-sa

ch-snyk-sa commented Aug 27, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Detroitiron and others added 2 commits August 27, 2026 16:03
…them

Reported during verification: botmonAlpha showed times on edges where
legacy showed N/A, and the numbers the two produced disagreed. Both come
from the edge label being an approximation of legacy rather than a port.

Read lag was measuring the wrong quantity. It used "time since the bot
last ran" (the read stat's `timestamp`), when legacy measures how far the
EVENTS are behind: `compare - source_timestamp`, the age of the newest
event consumed. A bot that polls every ten minutes but is fully caught up
reported ten minutes of lag; legacy shows "-". Worse, legacy zeroes the
lag outright when the reader's checkpoint has reached the queue's newest
write, which the old code had no notion of — so a healthy edge could
never render as healthy.

Now ported faithfully (lib/stats.js + stores/dataStore.js):
- Read: N/A with no read; "-" when caught up or under 100ms of lag;
  otherwise `lag: <age of newest event read>`.
- Write: N/A with no write; otherwise `<compare - last_write> ago`.
- `compare` is the viewed window's end, not wall clock. On a historical
  window legacy measures against the window end; using Date.now() grew
  every lag by however long ago that window was.
- initializeLinkStats now records source_timestamp and checkpoint per
  link and computes each queue's newest checkpoint, which is what makes
  the caught-up test possible.
- The no-stats fallback seeds source_timestamp and checkpoint from the
  cron record too, not just a time — seeding a bare timestamp let an edge
  claim freshness it had not earned. A read edge with no source timestamp
  now stays N/A rather than inventing a lag.

Label logic moved to link-label.ts with 15 tests covering the caught-up
case, the run-time-is-not-lag case, historical windows, and the N/A
paths, so this cannot drift back into an approximation.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread webapp/src/lib/client/components/features/bot/tree-utils.svelte.ts
…rop, link-key direction

Review of PR #45 found three defects that meant a fix in the PR never reached
production, plus two smaller items:

1. The dashboard header's ROGUE badge was still frozen at page load. It derives
   errorCount from `dashboardState.settings`, which only the id-change effect
   populates; the 45s timer refreshed the shared catalog instead, and the header
   prefers the page record, so a defined 0 shadowed the fresh catalog value.
   The timer now re-reads the page's settings record alongside the stats.

2. `checkPointValue` on generic-line-chart-inner was unreachable — the wrapper
   never forwarded it and no caller passed one — so removing the wall-clock line
   left those charts with no annotation at all. Per AD-003 that is the intended
   behavior for execution/error/write-lag charts (only Events In Queue and the
   read-side sparklines can lag), so the dead prop is removed and the intent is
   stated in the component rather than implied by an unused prop.

3. calculateRelationshipImportance built its linkStats key upstream-first in
   both directions, so every lookup missed after the getLinkStats fix landed:
   eventCount fell to 0 and `now - undefined` made every score NaN, which
   emptied the relationship list whenever "include inactive" was off. Both call
   sites now build keys through a shared `linkStatsKey()`, and a genuine miss
   degrades to a finite score instead of NaN.

Also: the error-rate alarm compared a rate that is forced to 0 at zero
executions, so a bot with errors and no successful runs never alarmed — it now
compares counts the way legacy does (`errors >= executions * limit`). And the
lockfile's 108 `peer: true` flips are reverted; a plain `npm install` rewrites
them, so they were version-dependent noise rather than a dependency change.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1365891. Configure here.

…ord is a bot

Found by the ES-4142 parity audit, in code this branch introduced.

Stats records invert their key shape by type: a BOT record's read/write
maps are keyed by the QUEUES it reads and writes, while a QUEUE record's
are keyed by the BOTS reading and writing it. Verified against LeoStats
on the cup test bus — `id: "queue:modified-order"` carries
`current.read` keyed `bot:order-test-modified-order-to-dim` and
`current.write` keyed `bot:order-test-order_changes-to-modified-order`.

Queue ids genuinely reach this code: `visibleIds` carries `queue:foo`
for every non-bot node on the canvas. initializeLinkStats assumed the
bot shape for all of them, with two consequences:

- Pass 1 filed the queue's newest checkpoint under the WRITER BOT's
  name, so `latestCheckpointByQueue` never received the value the
  caught-up test looks up. The test fell back to "not caught up" and the
  edge printed the raw age of the last event as lag, where legacy shows
  "-". The queue record is in fact the best source for that value: its
  write map lists every writer.
- Pass 2 emitted the queue record's read entries under `${queue}-${bot}`
  — the key reserved for the write edge — so for a bot that both reads
  and writes one queue the two links collided in the map and the later
  record won, letting a write edge render a read label.

Both maps now resolve the bot and the queue from the record type before
keying, so read edges always key `${bot}-${queue}` and write edges
always `${queue}-${bot}` whichever record they came from.

Tests use the real LeoStats shapes above; six of the seven fail without
this change.

Task: ES-4034
Brief: ES-4033
Co-Authored-By: Claude <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.

2 participants