Repository navigation
@flybot/timeline (team-activity): swimlane timeline of Bot turns, tool runs and exchanges #241
Description
Part of #3 (umbrella). Proposal reviewed with an interactive UI mockup (swimlanes / communication map / utilization) in chat on 2026-10-18.
Goal
Give the Bot team the observability DSH gives a single session, one level up: a peer plugin that visualizes where time goes across Bots and their communication — per-Bot turn spans, tool runs, and Bot→Bot exchanges with send→reply latency — the way DSH's sessionStats / Trajectory show prefill/decode/tool time for one session.
- Phase 1 — agent/communication level (local team): turn spans, tool spans, exchange arrows with latency, group-round brackets, live running turns.
- Phase 2 — model-time drill-down: click a turn to get the
sessionStats-style card (LLM ms, TTFT, decode, tok/s, token usage) folded from that Bot's embedded timed streams.
Motivation
- Multi-Bot latency is dominated by waits between turns (delegation queues, long tool runs). Nothing surfaces this today: the live work view only keeps
{since, tool}per Bot, and history vanishes when the turn ends. - DSH's timing viz is a pure derivation over the durable session event log (every event carries
time+seq;assistant/messageembeds the exact timed stream). That philosophy transfers to the team level with no DSH patch:ctx.on('session/event')is a public documented plugin hook (DSH extension cookbook), andassistantStreamFirstTokenTime/expandAssistantStreamare public helpers. Hard rule 1 stays intact, including for phase 2.
Data available today (no host changes)
state.exchanges[](core/src/host/delivery.js): every Bot→Bot send withtime+replyTo, capped at 600 → send→reply latency is computable today.- Relayed messages in the recipient's session log carry full source metadata (
kind:'bot', form:'relay', exchangeId, senderName, hop, roomId, replyTo) — the communication graph is reconstructable end to end. - Each Bot's session log has
turn/start|turn/end,tool/call|tool/result,assistant/messagewith event times; group rounds stamp every reply withtime.
Missing: turn durations as first-class records (derivable from event times), delivered/read stamps, timestamps on room-log lines (seq only), a historical archive, browser push (poll only — a 2 s poll is fine at team granularity).
Design
Plugin @flybot/timeline in plugins/timeline/ (name: 'flybot-timeline', inject: ['flybot']), packaged exactly like social/tree (dual exports, dsh manifest, cordis.patch.yml, new subsystem doc row).
- Recorder (host): fold live events into an append-only, capped trace (single writer, fsync+rename — the
dev/interopjournal discipline). Record turn spans, tool spans (callId-paired), and message send/receive stamped withexchangeId; a pass-throughbot.send.usemiddleware (returnsundefined, never blocks) stamps sends. - **Views (client,
window.__ModuleLoader__.load+ existing__flybot__slots — no new slot, so no core edit):- Swimlanes: one lane per Bot (+ You, group rooms); turn bars segmented prefill/decode/tools; dashed request / solid reply arrows; latency badges from
replyTo; live turns with a running clock; wall-time and sequence (idle-compressed) scales. - Communication map: per-window exchange graph, req→reply latency per edge ("who waits on whom").
- Utilization: per-Bot allocation bars (model / tools / idle) for the window.
- Swimlanes: one lane per Bot (+ You, group rooms); turn bars segmented prefill/decode/tools; dashed request / solid reply arrows; latency badges from
- Phase 2 drill-down: the sessionStats-style card per turn.
- Endpoint:
flybot.api.handle('timeline/window', …, { read: true }), polled every 2 s (face-room pattern).
You ●ask ────────────────────────────────────────●answer 89.5s
Atlas [turn 3]──req→ [turn 8]──req→ [round][turn 12]
Scout [ttft│decode│web_search│decode] ←reply 19.2s
Forge [ttft│decode│browser····│decode] ←reply 22.1s ▌live
Team Room [round ─●───●───●]
Design fork to settle (blocking)
How the plugin gets team events:
- (a) the peer plugin subscribes to DSH
ctx.on('session/event')directly — zero core changes for phase 1; - (b) core extends the
flybotservice with a turn/exchange event feed (flybot.on(...)currently only has roster events,core/types/flybot.d.ts) and the plugin subscribes through the documented adapter.
(b) is the cleaner contract (plugins talk through ctx.flybot, per the plugin-runtime invariants) but costs a core PR in plugin-runtime. Recommendation: start with (a), move to (b) when a second consumer appears.
Constraints
- Never patch DSH — public APIs only (hard rule 1).
- Untrusted strings (message text, display names) rendered by the panel go through the sanitization helpers, with negative tests (LESSONS: fence escape, Seen 2×).
- Claim honesty: do not render delivered/read states the recorder cannot see; label idle precisely as "no running turn".
- Trace capped, append-only, no secrets; tests use the mock model (
core/dev/mock-llm.mjs) and the bench. - Subsystem doc +
plugins/README in the same PR; arch-docs gate satisfied by the new doc row.
Out of scope (phase 1)
- Cross-runtime traces ([RFC] 跨运行时 Agent 对话与协作:以 OpenCode 为首个试点的目标、需求与两种方案 #198 / Add the offline cross-runtime collaboration harness #201 run on a FakeClock with no timestamps; revisit alongside Add real-model browser E2E nightly tests with reusable scenarios #204 real-model E2E — this panel is the observability layer that work will want).
- Delivered/read stamps (host change; propose separately if wanted).
- Full per-room lanes beyond round brackets.
Acceptance
-
plugins/timeline/passesnpm run verify(plugin suite + lint-plugins). - Timeline renders from a mock-model bench run: turn spans, tool spans and exchange arrows with latencies matching the session logs.
- Phase 2 card matches
sessionStatssemantics for a sampled turn (LLM/TTFT/decode/tools within tolerance). - Sanitization negative tests for displayed strings.
- Subsystem doc + README +
changelog.d/fragment.
Open items
- Settle the (a)/(b) event-source fork.
- Trace retention cap and rotation policy.
- Group rooms: full lanes or round brackets only.
Activity
hsliuustc0106 commentedon Oct 18, 2026
@Birfy — requesting your discussion on this proposal before any implementation starts.
Background: it came out of asking what DSH's prefill/decode/tool timing would look like one level up, at the Bot-team level. The data exploration is done (summarized above:
exchanges[]already carriestime+replyTo, relay metadata is in the recipient session logs, and DSH's timed streams make even the phase-2 model-time drill-down derivable without a patch). The three views are based on an interactive mockup reviewed in chat (swimlane timeline with turn/tool spans and latency-badged exchange arrows; communication map; per-Bot utilization bars).The decisions where I'd most like your input:
session/eventdirectly, vs (b) core extends theflybotservice with a turn/exchange event feed. I lean (a) for phase 1 since it avoids core changes, but you own the plugin-runtime contract: if plugins should not touch raw DSH events, (b) is worth the core PR upfront instead.Anything else you'd change about the constraints or the acceptance list is welcome too.
Birfy commentedon Oct 18, 2026
Decision from Birfy (2026-10-18):
flybotservice with a turn/exchange event feed; the timeline plugin subscribes throughctx.flybot, not raw DSHsession/event. Do the core PR upfront rather than starting on (a).One-step preference: land the clean contract and the three views together, not a temporary (a) shortcut.
Birfy commentedon Oct 18, 2026
Progress (option (b), per the decision above):
7ce0586) — theflybotservice now has a team activity feed:ctx.flybot.on('turn.start' | 'turn.end' | 'tool.start' | 'tool.end' | 'exchange.delivered'). Own chat, group member Session and the group's own Session (its turn is one round) are told apart byplace. Metadata only (ids, times, outcomes); message text, tool arguments and output never leavecore/src/host/flybot-feed.js. Types inflybot.d.ts, plugin-runtime doc invariant 12.plugins/timelinerecorder (capped append-only trace, single writer, fsync+rename) +timeline/windowendpoint + packaging; PR 3 the three views (swimlanes, communication map, utilization). Phase 2 stays a follow-up unless it turns out cheap.Birfy commentedon Oct 18, 2026
@flybot/timelinephase 1 is onmain, in the three PRs planned in the decision comment:7ce0586): core activity feed.flybot.on('turn.start' | 'turn.end' | 'tool.start' | 'tool.end' | 'exchange.delivered'), metadata only, incore/types/flybot.d.tsand plugin-runtime. Plugins never read rawsession/event.c5a7ad3):plugins/timelinerecorder, capped trace and thetimeline/windowendpoint. Trace is about 3000 spans or 24 h, one writer (writer.lock), tmp + fsync + rename likedev/interop/journal.mjs. The endpoint is registered{ read: true }. Only ids, times, outcomes, tool names, message lengths and cleaned names are stored. Negative tests cover a secret in text, arguments or output, hostile names, fence markers and bad error codes. The host test drives the real core with a mini agent loop oncore/dev/mock-llm.mjs. It also tightened two things in the core feed: an error code must be an upper-case word, and theuntrustedfence marker is stripped when spaced out.dbd1fea): the panel. A Timeline row under the Bot list (and a sidebar + entry) opens swimlanes (turns with tool calls, request/reply arrows with the wait, group rounds as round brackets, "skip quiet time"), "who waits on whom" (map + table) and utilization (model / tools / no running turn). Polls every 2 s only while open.Not done: phase 2. The drill-down card (and a measured model latency) is not cheap: the feed only has turn and tool spans, so "model" time is turn time outside tools. Left as a follow-up: a card per turn with its timed stream data, if DSH's own records can supply it without patching DSH.
Not verified: no real-DSH bench on my side (no deepseek-harness checkout), so the event shapes come from
turns.js/delivery.jsand the host test's stand-in loop; and the panel was rendered with real React in headless Chrome from a scripted fixture, not in a live shell. The dialog isposition: fixedfrom the sidebar section, so a transformed ancestor in the shell would clip it.Design questions for the owner
timeline/windowhas no actor: any signed-in member who can read can see timing metadata of every local Bot (no text). Fine for a single-owner home; should it be owner-scoped once team mode lands?ds-botpeer range is still>=0.1.5-flybot.1. The plugin refuses to start with a clear error on a core without the feed; do you want the peer range bumped when a core version is cut?turns.js,tool/resultcarriescallIdatdata.callIdordata.message.toolCallId; the feed reads both. If DSH only ever sends one, the other branch can go.