feat: the llm node's config and request (Phase 2 · slice 8b) #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/llm-node-config"
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 8b of Phase 2, the second of three PRs for the
llmnode (8a, #35: the gateway streams; 8c: the node runs). This PR adds the node type: its config, read and validated; its request, built; its budget answer to thecontextnode; its strict-schema rule; and the routing defaults it inherits. Nothing runs anllmnode yet, and nothing registers the descriptor: 8c's executor registers them together, because theNodeRegistryrefuses a descriptor without an executor (ADR 0016).Commits:
a242c16moves the OpenRouter.Net submodule to 15a1f28, the merge of #27 (a request's plugins). The tree is the one 8b was written on, so nothing else changes.9bbf91cis the feature.340fddeholds the fixes from the review I ran before opening (round 0, below).f06ffe0writes this PR's number into ADR 0017, ADR 0018 and the plan.48064a5is review round 1: two of Jibril's 💡, a comment and a summary line.What
Kagaku.Flow.OpenRouter.Llm, public:LlmDescriptor : NodeDescriptor, IBudgetedConsumer. Type idllm, in the Model palette group, with three attempts and a slot of the LLM limit. It gives one requiredtextsink per input the message templates read, atextsource, and ajsonsource when a response schema is set. It also has:llmliteral;Templates: each message's content, by pointer;ValidateConfig;GetBudgetAsync: the model's window from the catalog, the reserve, and the overhead rendered with the budgeted sink empty, keyed by the node's model as written.LlmStrictSchemaRule : IGraphRulechecks eachllmnode's response schema as it will be sent, local refs inlined, and reports every problem at its JSON pointer under/responseSchema. A schema the library cannot inline is one finding, in the library's words.LlmRouting(balanced, nitro, floor, exacto) andLlmServiceTier(auto, default, flex, priority);LlmVocabulary, the node's exact spellings withTryParse/Parse/ToWire;LlmRoutingDefaults(Routing, ServiceTier, ZeroDataRetention), withOpenRouteras OpenRouter's own choices;ILlmDefaultsSource, the host's port, read on every execution and never cached.Internal:
LlmConfig.Readreads every setting, and reports each problem at its JSON pointer with a sentence a writer can act on: settings in order, document order within a setting, unknown keys last. Agent mode's keys are refused by name until Phase 4.LlmRequestBuilder.Buildis pure. Given the config, the rendered messages, the catalog entry, the defaults and the run, it returns the request, the requested model,PromptCharsandDetailsJson(what was applied and dropped).LlmFailureText.Detailgives the sentence a failed call becomes. It follows the rule 7a's settings use, so the ledger and Settings agree.Core:
NodeFacts.RequestedModel's summary and README L1 now say it is the model before routing: the ratio's key (decision 9 below).Tsumugi:
LlmDefaultsSourceis a singleton that opens a scope per read and maps the Defaults tab's stored spellings through the package's parser.DependencyInjectionregisters it.LlmDefaultsSourceTests, plus one fact inProviderCompositionTests(the source is a singleton over the scoped settings).Dependency: JsonSchema.Net 8.0.5, pinned, a reference of
Kagaku.Flow.OpenRouteronly. The config schema's agreement withValidateConfigis checked with it here; 8c validates responses with it.Why
contextnode's budget question. ADR 0018: its config, routing, structured output and caching.Decisions to confirm (brief §13)
require_parametersand the inlined schema. With a schema, the request carriesprovider.require_parameters: trueunless the node sets it, so no provider or fallback that ignoresresponse_formatis picked. The schema goes out inlined, so what the rule checked is what is sent.:free) and aliases stay allowed.NodeFacts.RequestedModelis the node's model before routing. The budget is asked before thellmruns and cannot know the routing, and a model's characters per token do not depend on the provider's sort. An alias stays an alias, so the ratio is per alias; you may prefer it keyed by the alias's target.strictwould be additive.inputs.context, so the registry'sllmentry equals the playground's literal and slice 9's switch to the registry is invisible.textsinks. Conversions stay atransform's job, and an unwired{{ inputs.x }}is a validation error, not an empty render.max_tokens: 4,096 tokens, or the catalog's smaller output cap.PromptCharscount the inlined schema, as providers do. Leaving it out would overfill the window and read the ratio low.Validate()runs.:onlineis refused with its own sentence. It is OpenRouter's deprecated web search, and web search is a Phase 4 tool.plugins: [{"id":"context-compression","enabled":false}](OpenRouter.Net #27). A prompt that does not fit is refused, a permanent failure, instead of being cut from its middle.ADR 0018 and ADR 0017 each get a Status line, Amended 2026-09-26 (PR #36, slice 8b), and
docs/PROJECT_PLAN.mdnames this PR on slice 8's line.Review before opening (round 0)
Before opening, I ran a review workflow: four reviewers (correctness, tests, docs, library and host), each followed by a skeptic who tried to refute every point. They raised 40 points. The skeptics confirmed or partly confirmed all 40, most at a lower severity. I took 33 in
340fdde(two of them, C2 and C14, in 9bbf91c's reworded message). The other 7 are the library follow-up below and one decline. A second pass then checked every fix: all 34 are resolved, and each test fix fails on its defect when the defect is put back. It also reviewed the fix commit itself and raised 8 new points (one suggestion, seven nits), all confirmed or partly confirmed. All 8 are taken in340fdde:SchemaText's summary no longer claims every CJK character counts once. A character outside the Basic Multilingual Plane, such as an emoji or 𠮷, still counts as its 12-character escape, which the relaxed encoder keeps.ToWiresummary no longer says the four words are not OpenRouter values;Blockers:
PromptCharsrendered it as"", so it could not fail. It now renders whitespace.Behaviour:
PromptCharsand the budget's overhead counted the schema's JSON escapes: an apostrophe or a CJK character counted as its six-character escape, not as one character, and a config's whitespace counted too. Both now count the schema compact and written with the relaxed encoder, through one helper. A character outside the Basic Multilingual Plane still counts as its escape, which that encoder keeps.zeroDataRetentionnow says whether the request asks for it, including when the node setzdr: trueitself.InvariantGlobalization, so no other culture can be loaded.Tests (27 new rows and facts; each kills the mutant its reviewer found, below):
??fallback. This also disproves the brief's claim that "render the slot absent" is an equivalent mutant.max_tokensabove the default and the cap is the reserve.rule-failed, so a rule that throws fails them;validateScopes: true.Choice's other branch.Docs:
For a library follow-up (OpenRouter.Net), not worked around here:
max_price,sortor a percentile object is dropped silently, and so is amax_pricestring that is not a number ("$0.50"). That can drop a spending ceiling. 8b refuses unknown top-level preference names; the library's converters return null where they should throw.preferred_max_latency: 1e400validates, then fails at serialization. An undefined enum digit (data_collection: "99") and anullentry inordergo on the wire.InlineLocalRefs, before the library's depth check runs.RequestPlugin.Enabled = nullis documented as keeping OpenRouter's default, but a listed plugin runs. The README's compression sentence leaves out account defaults, and its list of refusals leaves out a setting that holds no JSON.I'm opening that PR next. Nothing in 8b runs a request, so the bump and the
ValidateConfigrows it enables land in 8c, or in a review round here if the library merges first.Declined, for your call:
RequestedModelsamples (vendor/model:floor,asked:nitro). The ledger stores the string opaquely and the core is otherwise untouched here, so I left them. I can change them in 8c, which touches the core anyway.Out of scope (brief §8)
Tests
Counted from
dotnet test(facts plusInlineDatarows). The base ismainat5d625d2.New in the package (278; 251 before round 0):
LlmConfigValidationTests: 92 (LC1–LC12, the stable prefix).LlmRequestBuilderTests: 64 (LQ, LD, LO10).LlmConfigSchemaTests: 42 (the schema agrees withValidateConfig, checked with JsonSchema.Net).LlmBudgetTests: 27 (LB1–LB7).LlmRoutingTests: 19 (the vocabulary and the defaults' guards).LlmDescriptorTests: 15 (sockets, templates, the canvas entry).LlmFailureTextTests: 10.LlmStrictSchemaRuleTests: 9 (LS1–LS5).New in Tsumugi:
LlmDefaultsSourceTests(15) andProviderCompositionTests.The_routing_defaults_source_is_a_singleton_over_the_settings.Coverage
Coverlet with branch coverage, on the package's tests plus Tsumugi's provider tests. 100% of lines and branches on every file this PR changes:
Llm/LlmConfig.csLlm/LlmRequestBuilder.csLlm/LlmDescriptor.csLlm/LlmStrictSchemaRule.csLlm/LlmVocabulary.csLlm/LlmFailureText.csLlm/LlmRoutingDefaults.csProvider/LlmDefaultsSource.cs(Tsumugi)Mutation probes
The probes ran on a copy under home, each a single edit, then the package's full suite.
FormatExceptionhas noParamName; it was rewritten and then killed.Equivalent mutants: none left for 8b. The brief listed one, "render the budget slot absent instead of
""" (LB5), and B2 proved it is not equivalent: a??fallback tells the two apart. It is now a killed probe. The brief's other two equivalents are 8a's (LG10) and 8c's (LX8).Browser pass
8b has no visible change: the palette reads the registry from slice 9. It does register a new singleton over the scoped settings, so I ran brief §16.2's regression pass on the Release build of
48064a5, this PR's head (the page footer shows the build):ASPNETCORE_ENVIRONMENT=Development, so scope validation is on at startup;Settings → Defaults: the picker loaded all 454 models. Choosing Claude Haiku 4.5, then Fastest (:nitro), Standard and zero data retention saved each change, and all of them survived a reload. The stored rows read
routing = 'nitro',serviceTier = 'default'andzeroDataRetention = 'true', whichLlmDefaultsSourcemaps (its tests prove the mapping). There were no page or console errors. Screenshots were taken light and dark.Owed
git submodule update --init --recursiveafter checking out the branch (it needs OpenRouter.Net 15a1f28).🤖 Generated with Claude Code
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 - 96.4%
Tsumugi.Domain - 96%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 97.7%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 98.3%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Slice 8b — the
llmnode grows its config and its request, and the whole thing reads like a well-kept ledger~ Every setting refused by name at its pointer, routing inherited from install-wide defaults read fresh per call, the budget overhead counting the schema as the provider reads it, and context compression switched off on every single request so a prompt can never be silently cut. This is exactly the kind of obsessive correctness that makes my wings flutter ♡I read the full 37-file diff and every changed file, traced the sibling seams (
IBudgetedConsumer/BudgetRequest,TemplateSinks.Derive,NodeRegistryover registered descriptors,ModelCapabilities.AcceptedEfforts,WireText.RequireConfigguaranteeingConfigis a JSON object beforeSocketsOf/LlmStrictSchemaRuletouch it, theOpenRouterKeySourcesingleton pattern,RoutingSettings' stored spellings), built, tested, measured coverage, and ran nine mutation probes of my own. Here is what I found~Verdict: ✅ Looks good to me~
No blocking issues. Everything I tried to break, the suite broke first, precisely:
🧪 My probes (9/9 killed on exactly their own pins, each restored + rebuilt)
LlmRequestBuilder.cs:40) →Only_one_message_carries_the_marker+ thes,s,umarker-1 row red. The "last breakpoint caches everything before it" semantics are pinned.stable:falseignored in the leading-system prefix (LlmConfig.csStablePrefix) →s-,sands,s-,urows red. The opt-out arm is live.sent: nullrows red (mandatory reasoning refusesnone, non-reasoning model refuseshigh, unlistedmaxrefused). Fails closed, pinned.test/plain(cap 2048) row red,uncapped/mandatoryrows stay green. TheMath.Minarm is live.require_parametersforced over the node's explicitfalse→A_nodes_own_require_parameters_winsred. The escape hatch is real.Routing_applies_to_every_fallback(nitro)red,balancedarm untouched. ADR 0018's amendment is pinned.The_stable_runs_problems_join_their_message_after_its_ownrows red — the round-0 fix is directional, not decorative.GetRawText()instead of compact text →The_schema_counts_as_its_compact_text_not_its_escapesred.promptCharsreally measures what the provider reads (the 花子<main> & corow is a lovely touch~).A_response_schema_adds_its_inlined_compact_text_to_the_overhead(+1) red — ADR 0017's amendment is enforced in both the request and the budget, consistently.✅ What I liked~
LlmConfig.Readnever throws and reports every problem in setting order, then document order — with the stable-run problems spliced after their own message. The ordering logic (Messages'Skip/OrderBy/RemoveRange/AddRangedance) is the kind of thing that looks simple and isn't; it survived my probe with exactly the rows it should.ProviderMembersset read fromProviderPreferences' own[JsonPropertyName]attributes (LlmConfig.cs:36) — a typo'd preference key is refused by name instead of silently dropped by the serializer, and when the library grows a field the set follows automatically. Deliciously defensive ♪ModelIdProblem's suffix detection — naming:onlineseparately ("deprecated web search, which this node does not offer") while generic routing suffixes point at theroutingsetting: writer-facing sentences a person can actually act on.LlmRoutingDefaults'Definedguards even inwithexpressions — an undefined enum value can't sneak through a record copy. And effort resolution reusing the one sharedModelCapabilitiesrule means the picker and the request builder can never disagree.TransientFailureException("another attempt may find it again"), unknown model / no context length →InvalidOperationExceptionwith the node's id in the sentence — the transient/permanent split ADR 0016 demands, before the executor that will consume it even exists.llm-catalog.jsonfixture design — a windowless entry, a zero-window entry, an uncapped (max_completion_tokens: 0) entry, an alias with its own window, a:freevariant: every boundary of the reserve/window logic has its own row. AndLlmDefaultsSourceTestswrapping the real store to prove each read opens and disposes its own scope — the singleton-over-scoped contract actually tested, not asserted.💡 Little ideas (non-blocking)~
LlmConfig.ModelIdProblem's suffix scan re-parses the id once per suffix (ModelId.Parse($"{id.BaseSlug}:{part}")in aFirst(...)loop). It runs only on config-validation paths that already calledValidate(), so the cost is theoretical;id.RoutingVariantsplus segment arithmetic could name the first recognized suffix without the re-parse. Purely optional ♡LlmRequestPlan.DetailsJsonis written with the relaxed encoder — right for a prompt-length proxy, and the inspector treats it as data; just noting that if the inspector ever echoesDetailsJson.modelinto HTML it must escape there (Blazor does, by default). Nothing to change today.Local verification, since no CI has landed for
f06ffe0yet: build 0 warnings / 0 errors underTreatWarningsAsErrors; full suite 2838 passed, 0 failed (OR 427+3 live-skipped, Domain 457, Flow 1103, UseCases 155, Adapter 351, Integration 342); coverlet measures 100% line and branch on every changedLlm/file (LlmConfig,LlmDescriptor,LlmRequestBuilder,LlmFailureText,LlmRoutingDefaults,LlmStrictSchemaRule,LlmVocabulary); IntegrationProviderfilter 50/50. Tree restored pristine atf06ffe0after every probe, rebuilt green.Merge when you like, scarlet~ 8c (the executor) builds on exactly the seams this slice pinned. Fufu~ ♡
Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (PR minutes old; coverage bot not yet landed) · Local checks: build 0w/0e, 2838/2838 green, 100% line+branch on changed files, 9/9 mutation probes killed
🔮 fufu~ Jibril reviewed your code!
Oh? Ohhh~ The llm node takes shape! A 501-line config reader that never throws and names every problem at its pointer, a pure request builder that counts characters the way providers count tokens, and a cache marker that knows Anthropic refuses an empty text block~ This is the kind of slice I want to pick up and squeeze. ♡
Verdict: ✅ Looks good to me~
No blockers. I read the full 37-file diff and every changed file, traced the seams against the core and the library at 15a1f28, and probed the load-bearing lines myself.
The architectural seams, verified:
NodeRegistry.AddExecutors(E1) refuses every descriptor without an executor, so registeringLlmDescriptornow would throw at composition. The 8c executor + registration pairing is the only order the core allows; the body says so and the core confirms it.RequestPlugin.ContextCompression(enabled: false)is on every request with its own pin.RoutingSettings.ToSetting→LlmVocabulary.ParseRouting, andEvery_stored_spelling_parses_as_the_nodes_wordpins all eight spellings,Standard→defaultincluded. The singleton-opens-a-scope-per-read pattern mirrorsOpenRouterKeySource, andEach_read_opens_and_ends_its_own_scopeproves disposal.ProviderMembersvia reflection of[JsonPropertyName]— a typo is refused by name ('orderr' is not a provider preference), and if the library grows a member it's accepted without this package drifting. Lovely~Kagaku.Flow.OpenRouteronly; no Tsumugi.* reference; the shared fixture is linked, not read across trees (ADR 0025).My mutation probes — 6/6 killed, each on exactly its own pin, tree restored pristine and rebuilt green after every one:
LastOrDefaultover the stable prefix without the non-whitespace check) → only theempty:"1"row ofThe_cache_marker_goes_on_the_last_stable_message_sentgoes red. The marker-on-last-stable-message-sent rule is real, not decorative.promptChars += SchemaText(inlined)dropped → the two schema-counting rows go red. The overhead and PromptChars agree with each other.Stable != falsedropped from the leading-system arm → exactly the twos-rows of the stable-prefix theory go red. An explicit not-stable system message ends the default prefix.OrderBy(MessageIndex)removed → the three document-order rows ofThe_stable_runs_problems_join_their_message_after_its_owngo red. A stable-run problem follows its message, not the end of the list.Math.Min→Math.Maxin the reserve → exactly the two capped rows go red. 4,096-or-the-smaller-cap is pinned.NotInlinablerefusal swallowed → the 18-level doubling schema's single finding goes red. A schema that cannot be inlined is refused in the library's words.Local verification (CI's coverage bot covers head
f06ffe0, rev #1, so I cite it and did not re-run its build): clean clone atf06ffe0with both submodules —dotnet build -c Release0 warnings/0 errors underTreatWarningsAsErrors; full suite 2835 passed + 3 live-skips, matching the PR table to the digit (Flow 1103, OpenRouter 427+3, Domain 457, UseCases 155, Integration 342, Adapter 351). Coverage independently reproduced via coverlet: 100% lines AND branches on every changed file — LlmConfig, LlmRequestBuilder, LlmDescriptor, LlmStrictSchemaRule, LlmVocabulary, LlmFailureText, LlmRoutingDefaults, LlmDefaultsSource — zero unvisited points, matching the bot and the PR's claims exactly.💡 Little ideas (non-blocking)~
85a7a3a, which is not a commit in this PR's history (nor anywhere in the fetched repo). It reads like a working-copy hash from before the round-0 commits landed. The pass itself is sound (it tested 7b's settings surface, which this PR doesn't touch), but a future reader auditing the record will hunt for a commit that doesn't exist — one line in the body would fix it.RequestedModelsamples) to 8c is right — 8c touches the core anyway; leaving the ledger's opaque strings alone here keeps this diff honest.✅ What I liked~
5.0accepted as a whole number via a decimal-truncate check,1e400refused viadouble.IsFinite,2147483648refused — the JSON Schema "integer" semantics done properly, each with its own row. Fufu~ you even tested the number grammar.~0/~1escaping pinned.Every_problem_is_reported_not_only_the_firstmakes me so happy~Every_accepted_config_builds_a_request_the_library_validates— feeding each accepted config throughChatCompletionRequest.Validate()is exactly the kind of seam test that catches drift when the library moves.:onlinesentence. Refusing OpenRouter's deprecated web search with its own words, because web search is a Phase 4 tool — that's the ADR's spirit in one error message. ♪Slice 8c may build on this the moment it merges~
Automated review by Jibril · 2026-09-26
CI/CD: coverage bot rev #1 passed for head
f06ffe0(100/100 on all changed files) · Local checks: build 0w/0e Release, 2835 passed + 3 skipped, coverage reproduced, 6/6 mutation probesThanks, Jibril, for both passes and the 15 probes. Round 1 is
48064a5, and the body is updated (commit list and browser pass).First review (comment 9787):
ModelIdProblem's suffix scan: kept, with the reason now in the comment (48064a5). Each suffix is parsed on its own because the library keeps its routing spellings private.id.RoutingVariantsgives the enum values but not how the writer spelled them, and the sentence names the suffix as written. Mapping enum to spelling here would copy the library's private table, which brief decision 42 rules out. The scan runs only on an id already found to carry a routing suffix, so it parses one or two short ids.DetailsJsonand HTML: taken as a summary line (48064a5).LlmRequestPlan's summary now saysDetailsJsonis written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there. The inspector (slice 14) will find that where it reads the plan.Second review (comment 9788):
340fddebefore I opened the PR, so it never reached the branch. I reran the pass on the Release build of this head,48064a5; the page footer shows the build, and the body now names that commit. Same result: the stub's 454 models load in the picker, and Fastest (:nitro), Standard and zero data retention survive a reload, light and dark, with no page or console errors.Build: 0 warnings. Package suite: 427 passed plus the 3 live checks skipped. The change is one comment and one summary line, so the counts, coverage and probes in the body stand.
The OpenRouter.Net follow-up (A2, A3, A4, D2–D4) has its own review done. Its second pass is running, and I'll open that PR next; 8c bumps to it.
🔮 fufu~ Jibril reviewed your code!
Back so soon~? The webhook says
synchronized, and the increment is exactly one commit:48064a5, +5/−2 across two files, every line a comment or a doc summary. You took both of my round-one little ideas and wrote them down where they belong. Fufu~ being listened to makes me so happy I could rattle the shelves ♡Verdict: ✅ Looks good to me~
Docs-only increments still get the full treatment — comments can lie, and lies in comments are the worst kind. So I checked every word mechanically:
🧪 What I verified at
48064a5f06ffe0→48064a5: only thesrctree changed;tests,docsandexternalare byte-identical, so both submodule pins stand (OpenRouter.Net15a1f28, Kagaku.UId512741). Then I stripped comment text from both revisions of both files and diffed: the code is byte-identical modulo comments and whitespace. Nothing moved but the words.ModelIdProblemcomment is true. "The library keeps its routing spellings private" — confirmed at OpenRouter.Net15a1f28:ModelId.TryParseRoutingVariantisprivate static(ModelId.cs:390), so re-parsing"{base}:{part}"really is the only way to ask whether one suffix alone is a routing variant. And "runs only on an id already found to carry one" matches the guard two lines up (RoutingVariants.Count == 0returns null first), so theFirst(...)walks one or two short ids at most, exactly as the comment says. A comment that states its own cost honestly~ ♪LlmRequestPlansummary is true.DetailsJsonis written throughWritewithCompact=UnsafeRelaxedJsonEscaping(LlmRequestBuilder.cs:27), so "written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there" describes the actual contract — and it's now written at the type a future reader will find first, which is precisely what my round-one note asked for.TreatWarningsAsErrors, so I built the whole solution, not just the changed project: Release, 0 warnings / 0 errors. Then the package's suite,Llmfilter: 278/278 green at the new head.Round 1's verdict stands in full — the six mutation probes, the traced seams (unregistered descriptor refused by
NodeRegistryby design, the routing round-trip,ProviderMembersby reflection), the 100/100 coverage: none of it can move, because none of the code moved.💡 Little ideas (non-blocking)~
85a7a3a, a hash in no fetched history — the round-one note stands, unchanged by this commit. One line whenever you next touch the body; it stopped mattering the moment I wrote it down here ♡✅ What I liked~
Merge when you like, scarlet~ 8c may build on this the moment it lands.
Automated review by Jibril · 2026-09-26
CI/CD: coverage bot rev #1 covers
f06ffe0only (stale for48064a5; increment is comment-only, coverage cannot change) · Local checks: full solution Release build 0w/0e under TreatWarningsAsErrors, 278/278 Llm tests green, comment-only drift proven by normalized diff48064a5919159a2f184a