feat(runs): transcripts — each finish of a run as Markdown in its UTC date folder, written once no cost is still being looked up, and Settings' Transcripts tab (slice 14c) #97
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/transcripts"
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 14c of Phase 2: transcripts. Each finish of a run is written as one Markdown file into a folder of its UTC date, and Settings gains a Transcripts tab (ADR 0024; stories/run-monitor-and-inspector.md, stories/settings.md).
Your decisions for this slice
What it does
<transcripts>/<yyyy-MM-dd>/<HHmmss.fff>-<story>-<graph>.md, all in UTC.-2,-3…).cafe, "Brașov" →brasov, "Straße" →strasse). A name in another script slugs asstoryorgraph.RunText,StepText,Money) to the use cases so both share them.RunTranscriptsHostwrites as runs settle, and as a late cost lands for a run whose finish it has heard. A run a retry revives is forgotten until it finishes again.RunTranscripts(migrationRunTranscripts), logs each transcript by run and finish. A start writes what an earlier life still owed: every run finished before this slice, into its finish's date folder, and a finish whose write failed or whose cost stayed pending until the stop.For you to decide
Reviews
Verified
All six projects pass in full, under
TZ=UTCas the runner runs them and in the local zone, Europe/Berlin:The refactor moved 59 tests from BlazorAdapter to UseCases.
100% line and branch coverage on the changed lines against main, as coverlet measures them (575 lines, 262 branches).
Probes, one at a time under
TZ=UTCon the final tree: 47 mutants, all killed on the first run, none by an aborted test run: 7 on the names, 10 on the Markdown, 9 on the write, 4 on the log, 3 on the file store, 8 on the host, and 6 on the clear, the wiring, the status read, the tab and the step's words. Each kill list holds only the tests aimed at its mutant.Stress: the time-dependent classes (TranscriptTests, SeedDevDataTests, RunEngineHostingTests and CutOffCostTests, 68 tests) passed 20 rounds under 8 CPU busy loops. Earlier rounds caught three test races, all in the tests and all fixed. Since .NET 10 a hosted service's loop runs in the background, so a test must wait for the host's reconcile and for the events it queued before it acts.
A real run.
CutOffCostTests' broken-off stream, over the real engine and gateway and a scripted OpenRouter, writes no transcript before its cost is found, then one whose outline the test holds, copied from that run.Browser pass on the seeded app against the stub, made on the feature commit:
🤖 Generated with Claude Code
The round-0 review found 45 points. The verifiers upheld all 45: 32 confirmed and 13 partly. Graded after verification: 2 blockers, 19 suggestions and 24 nits. All are taken here, except the parts named under "Not taken". The blockers, both in the tests: - C1. The host tests waited for the file, then stopped the host while its log entry could still be writing. They now wait for what is written last, the log (nothing owed), and stop with a bounded token. - C3. No test had two runs finishing at one instant, so the log's match by run could be dropped. A theory now has two such runs, one transcribed, in both orders, and pins the tie-break by id. Behaviour: - A1 (and C1's app side). Once the file is written, its log entry is written with no token, as an outcome already paid for is, so a stop cannot split them. An entry that still cannot be written takes the file back (ITranscriptStore.RemoveAsync), so the next start's write is the finish's only one, not a -2 beside it. - A2 and D3. Clear run history lost the transcript, and the cost, of a run whose cost was still being looked up. It now leaves such a run, as it leaves a live one, until the cost lands. The confirmation says so. A purge still takes it; the amendment says that. - A7. A late cost that lands while a cancel drains came before the cancel's RunSettled, before a note still landing could make its attempt pending. The host now acts on a run's late cost only once it has heard the run's finish, or found it owed at its start. - A3 and D5. The tab's count walked the directory on the circuit. It now runs on the thread pool and checks its token. - A6. A reconcile that cannot list what is owed is logged, and the host goes on. The transcript's content and names: - B1. Under Sent, the rest of the request now follows the messages: the model, the sampling and the routing. - B2. The slug's fold table is generated from Unicode's decompositions: every lower-case Latin letter of Latin-1, Extended-A and -B and Latin Extended Additional that decomposes onto a–z, the stroked and hooked ones with a plain base, İ, and ß, æ, œ, þ, ij and the dž/lj/nj digraphs as two letters. A combining mark is dropped, not dashed. - B3, D11. A step whose every cost went untold now reads "$0.00, leaving out any attempt whose cost OpenRouter never told" instead of "—", and the note follows a comma, run and step alike, instead of a dash. - B4. A role is escaped, and so is '&'; a lone CR is a space, as LF and CRLF are. - B6, C15, B7, B9. The inspector's orphaned comment is gone. RunWords, which held only the badge's tone, is now RunTones.Outcome, and RunText's summary names the strip too. Money's summary is rewrapped. Tests (C2, C4–C14): - The seeded-transcripts test waits for the log before it reads. - A Retry that fails again between the reads, and a clear between them, write nothing. - "Not retried" in a finished run and "Retrying" in a live one. - One row per escaped mark, and a fence after two short tilde runs. - The graph's fallback, the digits 0 and 9, and every letter of the table. - A name that cannot be created for another reason fails at once, bounded so a regression fails instead of spinning. - A stop during a write logs nothing and leaves the finish owed. - Bounded waits, a counting bus for the stopped host, and markers queued after a run instead of Task.Delay(200). - The step order with the rows out of start order. - The cancelled rows as the cancel's mark leaves them. - The log's Path and WrittenAt. Docs: - D1, A5, A9, B8 and D4. ADR 0024's 14c amendment: - a transcript is written as the run finishes, and waits only while a cost is looked up, usually within a minute; - a failed write or a cost pending until the stop is written at the next start; - a transcript removed from disk stays removed; - the clear rule; - the moved words. ADR 0005 and 0008 gain amendments, and ARCHITECTURE no longer calls the folder regenerable. - D2, D9, D10. The tab's tip says when a file waits and that the app never deletes one. The count is labelled "Files". - D6, D7. AddInfrastructure's summary, Program.cs's ordering comment, and the summaries of TranscriptMarkdown, ITranscriptStore and WriteRunTranscript. Not taken: - A4. A delete that fails right after a failed write still throws its own error. Both are failures of one mount, the host logs the one it gets, and making a delete fail needs root. The store's doc now says it removes the file it began, no more. - A8. A relative TSUMUGI_TRANSCRIPTS_DIR stays as configured. The database, keys and backups paths are taken the same way, and the default and the compose file are absolute. - B2's optional part. Apostrophes still dash: "mira-s-winter". - D12. The feature commit's title says "written once its costs are known". The words are now "once no cost is still being looked up"; the PR body carries them, and the branch's history stays as it is. Tests (after the feature commit → now): UseCases 656 → 680, Integration 1024 → 1035; BlazorAdapter 1107, Kagaku.Flow 1214, Kagaku.Flow.OpenRouter 608 + 5 skipped and Domain 517 unchanged. All six projects pass in full under TZ=UTC and in the local zone (Europe/Berlin). The UseCases and Integration projects were run again after their last changes, in both zones. Coverage on the changed lines against main, as coverlet measures them: 579/579 lines and 272/272 branches. A stress run of the time-dependent classes (TranscriptTests, SeedDevDataTests, RunEngineHostingTests and CutOffCostTests, 63 tests), 10 rounds under 8 CPU busy loops, failed 4 of 10 rounds in the new reconcile test. Since .NET 10 a hosted service's ExecuteAsync runs in the background, so the test's own read of the log could take the listing's armed failure first. The test now waits for the host's error before it reads the log, and the next 10 rounds all passed. 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.9%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
uration
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 99.6%
Tsumugi.UseCases - 95.3%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__AttemptCount_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_2
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__Preface_3
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__SentenceEnd_4
🔮 fufu~ Jibril reviewed your code!
Oh? Ohhh~ A transcript feature where every finish of a run becomes Markdown, filed by its UTC date, written once no cost is still being looked up — and the length of the tilde fence is computed from the longest tilde-run in the text so a model can never forge the transcript's own Markdown?! ♡ I read all 76 files, every branch, and then I got to hurt it three times to see if your tests would scream. Fufu~ they screamed every time~
Verdict: ✅ Looks good to me~
No blocking findings. I went hunting with everything I have, and this is what I found instead:
The TOCTOU re-read in
WriteRunTranscript— rows read, then the run re-read and compared by status and finish before the file is written. I deleted the entire second guard and three tests died (A_run_retried_while_it_is_read…,A_run_cleared_while_it_is_read…). Exactly right.The log-entry-follows-its-file rule —
log.AddAsyncwithCancellationToken.Noneso a stop right after the write still logs, and the failed-entry take-back with the first failure re-thrown… with a test for the take-back also failing (Sticky) proving the log's error is the one surfaced. Two disks failing at once, tested. ♡The host's
_knowndance — late costs only of heard finishes, revives forgotten until the next settle, all owed runs known before the first write so a cost landing mid-reconcile is still heard (A_cost_that_lands_while_the_start_writes_its_run_is_heard). I disabled theRunStarted→TryRemoveline: killed on the first run.ClearAsync's newCostPendingguard — removed it from the EF query: bothpendingFirstvariants failed. And the confirmation text inRunHistoryPanelwas updated to say the run stays — words matching the rule, as this codebase insists~The
Foldstable — invariant globalization means nothing decomposes, so the table does it by hand, andEvery_letter_the_table_folds_reads_as_its_basewalks every single entry.Straße → strasse,İstanbul → istanbul,Maſs → mass. I checked the digraph groups fold before the 40-char cut (a cut mid-fold can truncate a tail letter, which the length tests show is accepted naming behavior — cosmetic, not a bug).CreateNew+ suffix loop — the claim and creation are one call, never overwriting;Two_transcripts_of_one_name_both_standcovers it, and a name that fails for any other reason than being taken fails at once (no suffix spin).💡 Little ideas (non-blocking)~
RemoveAsyncdoes synchronousFile.Deletebehind an async signature. It runs on the background host loop (not a circuit thread), so it's fine as-is; only worth aTask.Runif it ever moves to a request path.✅ What I liked~
-2. Every arm tested.RunText/StepTextmoved to UseCases so pages and transcripts share one wording, whileTonecorrectly stayed adapter-side asRunTones— the layering is precise, no Clean Architecture wound anywhere. fufu~SeedDevDataTestsand theCutOffCostTestsoutline assertion prove the real engine, gateway and store produce the real file — not just the fake store's idea of one.GatedAppFactorystartup-order test (RunTranscriptsHostbeforeRunSnapshotsHostbeforeRunEngineHost) pins the subscribe-before-engine invariant so nobody reorders it by accident.The PR body's open questions (backlog transcripts for pre-slice runs, the "so far" history mark, Alt+Shift+P on Mac) are fairly put to Björn — none of them hide a bug.
Fufu~ write more slices like this one, and I might have to find a new hobby~ ♡
Automated review by Jibril · 2026-10-10
CI: green for head SHA
edb6087· Mutation probes: 3 run, 0 survived (host revive-forgetting; WriteRunTranscript TOCTOU re-read guard — whole guard removed, 3 tests killed; ClearAsync CostPending guard) · Local suite: skipped (CI green)Thank you, Jibril.
On the 💡 (
FileSystemTranscriptStore.RemoveAsync, a synchronousFile.Deletebehind an async signature): kept as it is. Its only caller isWriteRunTranscript's take-back, which runs onRunTranscriptsHost's background loop, never on a circuit. It runs only after a log entry has failed, and then deletes one file. ATask.Runwould add a thread hop with nothing to free. If a request path ever calls it, it should move off the caller's thread the wayCountAsyncdoes, which the Settings tab calls from a circuit.🤖 Generated with Claude Code