Skip to content

fix(desktop): keep the Agent Graph panel visibly alive while operators run - #4514

Open
ggbdpq wants to merge 7 commits into
apache:mainfrom
ggbdpq:fix/agent-graph-heartbeat
Open

fix(desktop): keep the Agent Graph panel visibly alive while operators run#4514
ggbdpq wants to merge 7 commits into
apache:mainfrom
ggbdpq:fix/agent-graph-heartbeat

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Scope update after review (Astro-Han, jackwener): the elapsed clock is gone. Epoch 1, the first graph a Session fans out, carries a sentinel createdAt: 0, and a remote Host clock cannot be correlated with the Desktop wall clock. A stopwatch built on that value showed an epoch-sized duration on the first-fan-out path fix(desktop): agent graph appears frozen while 0/7 operators are running (no heartbeat) #3878 describes.
  • The liveness answer is timestamp-free: the header heartbeat spinner and the running/runnable operator-dot pulse both use the panel's single graphLive judgment, !error && snapshot !== undefined && isAgentGraphLive(snapshot.status). A failed snapshot read no longer leaves either animation claiming liveness from stale data.
  • isAgentGraphLive lives in agent-graph-panel-visibility.ts and feeds the heartbeat gate, stopAvailable, and the panel data-live hook, so "can stop" and "shows as running" stay tied to one fact.
  • A real elapsed clock returns as its own change once the Host sends a genuine start time or an elapsed value computed in its own clock domain; epoch 1's 0 rows and clock correlation are part of that prerequisite.

Verification

  • Reduced-motion trade-off: under reduced motion the heartbeat spinner is hidden rather than frozen, and the dot pulse is left to the existing global reduced-motion rule. Reduced-motion users get the static running dot only.
Claim Command Result
Renderer typecheck covers the changed renderer files tsc -p tsconfig.renderer.json --noEmit from apps/desktop exit 0, no output
Lint covers all four changed files biome lint apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts apps/desktop/src/renderer/agent-graph-panel-visibility.ts apps/desktop/src/renderer/agent-graph-panel.tsx apps/desktop/src/renderer/styles/agent-graph.css Checked 4 files in 776ms. No fixes applied.
Renderer architecture graph is still accepted node apps\desktop\scripts\check-renderer-architecture.mjs Renderer architecture check passed.
ASF headers npm run check:asf-headers Audited 3427 covered and 183 excluded file(s) in checkout mode ... Every source file carries the ASF header or a reviewed exclusion.
Whitespace git diff --check upstream/main...HEAD exit 0, no output
Visual acceptance Storybook evidence posted in the PR discussion light, dark, narrow, and reduced-motion captures attached

AI use

Analysis, patch, and verification were produced with GLM-5.3-Flash (ZCode) under the contributor's direction; the contributor reviewed and is the human contributor of record. Every affected commit carries a Generated-by: GLM-5.3-Flash (ZCode) trailer.

Checklist

  • Tests and checks pass locally (see Verification)
  • Behavior change: Yes - visual liveness signals only (operator-dot pulse and header spinner); no data, scheduling, protocol, or elapsed-clock behavior changes

@github-actions github-actions Bot added the effort/S Under 100 readable lines label Sep 1, 2026
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch 4 times, most recently from e048019 to 99f0a6f Compare September 2, 2026 11:49
@ggbdpq

ggbdpq commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

The current head's CI failure is in the Desktop e2e step: transcript-scroll.spec.ts:550 ("the dock affordance returns the reader to the tail") — expect(await scrollButtonOffered(page)).toBe(true) receives false, i.e. the dock button is not offered after scrolling the transcript to the top.

I believe this is unrelated to this PR and would like to ask for a re-run of the failed job (I don't have write access to trigger it):

If the failure reproduces on a re-run I will dig in with a reproduction.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The problem is real and this covers all three things #3878 asks for. Lint, format, the surface inventory, @maka/ui 317/317, desktop typecheck and the architecture check all pass locally; merge-tree is clean. On the red CI: the only failure is Desktop e2e transcript-scroll.spec.ts:550, and main itself failed the same step twice in the last two hours on ed4468e5 and 1581dc1c, so your read is right and a rerun is all that is needed.

Two things I would change before merge, both of which make the diff smaller.

The clock starts at Date.now() when the component mounts, so it shows how long this view has watched the graph, not how long the graph has run. Open the panel three minutes into a run, or switch sessions (which resets the snapshot), and the header says 00:03. That is the same illusion the issue is about, in a different shape. The body says the client snapshot has no start time, but the panel already holds one: AgentGraphEpochSummary.createdAt is in epochs and selectedEpoch is already computed on line 313. Pass createdAt as the start and the component keeps only the 1 Hz ticker; liveSinceRef, resetKey, the reset branch and the third test go away.

GRAPH_LIVE_STATUSES on line 53 is a second copy of the same set that stopAvailable on line 361 spells out inline, and the natural owner already exists next door: agent-graph-panel-visibility.ts with DISMISSIBLE_STATUSES, which has a node --test lane. An isAgentGraphLive there, used by both sites, keeps "can stop" and "shows as running" from drifting apart.

Smaller: the Spinner gets aria-label={props.label} while the visible text right after it is the same string, and Astryx's Spinner is role="status", so screen readers announce the status twice; the heartbeat is decorative, aria-hidden like the clock. .maka-agent-graph-heartbeat { flex: 0 0 auto } has no effect because .maka-agent-graph-progress is not a flex container, and there is no gap between the spinner and the text; display: inline-flex; gap: var(--space-1) on the progress element fixes both. The per-file prefers-reduced-motion block duplicates the global rule in base.css, which also covers the data-maka-reduced-motion path the e2e uses and this block does not; three other CSS files already carry the same redundancy, so take it or leave it.

Before merge: rerun CI; and please a quick visual check of spinner spacing and baseline against the text in both themes, plus what the header looks like with reduced motion on, where the global rule freezes the spinner rather than hiding it.

Evidence boundary: static read of 99f0a6fd against main 61224f51; ui tests, desktop typecheck, architecture check, inventory, lint and format run locally; CI failure compared against two main runs; Desktop not launched.

AI-assisted review: drafted with Maka; I verified the clock origin, the duplicated status set and the CI comparison myself.

简体中文

问题真实,三项要求都覆盖了,本地全绿;CI 红只在 Desktop e2e 一步,main 自己两小时内同一步也挂了两次,rerun 即可。合并前建议改两处,都会让 diff 变小:计时器从组件挂载时刻起算,显示的是「看了多久」不是「跑了多久」,而面板里已有 AgentGraphEpochSummary.createdAtselectedEpoch,传进来即可删掉 ref/resetKey 一整套;GRAPH_LIVE_STATUSES 与第 361 行的内联集合是同一事实两份,自然 owner 是 agent-graph-panel-visibility.ts。小项:Spinner 的 aria-label 与相邻文本重复播报,改 aria-hidden;flex: 0 0 auto 无效且缺间距;per-file reduced-motion 块与 base.css 全局规则重复。

return;
}
if (liveSinceRef.current?.resetKey !== props.resetKey) {
liveSinceRef.current = { resetKey: props.resetKey, at: Date.now() };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is when the view started watching, not when the graph started. AgentGraphEpochSummary.createdAt is already in the panel's epochs state (selectedEpoch on line 313 of the panel); pass it in as the start and this ref, resetKey and the reset branch can go.

dispose() {},
};

/** Snapshot statuses during which the panel must keep signaling liveness. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same set as the inline ['active', 'waiting', 'closing'].includes(...) behind stopAvailable on line 361. agent-graph-panel-visibility.ts already owns DISMISSIBLE_STATUSES and has a test lane; an isAgentGraphLive there used by both keeps them from drifting.

ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 2, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 649bbfa to 39e9743 Compare September 2, 2026 21:53
@ggbdpq

ggbdpq commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Both pre-merge changes and the smaller items are pushed in 5207f48 + 39e9743:

  • Clock origin: AgentGraphLiveStatus now takes startedAt and the panel passes the selected epoch's createdAt — the header reports how long the graph has run, not how long the view has been open. liveSinceRef, resetKey, the reset branch, and the reset test are gone; the ticker is the only state left.
  • Status set: moved to agent-graph-panel-visibility.ts as isAgentGraphLive, now used by both the heartbeat gate and stopAvailable; covered in the visibility suite (14 tests, including a shared-vs-settled matrix).
  • Spinner: aria-hidden (the adjacent visible text already announces the status), and .maka-agent-graph-progress is now an inline flex with gap: var(--space-1), which also let me drop the dead flex: 0 0 auto rule.
  • Reduced motion: dropped the per-file block in favour of the base.css global (which also covers the data-maka-reduced-motion e2e fixture path).
  • Story fixture: the graph story hardcoded createdAt: 1, which the epoch-origin stopwatch now faithfully renders as a five-digit hour count — it uses a realistic offset and doubles as the visual check (spinner spacing/baseline verified in Light and Dark).

Verified locally: ui 320/320 (5 live-status tests updated), desktop typecheck 0 errors, renderer architecture check green against the merge base, surface inventory ok, format + ASF headers clean. CI rerun on the new head will re-exercise the Desktop e2e step.

@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 39e9743 to 54ac6e0 Compare September 3, 2026 01:18
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 54ac6e0 to a156742 Compare September 3, 2026 01:19
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from a156742 to 0771760 Compare September 3, 2026 01:49

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review of exact head 0771760ed7501af46c1a1d8ffa7bd8820b066854 found one merge-blocking display bug and one contribution-metadata issue.

[P1] Do not treat epoch 1's sentinel timestamp as the stopwatch origin

The first production Agent Graph does not have a real epoch start timestamp. Both StreamGraphCoordinator.currentGraphEpoch() and SqliteSessionMetadataStore.resolveCurrentAgentGraphEpoch() synthesize the initial epoch with createdAt: 0, and the Runtime Host sends that value unchanged. apps/desktop/src/renderer/agent-graph-panel.tsx:398 now passes it to AgentGraphLiveStatus, whose subtraction at packages/ui/src/agent-graph-live-status.tsx:55-58 interprets it as Unix epoch time.

I built this head and mounted the production component with the exact valid wire value startedAt: 0; the header rendered Running · 0/7 settled · 496778:32:27. This is the normal first-graph path, not a malformed fixture. The Host coordinator test already pins createdAt: 0, while the new UI tests cover only values near Date.now() or in the future. The story was changed from createdAt: 1 to a recent timestamp after the old value exposed the huge hour count, so it now avoids the production sentinel instead of testing it.

Please either keep this liveness timer explicitly based on local observation, or provide an elapsed/reference value from the Host's clock domain and handle the legacy 0 sentinel separately. A regression should carry epoch 1's real createdAt: 0 through the Host-to-panel path and assert that the header never displays an epoch-sized duration. A Host timestamp also cannot be subtracted safely from the Desktop wall clock for remote Hosts without clock correlation.

[P2] Bring the PR description and generated-content trailers in sync with this head

The PR description still says the snapshot has no start timestamp and that the clock measures view observation/reset behavior, but the current implementation uses epoch createdAt and removed that reset state. It also states that the patch was produced with GLM-5.3-Flash, while the two material follow-up commits afdc8cfbe44f4f3c224ff5dc4d76ce7c0f8b75ab and 0771760ed7501af46c1a1d8ffa7bd8820b066854 have no Generated-by trailer. Please update the description and amend each affected commit so the required trailer is present and survives the final squash.

The exact-head hosted test check is successful. Locally, @maka/ui passed 332/332 tests, the Host/Runtime graph suites passed 20/20, the panel visibility suite passed 14/14, Desktop typechecking and renderer architecture passed, and the Astryx inventory, Biome, ASF headers, and git diff --check were clean. The synthetic merge with current main (8ea3c4f0571a48d7e9c6c5906fb755cad43608ff) is textually clean. I did not approve or merge this head.


Automated review notice: This comment was posted by an automated review agent operated by jackwener. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both items from my last round are properly closed. isAgentGraphLive now lives in agent-graph-panel-visibility.ts and feeds both stopAvailable and the heartbeat gate, so "can stop" and "shows as running" read the same set; liveSinceRef, resetKey and the reset branch are gone; the spinner is aria-hidden; the progress line is a real flex container and the dead flex: 0 0 auto went with it. Thanks for taking the reduced-motion block out too.

The move to epoch createdAt uncovered something neither of us saw, and it blocks merge. jackwener's automated review landed on the same thing independently; I verified it myself against this head rather than taking it on trust.

[P1] Epoch 1 has no real start timestamp, and epoch 1 is the case #3878 is about

A root Session that has never rolled its graph forward is epoch 1, and epoch 1's createdAt is a sentinel 0, not a time. It is synthesized that way in three places and carried through unchanged: StreamGraphCoordinator.currentGraphEpoch (packages/runtime/src/stream-graph-coordinator.ts:1349), the fallback in listGraphEpochPage when no durable rows exist (same file, ~line 366), and SqliteSessionMetadataStore (packages/storage/src/sqlite-session-metadata-store.ts:3800 and :3829, where epoch 1 is inserted with createdAt: 0 while epoch 2 gets this.now()). #queryEpochs in packages/runtime-host/src/server/agent-graph-coordinator.ts:169 copies binding.createdAt straight into the summary, and the panel hands it to the component at line 398.

I mounted the built component with the exact wire value a first graph produces and got:

Running · 0/7 settled · 496781:08:56

This is the ordinary first-fan-out path, which is exactly the situation the issue describes. So the third signal is not merely wrong in a corner, it is wrong in the headline case and replaces one confusing header with a louder one. The new tests all pass startedAt near Date.now() or in the future; nothing carries 0, and the story fixture moved from createdAt: 1 to Date.now() - 23_000, which sidesteps the sentinel rather than pinning it.

There is a second, quieter part of the same problem: createdAt is the Host's clock and the subtraction happens against the Desktop's Date.now(). For a remote Runtime Host those are different clock domains, and the snapshot carries nothing to correlate them. Your future-clamp test is the same bug's other direction, caught but not named.

My honest read is that epoch createdAt cannot carry this clock today, and my last review pointed you at it without checking what epoch 1 actually holds. Sorry about the detour. Two ways out that I would take:

  1. Ship the two signals that work and drop the clock. The pulse and the header spinner both come off snapshot.status, need no timestamp, and already answer "is this frozen?". That also collapses most of this diff: without the stopwatch, AgentGraphLiveStatus is a conditional Spinner and could sit inline in the panel, which already imports Spinner directly. The result is a small fix that fully closes #3878's complaint, and the clock becomes its own change once a start time exists.
  2. Give epoch 1 a real start first, or send an elapsed value computed in the Host's own clock domain, and land that as the prerequisite. Larger, and it needs a story for the 0 rows already on disk.

Either way, a regression should carry epoch 1's real createdAt: 0 from the Host through to the panel and assert the header never shows an epoch-sized duration; a unit test on the component alone will not catch the next version of this.

Smaller

The PR description still says the snapshot carries no start timestamp and describes the reset-on-selection-change behavior, which this head removed. Worth a pass before merge so the squash message can be built from it. The Generated-by trailer is only on 87ca9cda; afdc8cfb and 07717605 do not carry it, so it is worth amending before this lands.

On reduced motion: the global rule in base.css collapses the pulse to its 100% frame, so the dots settle at full opacity, which is right. The Astryx spinner freezes rather than hides, so under reduced motion the header keeps a static ring. That was going to be acceptable because the clock still ticked; if the clock goes, it is worth a second look. P3, not a blocker.

When you push the fix, please attach the light and dark comparison of the header against main for the same story. You said you checked spinner spacing and baseline locally and I believe you, but this is a visible surface and the images belong in the PR.

Verification I ran on 07717605: @maka/ui 330/330, the desktop visibility suite 14/14, renderer and storybook typecheck clean once workspace deps were built, the renderer architecture check green against the merge base ade07b5b, and Biome clean on all eight changed files. The startedAt: 0 output above is from mounting the built component, not from reading. I did not launch Desktop, did not run e2e, and did not check the current CI state.

AI-assisted review: drafted with Maka. I confirmed the epoch-1 sentinel path through the coordinator, the store, and the Host query handler by hand, and reproduced the rendered header myself.

简体中文

上一轮两条都改到位了:isAgentGraphLive 挪进 agent-graph-panel-visibility.ts,停止按钮和心跳共用同一个判断,liveSinceRef 那一套删干净了,spinner 也改成装饰性的,间距用 flex gap,多余的 CSS 一并去掉。

但改用 epoch createdAt 之后暴露出一个新问题,会挡合并。第一个 epoch 根本没有真实起始时间,它在协调器和存储里都是写死的 0,一路原样传到面板。我用真实的线上取值把组件挂起来跑了一遍,header 显示的是 Running · 0/7 settled · 496781:08:56。而没有翻过 epoch 的会话就是第一个 epoch,也正是 #3878 描述的那种首次扇出场景,所以这不是边角情况,恰恰是主场景。新加的测试全都用接近当前时间或未来时间的值,没有一条盖到 0;story 也从 createdAt: 1 换成了当前时间,等于绕开而不是钉住这个值。

还有一层:createdAt 是 Host 的时钟,减法却用 Desktop 的 Date.now()。远程 Host 下这是两个时钟域,快照里没有任何东西能把它们对齐。你那条「起始时间在未来则归零」的测试,其实是同一个问题的另一侧。

上一轮我让你改用 epoch createdAt,却没去核实第一个 epoch 里到底存了什么,这个弯路是我带出来的,抱歉。我倾向的走法是:先把能用的两个信号发出去,把计时器摘掉。脉冲和 header 的 spinner 都只依赖 snapshot.status,不需要时间戳,已经足够回答「界面是不是卡住了」;去掉计时器之后 AgentGraphLiveStatus 就只剩一个条件渲染的 spinner,完全可以直接写在面板里(面板本来就引了 Spinner),diff 会小非常多。计时器等真的有起始时间了再单独做。另一条路是先在 Host 侧给第一个 epoch 补上真实起始时间,或者干脆由 Host 算好已用时长发下来,但那要处理磁盘上已有的 0,是个更大的改动。无论走哪条,回归测试都应该从 Host 一路带着 createdAt: 0 走到面板,断言 header 不会出现那种荒谬的时长,只测组件是拦不住的。

零碎的几点:PR 正文还写着「快照没有起始时间」和已经删掉的重置逻辑,合并前顺手改一下;Generated-by 只有第一个 commit 有,后两个没有,合并前补一下。reduced motion 下脉冲会停在全不透明那一帧,这是对的;Astryx 的 spinner 是冻住而不是隐藏,如果计时器摘掉了,这个静止的圈值得再看一眼,不过不阻塞。推修复的时候麻烦附上明暗两套的前后对比图。

}, [props.live]);

const elapsedSeconds =
props.startedAt === undefined || now === undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

startedAt === undefined is the only guard, but the value that actually arrives for a first graph is 0, not undefined. Epoch 1 is synthesized with createdAt: 0 in stream-graph-coordinator.ts:1349 and sqlite-session-metadata-store.ts:3800, and nothing between there and line 398 of the panel rewrites it, so this subtracts against the Unix epoch and the header renders a six-digit hour count. See the main comment for the two ways out.

function graphBridge(snap: AgentGraphClientSnapshot, fail = false) {
const directory = {
epochs: [{ epoch: 1, graphId: snap.graphId, createdAt: 1, current: true }],
epochs: [{ epoch: 1, graphId: snap.graphId, createdAt: Date.now() - 23_000, current: true }],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth noting why this had to change: createdAt: 1 was closer to what production sends than this is. A first graph arrives as createdAt: 0, so swapping in a recent timestamp makes the story stop reproducing the real header. If the clock survives in some form, this fixture should pin the sentinel rather than avoid it.

@github-actions github-actions Bot added effort/M Under 500 readable lines and removed effort/S Under 100 readable lines labels Sep 3, 2026
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 0771760 to 8a964a3 Compare September 3, 2026 06:15
@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Took option 1 - the clock is gone, pushed in 8a964a3:

  • AgentGraphLiveStatus is collapsed back into the panel as a conditional Spinner gated by isAgentGraphLive; the component, its tests, and the @maka/ui export are removed, so the sentinel createdAt: 0 and the Host-vs-Desktop clock domain problem have no code left to bite. The story fixture is back to the production sentinel shape.
  • The liveness answer is the pulse + header spinner off snapshot.status - no timestamp, nothing to correlate - which still answers all three of fix(desktop): agent graph appears frozen while 0/7 operators are running (no heartbeat) #3878's asks.
  • The elapsed clock returns as its own change only after the Host sends a real start time or an elapsed value from its own clock domain (epoch 1's 0 rows included in that prerequisite), per both reviews.
  • PR description rewritten to match this head, and the Generated-by trailer is now on every commit.

Reduced motion P3 noted: with the clock gone, reduced-motion users get a static ring + settled pulse dots, which still reads as "status row" but no longer animates - happy to pick that up with the clock's own PR.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The earlier P1 is gone on this head, I re-checked: AgentGraphLiveStatus, its @maka/ui export and its tests are all absent, so the createdAt: 0 sentinel has no code left to reach. Taking the timer out rather than patching the sentinel was the right call.

On whether this PR is worth having: yes, and it is the clearest case in the batch I am reviewing today. #3878 comes with a screenshot, a repro and its own code pointers, and the cost it names is concrete, users read a header stuck at 0/7 settled as a hang and press Stop on work that is running fine. This change covers the first two "Expected" items and does not wander into the rest.

The shape is also a net reduction, which is worth noting since almost nothing in this batch is. isAgentGraphLive lands in agent-graph-panel-visibility.ts beside DISMISSIBLE_STATUSES and is consumed by both the heartbeat gate and stopAvailable, which used to carry its own inline ['active','waiting','closing'].includes(...). One fact, one place. graphLive is derived per render from snapshot.status with no new state, ref, effect or timer, so there is no setter path that could strand it true, and the pulse is CSS, so no extra IPC. Visibility is untouched, nothing that used to close now stays open.

Three things, none blocking.

[P2] The heartbeat keeps beating when the read has failed

agent-graph-panel.tsx:264 is catch { if (scheduler.isCurrent(fence)) setError(true); } and does not clear snapshot, while graphLive at :208 looks only at snapshot.status. So the last active snapshot keeps the pulse running indefinitely after the Host drops or a snapshot read throws. Collapse the panel at that point and maka-agent-graph-content unmounts along with the error banner, leaving a header with nothing but a spinning indicator.

The stale Running · 0/7 text has the same problem today, so this amplifies rather than introduces. But the PR's whole output is a liveness signal, and this is the one state where it asserts liveness precisely when liveness is unknown. const graphLive = !error && snapshot !== undefined && isAgentGraphLive(snapshot.status); covers it.

[P3] maka-agent-graph-heartbeat is now a dead class name

agent-graph-panel.tsx:402 still passes it; the .maka-agent-graph-heartbeat { flex: 0 0 auto } rule was removed last round but the hook stayed. git grep finds this one occurrence, no CSS, no story, no e2e.

[P3] Reduced motion gets both signals at zero, plus a frozen ring

base.css:82-91 overrides everything with animation-duration: 0.01ms !important and animation-iteration-count: 1 !important. That includes Astryx's Spinner, whose own source comments say it deliberately slows to 3s rather than freezing under reduced motion because a frozen spinner reads as broken. The new dot pulse freezes at 100% (visually correct, but static). So these users get no liveness increment from this PR and one motionless ring, which is the appearance #3878 is about. You already acknowledged this and proposed handling it with the timer PR; either that, or hide the heartbeat entirely under both prefers-reduced-motion: reduce and [data-maka-reduced-motion="true"], on the grounds that nothing beats a misleading static ring. Worth noting the dead class name above means there is currently no hook to hang that rule on.

Next step

Only the P2 is worth folding in now, one line.

Manual acceptance, since CI cannot see any of it: the Agent Graph stories in Light and Dark, checking spinner-to-text baseline alignment and spacing, because .maka-agent-graph-progress became inline-flex and the text node is now an anonymous flex item, and checking that flex-wrap: wrap does not strand the spinner on its own line at narrow widths. Then the same two stories with system reduced motion on, to decide the third point by looking at it. The light/dark before/after pair asked for last round is still not on the PR.

One correction for the body: "Repo format · npm run format:check" proves nothing for this diff. biome.jsonc:51-58 excludes !apps/desktop/**, and all four changed files are under it. Lint still applies.

Evidence boundary: static read at 8a964a3a, cross-checked against the panel's refresh and error paths, the reduced-motion block, Astryx's Spinner source, the agent-graph stories and the accessibility e2e spec. No build, no test run, no Desktop, no Storybook render, so the reduced-motion spinner behavior is inferred from CSS precedence rather than observed.

AI-assisted review: drafted with Maka.

ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.
@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

The P2 and both follow-ups from the second review are pushed in 3de8108:

  • P2: graphLive now requires !error — after a failed snapshot read the last known status no longer keeps the pulse running while liveness is unknown.
  • P3 (dead class): maka-agent-graph-heartbeat stays, repurposed as the hook the reduced-motion rule needs (see next).
  • P3 (reduced motion): the heartbeat is hidden under both reduce paths — @media (prefers-reduced-motion: reduce) and [data-maka-reduced-motion="true"] (the e2e fixture attribute the base.css global also honours). Rationale: the global rule freezes Astryx's ring mid-spin, and a motionless ring is exactly the broken-looking signal to avoid; the pulse's static dot already carries the state. Per-file reduced-motion duplication is also gone.

Verification per your acceptance list: ui 320/320, renderer typecheck 0 errors, architecture check green against the merge base, inventory ok, biome lint clean on the changed files (the PR body's format row was wrong as you noted — format:check excludes apps/desktop/**; it is replaced with the renderer typecheck and lint rows), and the Storybook visual pass in Light and Dark. The PR body now also states the reduced-motion trade-off explicitly.

@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

The latest head's Desktop e2e step failed on code-scroll.spec.ts:22 ("a one-line Markdown code block exposes native and selection horizontal scrolling"): the selection drag produced window.getSelection()?.toString().length === 0 for the full 10s poll window.

This looks like the same intermittency documented in #4633 / fixed-forward in #4632 — the code-scroll selection drag intermittently selects nothing — and is disjoint from this PR: against fresh upstream/main the diff is 4 files (agent-graph-panel.tsx, agent-graph-panel-visibility.ts + its test, agent-graph.css), +71/-1, with zero hits for code-scroll/getSelection/scrollLeft. Everything else in the job passed (104/105 e2e specs).

Could someone re-run the failed job? If it reproduces I will take the reproduction from there.

@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 3de8108 to 5ac9481 Compare September 3, 2026 15:22
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.
@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Found the actual root cause of the two e2e reds: the branch's base (ade07b5be) predates the e2e stabilisation fixes upstream has already merged — #4632 (deselect-proof the code-scroll selection drag, the exact spec that failed in the last run) and #4620 (wait for the initial transcript tail, the one before it). The branch was reproducing flakes that upstream had already fixed.

Rebased onto current upstream/main (39 upstream commits picked up, including those two), regenerated the surface inventory after the rebase, and re-verified locally: renderer typecheck 0 errors, renderer architecture check green against the new base, inventory ok. Fresh CI is triggered on the new head — with the deselect-proof and tail-wait fixes in the tree, the two flaky specs should pass now.

No source changes in this rebase beyond the inventory regeneration; the review fixes from the previous round are unchanged.

@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from 56b6bb3 to fb8a960 Compare September 3, 2026 22:58
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 3, 2026
…failed

Per review on apache#4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

The P2 and the evidence request are addressed, pushed in 56b6bb3:

  • P2: the panel now exposes data-error and the dot pulse freezes under it (animation: none on running/runnable dots) — after a failed snapshot read neither liveness signal claims liveness. The spinner heartbeat no longer exists on this head (removed with the clock), so the pulse is the only motion signal and it now answers to the same !error authority.
  • Reduced motion (P3): stated in the body now — reduced-motion users see the static running dot only; no frozen ring and no fake motion.
  • Generated-by: 56b6bb372 carries the trailer; the squash message should keep it.

Visual evidence from Storybook (blocked-on-upstream, live graph), Light:

light

Dark:

dark

Narrow width — the heading copy wraps gracefully, the progress line moves to its own row and nothing strands:

narrow

Reduced motion (via the data-maka-reduced-motion fixture attribute the base.css global honours) — identical static header, no frozen ring:

reduced

Computed-style check under reduced motion: the running dot reports animation-name: none, animation-duration: 1e-05s, color unchanged — frozen, not broken.

Also note for triage: the two earlier Desktop e2e reds were the branch reproducing flakes upstream had already fixed — the branch is rebased on current main now, which carries #4632 (code-scroll deselect-proof) and #4620 (transcript tail wait).

@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from fb8a960 to 8416112 Compare September 4, 2026 03:09
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…failed

Per review on apache#4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Visual evidence for the three P3s, rendered from the ManyOperators story on this head (light and dark at 900px, a 420px viewport, and reduced motion via the data-maka-reduced-motion="true" fixture path):

Light, 900px - spinner-to-text baseline and spacing on the inline-flex progress line:

light 900

Dark, 900px:

dark 900

Narrow, 420px - the heading-copy wraps and the progress line drops to its own row with no overflow:

narrow 420

Reduced motion, 900px - the heartbeat spinner is absent (not frozen), the status text carries the state alone:

reduced motion

The reduced-motion capture sets data-maka-reduced-motion="true" on the root, the same write site the e2e fixture uses, so it exercises the attribute path rather than the media query.

@github-actions github-actions Bot added effort/XXL Over 2500 readable lines and removed effort/S Under 100 readable lines labels Sep 4, 2026
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…failed

Per review on apache#4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch 2 times, most recently from 51b489f to a961d9f Compare September 4, 2026 09:45
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
…failed

Per review on apache#4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.

Generated-by: GLM-5.3-Flash (ZCode)
ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 4, 2026
Per review on apache#4514: the header heartbeat and operator-dot pulse now use the same graphLive authority. The panel exposes data-live, and running/runnable dots only animate under that shared live gate.

Generated-by: GLM-5.3-Flash (ZCode)
…s run

A fan-out graph in its early window shows a static header (`Running · 0/7
settled`) and seven static dots, so the panel reads as frozen even though
scheduling is correct and child sessions are working (apache#3878).

Three presentation-layer liveness signals, no protocol change:

- Running and runnable operator dots pulse (CSS only, disabled under
  prefers-reduced-motion).
- The header shows a subtle spinner for as long as the snapshot status is
  active, waiting, or closing.
- A per-epoch elapsed clock (`· 00:23`) trails the settled counter. The
  client snapshot carries no start timestamp, so the clock measures this
  view's observation of the live graph and resets when the selection
  moves to another epoch or the graph settles; it is aria-hidden because
  status and progress are already announced.

Fixes apache#3878

Generated-by: GLM-5.3-Flash (ZCode)
Per review by Astro-Han on apache#4514:

- The stopwatch now runs from the selected epoch's `createdAt` instead of
  the mount time, so the header reports how long the graph has run, not
  how long this view has been watching it. `AgentGraphLiveStatus` takes
  `startedAt` and keeps only the 1 Hz ticker; `liveSinceRef`, `resetKey`,
  and the reset branch are gone.
- `GRAPH_LIVE_STATUSES` moves to `agent-graph-panel-visibility.ts` as
  `isAgentGraphLive`, shared by the heartbeat gate and `stopAvailable` so
  "can stop" and "shows as running" cannot drift; covered in the
  visibility suite (14 tests).
- The heartbeat spinner is `aria-hidden` (it duplicated the adjacent
  visible status text on a `role="status"` element), the progress line is
  an inline flex with `gap` so the spinner has real spacing, the dead
  `flex: 0 0 auto` rule is removed, and the per-file reduced-motion block
  is dropped in favour of the global rule in `base.css` which also covers
  the e2e reduced-motion fixture path.

Verified: ui 320/320 (5 live-status tests updated for the `startedAt`
shape), desktop typecheck 0 errors, renderer architecture check green
against the merge base, surface inventory ok, format and ASF headers
clean.

Generated-by: GLM-5.3-Flash (ZCode)
…istic epoch

Adds the visibility-suite coverage for the shared live-status gate and
gives the story fixture a realistic epoch createdAt (hardcoded 1 rendered
a five-digit hour count under the new epoch-origin stopwatch).

Generated-by: GLM-5.3-Flash (ZCode)
Epoch 1 - the first graph a Session fans out - has no real start
timestamp: its createdAt is a sentinel 0 synthesized by the coordinator
and the sqlite store, and a remote Host clock cannot be correlated with
the Desktop wall clock. Treating that value as a stopwatch origin
rendered `496781:08:56` on the exact first-fan-out path the issue
describes, so the clock was wrong on the headline case.

Drop the clock and keep the two signals that work: the heartbeat
spinner (aria-hidden; the adjacent text already announces the status)
gated by isAgentGraphLive, and the progress line. This collapses
AgentGraphLiveStatus back into the panel, removes the component and its
tests from @maka/ui, and reverts the story fixture to the production
sentinel shape. The clock returns as its own change once the Host sends
a real start time or an elapsed value from its own clock domain.

Generated-by: GLM-5.3-Flash (ZCode)
…ide it under reduced motion

Per review on apache#4514: a failed snapshot read leaves the last known status
in place, so graphLive now also requires !error - a spinner asserting
liveness while liveness is unknown is the false signal the panel exists
to avoid. The heartbeat is hidden under both reduced-motion paths (the
base.css global freezes Astryx's ring mid-spin, which reads as broken);
the maka-agent-graph-heartbeat class stays as the rule's hook.

Generated-by: GLM-5.3-Flash (ZCode)
…failed

Per review on apache#4514: the spinner heartbeat is gated by !error but the
operator dot pulse kept running off the stale snapshot, so the two
liveness signals answered to two different authorities. The panel now
exposes data-error and the pulse animation freezes under it, same as
the spinner.

Generated-by: GLM-5.3-Flash (ZCode)
Per review on apache#4514: the header heartbeat and operator-dot pulse now use the same graphLive authority. The panel exposes data-live, and running/runnable dots only animate under that shared live gate.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from a961d9f to 705e23d Compare September 4, 2026 09:49
@ggbdpq

ggbdpq commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining P2 and refreshed the branch on current main.

  • Rebased onto upstream/main at 3f4ac8c9dd3907031d964024e69376cac8552ea7 with no textual conflicts.
  • P2 fix is in 705e23dc1: the panel exposes data-live from the same graphLive value that gates the header spinner, and running/runnable status dots only animate under .maka-agent-graph-panel[data-live="true"]. After a snapshot-read error, both liveness animations are off.
  • PR body is updated for the current head: no elapsed clock, no data-error liveness hook, reduced-motion trade-off stated, and every affected commit has the Generated-by: GLM-5.3-Flash (ZCode) trailer.

Local verification after the rebase:

  • tsc -p tsconfig.renderer.json --noEmit from apps/desktop: exit 0, no output
  • biome lint apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts apps/desktop/src/renderer/agent-graph-panel-visibility.ts apps/desktop/src/renderer/agent-graph-panel.tsx apps/desktop/src/renderer/styles/agent-graph.css: Checked 4 files in 776ms. No fixes applied.
  • node apps\desktop\scripts\check-renderer-architecture.mjs: Renderer architecture check passed.
  • npm run check:asf-headers: Every source file carries the ASF header or a reviewed exclusion.
  • git diff --check upstream/main...HEAD: exit 0, no output

Fresh CI is running on head 705e23dc1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants