Skip to content

feat(inspector): surface the session trace as a workbar tab - #2018

Merged
Astro-Han merged 3 commits into
apache:mainfrom
ARE404:are404/feat-session-inspector-panel
Aug 3, 2026
Merged

feat(inspector): surface the session trace as a workbar tab#2018
Astro-Han merged 3 commits into
apache:mainfrom
ARE404:are404/feat-session-inspector-panel

Conversation

@ARE404

@ARE404 ARE404 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Second layer of the Session Inspector: a read-only IPC that projects one session's trace, and a fifth SessionWorkbar tab that renders it. Consumes the projection merged in #1956.

Refs #1625.

The read boundary is the AgentRun stream, not the Usage read model. Usage is range-queried and carries no session predicate; the stream is the authority and is already keyed by session and run, which is how context-diagnostics.ts reads it too. inspector:trace reads both ledgers and writes nothing — no new store, no new permission path, no execution.

A record that will not decode is counted, not dropped. SessionTraceCoverage gains unreadableRecords. Spend the trace cannot show is exactly the kind of gap that otherwise renders as an absence of activity — the same failure the pi-backend case already forced into the open, one layer up.

The panel's judgements are pure and tested without a DOM. deriveInspectorPanelModel decides what counts as a gap worth showing, whether a cost may be rendered at all, and which step a failure is attributed to; SessionInspectorPanel only lays the result out. Two rules carried through from the ledger: an unpriced call renders "cost unavailable" and never $0, and failure highlighting follows the attributed step rather than every step after it.

Tab placement. SessionWorkbar, not AgentGraphPanel — the latter is gated on orchestrationMode === 'graph' and would have hidden the Inspector from ordinary sessions, which was the failure mode called out early in #1625. The panel refreshes only while visible: a hidden panel re-projecting a long session is a cost with no reader.

Tests

  • 3 for the IPC read: the two-ledger join across runs, an undecodable record counted rather than skipped, and non-canonical AgentRun events ignored.
  • 5 for the panel model: no notice when there is no known gap, a partial notice carrying what is missing, unpriced calls left without a cost, failure attribution not bleeding onto later steps, and the empty case.
  • The workbar persistence whitelist, which had no coverage at all — a tab that renders but is missing from readSessionWorkbarTab silently reverts to Tasks after a restart, which reads as the panel forgetting rather than as a missing case.

Verification

@maka/core 703/703; session-trace projection, inspector IPC, panel model and workbar layout suites green (29 targeted tests). check-a11y, check-console, check-copy, check-dead-css and format:check clean.

Not here

No search or replay step-through — that is PR 3. No changes to the ledger, the pricing authority, the permission path, or the execution runtime.

🤖 Generated with Claude Code

ARE404 and others added 2 commits August 3, 2026 21:34
Second layer of apache#1625, stacked on apache#1956: a read-only IPC that projects one
session's trace, and a fifth `SessionWorkbar` tab that renders it.

**The read boundary is the AgentRun stream, not the Usage read model.** Usage is
range-queried and carries no session predicate; the stream is the authority and
is already keyed by session and run, which is how `context-diagnostics.ts` reads
it too. `inspector:trace` reads both ledgers and writes nothing.

**A record that will not decode is counted, not dropped.** `coverage` gains
`unreadableRecords`, because spend the trace cannot show is exactly the kind of
gap that otherwise looks like an absence of activity — the same failure mode the
pi-backend case already forced into the open.

**The panel's judgements are pure and tested without a DOM.**
`deriveInspectorPanelModel` decides what counts as a gap worth showing and
whether a cost may be rendered at all; `SessionInspectorPanel` only lays the
result out. An unpriced call renders "cost unavailable" rather than `$0`, which
is the same distinction the ledger keeps. Failure highlighting follows the
attributed step rather than every step after it.

The tab lives on `SessionWorkbar` and not on `AgentGraphPanel`, which is gated
on `orchestrationMode === 'graph'` and would have hidden the Inspector from
ordinary sessions. It refreshes only while visible: a hidden panel re-projecting
a long session is a cost with no reader.

Tests: 3 for the IPC read (ledger join, undecodable record, non-canonical run
events), 5 for the panel model, and the workbar persistence whitelist — which
had no coverage at all, so a tab that renders but is not listed there would
silently revert to Tasks after a restart.

`@maka/core` 703/703, session-trace projection 16/16, desktop main targeted
suites green; `check-a11y`, `check-console`, `check-copy`, `check-dead-css` and
`format:check` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Result<T>` is `{ ok: true; data: T }`, not `{ value }`. The renderer typecheck
caught it; I could not, because `@maka/ui` does not build in this worktree, so
`tsconfig.renderer.json` never ran here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ARE404

ARE404 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

CI triage — one of the three failures was mine, and it's fixed in a5566826.

Mine: typecheckResult<T> is { ok: true; data: T } and I read result.value. I couldn't catch it locally: @maka/ui doesn't build in my worktree, so tsconfig.renderer.json never runs there. Fixed.

Not mine — test_workspaces SQLite migration suites. The same tests fail on main in #1994's own run (30814497891): "migrates legacy claims conservatively as already executing", "SQLite agent graph intent claims", "migrates version 11 wake attempts without losing correlation". This PR touches no storage. They surface here rather than on later main pushes because touching packages/core makes ci-test-plan.mjs select every workspace, while a UI-only push does not.

Not mine — e2e_shard (2/2). sidebar-navigation.spec.ts:72 "project rename owns Enter without toggling the project disclosure", failing identically on main @ ba744509 (30817795817) at the same line. This PR touches the right-rail workbar, not sidebar navigation.

main has been red since 12:39 today, so a fully green run here isn't available until those land. Happy to rebase once they do.

@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.

Review findings (2 independent deepseek-v4-flash perspectives)

The inspector tab is clean and minimal overall (3 solid IPC tests + 5 panel-model tests, correct data sources, no new store/permission surface), but there is one P2 that needs a decision.

P2 — useSessionTrace documents event-stream refresh that is not implemented; a live session's trace goes stale while the tab is open

  • Predicted failure: a user opens the Inspector tab on a running session; new turns/steps/costs never appear — the panel is a snapshot frozen at activation time until the user switches away and back or clicks Retry.
  • Evidence: apps/desktop/src/renderer/use-session-trace.ts (new) — the doc comment says "Reloads on the session's own event stream rather than on a timer… the moment worth re-reading is when one of them gained an event. Only refreshes while the panel is visible", but the effect deps are [active, load, sessionId] with stable load deps — there is no subscription of any kind. The sibling use-session-tasks.ts subscribes via window.maka.tasks.subscribeChanges, and window.maka.sessions.subscribeEvents(sessionId, …) exists in preload (preload.ts:291) as a ready seam. The PR description's "The panel refreshes only while visible" implies refresh-while-visible is intended.
  • Fix (pick one): subscribe to session events while active (debounced, refresh on new events), or explicitly scope this as activation-only — and correct the hook comment + PR description to match.
  • Suggested verification: a hook test with a fake event emitter proving reload-on-event (fails today); or an e2e with the fake backend showing a new step appear while the tab is open.

P3 (non-blocking)

  1. Every activation re-reads the entire session unbounded (inspector-ipc-main.ts Promise.all(runs.map(readRunEvents)) + renderer renders all turns without virtualization) — large sessions will stall the UI; consider incremental append.
  2. Error and empty states render simultaneously when readRunEvents throws generically ("Could not read the trace" + "Nothing to trace in this session yet") — hide the empty-state copy on error.
  3. emptyTotals() duplicates core's already-exported emptyTraceTotals() (@maka/core/session-trace) — import it instead.
  4. No component/story/e2e coverage for the panel's interactive states (retry, empty, coverage notice, aria-busy) — per AGENTS.md, add a story variant and extend session-workbar.spec.ts with one inspector journey.
  5. Test fixtures use as never casts that bypass shape validation; registerInspectorIpc's error wrapper has no behavior test.

CI note: the red jobs (storage migration suite, e2e/sidebar-navigation timeout, attachment.spec.ts geometry flake) reproduce on clean local main and are not attributable to this PR; rebase onto a green main before merging.

Gate: PASS (no P0/P1). P2 must be handled (fix or explicit deferral with reason) before this is done.

Review on apache#2018. The P2 was right, and it is the same failure I have now made
four times in this track: the hook's comment described refreshing on the
session's event stream and nothing subscribed to anything. A user watching a
running turn would have seen a snapshot frozen at the moment they opened the
tab.

Implemented rather than documented away, because the seam already existed
(`sessions.subscribeEvents`) and a trace panel that silently stops tracking is
worse than no panel.

The policy is its own pure module so it is testable without a DOM, which is why
the claim can now be checked instead of asserted:

- Only events that can change a ledger trigger a read. A streaming turn emits
  text and tool-output deltas continuously; re-projecting a whole session per
  delta would spend the session's own latency budget watching it.
- A burst coalesces into one read. A finishing turn emits `tool_result`,
  `token_usage` and `complete` within milliseconds — each alone justifies a
  re-read, three do not.
- The subscription and any scheduled read are dropped when the panel hides, so
  "refreshes only while visible" is enforced rather than described.

Also from the review: the empty state no longer renders beside an error ("nothing
to trace" is a claim about the session, and a failed read cannot make it); the
panel model imports core's `emptyTraceTotals` instead of redefining it; the
fixtures use `satisfies TraceStep` instead of `as never`, which is what let a
malformed step through in the first place; and `registerInspectorIpc` has a
behaviour test proving a failed read answers as a typed `Result` rather than a
rejected invoke.

Deferred with reasons, per the review's own framing: unbounded per-activation
reads and timeline virtualization (a real limit, but it wants a paging contract
rather than a patch), and story/e2e coverage of the panel's interactive states.

19 desktop inspector tests green; `check-a11y`, `check-dead-css`, `check-console`
and `format:check` clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ARE404

ARE404 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

P2 was right, and it's the same failure I've now made four times in this track: the comment described a subscription and nothing subscribed. Implemented rather than documented away — the seam already existed, and a trace panel that silently stops tracking a running session is worse than no panel. 93df6916.

The refresh policy is its own pure module, so the claim is checkable instead of asserted:

  • Only ledger-changing events trigger a read. A streaming turn emits text and tool-output deltas continuously; re-projecting a whole session per delta would spend the session's own latency budget watching it. tool_start, tool_result, token_usage, provider_retry, error, complete, abort do; deltas don't.
  • A burst coalesces into one read. A finishing turn emits tool_result, token_usage and complete within milliseconds — each alone justifies a re-read, three don't. Restart-not-stack, because the last event of the burst is the state the reader wants.
  • Hiding the panel drops both the subscription and any scheduled read, so "refreshes only while visible" is enforced rather than described.

Four tests with an injected timer, including the one you asked for — a burst producing exactly one read, and a cancel that lets a fired timer produce none.

P3, taken: the empty state no longer renders beside an error (2) — "nothing to trace" is a claim about the session and a failed read cannot make it; the panel model imports core's emptyTraceTotals (3); fixtures use satisfies TraceStep instead of as never (5), which is precisely what let a malformed step through; and registerInspectorIpc has a behaviour test proving a failed read answers as a typed Result rather than a rejected invoke (5).

P3 deferred, with reasons: unbounded per-activation reads and timeline virtualization (1) — a real limit, and the fix is a paging contract over the trace rather than a patch here, so I'd rather do it deliberately than half. Story and e2e coverage of the interactive states (4) — worth doing; flagging that session-workbar.spec.ts currently can't reach a session with canonical records under the fake backend, so an inspector journey needs a fixture first. Happy to take either now if you'd rather they land with this.

On the CI note — agreed, and thanks for confirming independently. I'd triaged the same three: the typecheck failure was mine (Result is { data }, not { value } — my worktree can't build @maka/ui, so the renderer typecheck never runs locally), and the storage and sidebar failures reproduce on main. I'll rebase once main is green.

@Astro-Han

Copy link
Copy Markdown
Contributor

Review — approve with comments

Thanks for the reworked refresh — the prior P2 is genuinely resolved: the subscription is real (visible-only subscribe, cleanup drops both the subscription and any scheduled read), and the coalescer policy is implemented and tested with injected timers. Re-ran the suites at 93df6916d from a few independent perspectives: desktop 18/18, projection 16/16, and main/preload/renderer typechecks clean.

Everything below is suggestions, nothing blocking.

Worth considering:

  1. A single unreadable run fails the whole trace. Promise.all(runs.map(readRunEvents)) (inspector-ipc-main.ts:46-48) has no per-run catch; the only try/catch is the per-event decode (:52-58). A rejecting run — corrupt row, or events whose run header is missing (agent-run-store.ts:596-613) — turns the whole inspector:trace into INSPECTOR_TRACE_FAILED, and retry hits the same row forever. This sits one level above the "counted, not dropped" principle this PR argues for: a read failure is just another way a record can be unreadable. The codebase already has the pattern — usage repair wraps each run (model-call-ledger.ts:288-305), context-diagnostics degrades instead of hard-failing. Folding a failed run into the unreadable count is ~5 lines.

  2. The hook deserves its regression test. The original P2 was precisely a doc-vs-implementation drift in useSessionTrace, and the fix landed without the test that would have caught it. The repo idiom exists (ui-render-memo-boundary-contract.test.ts renders hooks via createRoot+act), so a fake-emitter test — subscribe only while active, one read per burst, no reads after hide, stale revisions discarded — is cheap and pins the class that already bit once.

  3. empty and a detectable gap can render together. When every record of a session fails to decode (activity=0, unreadable>0'partial'), the panel shows "Nothing to trace in this session yet" beside "N unreadable records" — and unreadable spend is exactly the case this PR wants surfaced, not hidden. One line in the model fixes it: empty: turns.length === 0 && coverage === undefined.

Nits:

  • The IPC test titled "joins the AgentRun stream with the session runtime events" doesn't prove the join — with readSessionRuntimeEvents returning [] all six assertions still hold. One assertion that only the runtime-event ledger can satisfy would pin it.
  • The new resolveCoverage branches are pinned by that one IPC test alone; the projection suite has no unreadableRecords reference.
  • Switching tabs away and back re-blanks the timeline (load(sessionId, false) on every activation) — slightly inconsistent with the file's own preserve-trace rationale. Intentional?
  • InspectorCoverageNotice.turnsMissing/turnsShort are computed and tested but never rendered — either show them or drop them.
  • recovered: ${disposition} is unlocalized English in the zh UI (defensible for a technical inspector, flagging only).
  • PR body counts are now stale (4 IPC tests, 34 targeted).

Gate: approve with comments. No P0/P1; happy to help land any of the above.

@Astro-Han
Astro-Han merged commit 1a508c3 into apache:main Aug 3, 2026
11 checks passed
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 4, 2026
The three substantive suggestions from apache#2018's approving review, plus its nits.

**One unreadable run no longer fails the whole trace.** `readRunEvents` had no
per-run catch, so a corrupt row or a run whose header went missing turned every
`inspector:trace` into `INSPECTOR_TRACE_FAILED` — and retry hit the same row
forever. A read failure is just another way a record can be unreadable, which is
the rule this projection is built on; a failed run now folds into
`unreadableRecords` as one counted gap rather than a guess at its size.

**The hook has the regression test the original defect deserved.** The P2 in the
last round was a doc comment describing a subscription that did not exist, and
the fix landed without a test that could catch that class. Four now render the
hook for real: subscribe only while active, unsubscribe on hide, one read per
closing burst, no read for streaming deltas, and no read at all after the panel
hides. Verified they fail with the subscription removed.

Two things made that possible. The fake DOM in
`ui-render-memo-boundary-contract.test.ts` moved to `fake-dom.ts` so a second
suite can render without duplicating it — I had wrongly concluded this repo had
no hook-test idiom, having searched only for `renderHook`. And `useSessionTrace`
now takes its copy as an argument instead of reading the locale context, so the
one hook whose comment outran its code is renderable without the UI package
behind it.

**A reported gap is never an empty session.** With every record undecodable the
panel showed "Nothing to trace in this session yet" beside "N unreadable
records" — opposite claims, and the second is exactly what this surface exists
to show.

Nits: the IPC join test now asserts something only the runtime-event ledger can
satisfy (it passed with that ledger empty); the projection suite pins
`unreadableRecords`, which only the IPC test referenced; re-activation preserves
the timeline rather than blanking it, matching the file's own rationale; the
coverage notice renders the turn counts it computes; and the recovery label is
localized instead of leaking English into the zh UI.

Deferred, unchanged: unbounded per-activation reads and timeline virtualization
want a paging contract over the trace, not a patch here.

25 desktop inspector tests, `@maka/core` 755/755, projection 18/18; a11y,
dead-css, copy, console and format checks clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Astro-Han pushed a commit that referenced this pull request Aug 4, 2026
* fix(inspector): close the follow-ups left on the trace panel

The three substantive suggestions from #2018's approving review, plus its nits.

**One unreadable run no longer fails the whole trace.** `readRunEvents` had no
per-run catch, so a corrupt row or a run whose header went missing turned every
`inspector:trace` into `INSPECTOR_TRACE_FAILED` — and retry hit the same row
forever. A read failure is just another way a record can be unreadable, which is
the rule this projection is built on; a failed run now folds into
`unreadableRecords` as one counted gap rather than a guess at its size.

**The hook has the regression test the original defect deserved.** The P2 in the
last round was a doc comment describing a subscription that did not exist, and
the fix landed without a test that could catch that class. Four now render the
hook for real: subscribe only while active, unsubscribe on hide, one read per
closing burst, no read for streaming deltas, and no read at all after the panel
hides. Verified they fail with the subscription removed.

Two things made that possible. The fake DOM in
`ui-render-memo-boundary-contract.test.ts` moved to `fake-dom.ts` so a second
suite can render without duplicating it — I had wrongly concluded this repo had
no hook-test idiom, having searched only for `renderHook`. And `useSessionTrace`
now takes its copy as an argument instead of reading the locale context, so the
one hook whose comment outran its code is renderable without the UI package
behind it.

**A reported gap is never an empty session.** With every record undecodable the
panel showed "Nothing to trace in this session yet" beside "N unreadable
records" — opposite claims, and the second is exactly what this surface exists
to show.

Nits: the IPC join test now asserts something only the runtime-event ledger can
satisfy (it passed with that ledger empty); the projection suite pins
`unreadableRecords`, which only the IPC test referenced; re-activation preserves
the timeline rather than blanking it, matching the file's own rationale; the
coverage notice renders the turn counts it computes; and the recovery label is
localized instead of leaking English into the zh UI.

Deferred, unchanged: unbounded per-activation reads and timeline virtualization
want a paging contract over the trace, not a patch here.

25 desktop inspector tests, `@maka/core` 755/755, projection 18/18; a11y,
dead-css, copy, console and format checks clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(inspector): narrow the unreadable-run claim to what the code does

Second review round on #2041. The P2 is the same shape as the defect this PR
set out to fix, one layer up — and my PR body claimed more than the code
delivered.

`readSessionTrace` catches per run, but the `Promise.all` above it does not.
`listSessionRunsForRecovery` (`agent-run-store.ts:333`) parses every header row
with no per-row tolerance, so one corrupt header still rejects the whole trace,
and retry hits the same row forever. Fixing that belongs in storage, where it
changes the contract every recovery caller depends on — a decision about
recovery, not about this read model. Stated in the code and in the PR body
rather than quietly half-fixed.

Docs corrected to match: `unreadableRecords` counts records that could not be
**read or decode**, and a whole unreadable run counting as one is documented as
a floor, since nothing is known about how many records it held.

Re-activation now has the test it lacked: reverting the preserve behaviour
passed the old suite unchanged, which made it an unpinned behavioural change.
The new one asserts that once a trace has arrived no later render goes back to
nothing, and it fails when the preservation is removed.

Housekeeping from the review: `preserveTrace` was dead at every call site and is
gone; the injected copy takes `UiLocale` instead of restating it;
`TRACE_REFRESH_DEBOUNCE_MS` is exported so the test's delay derives from it
rather than duplicating 400 as 450; the IPC test types its stub against
`IpcMain['handle']` instead of casting past it; and the extracted `FakeElement`
import is dropped from the suite that no longer uses it.

25 desktop inspector tests, projection 18/18; a11y, dead-css, copy, console and
format checks clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(inspector): import UiLocale from the package root

`@maka/core/ui-locale` is not in core's export map, so the desktop main build
could not resolve it. Every other renderer file imports the type from the
package root; this one now does too.

It typechecked locally because this worktree resolves the subpath through the
built `dist` regardless of the export map. CI builds from the map, which is the
honest surface — a second reminder that a local typecheck here is weaker
evidence than it looks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants