Repository navigation
Add durable timeline alerts (#584) - #731
Merged
Merged
Conversation
A read-only timeline/alerts endpoint folds the window into {open, resolved, changed}:
one alert per holding condition (errors, waiting, stuck) with its since, resolved when
it clears and kept readable for a minute. A look-back window never resolves a live alert.
The state lives in memory; nothing new reaches the trace.
Collaborator
Author
|
Verdict: PASS at ce44cc9 Scope matches issue #584 exactly: new Ran:
Not run: root Three non-blocking notes, all already documented in invariant 12 rather than defects here:
|
HugoXK
pushed a commit
to HugoXK/FlyBot
that referenced
this pull request
Oct 23, 2026
…lowLab#735) Written-by: Dev Fixes ThinkFlowLab#582. Tip SHA: 6cd23a1 ## Reproduce The window already answers `timeline/window` with per-Bot turns, failures, busy time, tokens, tool use, unanswered requests and the longest open wait, but nothing turns it into a report: a person reads the panel and retypes what they saw. There is no endpoint to ask for a report and no control to copy one. ## What changed - `plugins/timeline/src/digest.js` (new, pure): `buildDigest(window, { title, now })` resolves `{ title, range, live, bots, team, markdown, line }` — per Bot its turns and failures, busy share, tokens (in/out/cached), top five tools and unanswered requests, then the team rollup (turns, failures, tokens, calls) and the longest still-open wait — plus `formatCount` (`840`, `12.3k`, `1.2M`) and `formatMs` (`40s`, `12m`, `1h05m`). It reads only what the window carries: `usage`, `toolUse` and `stuck` when present, and it falls back to measuring the waiting exchanges against `now` when `stuck` is absent, so an older host gets a shorter report rather than a broken one. - `plugins/timeline/src/timeline.js`: `DIGEST_ENDPOINT = 'timeline/digest'`, wired the way `timeline/health` is (same `window(body)` answer, `{ windowMs?, to?, title? }`, title trimmed to 80), returned as `digest` and unregistered in `dispose()`. - `plugins/timeline/client.js`: `DIGEST_ENDPOINT`, `store.digest()` (asks for the window the panel is showing, on demand — the poller never does), a `copyText` helper over `navigator.clipboard`, and a **Copy report** button in the header that says `Copying…` then `Copied`. A page without a clipboard, or one that refuses it, shows `Could not copy the report: …` instead of claiming a copy; the button stays available. - `plugins/timeline/test/digest.test.js` (new, 8 cases) and three cases in `test/panel.test.js` (the data layer's request and its "no report" rejection, the copy, and the refused clipboard). `docs/subsystems/timeline.md` gains the endpoint, the local-clock rule and the button; `changelog.d/582-timeline-digest.added.md` is the fragment. ### INC at `5f1f91a9`, answered - **The endpoint was not pinned at the host.** `digest.test.js` now builds a real timeline on `mockAdapter()` and asks `timeline/digest` through `adapter.endpoints`, then checks `dispose()` removes it (the shape `alerts.test.js` uses). Deleting either host line now fails that case. - **The report printed UTC while the panel header prints local time.** `clock()` in `digest.js` now formats with `getHours()`/`getMinutes()`, like `fmtClock` in `client.js`, so a report copied at 17:05 says 17:05. The test file runs in `Asia/Shanghai` (`process.env.TZ` at the top) so a UTC formatter cannot pass by accident on a UTC runner; one case pins `Date.UTC(2026-10-22 09:05)` to `17:05–17:20`. ## What I ran Rebased onto main `d5fefe01` (it conflicted with ThinkFlowLab#731, which landed `plugins/timeline/src/alerts.js` and its endpoint in the same file). Locally, in a full checkout at that base with this branch's files, `DSH_HARNESS=/opt/flybot`: ``` $ cd plugins/timeline && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test --test-concurrency=2 "test/*.test.js" # tests 124 # pass 124 # fail 0 $ TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/digest.test.js # tests 8 # pass 8 # fail 0 $ node --check src/digest.js && node --check test/digest.test.js (no output) ``` The 17 pre-existing panel cases stay green, including the header/tab/close ones that now render one more button. ## Red without the source change Seven cuts, each restored afterwards: - delete `src/digest.js` → `digest.test.js` cannot import it (8 cases red). - delete the two `Copy report` renderings from `client.js` → **18/20**, red: `ThinkFlowLab#582: Copy report copies the Markdown and says so` and `ThinkFlowLab#582: a refused clipboard is reported, not swallowed`. - delete `store.digest()` → **19/20**, red: `ThinkFlowLab#582: the data layer asks for the report at the window the panel shows`. - put `clock()` back on `toISOString()` → **122/124**, red: `ThinkFlowLab#582: per-Bot sections, the team rollup and the longest wait` and `ThinkFlowLab#582: the report names local hours, like the panel header`. - delete both host lines (`adapter.handle(DIGEST_ENDPOINT, digest)` and `offDigest()`) → **123/124**, red: `ThinkFlowLab#582: the endpoint is registered read-only and removed on dispose` (the endpoint is missing). - delete only `offDigest()` from `dispose()` → **123/124**, same case (the endpoint survives dispose). ## Not run - Root `npm run verify` and the core suite: this box has no `core/` sources (only the plugin, plus `DSH_HARNESS` for `@deepseek-ai/*`), so the plugin suite ran against an assembled checkout and the rest is left to CI. - `npm run lint`, `arch-docs`: no full checkout with `origin/main`; `docs/subsystems/timeline.md` changes in the same PR, which is what `arch-docs` asks for. - The panel in a real browser: the copy path is driven through an installed `navigator.clipboard`, not through a live page. No check in a zone other than the two above was run. ## Risk T2 by the repository's own rules (`plugins/timeline/**`), so it needs a PASS at the tip. The change is read-only and additive: one new endpoint, one new button, one new pure module. It does not touch `core/`, `ds-bot/` or `patches/`, and it adds no configuration. A host that predates `usage`/`toolUse`/`stuck` still answers with a shorter report rather than an error.
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.
Written-by: Dev4
Fixes #584
Reproduce
Call
timeline/healthtwice while one Bot keeps failing: every answer re-derives the flags from the window, so nothing says since when — and once the condition clears, the next answer simply has no flag, leaving no record that it was ever open. There is also no endpoint that keeps state at all:GET-styletimeline/alertsdoes not exist, andwindow()is pure, so a routine or a person can only poll and diff by hand.What changed
plugins/timeline/src/alerts.js(new, pure) —createAlertTracker({ stuckMs, resolveMs, now })folds atimeline/windowanswer into{ version, now, open, resolved, changed, stale }.errorsandwaitingread the same numbers and thresholds astimeline/health(buildHealth), andstuckuses the open requests the window already carries (window.stuck) past a 15 min default (stuckMs, the threshold the issue left open). One alert per condition key (errors:<bot>,waiting:<bot>,stuck:<exchange>) withsince; polling again updatesseenAt, never opens a second. A condition that clears moves the alert toresolvedwithresolvedAtand it stays readable forresolveMs(1 min).changedis true only on an open or a resolve, not when a holding alert's detail moves. A window that ends before the last one folded is answeredstalewith the state untouched, so a look-back never resolves a live alert.plugins/timeline/src/timeline.js— the read-onlytimeline/alertsendpoint over that tracker (body{ windowMs?, to? }liketimeline/window), one tracker per plugin instance, removed ondispose().plugins/timeline/test/alerts.test.js(new) — 8 cases: one alert per condition and no second on re-poll; resolve once, readable through the grace window and then dropped;errors/waitingas separate alerts withidle/busyopening none; the stuck threshold, one alert per request and resolution on the reply;changedfalse when only the detail moves; the stale look-back; a re-open with a freshsince; the endpoint registered, its empty answer and its removal on dispose.docs/subsystems/timeline.md— the new file in Key files, the endpoint in Entry points, a new invariant 12 (state in memory, one alert per condition, stale look-back, nothing written) and the test in Tests.changelog.d/584-timeline-alerts.added.md.What I ran
Tip SHA:
ce44cc98e2c4db15eb1a38c5adb69a93bcc97d56(base9c835e5a, Node 22.23.3,TZ=UTC):Red without the source change
Two runs, both on this branch's test file:
Restoring
alerts.jsgives 8/8 again (and 113/113 for the plugin suite).Not run
npm run verifyas one command: this worktree is a main tarball with no installed toolchain (npm ci/npm run harnessincore/); its steps that cover this change are the ones listed above. Nocore/file changed, so the core suite is untouched, and nopatches/ds-bot/regeneration is needed (the plugin is not part ofcore/); CI'spackagejob still runs both checks.setsidhere. The endpoint is host-side only; the panel does not read it yet (not in the issue's scope).Risk
A plugin-only, read-only change: no
core/, nopatches/ds-bot/, no security surface, no new config key, and nothing is written to the trace. The new state is one tracker per plugin instance, in memory, bounded by what the window already caps (one alert per Bot condition plus at most the 20 open requestswindow.stuckcarries); it dies with the process, so a restart starts with no alerts rather than replaying old ones. The one behaviour worth knowing:stuckuses a 15 min default that the issue did not fix, it is a constructor argument (stuckMs) and is documented in the invariant; and a window that looks back is answeredstaleinstead of changing the state.