ES-4034 botmonAlpha: workflow edge times, trace icons, rogue detection, chart checkpoint line, humanized durations - #45
Open
Detroitiron wants to merge 11 commits into
Open
Conversation
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>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…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>
…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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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:
errorsfield 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.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)
mergeStatsResultsthrewObject.entries(undefined)on any record lackingreadorwrite(write-only bots, queue records), which 500'd/api/workflow/statsand blanked every edge label. Guarded with?? {}, matching legacy's|| {}.linkStatsKey()helper.queue:foo). Quiet edges now show the checkpoint time like legacy instead of N/A.fetchBotStatschecksres.okand response shape instead of mergingundefinedand throwing inside an untracked async.2 · Event-trace icons —
trace-fanout-treebuilt PNG URLs from barebase. Static files are served from CloudFront viaassets;baseroutes through API Gateway into the SvelteKit Lambda, which has no static routes → 404 → broken-image placeholders. Now uses the app-wideassets || baseprefix that every other icon consumer already uses. Invisible in dev, where both prefixes are empty.3 · Rogue / blocked detection
stats.execution— the only place the bus records errors.errorCountthat 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.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
|| 0call site that would otherwise pin it at epoch 1970 is guarded.GenericLineChartgets 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
dataIsTimeBasedtoGenericBucketLineChart(mirroring the propGenericLineChartalready had) so Execution Time and Read Lag axis ticks and tooltips go throughhumanize(), 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
resolveHeaderBotStatus/isSameBotIdso it is unit-testable rather than living untestable in markup.ReadWriteStats.errorsfield from the type and frommock-botsfixtures, which had been encoding the wrong record shape and validating the bug.AGENTS.mdgains 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).webappbumped 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.
0from 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.checkPointValueonGenericLineChartwas 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.calculateRelationshipImportancestill built its key upstream-first in both directions, so every lookup missed once the edge-label fix landed: event counts fell to 0 andnow - undefinedmade every importance scoreNaN, 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
0at 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 108peer: trueflips are reverted; a plainnpm installrewrites 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
blockedstatus 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/relationshipssitting on two timers, the tree timer's every-other-tick cadence,buildRelationShipTree()running where no tree is mounted, andmergeStatsResults'min_durationseed of0.Tests / Validation (Ran / Not Run)
npm test— 192 passed (19 files), node + Storybook chromium projectsread/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-stats-key.test.tsbuilds 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 failx={now}, any component builds a PNG URL from barebase, 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 themsvelte-check38 errors vs 43 onorigin/master(measured in a clean baseline worktree); net −5, unchanged by the 4.3.4 fixes, none in touched codercs-service-prod-satori-attempt-emitterhas LeoCronerrorCount29 vs threshold 10 — confirming the tree was right and the dashboard was the bugtest-cup./scripts/deploy.sh test-cup— https://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 yettest-cup:bot.pngreturns 200 via the CloudFrontassetsprefix the fix now uses, and 302 (auth redirect, not the image) via the bare-basepath 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 thereerrorCountback to 0) — rogue is a transient, reset-on-success counter and history is not stored. Verified against a second, currently-rogue bot instead.Rollout
AI Usage
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 defectcalculateErrorCountsummederrorsoff read/write stat entries — a field the Leo stats table never writes — by cross-reading legacylib/stats.js, which records errors only undercurrent.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 readsexecution.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.settingsErrorCount ?? catalogEntry?.errorCountagainst 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
~/code/...paths in the bodyfeature/<CODEWORK-KEY>-<slug>branch;<CODEWORK-KEY> <description>titleNote
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 graph —
mergeStatsResultsno longer throws on records missingread/writeand 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 sharedlinkStatsKey(), with correct handling of queue vs bot LeoStats shapes, checkpoint catch-up, andgetLinkLabel()read lag (event age vs run time, windowcompareTimestamp). Refresh paths also re-fetch bot settings and rebuild the relationship tree when status changes.Rogue / blocked / alarms — Window errors and executions read
current.executiononly; error-rate alarms use legacy’s count comparison. Dashboard header rogue uses the page settingserrorCount(with catalog fallback andbot: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
dataIsTimeBasedhumanization. Trace PNGs useassets || base.Docs & tests —
AGENTS.mdfootguns, fixtures/types drop phantom read/writeerrors, 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.