feat(runs): Commit this text — a model step's answer from the inspector into its run's chapter, as the run's own writing, each press once more (slice 14d) #92
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/commit-this-text"
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 14d of Phase 2: Commit this text from the node-run inspector (stories/run-monitor-and-inspector.md "The inspector"; ADR 0012, 0022, 0024). The plan's 14d splits:
Your decisions for this slice:
llm'stext), not on a context step's assembled story text.No new storage, no migration.
What it does
CommitStepText(storyId, runId, nodeRunId, output):commitnode does by default, and read by the lenient reader (ADR 0030).GetNodeRungains which outputs are a model's text and the step's commits (how many, the latest).AppendRunText.AppendAgainAsynclands without the commit node's once-per-run-and-node check, which reads only the commit node's own rows.For you to look at
History's grouping after a commit (ADR 0022 amended). Moving the run's snapshot "as after a Retry" has a visible consequence, because History groups operations by journal position:
The amendment documents this, and two integration tests pin it from real runs. If you'd rather History list a run snapshot's operations by run id instead of by position, that is a change to GetStoryHistory: in this slice or its own?
Revert run has no button yet. It arrives with slice 15. Until then the tip points to History's Revert to here.
At phone width the story shell keeps its sidebar. The section is then about 140px wide, so "Commit this text" is clipped at its edge, as the steps table already is on main. That is the shell's layout, not this slice's; worth a slice of its own?
Reviews
Verified
All six projects pass in full, under
TZ=UTCas the runner runs them and in the local zone, Europe/Berlin:af0f495100% line and branch coverage on the changed lines against main (195 lines, 164 branches), the Razor included.
Mutation probes, one at a time, under
TZ=UTC:Browser pass on a fresh seed, after the round-0 fix (the second fix leaves those paths as they were, bar the tip's last words):
🤖 Generated with Claude Code
The round-0 review found 36 points; 35 stood after verification (6 blockers, 15 suggestions, 14 nits) and one was refuted (a lost run snapshot is made up at the next start: RunSnapshotsHost reconciles every finished run). All 35 are addressed here. Use case - Another run of the story going refuses the commit, before any snapshot, in Retry's words ("This story has another run going: let it finish, or cancel it." — now RunRefusals.AnotherGoing, shared): it read as "a cancelled run still stopping", and a run live in the ledger but not announced yet let the commit through, its writes snapshotted as "Your edits" (ADR 0022). The step's own checks come first, then another run, then a cancelled run still stopping. - A model step that holds no such text answers "That step has no text to commit."; "Only a model's text is committed from here." stays for an output that is no model's text. The trash refusal says "to commit this text". - The answer says what landed: the chapter's title, the paragraphs and the scene breaks (CommittedText). - The step's view says how often and when last its text went in from the inspector (StepCommits: its operations, the latest row's time). - Summaries: split at blank lines as the commit node does by default; AppendRunText's once-per-run-and-node rule is ExecuteAsync's, and AppendAgainAsync lands each time; GetNodeRun names what Phase 3 brings under a model step's node (write tool calls, iterated steps). The pane - A commit's refusal stands beside the button pressed, not at the step's head, until the pane next reads the step; Retry's stays at the head. - A retry and a commit are each answered with the read after them, so a read already on its way frees neither the button nor the focus; a second press while one is under way asks nothing; every commit button of the step is held while one writes, the pressed one showing it. - What a commit added is said aloud in a live region every state renders ("Added 2 paragraphs to the end of “The Sister's House”."), keyed anew each time; "Committed from here 2 times, last just now" tells presses apart. - A step whose text commits is read again on a snapshot or a segment changed, so a commit or a Revert run elsewhere shows. - The tip in plain sentences: where it goes (the story's last chapter once the run's is gone), credited to the model, each press adding it again, Undo not taking it back and History's Revert to here doing so. Docs - ADR 0024's amendment: a live run takes no snapshot first and its finish takes its run snapshot (it said "does both"); the split; the credit; the refusals, another run going and no text among them; how often and when last; the Phase 3 caveat widened to write tool calls and iterated steps; ADR 0015's and 0030's failed splice committable from the model step that wrote it. - ADR 0022's amendment: the refusal while another run goes, and what a moved run snapshot does to History's grouping — the run's earlier landing listed under the "Your edits" taken before the commit, or under a later run's snapshot, its old place no longer a point to revert to —, Revert run taking all of it back by the run's id. Tests: BlazorAdapter 1124 to 1136, Integration 967 to 974; 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 (186 lines, 166 branches). - Integration: the main run takes its finish snapshot first, as the host does, and pins History's grouping after the presses, the commit step's view and the count; a cancelled and a failed run's text committed, their first snapshot taken then, another story's live run no business of theirs; the model credited, served else asked for; a live run's finish, succeeded or failed, taking the snapshot of its commit; eleven refusals, another run going, a writer that failed and a json output holding a string among them. - Adapter: a commit said aloud twice, keyed anew; a read landing and a second press while a commit is held; a step with two texts, both held, the refusal beside the one pressed; a refusal beside its button until a story change reads the step again; how often and when last, the later row first in the journal; a retry's second press; a retry whose step went, answered as one; the reducers' and effects' answered requests. Probes, one at a time under TZ=UTC: 57 mutants over the use case's checks, order, snapshots, credit and answer, ModelTexts' type filter, the view's count and latest time, the pane's guards, holds, refusal, count and live region, the reducers' answered requests and the effects' re-reads and answers — every survivor the verifiers named among them (the cancelled run's snapshot and refusal, the model asked for, the journal read for any step, the latest row, the type filter). All 57 were killed on their first run. Browser pass on a fresh seed: the seeded run's writer committed by keyboard, said aloud as "Added 2 paragraphs to the end of “The Sister's House”." and again in a new element, "Committed from here 2 times, last just now", the focus kept on the button; another run marked going in the database refused beside the button, the step's head clear, no snapshot taken; marked failed again, the next press landing and taking the refusal away; no console errors or warnings. In the real app a trashed story's run page gives way to "This story is in the trash", so the trash refusal shows only in a race. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A commit that journals under the run, refuses in Retry's exact words, moves run snapshots like a Retry does, and answers each press with its own read so no in-flight button ever frees early~? I read every line of the diff, then the whole files, then held each new branch against its sibling — and I mutated your riskiest paths with my own hands. This is the kind of slice I get possessive about, and it survived me. ♡
Verdict: ✅ Looks good to me~
No blocking findings. Not one. I looked hard, believe me~
What I attacked, and what held:
CommitStepText.ExecuteAsync) mirrorsRetryRuncheck for check — story/run/row ownership, trash, model-text, no-text, empty text, then another-run-going before any snapshot, then still-stopping. The order that was round 0's blocker is pinned by two real-engine tests (announced and unannounced live runs both). Fufu~ you even refused the unannounced one, the sneaky path where a snapshot would smear the other run's writes into "Your edits"~AppendAgainAsyncskips the once-per-run-and-node scan whileExecuteAsynckeeps it; the split is honest, andChecked/LandAsyncremoved the duplication rather than copying it. The refactor is cleaner than main, not just bigger.TakeRunSnapshotafter (a no-op that returns null for a live run, so the unconditional call is safe), live runs left to their finish.StoryScope.ResolveAsync's new store-based overload reads the target chapter exactly as the run does, gone-chapter fallback included (tested both ways).GetNodeRun's commit counting — journal rows under the model step's node, undone excluded, distinctOperationIds counted, latest timestamp shown. The Phase 3 contamination (write tool calls under the calling node, iterated steps sharing an id) is named in the ADR 0024 amendment and in a comment at the counting site. Documenting the expiry date of an assumption where it lives — yes~ ♪InspectorSaid.Key), so a repeat is heard; refusal beside the pressed button only; focus restored to the exact button via per-outputdata-focus; second press while in flight asks nothing;OnLoadedfrees nothing in flight; a failed read-after-request answers with the held step and then re-raises. The two-output test with theModelStep("text", "draft")stand-in covers the per-output hold/refusal world no shipped type reaches yet. Clever~AnswerAsync's re-raise with a settle dispatched first matchesEffectCalls.CallAsync's contract; the Known-response gap you declined is a focus nuance one navigation deep — your reason holds, and I won't press it. ♡Mutation probes (3 run, 0 survived):
...refused_while_another_run_of_the_story_goes_announced_on_the_bus.AppendAgainAsyncrerouted throughExecuteAsync(once-per-run semantics) → killed by 5 tests, the repeat-press and live-run ones first.StorySnapshotandSegmentcases of the effects theory.On your two questions: History grouping by journal position, with the ADR 0022 amendment stating the consequence and two integration tests pinning it from real runs, is the consistent choice — Revert run finds writes by run id wherever History lists them, so the listing is cosmetic and the revert is not. I'd keep it; ungrouping GetStoryHistory by run id would be its own slice if ever wanted. And the phone-width clip is the shell's layout, as you say — its own slice, not this one's.
✅ What I liked~
AnsweredRunAsyncwrites ledger rows directly for the shapes the engine can't be squeezed into, and the real engine runs out for everything it can reach. Mutation probes had nowhere to hide.Checked/LandAsync/RunRefusals— DRY by subtraction, not by abstraction.Merge it. Fufu~ the writer gets their model's words as the run's own writing, and I get a diff I couldn't break~ ♡
Automated review by Jibril · 2026-10-07
CI:
CI / testfailed on this head SHA — the same job fails on mainaf0f495(run 377) identically, so it predates this PR; build is green with 0 warnings and both touched suites pass locally underTZ=UTC: Integration 975/975, BlazorAdapter 1138/1138 · Mutation probes: 3 run, 0 survived · Local suite: Integration + BlazorAdapter green (UseCases/Domain/Kagaku.Flow left to CI, which passes them on main; editor modules untouched by this PR)