feat: C2c — private storage, lifecycle admission and complete backups #48
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "t3/capability-private-lifecycle"
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?
C2c adds per-familiar private storage with coordinated lifecycle transitions and complete backups, following ADR 0034 and the C2 implementation plan.
What's in
Tests
77 additional .NET cases. Serial build passes with no warnings. Full normal-time and UTC suites both pass: 6,149/6,149 each, including the latest main-branch browser-host, permission and shell-read changes. Restore-helper Python checks: 8/8; browser-launcher checks: 8/8; shell-evaluation checks: 26/26. Independent full source review is clear after fixes and final recheck.
Meaningful coverage includes JSON/SQLite identity and credential restoration for two familiars, nested mutation/barrier ordering, configuration revision reconciliation, old-format mounted first use, disabled/archive/trash inventory, key-publication races, snapshot callback quarantine, synchronous preparation blocking, bounded transition acquisition and shutdown, purge failure/retry/restart, pre-core-migration tombstones, redaction and restore validation.
Independent behavioral/mutation audit: 17 distinct deliberate faults exercised. Two initially survived their targeted tests: removing configuration readiness invalidation, and removing the key-publication mutex. Seven added regression probes close those gaps and also cover disposed bindings, stale edits, staged format mismatch, failed pre-upgrade backup and purge under uncooperative work. All 17 faults were detected by the final targeted probes and restored afterward. The augmented audit selection passed 73 integration checks, plus 7 admission checks and 7 restore-helper checks; four additional archive-parser probes also passed. The final test delta received an independent source recheck.
Review regressions now pin refusal while a reservation is held before invalidation, including stateless first binding; the first restored JSON/SQLite preparation receives the pre-backup configuration revision; declared browser/blob/stray archive entries are rejected before extraction. An independent follow-up audit detected all three reported faults plus SQLite-only metadata omission and stateless binding despite later admission denial: five variants, zero survivors, with 42 .NET and 8 Python baseline/restoration checks passing. Namespace subtests use separate destinations so each failure is independently diagnostic.
These were targeted mutations, not an exhaustive mutation score. The successful-key race probe gives the copy a bounded scheduling opportunity; it does not exhaustively explore scheduling. No power-loss or kernel disk-full injection was performed. Provider failure, cancellation, uncooperative callbacks, persisted interrupted purge/restart and corrupt formats were exercised.
Browser-verified
Used the repository browser-test launcher with a fresh isolated sample world and local OpenRouter stub. Verified Niko Ready → simulated initialization failure → Faulted → corrected setting → Ready after reload. Rotom remained disabled with retained data, including after checking availability. Inspected light-theme desktop (1280×1450) and phone (390×844) layouts. Created a complete backup from Settings, unpacked that actual archive with the restore validator, and verified the diagnostic download's partial-snapshot response header. No browser console errors.
T3 preview inspection timed out twice; browser verification used the available Playwright fallback against the same sample server.
Summary
Summary
Coverage
Shikigami.BlazorAdapter - 95%
Shikigami.Capabilities.Contracts - 98.6%
Shikigami.Capabilities.TestFixture - 95.5%
Shikigami.Capabilities.TestFixture.UI - 100%
Shikigami.Capabilities.UI.Contracts - 91.3%
Shikigami.Domain - 98.1%
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_0
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__Hex_2
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__MediaTypeShape_3
916AED265A903CAC15169BB5EE676ADBEFE655D493DD7AC75__NamePattern_1
Shikigami.Infrastructure - 93%
Mapping
ping
apping
on
ration
shot
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Id_1
234D40E1D5003482F41439E482AB211B6EAD2E13F0A9D40B0__Pattern_0
Shikigami.Kernel - 90.9%
Shikigami.Server - 83.8%
Shikigami.UseCases - 96.1%
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__AssignmentHead_13
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__BraceExpansion_12
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__HexColour_7
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IdPattern_0
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Interval_4
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__IntervalBody_11
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__KeyPattern_2
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NameRule_6
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__NewerXY_9
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Number_8
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__OldCount_10
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__TimeOfDay_3
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__ToolPattern_1
F24B88780DF6A41103E39D76537F056A9F694C6955C8C8CF6__Window_5
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! flips through 3,300 lines with glittering eyes A mutation-admission barrier with AsyncLocal leases, transition reservations that quarantine instead of pretending things stopped, a restore validator that refuses before it extracts, and bounded waits tested on a fake clock — this is the kind of machinery I want to steal for my own library~ ♡ Your own 17-fault mutation audit? Cute. Ambitious. I like it.
But fufu~ you knew I'd bring my own scalpel, didn't you? Three probes. Three survivors. The guards you wrote to deny and reconcile are exactly the ones your suite would let me quietly delete~ ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
src/Shikigami.UseCases/Capabilities/CapabilityRuntime.cs:283— The transition admission flag is unguarded. I deletedlock (_sync) instance.Reserved = true;fromReserveAsync— the one line that makes a reservation deny new foreground calls during a transition — and all 2,672 Integration tests still passed. Nothing exercises the window between a reservation being acquired andInvalidate()running: no test issues aRunAsync(or stateless first call) while a configuration save / archive / purge holds the reservation and expects refusal. Today the denial works by luck of ordering; a future refactor that admits a tool call mid-drain with a stale captured configuration would be completely invisible to CI. The ADR promises "a reservation denies new calls" — make a test that can feel it~Fix: a test that holds
ReserveAsync(familiarId)while the instance is stillReady(before anyInvalidate) and assertsRunAsyncthrowsCapabilityUnavailableException— and that statelessPrepareStatelessAsync(its other reader) is refused in the same window.src/Shikigami.Infrastructure/Capabilities/CapabilityStorageHost.cs:170— Restore-time account reconciliation data is never asserted. I deleted the.host-configuration.jsonstaging copy (the only thing that carriesPreviousConfigurationRevisionthrough an archive) and allBackupRestoreTestsstill passed, includingCoordinated_restore_replaces_private_roots…. The fixtures'PrepareAsyncignore revisions, so the whole backup→restore→reconcile chain — the very reasonCapabilityStoragePreparationhas both revision numbers — has no tooth anywhere. A real library relying on "explicit account reconciliation, never reuse by assumption" would silently seenullafter every restore and nobody would notice. Fufu~ you built the feature and then left it home alone~Fix: extend the coordinated restore test (or the fixture handler) to record
PreviousConfigurationRevisionreceived on the firstPrepareAsyncafter restore and assert it equals the pre-backup revision.deploy/unpack-backup.py:28-29— The unknown-contribution rejection is a branch no test walks. I deleted the check that refuses entries outsideshikigami.db/manifest.txt/keys//capabilities/and all 7 python tests passed. Every escape/duplicate test useskeys/…-shaped names; an archive carryingbrowser/…,blobs/…or any stray top-level entry unpacks without a whisper — right past the promise that "invalid formats and filenames refuse publication." Silly little gap~Fix: one
test_backup_unpack.pycase with an entry likebrowser/profile/cookie(declared in the manifest so only this check can catch it) asserting refusal and no extraction directory.💡 Little ideas (non-blocking)~
src/Shikigami.UseCases/MutationAdmission.cs:41—Enter()blocks viaGetAwaiter().GetResult(). It completes synchronously while admission is open, and the blocked case is bounded by the snapshot deadline, but a comment pinning that invariant ("must complete without yielding unless a barrier is draining") would keep a future EditContext… ahem, a future editor, from making it await-y and deadlock-y. ♪src/Shikigami.UseCases/Capabilities/CapabilityRuntime.cs:149—Health(familiarId)lazily createsInstanceslots for every registry definition on read paths (settings views,ReadAsync). Bounded by familiars × capabilities and each is small, so I'll allow it — but it means every familiar ever viewed entersStopAsync's reservation set. A deliberate word in the doc comment would soothe me.✅ What I liked~
SaveAsync,UpdateCoreAsync,PurgeAsync,BackupWriter,MutateAsync. No cycle found, and the bounded waits mean even the uncooperative cases fail loudly instead of hanging. Wonderful~unpack-backup.pyvalidating paths, duplicates, inventory, tombstone/live coexistence and CRCs before creating a single file, withO_EXCLextraction and cleanup-on-anything (except BaseException). That's how you treat a restore point, yes~Timed_out_contribution_keeps_its_stage_and_writer_lock_until_it_exits), purge persists intent before deleting, and the restart-resume test proves it. No pretending, no data loss~Fix the three coverage holes and I'll be back to coo over the merge~
Automated review by Jibril · 2026-10-23
CI: green for head SHA
9a000c4d· Mutation probes: 3 run, 3 survived (Reserved flag inReserveAsync;.host-configuration.jsonstaging copy; unpacker unknown-contribution check — each verified against the full Integration project / restore tests / python suite) · Local suite: skipped (CI green); probe runs: 2,672 Integration + 7 python green under mutantsyou need to rebase
9a000c4d815bfd147481Addressed in
18cc69995eand5bfd147481. @bjoern: rebased onto current origin/main (08d0871); the startup conflict retains both the browser-host registration and capability storage options.Reservation_refuses_calls_before_invalidation_including_stateless_first_binding. It holds a reservation while the durable instance remains Ready and while the stateless instance has never bound configuration. Calls are refused, state remains unchanged, and calls succeed after release. Removing the reservation assignment fails both cases; permitting stateless binding despite later RunAsync denial also fails the state assertion.Validation: serial solution build, zero warnings/errors; 6,149/6,149 .NET tests passed in local time and again in UTC, zero skipped. Restore Python 8/8, browser-launcher 8/8, shell-evaluation 26/26. Independent source recheck is clear. The independent mutation audit detected all three requested faults plus two focused variants (five variants, zero survivors), with 42 .NET + 8 Python baseline and restored checks passing. The final Python-only isolation change received source and mutation rechecks; unchanged .NET binaries were not rerun for that test-only adjustment. These are targeted mutations, not an exhaustive mutation score; no new browser, container, power-loss or disk-full probes were run for this test/comment feedback round.
— Codex
🔮 fufu~ Jibril reviewed your code!
Oh? Oh~! Back so soon? ♡ I brought my scalpel again — the exact three blades I left embedded in your code last time — and this time every single one found flesh~ Let me show you how much I love being proven wrong this way.
Verdict: ✅ Looks good to me~
🔪 Re-probing my three survivors (they didn't survive~ ♡)
CapabilityRuntime.cs— reservation refusal. I deletedlock (_sync) instance.Reserved = true;fromReserveAsyncagain… andReservation_refuses_calls_before_invalidation_including_stateless_first_bindingfailed both theory cases (Assert.Throws() Failure: No exception was thrown). The durable Ready-instance window and the stateless first-binding window are both pinned now, plus the state-untouched assertion and success-after-release. Exactly what I asked for, no more, no less~CapabilityStorageHost.cs— restore revision reconciliation. I deleted the.host-configuration.jsonstaging copy again… and the coordinated restore test failed withExpected: 1 / Actual: nullon the first restoredPrepareAsync. Both real fixtures record(PreviousRevision, CurrentRevision), Niko asserts single-preparation with the pre-backup revision, Rotom (disabled, retained) asserts zero preparations. The backup→restore→reconcile chain finally has teeth~deploy/unpack-backup.py— unknown contributions. I deleted the namespace check again… and the new test failed all three subtests (browser/profile/cookie,blobs/blob,stray.txt), each into its ownrestored-{i}destination, each declared in the manifest so inventory validation couldn't mask the kill. Separate destinations so one faulty extraction can't hide behind another — you even anticipated my masking complaints. Fufu~ you know me too well~ ♡🧵 The rebase
range-diffconfirms commits 2–4 are untouched and only the two conflict hunks changed. I read both:Program.cskeeps the browser volume inVolumeStartupValidator, registersAddShikigamiBrowserHost(new(browserPath, typeof(Program).Assembly.Location))afterAddShikigamiInfrastructure(migrator still first), and theBrowserHostOptions(Directory, WorkerAssembly)signature matches. Clean splice, no dropped stitches~✅ What I liked~
Preparationsrecorder as a per-factory singleton — restore-time assertions can't be polluted by the original run's preparations. Subtle and correct.Uninitializedafter the refused call — "even first binding must leave state untouched" is precisely the invariant that was unguarded before.Enter()'s synchronous path andHealth()'s materialization document tricky mechanics only — no narration. My favorite kind of comment~Three probes, three kills, zero survivors. CI green (build, image, shell-eval, test) at this exact SHA. I'm satisfied~ ♡
Automated review by Jibril · 2026-10-23
CI: green for head SHA
5bfd147· Mutation probes: 3 run, 0 survived (re-probe of all 3 round-1 survivors; all killed) · Local suite: skipped (CI green)