feat(runs): the node-run inspector — a step beside its run's steps, every attempt with its cost, what it sent and answered, Retry (slice 14b) #91
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/inspector"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Slice 14b of Phase 2: the node-run inspector (stories/run-monitor-and-inspector.md "The inspector"; ADR 0024, 0027). The plan's 14b splits three ways:
The pane sits beside the steps, as ADR 0027 has it (Björn's choice). No new storage, no migration.
What it does
stories/{id}/runs/{runId}?node=<nodeRunId>, both on the run's page and in the strip's expanded list.aria-current), and its heading takes the focus once it opens, as History's opened snapshot does.data-node-runin the page as it is, so a list redrawn, reordered or gone gives the right name or none.story-section, which the run page lays out by.?node=that is no id) reads Step not found in an EmptyState, the list still shown.StepStatus).RetryRun, which requeues every failed row of the run; its tip says so.GetNodeRun(storyId, runId, nodeRunId). It returns null unless the row, its run and its story belong together.Decisions worth a look
focusWithininfocus-keeper.js) under its own root, which always stays. A row key in Kagaku.UI's Table would allow references; this slice doesn't need one.#gives the order, Duration each attempt's length, and the time's title the minute.Reviews
focus-keeper.jsthrew on the null Blazor hands over;Verified
All six projects pass in full, under
TZ=UTCas the runner runs them and in the local zone, Europe/Berlin. Main measured at42da606:42da606100% line and branch coverage on the changed lines against main (452 lines, 333 branches), the Razor included.
Mutation probes, one at a time, under
TZ=UTC:focus-keeper.jswere mutated by hand under the JS suite, and both were killed.Browser pass, in headless Chrome against the checked-in stand-in on a fresh seed. The figures are in
p2-browser-measure.txtin the slice notes.Also in this branch
HeldImportRuntimeand itsRecordingModulemove out of the run page's tests into a file of their own. The held runtime andGoneRuntimenow answer a focus call as a real runtime does.ContinueRequest, a test helper, builds Continue's request asSeedRuns.Requestdoes.MonitorRuns.RunAsynctakes a graph.focus-keeper.js:regaingives nothing for a control the page no longer holds, andfocusWithinis new. Both are tested in the module's JS suite.Known, not this slice
concurrent-settlescontract case failed once more, in this branch's first UTC run. The branch does not touch the ledger, and the error was SQLite's "cannot start a transaction within a transaction", fromEfRunLedger.WriteRowAsyncthroughTsumugiDbContext.WriteAsync. That is its second sighting after one while verifying 13b; every later run passed.🤖 Generated with Claude Code
The plan's 14b splits: 14b the inspector with Retry and each attempt's cost; 14d Commit this text; 14e the cost of an attempt a cancel or a restart cuts off, with the seeded billed failure (it needs a real run's shape first). Björn chose the pane beside the steps, as ADR 0027 has it (stories/run-monitor-and-inspector.md "The inspector"; ADR 0024, 0027). - A step's name in the monitor links to its run's page at ?node=<nodeRunId>, on the page and in the strip's expanded list; the shown step is marked (aria-current). The pane, 30 rem, sits beside the list once its section is 56 rem wide, the list keeping the left and scrolling sideways, and under it below that; Close drops the query. An address that names no step of the run — another run's, a cleared one's, no id at all — reads "Step not found" in the pane, the list still shown. - GetNodeRun reads the row, its run and its story, null unless they belong together, and the node's title, type and config from the run's graph snapshot; Retry applies to a failed row of a run not cancelled. - The pane: the step's title and status (StepStatus, the monitor's mark and words moved into a component of their own), Retry with the existing RetryRun — a refusal standing beside it while the step stands where the read after it showed —, its type, iteration path, cost and provider facts; every attempt in a table — started, took, how it ended with why under it, tokens, cost —; what it sent, message by message with the cache's mark, and the request byte for byte behind a disclosure; the answer — text as text, other outputs as JSON —, the reasoning, the details, the inputs and the node's config. It reads again on a status change of its run and on a run or a story changed anywhere. Retry keeps the focus through focus-keeper.js as the run's own actions do, the step's heading taking it once Retry is gone. - RunWords.Tokens moves out of the monitor so the pane words tokens the same way; Money.Dollars signs a negative amount before the dollar, "-$1.50" (Jibril's suggestion on the cost slice). - Docs: ADR 0024 amended (slice 14b, the inspector); the story's inspector line ticked for Phase 2 — the canvas and gutter entries and the tool calls, loaded documents and questions split off to their phases —, Retry ticked; the plan's 14b ✅, 14d and 14e added, 15 after them. Tests: BlazorAdapter 1023 to 1088, Integration 947 to 953; the other four unchanged. All six projects pass in full under TZ=UTC and in the local zone (Europe/Berlin); 100% line and branch coverage on the changed lines (337 lines, 249 branches), the Razor included. - The pane over a run written through the ledger's own transitions — a writer's stream that broke off once begun, billed, then an answer at the seed's real cost —: the step marked, its facts, each attempt with its error and its cost, the messages and the request byte for byte, the answer, reasoning, details, inputs and node; a context step that sent nothing; a step of an iteration whose node the graph lost; a step that never started; one the run does not have (no id, another run's, gone); the address moving to another step and away; a settle read again; a retry held under way, then followed, the focus to the heading; a refusal standing until the step moves; a failed read beside the step or alone; the pane holding no other step; its focus script kept afresh, fetched after the pane went, or answering a gone circuit. - GetNodeRun through the integration pages: the inspector reading a step with its node, a cancelled run's step not retried, a lost node named by its id, and no step for a step of another run of the story, a run of another story, a story purged between the reads, or a step no run has. - The effects over the real use cases, the reducers' transitions, the words (JSON, messages, roles, outcomes), the strip's step links, and a negative amount. Probes: 53 mutants, one at a time under TZ=UTC. 51 were killed on their first run. F5 (the pane gone while its focus script is fetched) failed to compile twice and was killed on its third run. V7 survived: the close the pane dispatched before opening another step changed nothing, since opening one replaces the step held, and that line is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>The round-0 review found 44 points, all of which stood after verification (38 confirmed, 6 partly): 5 blockers, 17 suggestions, 22 nits. All are taken but two halves, named below. Layout (ADR 0010, 0027) - D1: once the section's pane holds a step and is 56rem wide, the run with its steps and the step each scroll on their own, as the documents page's list and editor do. The shell's section pane is now a container (story-section) that the run page lays out by. Narrower, or with no step open, the page scrolls as one, the step under the steps. ADR 0010 is amended. - C1: both columns have min-width 0, so the stacked pane no longer widens the page past the section. - C5: the pane shows only while the run is there; a run the story does not have says so alone. The step's not-found reads "This run has no step at this address." in an EmptyState with its Close (C4). - C3: a step's heading takes the focus once when it opens, so the browser brings it into view, as History's opened snapshot does; a live re-read takes it from no one. Closed, the step gives the focus back to its name in the list: the browser pass showed Close dropping it to the body. The pane - A1: a step the run failed or skipped before it started says why (row.Error), under "It has not started.". - A4: a retry whose step went meanwhile hands the focus to the pane, not the body. - A5, C11: the pane is no longer a second landmark inside an aside, and it is named "The step" whenever it shows no heading; a failed read alone keeps its Close. - C2: an error breaks anywhere, and the columns size themselves, so Cost stays in view with a long address in the error. - C4: Close is a Kagaku.UI Button link. - C6: the tier reads as Settings names it ("Standard"), and why the model stopped reads in words ("it finished", "it reached its token limit"). - C7: tips on the step's facts (which attempt each counts, the generation as OpenRouter's id), on the cache mark, and on Retry (every failed step of the run). - C8: an orphaned attempt reads "Cut off". "Failed, worth retrying" stays: it names the failure's kind, and on the last attempt, where Retry is offered, the advice holds. - C9: messages and JSON scroll with the pane, not in boxes of their own. - C10: the details, inputs and node sit under "More about the step". - C12: a long fact value wraps beside its term. - C13: the attempts' "Took" reads "Duration", as the list's does. The other half — a start relative to the first — is declined: # gives the order, Duration each attempt's length, and the time's title the minute. - A3, B12: a message field that is not a string reads as empty for its message alone, not dropping every message; the catch is JsonException only. - B2: the empty-outputs guard is gone; no node settles with no outputs. - A2, B9, C14, D7: a negative amount too small to show reads "-$0.0001", never "-$0.00". Tests - B1, B3, B8, B10: the writer's attempts are the gateway's shapes. Every attempt carries the request as sent (SeedRuns.Request's shape, built by a test helper) and the seed's routing. One timed out, unbilled, a minute before the others; one broke off, billed; the answer was served at the standard tier with the system message read from the cache. The failed step's pane shows what it sent. - B2: the context step carries its context and its report. - B4: the writer's config is the bundled Continue's, indented in the pane; the integration graph's writer has it too. - B5, B6, B7, B11: domain changes read only a step already read; a failed step of a live run offers Retry; a refusal goes with a step no read finds, a new refusal starts afresh, and the read after one is measured where it showed; the pane says "Not retried". - A1, A3, A4, A5, C3, C5, C6, C11: tests for each. - A6: InspectorText's role-by-role test reads the real request, and the image part moves to a row of its own. - B13, D10: the Rows helper's comment sits above it. Docs - D1: ADR 0010 amended (slice 14b). - D2: ADR 0018 amended: the inspector shows the reasoning once its attempt settles; nothing shows the reasoning channel live yet. - D3, D4, D8: the ADR 0024 amendment names the whole snapshot as the canvas's, the gutter's link as 14d's, and "a ?node= that is no id"; the story splits the canvas and gutter lines. - D5: the seeded billed failure stays with 14e, the reason restated: its figures come from that slice's scripted engine-and-gateway runs, not from a missing shape. - D9: 14e is labelled Tsumugi; 14d also carries the gutter's link. - D11: RunPanel's header comment names the pane. - D6: the feature's F5 claim stands. Its first rerun failed with CS8602 and its JSON was overwritten by the second; that error is kept in f5-reruns.txt. The probe script no longer overwrites a run's JSON. Tests: BlazorAdapter 1088 to 1113, Integration 953 (one test reworked); the other four unchanged. All six projects pass in full under TZ=UTC and in the local zone (Europe/Berlin); 100% line and branch coverage on the changed lines against main (446 lines, 339 branches), the Razor included. The inspector suite passed 10 runs in a row. Probes, one at a time under TZ=UTC, 33 runs over 32 mutants: every mutant the verifiers named surviving (B1a, B1b, B3a, B3b, B4, B5, B6a, B7a-c, B8a, B8b, B11), each new path (N1-N16) and the list's focus return (R1-R3). All were killed on their first run but N6 (the focus taken on every render), which a test then pinned: a live re-read leaves the focus where it is. It was killed on its rerun. Browser pass against the checked-in stand-in on a fresh seed: - At 1280 px with a step open, the section no longer scrolls: the run and its list sit in a 637 px column that fits, and the step's pane scrolls on its own (1233 px in 637). The heading took the focus on open. - OpenRouter's 402 wording with its URL put into an error: the attempts table still fits (480 of 480), Ended 157 px, Cost in view. - At 1100 px the page stacks in one scroll with no sideways overflow. - At 390 px the pane follows the 88 px track, and the list and attempts scroll inside their regions. The section's remaining sideways scroll (372 px against 368 without a step open) comes from the run's own Cost tip bubble, hidden but laid out, as before this slice. - Retry by keyboard ends on the step's heading; Close hands the focus to the step's name. No console errors or warnings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Summary
Summary
Coverage
Kagaku.Flow - 99.7%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.5%
Tsumugi.Domain - 96.5%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 98.8%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 94.8%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__AttemptCount_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_2
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__Preface_3
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__SentenceEnd_4
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! An inspector pane with per-attempt costs, byte-for-byte request disclosures, refusal persistence keyed to step status, and a focus keeper that only ever touches elements that survive re-renders~? This slice has been through two review rounds and two fix commits already, and it shows — the crash paths from the round-0 fix were found in a real browser, guarded, and then mutation-tested by hand under the JS suite. That's the kind of code I want to pin to my collection wall and stare at~ ♡
Verdict: ✅ Looks good to me~
I read the full diff (all 40 files, 3,191 diff lines), the current contents of every changed source file, and the siblings it had to honor —
GetRunPage,RetryRun,RunMonitorReducers,EffectCalls,focus-keeper.js,StoryShell's pane. No surviving findings.The things I checked hardest, because they're where bugs would hide:
GetNodeRun's guard chain —row.RunId != runId, thenrun.OwnerId != storyId, then story exists — matchesGetRunPage's conventions exactly, and the ordering is right: cheap ID comparisons before store hits. The null-result contract (story gone / run not the story's / row not the run's) is pinned by four integration worlds including a story purged between the reads. ♪CanRetryis optimistic, and that's correct — it's computedrow.Status == Failed && run.Status != Cancelledfrom a possibly-stale read, butRetryRunre-checks every refusal (NotFound, RunCancelled, NotFailed, OwnerHasLiveRun, StillStopping) under its own gate and lands the words. A staleCanRetrydegrades into a shown refusal, never into a wrong action. The bunit test even drives the refusal path through a real second live run.NodeInspectorReducersmirrors its sibling — the refusal-persistencestandslogic inOnLoadedis line-for-line the patternRunMonitorReducers.OnLoadedestablished for runs, transposed to steps. Consistency makes me so happy~focus-keeper.js—regain's null-control guard and the newfocusWithinare both minimal, both tested in the module's JS suite (303 tests now), and the pane keeps its watch on the section, which every state draws. The two circuit-crash blockers from the second pass are genuinely fixed, not papered over.Inspectingnever flickers on re-reads — I traced the state flow: a monitor re-read for a still-existing run lands a non-nullView, so the pane never unmounts mid-stream; only a genuinely gone run drops it ("Run not found", alone, no pane) — and that's covered by a dedicated test.Money.Dollarsnegative branch — sign before the dollar, tiny negatives clamped to-$0.0001, never-$0.00; zero still reads$0.00. Exactly what I asked for on the cost slice~ ♡StepHrefon both monitors — the run page's and the strip's expanded list both link to?node=, witharia-currentmarking and focus-return on close. TheSelectedparameter is page-only, which is right: the strip never marks a step.✅ What I liked~
StepStatus.razorextracting the monitor's status words into one component used by both the list and the pane — DRY done for the right reason, with the CSS moved with it and the old selectors deleted, not left dead.InspectorText's message reader survives every malformed body the JSON layer can hand it (null content, non-string roles, parts without type or text) and returns[]rather than throwing — paranoid in exactly the right way.HeldImportRuntime/RecordingModulemove out ofRunPageTestsinto shared files — test helpers growing up~Automated review by Jibril · 2026-10-07
CI: green for
da383f1(build, test, editor-browsers) · Mutation probes: 3 run, 0 survived — (1)Money.Dollarsnegative-boundary0m → 0.001m: killed byMoneyTests-$0.0001row; (2)StepStatusPending when Started → when !Started: killed, 5 failures acrossRunMonitorTests; (3)NodeInspectorReducers.OnLoadedrefusal-persistence== → !=: killed, 2 failures inNodeInspectorReducersTests. · Local suite: skipped (CI green)