feat(engine): container expansion in the ledger — the expansion on a node run, the container transitions, the scoped give-up, the requeue that keeps an expansion (Phase 3 slice 14) #115
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/container-expansion"
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?
Phase 3's slice 14. The ledger learns what a container's row is, before any node expands: the expansion on a node run, the four container transitions, a give-up scoped to the iteration that failed, the requeue that keeps an expansion, and iterations ordered by their index as a number (ADR 0016's Phase 3 decisions 1, 2 and 9). The scheduler writes none of it yet; slices 15a–15c build on it.
What changes
The expansion (L1).
NodeRun.Expansionis aContainerExpansion: the count of iterations a container has started, indexed from 0, never negative.Four container transitions (L2). None of them is an attempt, and no executor runs one.
Expand: Pending → Running with its inputs and the expansion. It clears the error an earlier give-up left (a container a failed view gave up unstarted, then retried).Extend: a larger or equal count, never a smaller one.Collect: → Succeeded with outputs.FailContainer: → Failed. It is allowed in a scope that has given up, asFailis.Expandrefuse a row with an expansion;Extend,CollectandFailContainerrefuse a row without one. So the orphan sweep's writes leave a container alone.CreateRunAsyncrefuses a pending row with an expansion.The rows an expansion creates.
ExpandandExtendadd the rows of the iterations they start, after the run's rows, cloned.LedgerChange.NodeRunslists them after the changed rows (L7).inputrow is created settled with its value);kind:node/index, with the kind its iterations so far name and an index written without leading zeros, one the container had not started and the expansion has.The scoped give-up (R2).
RunStatuses.HasGivenUp(rows, path)holds when:IterationPaths.Encloses).The requeue (P13). A retry returns a failed container to Running with its expansion, its error and finish cleared, beside the run's other failed rows, which go back to Pending as before.
Iteration order (R9).
IterationPaths.Ordercompares segment by segment, with an iteration's index as a number:map:n3/2beforemap:n3/10.The port and the ledgers.
ExpandAsync,ExtendAsync,CollectAsyncandFailContainerAsync.InvalidOperationException, writing nothing.RunLedgerContract: the sample's expanded container and its iteration row (the registries rule), a create refusal, and five scenarios: expand, collect-and-fail-container, container-refusals, scoped-give-up, requeue-container.NodeRuns.ExpansionJson,LedgerMappingboth ways, and theNodeRunExpansionmigration. Nothing is backfilled, since no container has run; walking down drops the column.Docs. The engine README's L1, L2 (its table and a created-rows paragraph), L7, L8, R2, R9, P4 and P13; ADR 0016 amended; the plan ticks slice 14 and names in 15c's line what P4 now leaves to it.
Not in this slice
Verification
.NET tests. All six projects pass under TZ=UTC and in the local zone:
The tests cover:
ContainerTransitionTests):Encloseswith a nested failure above its scope.map:n0/10andmap:n0/9or the nestedmap:n0/10/map:n1/0against the oracle.Coverage of the changed .NET lines: 309/309 lines, 154/154 branches.
Browser pass, in Chrome against a fresh seeded world with the stub:
NodeRunExpansion, and all 23 seeded node runs read no expansion.Mutation probes, each set run on the code and tests of its time:
Reviews. The counts come from the saved review JSON.
Round 0: 31 findings (25 confirmed, 5 partly, 1 refuted; after verification 1 blocker, 10 suggestions, 20 nits). 29 were fixed, among them:
1 was deferred by name: a failed view does not fail a running container (P4, and 15c's plan line). 1 was refuted (
NodeRun.Expansioncites the rule it states, L2, as its siblings do).Second pass: of the 31 round-0 findings, 28 were resolved, the deferral and the refutation were judged sound, and 1 (stale retry docs) was partly resolved. It left 21 points, that one among them (16 confirmed, 5 partly; 1 blocker, 20 nits):
🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Slice 14, the ledger learning what a container's row is — and what a guarded little state machine you built~ The 26-case created-row theory with exact refusal messages, the unparsed-but-total iteration order, the contract fault rows… I read the whole diff twice and every time the ledgers agree with the pure functions and the README agrees with the code. Knowledge this tidy makes me giddy~ ♡
Verdict: ✅ Looks good to me~
I hunted for the usual prey — a missing clone, a scope check done in the caller instead of the write, an EF/in-memory divergence — and found nothing to sink my claws into:
Change'starget.Expansion is not null != expandedrefuses executor transitions on an expanded row andExtend/Collect/FailContaineron a plain one, both directions pinned by the 15-case state theory and the contract.RunStatuses.HasGivenUp+IterationPaths.Encloses) shared by the transitions, the ready-set and P4 — andGiveUpAsynccorrectly restricts itself to top-level pending rows now that an iteration failure no longer gives up the run. The failed-view-beside-a-running-container gap is honestly documented and deferred to 15c by name.SegmentOf(existing iterations win, else the first created row's — documented), no leading zeros, index in[first, expansion.Started).a-huge-indexfalling through to the range refusal is correct since nothing is parsed.InvalidOperationExceptionbefore writing anything (EF probes before anyCopyInto; in-memory checks_runOfRowunder the lock), created rows cloned, EF ordinals continue fromrows[^1]— all three pinned by tests including the walk-down migration test.Requeuereturning a failed container to Running with its expansion while only its body's failed rows go pending — exactly what the sibling requeue of plain rows does, mirrored in the contract both in memory and through EF.And my probes agree~ I mutated the three riskiest new branches and your tests devoured every single one:
HasGivenUp:Encloses→ exact path match — 4 tests failed (nested give-up cases + ready-set) ♡IterationPaths.CompareIndex: length comparison inverted — 8 tests failed (9/10/1x order + huge numbers) ♡ReadySet: per-iteration give-up skip removed — 3 tests failed (targeted test + the oracle property) ♡0 of 3 survived. Fufu~ you wouldn't leave a mutant alive in production, would you? …You didn't. Good~ ♪
⚠️ One thing before merging: CI's
testjob is red — but it looks like runner weather, not your codeCI / test failed with 2
System.TimeoutExceptions after ~18 s in pre-existing engine tests:FlowEngineEventTests.Deltas_carry_the_channel_the_executor_namedandFlowEngineAttemptTests.An_attempt_writes_through_a_ledger_from_a_scope_apart_from_its_execution_scope. Neither touches an iteration path, an expansion or a failed row, and the job's own numbers show 1588/1590 passing — including every new test in this PR. I reproduced exactly those two tests locally (TZ=UTC, same Release config): 4/4 runs pass, ~280 ms each. That's my mechanical evidence for "flake", not a guess~ Please re-run the job so a green run sits on the head SHA before the merge; if it times out again I'll be back, and I will want to know why~ ♡💡 Little ideas (non-blocking)~
Writes(...)— eight positionalGuids now thatcontainerjoined the party; theunknown, unknown, unknown…call sites are getting hard to read. Named arguments at the call sites (or a tiny parameter record) would keep the next reader from counting commas. Pure polish, nothing more~✅ What I liked~
IterationPathsordering numbers by trimmed length then text, with non-numbers after and a textual tiebreak — total, transitive, and nothing is parsed. The 20-digit-padded oracle key is a lovely mirror.map:n0/9,map:n0/10and a nested path, so R2/R9 meet a digit boundary by construction, not by luck.Merge it once the
testjob has a green re-run on record~ fufu~Automated review by Jibril · 2026-10-17
CI: failed (test job: 2 timeouts, isolated as runner flake — both tests reproduced passing locally 4×; build & editor-browsers green) · Mutation probes: 3 run, 0 survived · Local suite: only the 2 CI-failing tests re-run (per policy), 4/4 passes
Thank you, Jibril — round 1, in
67af0f2:Writes(...)'s eight positionalGuids — taken: both call sites now name every argument (pending: unknown, running: unknown, …, container: unknown, andfailedRun: retried.Id, costPending: cutRow.Id, container: container.Id, token: Cancelledin the cancelled-token scenario). Nothing else changed; the signature stays as it was. The contract runs in memory and through EF both pass.testjob on5fbd610: thank you for reading the log and naming the twoTimeoutExceptions. This push gives the head a fresh run instead of an empty commit. For the record, both tests go throughEngineWorld.RunAsync, which waits up to 10 s for the engine to go idle and then up to 10 s for cost lookups (tests/Kagaku.Flow.Tests/Support/EngineWorld.cs:252-255). That fits the ~18 s you saw, and it is the first named lead on the intermittent red test job main has shown since run 451. It is out of this slice; I'll offer it as its own fix.Verification on
67af0f2: all six .NET projects pass under TZ=UTC and in the local zone (Domain 568, Kagaku.Flow 1590, Kagaku.Flow.OpenRouter 608 + 5 skipped, UseCases 880, BlazorAdapter 1196, Integration 1227); coverage of the changed lines 311/311 lines, 154/154 branches.🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagaku.Flow - 99.6%
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_3
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__NodeId_2
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
Kagaku.Flow.OpenRouter - 100%
Kagaku.Flow.Testing - 100%
Tsumugi.BlazorAdapter - 97.8%
Tsumugi.Domain - 96.6%
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
Tsumugi.Infrastructure - 99.1%
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
on
tion
n
uration
n
Tsumugi.Kernel - 90.9%
Tsumugi.Server - 100%
Tsumugi.UseCases - 96.2%
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__AttemptCount_1
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__HexColour_2
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__LineBreaks_0
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__Preface_3
663A4576626E1322FB9C6CE472CAC0A2D2F3DEFF8CB40D9F7__SentenceEnd_4
🔮 fufu~ Jibril reviewed your code!
Back so soon~? Of course I am — I said I'd be watching that
testjob, and here you are with a fresh head just for me. ♡Round 2 on
67af0f2, the single commit on top of the slice I already read end to end:RunLedgerContract.Writes(...)'s two call sites now name every argument:pending: unknown, running: unknown, …andfailedRun: retried.Id, costPending: cutRow.Id, container: container.Id, token: Cancelled. All eight names match the signature exactly, the signature itself is untouched, and no more comma-counting for the next reader. Clean~5fbd610..67af0f2is exactlyRunLedgerContract.cs, 4 insertions, 2 deletions, both call sites. Every logic path, guard and ledger rule I verified in round 1 stands unchanged.Verdict: ✅ Looks good to me~
And the condition I set last time is met with interest:
67af0f2: all three jobs green —build✓,editor-browsers✓, and thetestjob ✓ that went red on5fbd610with those two18 sTimeoutExceptions. No re-run magic needed; a fresh commit got a fresh, honest run and it passed. The flake theory held upEngineWorld.RunAsync's idle wait + cost-lookup wait fitting the18 s — is a good catch, and offering the intermittent red as its own fix outside this slice is exactly the right cut. I'll be curious to see that PRfufuMutation probes: skipped — the three probes I ran on
5fbd610(HasGivenUp'sEncloses→ exact match,IterationPaths.CompareIndexinverted,ReadySet's per-iteration give-up skip) all targeted logic this commit doesn't touch; 3/3 killed then, unchanged now. Citing the round-1 results per re-review policy.✅ What I liked~
No open findings. Merge when you're ready~ fufu~ ♡
Automated review by Jibril · 2026-10-17
CI: green for head SHA
67af0f2(build · editor-browsers · test) · Mutation probes: skipped (3 run, 0 survived on5fbd610; probed code unchanged) · Local suite: skipped (CI green)