Skip to content

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

Open
ggbdpq wants to merge 66 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 66 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 (synthesized by StreamGraphCoordinator and SqliteSessionMetadataStore), and a remote Host clock cannot be correlated with the Desktop wall clock, so a stopwatch built on that value showed 496781:08:56 on exactly 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 now the two signals that need no timestamp: a heartbeat spinner gated by isAgentGraphLive (aria-hidden; the adjacent status text already announces it) on an inline-flex progress line. AgentGraphLiveStatus was collapsed back into the panel and removed from @maka/ui along with its tests; the story fixture is back to the production sentinel shape.
  • isAgentGraphLive still lives in agent-graph-panel-visibility.ts and feeds both the heartbeat gate and stopAvailable, so "can stop" and "shows as running" cannot drift.
  • 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 (stated for the record): under reduced motion the header matches main - the pulse freezes via the base.css global and the spinner heartbeat is hidden rather than frozen, so reduced-motion users get the static running dot only. This is deliberate: a motionless ring reads as broken.
Claim Command Result
Renderer typecheck stays clean tsc -p tsconfig.renderer.json --noEmit (apps/desktop, run on a clean main baseline first) exit 0, 0 errors before and after the change
Renderer typecheck (covers all four changed files) tsc -p tsconfig.renderer.json --noEmit (apps/desktop) exit 0, 0 errors
Lint (format:check excludes apps/desktop/** from the formatter, so it proves nothing here; lint still applies) biome lint on the changed files Checked, no issues
ASF headers npm run check:asf-headers Every source file carries the ASF header or a reviewed exclusion
Visual acceptance (Light + Dark, both agent-graph stories) Storybook render of product-agent-graph--blocked-on-upstream spinner inline with the status text, gap-driven spacing, elapsed absent after the clock removal
Verification runway note - apps/desktop renderer has no node --test runway (desktop's node --test covers the main process only), so verification is typecheck + format per the repo's convention for renderer-only changes; behavior is CSS/React-presentation only

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.

Checklist

  • Tests and checks pass locally (see Verification)
  • Behavior change: Yes - visual liveness signals only (pulse, header spinner, elapsed clock); no data, scheduling, or protocol 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.

Joob1n and others added 26 commits September 4, 2026 11:08
…pted (apache#4667)

When the summarizer's own provider rejects a fold as too large, the planner
halved the covered range and tried again. Halving is a guess in both
directions: it can discard verbatim history the summarizer would have taken,
and it can still be too large, paying another round trip to find out.

There is a boundary that needs no guessing. The last accepted request's input
covered everything before the newest model reply began; that span was accepted
by this model on this connection, so it is provably within the provider's
capacity. The fold retreats to it once. A rejection of that span too is the
provider saying this fold cannot be made, and the fold fails open with the
summarizer's own reason rather than a span-selection one.

The boundary is read from the ledger rather than persisted: the newest model
reply is the end of the proven span whether or not it sits at the tail, so a
turn's first request finds the previous turn's reply. A ledger with no model
reply has nothing proven and gets no retreat, because inventing a boundary is
the guess this change removes.

Refs apache#4559, apache#4634

Generated-by: Claude Code
Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J

Generated-by: GLM-5.3-Flash (ZCode)
…eat (apache#4671)

apache#4667 wired the proven-boundary retreat into the mid-turn and pre-turn call
site and missed the standalone one, which is the site manual compaction uses.
Without `runHeaders` and `acceptedRoute`, `acceptedInputBoundary` returns
nothing on its first line, so the first `input_too_large` fails open. That path
retreated before: its coverage gate admitted every halving step, so the loop
walked down until a span was accepted.

Four entries reach it: CLI `/compact`, Desktop `sessions:compact`, sub-agent
compaction from supervisor wake, and the pre-turn fallback. Its first attempt
covers the whole prior session with no reserved tail, which is the span most
likely to be rejected, so the regression landed on the ordinary long session
rather than an edge. The first three entries reported failure with a
`context_compaction_failed_open` note; the fourth sent the oversized history
and the turn died with `context_overflow`.

The fix passes the same two values the other call site already passes, so there
is one rule and two call sites rather than two rules. The test drives
`compactHistory` rather than the planner: the planner tests hand the route in
directly, so they would have stayed green with both call sites deleted, which
is exactly how this got through.

Mid-turn still cannot reach a retreat, because `priorRunHeaders` excludes the
current turn so the proven index lands at or below `headAnchorIndex` while the
gate wants it above. That is a pre-existing limit rather than something apache#4667
changed, and it needs its own change to the gate, so it is not in this PR.

No protocol or schema change.

Refs apache#4559, apache#4667

Generated-by: Claude Code
Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J

Generated-by: GLM-5.3-Flash (ZCode)
…ndary (apache#4669)

The provider-dropping note compared each step's input against the previous
step's, so it only saw a provider evicting context from inside one send. The
shape it exists for is not visible there: a provider that truncates to a fixed
window reports the same input on every later request while the user keeps
adding turns, and a send of one or two steps has no earlier step to compare
with. The live evidence in apache#4623 plateaus at 3,716 input tokens across eight
turns with nothing reported.

The first request of a send now compares against the last request a provider
accepted before it, read from the persisted anchor, which is route-validated
where it is read. A fold before that request would explain a smaller input by
itself, so it disables the comparison.

Across the boundary the test is equality, not "did not grow". Inside a send
Maka knows it only appended, so any shortfall is the provider's; across the
boundary it does not know that. A manual compaction leaves the pre-compaction
anchor behind, a turn can carry a smaller tool set, and a user can edit or
branch history. All three shrink the input legitimately and none lands on
exactly the same count, while a provider truncating to a fixed window lands on
it every time.

The note carries the two counts it is asserting from, so the claim can be
checked, and it is reported once per backend activation rather than once per
send, since the condition persists once a provider starts truncating.

No protocol or schema change: `context_provider_dropping` and the optional
`data` payload are both already in the shape.

Refs apache#4559, apache#4623

Generated-by: Claude Code
Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J

Generated-by: GLM-5.3-Flash (ZCode)
`OAuthCreateAdoptsExactConnection` asserted focus on the created connection's
button immediately, but the row restores focus on the next frame, so the
assertion could run before the restoration landed.

Wrapping it in `waitFor` matches what the rest of the file already does at two
other assertions, and keeps the assertion's strength: focus that never arrives
still fails.

Storybook story only, no production change.

Generated-by: OpenAI Codex

Generated-by: GLM-5.3-Flash (ZCode)
Design documents under `docs/` described work that was still open, but nothing connected those descriptions to a tracker. A reader could not tell whether a gap was known, owned, or already closed, and each document was free to keep its own private checklist of remaining deliverables.

Every design document that describes unfinished work now carries a `Tracking:` line pointing at the issue that owns it. Where a document held a backlog list that duplicated a tracker, the list is removed and the tracker becomes the single place delivery status lives: `mcp-runtime-architecture-draft.zh-CN.md` drops its post-V3 checklist for apache#4329, `side-conversation.md` and `web-search-provider-capability.md` drop theirs. The completed MCP dual-era rollout apache#1650 is folded into the status line rather than presented as open tracking.

Two paragraphs in `llm-compaction-events-log-projection-draft` were corrected while linking them: Active Tool Result Prune does record a durable projection transition and derives the current request from the effective-history reducer, so the earlier claim that it only changes current messages was wrong.

Documentation only. No code, contract, or behavior change.

Generated-by: OpenAI Codex

Generated-by: GLM-5.3-Flash (ZCode)
* feat(runtime): establish durable form interactions

Define a bounded provider-neutral primitive form contract and carry its request and acknowledgement facts through the Runtime Event Log. Broker pending forms through the existing InteractionStore authority so schema-invalid answers remain pending, concurrent equivalent answers converge on one canonical outcome, and Turn closure or Host restart closes the exact continuation.

Part of apache#4364.

Generated-by: OpenAI Codex

* fix(runtime): complete form interaction adapter seam

Expose one closed decoder for renderer-to-runtime form responses so surface adapters do not copy protocol validation. Queue the same canonical continuity refresh for form requests that user questions already receive.

Refs apache#4364.

Generated-by: OpenAI Codex

* fix(runtime): harden structured form boundaries

* fix(desktop): accept form interactions in composer region

* fix(runtime): complete form interaction lifecycle

Generated-by: OpenAI Codex

* fix(desktop): keep unsupported forms out of composer queue

Generated-by: OpenAI Codex

* fix(runtime-host): publish form graph activity

Generated-by: OpenAI Codex

* style(runtime-host): format form graph regression

Generated-by: OpenAI Codex

* fix(core): preserve projected form default semantics

Generated-by: OpenAI Codex

* fix(core): prove the escaped answer envelope at admission

Admission reserved the answer envelope with four raw bytes per code
point, but enforcement measures post-serialization bytes, where JSON
escaping inflates a code point to as much as six. A schema-legal answer
of backslashes, newlines, or control characters could pass admission yet
be rejected at decode, stranding the pending interaction.

Keep the schema semantics — maxLength stays a code-point count and each
value stays bounded at INTERACTION_FORM_VALUE_MAX_BYTES raw bytes — and
prove serializability at admission: the string envelope is now all
control characters (one code point and one raw byte each, six serialized
bytes after escaping), and select envelopes pick the serialized-longest
option rather than the raw-longest. A form whose limits permit an
undeliverable answer is rejected up front instead of stranding the
interaction after the user submits.

Regressions pin the preserved character semantics (2,048 plain
characters or 1,024 backslashes still satisfy a maxLength: 2048 field),
admission rejection for limits that cannot guarantee delivery, and
escape-heavy answers that decode and deliver for admissible forms.

* fix(core): reserve date and date-time envelopes in their legal language

A string field without maxLength reserved 2,048 control characters even
when the format was date or date-time — an estimate that can never pass
the format check yet inflates to 12 KiB, so a form asking for a calendar
day was rejected before publication while its whole legal answer is 64
bytes.

Compute the worst value inside each format's legal language instead:
date is fixed-length over [0-9-], and date-time adds only characters
that never JSON-escape, with fractional seconds bounding length at the
field caps. Other formats and unconstrained strings keep the
six-bytes-per-code-point worst case, since control characters remain
legal there.

Regressions cover date and date-time fields admitted with no maxLength,
their canonical answers decoding, and the fractional-seconds worst case
staying deliverable.

* feat(runtime-host): broker nested capability forms (apache#4397)

* feat(runtime): withdraw producer-owned forms

Let an exact hosted Run withdraw one form without closing its surrounding Turn. Commit producer cancellation through the existing InteractionStore authority, preserve an already-claimed Run closure, and compose provider-local cancellation with the Tool invocation signal.

Part of apache#4364.

Generated-by: OpenAI Codex

* feat(runtime-host): carry nested capability forms

Add strict request/result frames and expose one provider-neutral requestInteraction callback for admitted Client Capability invocations. Keep correlation inside the client channel and publish a new compatibility epoch for peers that understand the round trip.\n\nPart of apache#4364.\n\nGenerated-by: OpenAI Codex

* feat(runtime-host): broker nested capability forms

Route Client Capability interaction requests through the Runtime-owned form callback. Pause provider execution time only while the canonical form is pending, bound result delivery, and rearm a fresh execution timeout after delivery.\n\nClose the exact producer-owned form before settling provider failure, cancellation, or connection loss, while preserving Runtime Host as the only Interaction authority.\n\nPart of apache#4364.\n\nGenerated-by: OpenAI Codex

* fix(runtime-host): order nested form cleanup

* test(desktop): complete capability interaction fake

* fix(runtime-host): forward forms after capability admission

* fix(runtime-host): await prior capability releases

* style: format nested capability form files

The nested capability form sources predate the formatter rules now on
main; rebase onto the current parent and reformat so the changed-file
biome gate passes again. No semantic change.

* test: give nested form fixtures an explicit string bound

Admission now proves every legal answer serializes, so a string field
without maxLength is no longer admissible. Bound the fixtures to keep
them representative of forms a provider can actually publish.

* feat(tui): answer structured form interactions (apache#4392)

* feat(tui): answer structured form interactions

Review and edit Host-owned forms in the existing TUI interaction queue, preserving optional omission and all six primitive field kinds. Route accepted, declined, and cancelled responses through the generic Runtime Host interaction operation, while non-interactive runs stop the exact Turn instead of dropping the request.

Keep requester provenance and sensitive-data guidance visible, validate with the shared Core contract, and retire stale overlays when authoritative transcript state changes.

Part of apache#4364.

Generated-by: OpenAI Codex

* fix(tui): retain reconnecting form drafts

* test(tui): cover form constraint copy variables

* style(tui): format form interaction sources

The TUI form sources predate the formatter rules now on main; rebase
onto the current parent and reformat so the changed-file biome gate
passes. Reflow and trailing commas only, no semantic change.

* fix(cli): cover the form interaction TUI under copy boundaries

Generated-by: GLM-5.3-Flash (ZCode)
Formatting, ASF license headers, and protocol epoch bumps were only checked in CI, so a contributor learned about them minutes after pushing rather than at commit time.

A husky `pre-commit` hook now runs those three checks against the staged content before the commit lands. Each check gained the piece it was missing to work on an index rather than a worktree: `scripts/biome-staged-check.mjs` reads index snapshots so a partially staged file is checked as the bytes that will actually be committed, `scripts/asf-license-headers.mjs` and `scripts/protocol-epoch-check.mjs` accept an explicit staged file list, and both grew unit tests. `scripts/install-husky.mjs` installs the hook only when the tooling is already present, so a dev-dependency-free install neither fails nor silently downloads a fallback package. CI runs the same scripts, so this adds a faster seam rather than a second authority.

Commit messages are deliberately not linted. Intermediate commits are squashed and never reach main; the pull request title is what needs conventional validation, and CI already checks it.

Local developer tooling only. No product code, contract, or runtime behavior changes.

Generated-by: OpenAI Codex
Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>

Generated-by: GLM-5.3-Flash (ZCode)
apache#4270)

Long turns went silent. The model called tools, the UI showed tool activity, and the first user-visible text arrived only once the work was finished. The main-session prompt carried identity and response-format guidance but no contract asking for progress, so models had no reason to say anything mid-turn.

The fix is the text pipeline we already have. A `## Progress updates` fragment asks for a brief user-facing update before the first non-trivial tool call and at real phase changes. All three supported protocols can already emit ordinary assistant text around tool calls, so this needs no provider-neutral phase, no new stored semantics, no Runtime Host wire change and no extra provider request. The Turn's terminal state and timeline order remain the authority for what counts as the final reply, and that one selector now drives rendering, the Copy payload and Copy availability alike — including on aborted and failed turns, whose visible text stays copyable.

Alongside it, `text-metadata` becomes `text-end`, and native OpenAI Responses output item boundaries split into separate assistant messages so each item keeps its own `itemId` and `phase` for replay, instead of being folded into one row whose `itemId` was then dropped. `text-start` carries only the boundary flag: attaching provider metadata there would let an abandoned item's identity survive an idle-watchdog retry, and would forward Anthropic and Google `text-start` metadata that `main` discarded — including a Gemini thought signature belonging to a different text part. The Codex session importer keeps `phase` and leaves the foreign item id behind.

The rotating working phrases are gone. A quiet provider request is not evidence that the model is making semantic progress, so the live status line says only that Maka is awaiting model output; the phrase pool and its rotation timers are deleted with it.

No migration. Persisted messages, storage and Runtime Host protocols are unchanged, and legacy turns keep their existing final-reply fallback.

Generated-by: OpenAI Codex

Generated-by: GLM-5.3-Flash (ZCode)
Optimistic Session-setting overlays (model, thinking level, permission, orchestration) were retired on the next renderer-wide catalog revision. Any unrelated Session changing anywhere bumped that revision, so an overlay could disappear before the target Session's snapshot had observed the committed write, and the UI briefly showed the old value again. This was the open review note from apache#3749.

Each mutation already returns the Session revision it committed. That revision is now carried through the Desktop projection to the renderer, and the overlay is kept until the target Session's own catalog revision reaches it. Intent channels that return a revision are typed to require a matching Session-revision observer, so a new channel cannot silently fall back to the catalog-wide rule.

Plan mode stays on catalog-wide retirement: its pending-proposal path completes through `abandonPlanProposal`, which returns `PlanSessionState` rather than a Session revision. Moving it needs a separate contract change and is tracked as follow-up.

No Runtime Host wire protocol or persistence format change.

Generated-by: Codex

Generated-by: GLM-5.3-Flash (ZCode)
…k cuts (apache#4393)

A thinking-only streaming attempt that died from a retryable network error ended the Turn, while the same attempt state recovered when the 120s idle watchdog fired first. In apache#4284 an ECONNRESET landed seconds before the watchdog would have, so an identical fault got opposite outcomes depending on which detector saw it.

The plain retryable path now accepts thinking-only attempts under the seal contract the watchdog path already used: `flushStep()` closes the partial thinking as a message under its own id, the retry streams into a fresh id, and the sealed fragment stays out of the retried request's provider context. The shared predicate replaces the watchdog-specific one rather than adding a parallel branch. The path carries its own budget, `MAX_SEALED_THINKING_RETRIES_PER_STEP = 1`, so a gateway that systematically cuts long thinking streams fails fast instead of accumulating fragments across the whole attempt budget. The new `sealedThinkingRecovery` flag is mutually exclusive with the other retry paths by construction.

Answer text, tool activity, and provider continuation metadata remain non-retryable. The watchdog and truncated-stream paths are unchanged.

Fixes apache#4284

Generated-by: ZCode

Generated-by: GLM-5.3-Flash (ZCode)
… namespaces through (apache#4590)

Nine renderer feature slices each restated the same twenty-line services context, and their Desktop adapters hand-wrote forwarders whose signatures equal the bridge methods they forward to. Every new slice paid that join cost again, and the copies could drift apart without any test noticing.

`application/contracts/feature-services.tsx` now exports `createServicesContext<S>(providerName)`, which returns the Provider and hook pair. Each slice's `services-context.tsx` shrinks to a few lines while keeping every exported name, type, and error message. Desktop adapters hand a bridge namespace through where the port is a structural subset of it (`sessions`, `newTasks`, `scheduledTasks`, `shellRuns`, `todo`, `attachments`), removing 33 identity forwarders. Adapter blocks that rename, guard, filter, or translate stay hand-written. Preload namespaces are plain objects with no `this` usage, so passing one through is runtime-safe, and the port types stay narrow, so nothing new is reachable from feature code.

`composition/desktop-feature-services.tsx` is untouched; the tenth slice from apache#4498 can adopt the factory after it lands.

No behavior change. Refs apache#4582

Generated-by: Claude Code

Generated-by: GLM-5.3-Flash (ZCode)
…en the owned settle bound (apache#4672)

* test(runtime-host): give the owned settle assertion a CI-safe bound

The owned Host test closed its only connection and required the process
to settle within 500 ms. Shutdown takes about 30 ms on an idle machine,
but under a full CI suite on a 4-vCPU runner the Host is starved for CPU
and the same clean exit lands past 500 ms, so the assertion reported
false without any product regression. PR apache#3221 widened the connection
half of this test for the same reason.

The promptness claim is that an owned launch exits on its idleGraceMs of
0 instead of the 30 s default grace. A 5 s bound still separates those
by six times while covering the starvation the CI suite produces, and it
matches the settle budget the rest of the file already uses. Measuring
from a close acknowledgement would not help: the Client close is a local
abort with no reply, and the slow part is the shutdown work that follows
it inside the Host.

Refs apache#3190

Generated-by: Claude Code

* fix(runtime-host): claim a Daily Review archive before its reads

Two Clients running the same Daily Review at once could each get their
own archive. The coordinator consulted its in-flight map only after
awaiting the summary, the existing-archive read, and the config read, so
under CI load the first request finished and published while the second
sat between its existence check and its in-flight check, then generated
again. The two-client UDS test caught this on main as generatedAt values
6 ms apart.

The claim now happens before the first await. The archive id derives
synchronously from the clock and the requested day, so a second request
for the same archive joins the running generation or conflicts on
different options. Joining compares the requested modelKeyOverride and
trigger instead of the resolved model key: the resolved key is only
known after the config read, and reading it later is the same race.

The regression test holds the second request's session read until the
first has published; without the fix the model runs twice.

Generated-by: Claude Code

* fix(runtime-host): let a Daily Review replace outlive a non-replacing leader

The early claim treated replaceExisting true and false as one operation,
so a replace that arrived while a non-replacing run held the claim
joined it and received the archive that run merely found, with no
regeneration. Before the claim moved ahead of the reads, the replace
regenerated after the other run returned.

A replace now waits for a non-replacing leader to finish and then claims
the archive itself. Non-replacing runs still join anything compatible,
and a replace joins a replace. Reporting a conflict instead would have
turned a flow that used to work into an error.

Generated-by: Claude Code

* chore(runtime-host): state the Daily Review join key and release a rejected leader's laggard

Review nits on apache#4672: the code said the claim happens before the first
await but not why requests match on the requested override rather than
the resolved model key, and the lagging-read test only released its
gate when the leader resolved, so a rejected leader would leave close()
waiting on the laggard for the whole suite.

Generated-by: Claude Code

Generated-by: GLM-5.3-Flash (ZCode)
OpenCode Go requires every inference request to carry an `x-opencode-session` header with a stable conversation identity. Maka omitted it from model requests and from Connection Test probes, so the provider could reject them.

The header is added at the shared request-customization boundary and carries the existing Maka Session identity through normal model requests and tool-loop steps, history compaction, session-scoped auxiliary calls (title, recap, memory extraction, goal evaluation), and the OpenAI Chat, Anthropic Messages, and Responses wires. Identity stays with the Runtime Host: `ModelAdapterInput.sessionId` and `transportContextId` were already the seams, and every non-test `getAIModel` call site is covered. Operations without a Session use one identity per operation: Connection Test generates one UUID and reuses it across its internal attempts, and Daily Review generates one call identity and reuses it for provider retries.

The header is restricted to `opencode-go`. An explicitly configured `x-opencode-session` header wins, matched case-insensitively, and the generated value contains no user content or credential material.

Fixes apache#4663

Generated-by: OpenAI Codex

Generated-by: GLM-5.3-Flash (ZCode)
The Storybook visual smoke only rendered the catalog with the default palette in one color scheme, so a story that broke in dark mode or under a non-default palette passed CI.

Every story now renders with the default palette in both light and dark color schemes, and the Native Conversation reference story additionally renders across all 11 palettes in both schemes. Scheme and palette are part of the smoke URL, the diagnostics, and the test names, so a failure says which combination broke. The full catalog is deliberately not expanded across every palette; the reference story is the palette probe.

Test tooling only. No product code change.

Refs apache#3942

Generated-by: OpenAI Codex

Generated-by: GLM-5.3-Flash (ZCode)
Eval terminated child processes with POSIX signals. On Windows a signal only ends the direct child, so harness trials, external subject wrappers, and preflight timeouts left the supervisor's descendants running after a cancel or timeout.

A shared Eval process-termination helper keeps the POSIX signal sequence as it was and, on Windows, terminates the whole process tree with `taskkill.exe /T /F` from the first cleanup stage. Harness trials, external subject wrappers, and the preflight timeout and cancellation paths all call the helper, so termination policy lives in one place. A native Windows integration test covers the supervisor and descendant lifecycle.

Fixes apache#3892

Generated-by: Codex

Generated-by: GLM-5.3-Flash (ZCode)
…pache#4438)

`clipModelText` bounded MCP tool-result text with a raw `.slice()` at a UTF-16 index. A clip boundary inside an astral character (any emoji) left an unpaired high surrogate in the text block handed to the model, and every UTF-8 round trip after that (provider request body, durable storage) turned it into U+FFFD.

The boundary handling that `local-memory.ts` already used for the same problem moves into a shared `@maka/core` helper, and `clipModelText` drops a dangling high surrogate before appending the truncation marker. `local-memory.ts`, the MCP SEP-2243 path, and the Telegram bridge now use the same helper instead of their own copies, so there is one place that knows how to cut UTF-16 safely. Both MCP call sites (`appendText` and the resource-text path in `summarizeNonVisualBlock`) go through `clipModelText`, so one change covers both.

Fixes apache#4435

Claude-Session: https://claude.ai/code/session_01FTqr6ZjXfNthXT719ag7Qc
Generated-by: Claude Code (Fable 5)
Generated-by: Claude Code (Fable 5.1)

Generated-by: GLM-5.3-Flash (ZCode)
…pache#4681)

Neither update block sets `labels`, so dependabot applies its defaults:
`dependencies` plus a language label, `javascript` for npm and
`github_actions` for github-actions. `javascript` appears only on
dependabot PRs and nobody, human or workflow, reads it; the
`github_actions` label does not even exist in the repository.

Pin both blocks to `dependencies` alone. The configuration has to
change before the `javascript` label is deleted, otherwise dependabot
recreates it on its next update.

Generated-by: Claude Code

Generated-by: GLM-5.3-Flash (ZCode)
maka.apache.org returned 404 and the content direction voted in apache#4307 had nowhere to live. This adds the podling website as the `website/` Astro workspace and the path that publishes it, following the OpenDAL layout from apache#3404.

The site is one bilingual homepage (direction D from apache#4307: one turn of RuntimeEvents in the hero, the Terminal-Bench leaderboard and paired run drawn from docs/eval/, the Runtime Host and the log, three separate download paths, cards for the blogs and reports) and a downloads page with verification steps, Desktop Nightly and building from source, each labelled. English and Chinese share one `Copy` type so neither language can drift; the site test checks the built HTML for the Incubator disclaimer, the copyright and trademark line, every ASF-required footer link, the positioning sentence, identical link sets across languages, no third-party loads, distinct copy-button names, and the bundled OFL texts for Geist. Docs, Security, Community and Architecture stay where they live; the site links to them.

`.github/workflows/website.yml` builds the site and force-pushes the output plus LICENSE, NOTICE and a site-only `.asf.yaml` as an orphan commit: `asf-site` on a push to main touching the site, `site/<tag>-staging` on a release-candidate tag, or `site/<name>-staging` from workflow_dispatch; a dispatch without a stage name publishes only from main. The root `.asf.yaml` drops the publish and staging blocks, which asfyaml reads from the published branch, not from main. The ASF header policy learns `.astro` files. Astro and the font packages are devDependencies, so the production dependency audits stay on the shipped closure. Astro is pinned to 7.2.10 because 7.3.0 fails every build that uses astro:assets.

Refs apache#3404, apache#4307

AI use: Claude Code drafted the site, the workflow and the tests; the author reviewed every file, the ASF policy references and the numbers against docs/eval/.

Generated-by: Claude Code

Generated-by: GLM-5.3-Flash (ZCode)
…pache#4680)

effort/XL had no upper bound. Measured with the label's own readable-line rule, it held 75 of the last 300 merged pull requests and 57 of the 157 open ones, spanning 1000 to over 10000 lines under one label. Those ends are handled differently: up to roughly 2500 lines a change is still read as one unit, past that the first review question is whether it splits.

effort/XL is now capped at 2500 readable lines and effort/XXL sits above it. On the same data the split is 45 XL / 30 XXL for merged and 31 / 26 for open, so both tiers stay populated. Nothing else in the ladder moves.

Rollout: the effort/XXL label is created and the effort/XL description updated at merge time; the next workflow run relabels open pull requests above 2500.

Refs apache#3949

Generated-by: Claude Code

Generated-by: GLM-5.3-Flash (ZCode)
Prevent cloned Tokio watch receivers from redelivering an already-observed generation in a zero-delay loop. Use wait_for for seen-version tracking, keep reachability notifications token-only, and retain the snapshot getter as the single route-data authority.

No persisted-state or peer wire migration is required.

Generated-by: Codex

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)
@ggbdpq
ggbdpq force-pushed the fix/agent-graph-heartbeat branch from fb8a960 to 8416112 Compare September 4, 2026 03:09
@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
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.