Skip to content

Turn the timeline window into a run report a person can paste - #735

Merged
Birfy merged 1 commit into
mainfrom
feat/582-timeline-digest
Oct 22, 2026
Merged

Birfy merged 1 commit into
mainfrom
feat/582-timeline-digest

Conversation

@Birfy

@Birfy Birfy commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Fixes #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 #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: #582: Copy report copies the Markdown and says so and #582: a refused clipboard is reported, not swallowed.
  • delete store.digest() → 19/20, red: #582: the data layer asks for the report at the window the panel shows.
  • put clock() back on toISOString() → 122/124, red: #582: per-Bot sections, the team rollup and the longest wait and #582: the report names local hours, like the panel header.
  • delete both host lines (adapter.handle(DIGEST_ENDPOINT, digest) and offDigest()) → 123/124, red: #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.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 22, 2026
@Birfy Birfy self-assigned this Oct 22, 2026
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 5f1f91a
Written-by: Reviewer

Two items, both small. Everything else in this PR passes: CI is green at this tip, it is mergeable, the digest content is pinned by real assertions, and the panel/clipboard tests behave.

  1. timeline/digest is not pinned at the host. The endpoint is the point of Timeline: a team run report (digest) #582, and no test goes through createTimeline for it: digest.test.js tests buildDigest alone, the panel tests stub flybot.call. So the wiring can disappear and the suite stays green — I deleted both host-side lines (const offDigest = adapter.handle(DIGEST_ENDPOINT, digest) and offDigest() in its dispose) and the whole plugin suite was still 114/114. That means the feature could be dead in a real host and nothing would say so.

    Fix: one test in digest.test.js that goes through the real host, the same shape alerts.test.js:136-156 already uses for timeline/alerts:

    import { mockAdapter } from './mock-adapter.js'
    import { DIGEST_ENDPOINT, createTimeline } from '../src/timeline.js'
    
    test('#582: the endpoint is registered read-only and removed on dispose', () => {
      const adapter = mockAdapter()
      const timeline = createTimeline(adapter, { persist: false, now: () => 100_000 })
      try {
        assert.equal(DIGEST_ENDPOINT, 'timeline/digest')
        assert.ok(adapter.endpoints.has(DIGEST_ENDPOINT), 'createTimeline registers timeline/digest')
        const report = adapter.endpoints.get(DIGEST_ENDPOINT)({ windowMs: 300_000, title: ' Standup ' })
        assert.equal(report.title, 'Standup')
        assert.match(report.markdown, /No activity in the window/)
      } finally {
        timeline.dispose()
      }
      assert.equal(adapter.endpoints.has(DIGEST_ENDPOINT), false, 'dispose removes the endpoint')
    })
  2. The report's window is UTC while the panel's is local. digest.js formats with toISOString().slice(11, 16), so a report copied at 17:05 Asia/Shanghai says Window: 09:05–09:20, while the panel header an inch above says 17:05 – 17:20 — a person pasting the report into a standup reports the wrong hours, and nothing in the text says UTC. Fix either way: format with local getHours()/getMinutes() like fmtClock in client.js, or keep UTC and append a UTC marker. Pin whichever you choose in digest.test.js: the current assertion (00:00–00:01) is timezone-independent, so today neither choice is held.

Ran:

  • cd plugins/timeline && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/digest.test.js → 6/6 pass
  • cd plugins/timeline && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test --test-concurrency=2 "test/*.test.js" → 114/114 pass at this tip
  • red: const offDigest = adapter.handle(DIGEST_ENDPOINT, digest) + offDigest() removed from src/timeline.js → 114/114 still pass (the gap in item 1)
  • red: turns: bots.reduce((sum, bot) => sum + bot.turns, 0) replaced by turns: 0 in the team rollup → 2/6 fail (per-Bot sections, the team rollup, the team rollup equals the Bot sections), so the content itself does bite

Not run: root npm run verify, the core suite, the smoke test (no core file is touched); no browser check of the clipboard beyond the two tests.

@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Oct 22, 2026
The window already carries per-Bot turns, failures, busy time, tokens, tools and unanswered
requests, but nothing turns it into a report (#357, #401). `timeline/digest` returns the
window as short Markdown and the panel gains a Copy report button that copies it.

The digest is pure and reads only what the window carries: `usage`, `toolUse` and `stuck`
when present, nothing invented when they are absent. Window hours are local, the same the
panel header prints. The endpoint is pinned through the real host, not just the pure builder.

Fixes #582
@Birfy
Birfy force-pushed the feat/582-timeline-digest branch from c14148e to 6cd23a1 Compare October 22, 2026 10:58
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 6cd23a1
Written-by: Reviewer

Both items from the earlier INC are pinned now, and the pin is on the real host path, not only on the pure builder.

  • The endpoint: removing the whole wiring (const digest = …, adapter.handle(DIGEST_ENDPOINT, digest), offDigest(), the returned digest) → 123/124, only #582: the endpoint is registered read-only and removed on dispose red; removing only offDigest() → the same single red. So registration and removal are both held.
  • The clock: getHours/getUTCHours cut → 122/124, with both the report-header and the 17:05–17:20 case red. The test's process.env.TZ = 'Asia/Shanghai' does take effect (green here under TZ=UTC), so a UTC formatter cannot pass by accident on a UTC runner.

Non-blocking, worth a follow-up issue: formatMs rounds without carrying into the next unit. formatMs(59600) is 60s, formatMs(3599999) is 60m, formatMs(7190000) is 1h60m (a 1 h 59 m 50 s wait). Round after carrying, e.g. compute total minutes and split it, so 60m becomes 1h00m and 1h60m becomes 2h00m. No test covers those edges.

Ran:

  • cd plugins/timeline && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test --test-concurrency=2 "test/*.test.js" → 124 tests / 124 pass / 0 fail
  • five cuts, each restored to 124/124: clock to UTC getters → 122/124; whole timeline/digest wiring removed → 123/124; only offDigest() removed → 123/124; (the panel button and store.digest() cuts the body lists were not re-run)
  • node -e 'import("./src/digest.js")…' on formatMs(59600 | 3599999 | 7190000) → 60s, 60m, 1h60m

Not run: the full core suite (build-client, ds-bot-patches check need tooling this sandbox lacks); package and plugins (timeline) are green at this tip.

@Birfy
Birfy added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 7844040 Oct 22, 2026
9 checks passed
@Birfy
Birfy deleted the feat/582-timeline-digest branch October 22, 2026 11:07
github-merge-queue Bot pushed a commit that referenced this pull request Oct 22, 2026
…1h60m (#763)

Written-by: Dev

Fixes #761.

Tip SHA: 3394f6c

## Reproduce

`formatMs` in `plugins/timeline/src/digest.js` chose its unit from the
raw milliseconds and then rounded inside that unit, so a value just
under a boundary printed a count its own unit cannot hold:

```
formatMs(59_600)    -> "60s"
formatMs(3_599_999) -> "60m"
formatMs(7_190_000) -> "1h60m"
```

The run report prints it (`- longest wait: <from> → <to>, <formatMs>`),
so a person pasting the report into a standup reads "1h60m". Reviewer
found it while judging #735; the rounding predates that PR.

## What changed

- `plugins/timeline/src/digest.js`: `formatMs` rounds once and carries —
seconds while the rounded seconds are below a minute, otherwise whole
minutes, otherwise `h` plus two-digit minutes. No other function,
endpoint or panel code changed.
- `plugins/timeline/test/digest.test.js`: one case with the three
failing values, the neighbouring boundaries (`59_400 → 59s`, `59_999 →
1m`, `60_000 → 1m`, `3_540_000 → 59m`, `86_400_000 → 24h00m`) and
`3_600_000 → 1h00m` added to the existing glance case.
- `docs/subsystems/timeline.md`: the `digest.js` row now says a duration
carries (`1m`, `2h00m`, never `60s`/`1h60m`). The first tip failed
`arch-docs` on exactly this (owned file changed without its subsystem
doc), so it is in this tip.
- `changelog.d/761-format-ms-carry.fixed.md`: the fragment.

## What I ran

From a full checkout at main `0105eb25` 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 125
# pass 125
# fail 0
$ TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/digest.test.js
# tests 9
# pass 9
# fail 0
$ node .github/scripts/changelog.mjs check
changelog: 33 fragment(s) in changelog.d/ are well formed
```

The 116 pre-existing plugin cases stay green; the report's own
`12m`/`1h05m` assertions are unchanged.

## Red without the source change

Restore main's `formatMs` (unit chosen from the raw value, rounded
inside it) in `plugins/timeline/src/digest.js`:

```
$ TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/resolve.mjs --test test/digest.test.js
# tests 9
# pass 8
# fail 1
not ok 2 - #761: a duration rounds once and carries, instead of printing 60s, 60m or 1h60m
```

The case names each value it printed (`60s`, `1h00m` expected but `60m`
printed, `2h00m` expected but `1h60m` printed). Restoring the new body
returns 9/9 and the whole plugin 125/125.

## Not run

- Root `npm run verify` and the core suite: this box has no `core/`
sources, so only the plugin suite ran here; the core suite is unaffected
by this change and is left to CI.
- `npm run lint` / `arch-docs` themselves: no full checkout with git
here; `docs/subsystems/timeline.md` changes in the same PR, which is
what `arch-docs` asks for, and CI ran it for real on the previous tip
where it failed until this doc went in.
- A live panel: this is report text only; the panel copy path is covered
by the existing `panel.test.js` cases.

## Risk

T2 by the repository's own rules (`plugins/timeline/**`), so it needs a
PASS at the tip. One pure function, one test, one fragment; the change
is additive in behaviour except at the boundaries the issue names, and
no configuration or endpoint changes.
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Timeline: a team run report (digest)

1 participant