Repository navigation
Lite runs housekeeping on its own loops, so a long archive or retention step no longer skips collection slots (#5731) - #5740
Conversation
Times each step of the collection cycle, counts the grid slots a late cycle skipped, logs one Warning naming the slowest steps (or the wait that overran), and writes one collection_log row per enabled server (collection_cycle / SKIPPED). Readers that count collectors, runs or the last collection leave the row out.
…and get_collection_health (#5643)
…ingdata/PerformanceMonitor into fix/5643-skipped-cycles
…skipped-cycle surfaces (#5643)
…nstead of a copied literal (#5643)
…nd wait separately, pin the logged write failure (#5643)
… retries next tick; write holds past 5 s log a warning naming the holder (#5731)
…d retries, not holds collectors off (#5731)
Smaller things, and one stale invariant outside the diffFive notes that did not warrant their own inline comment, the last of which is in a file this PR does not touch. 1. The mechanism still holds — 2. The Darling's twin ( 3. 4. 5. The log watchdog writes while still holding the write lock. What I checked and found clean, so it does not get re-litigated:
|
There was a problem hiding this comment.
Verdict: CHANGES REQUESTED
Claims
- VERIFIED — "
ExecuteAsyncno longer awaits any housekeeping call" —CollectionBackgroundService.cs:288-291;CollectionCycleSkipTests.TheLoop_TimesEveryAwaitedStep_AndReportsAfterTheWaitasserts noawait Run*IfDueAsync(in the body - VERIFIED — "Three loops through the existing
RunPileupCadenceAsync, each ticking every minute behind the same gate" —:433-450,:493,:81, gate at:231 - VERIFIED — "No loop ticks before the first round, and again after the
while" — TCS at:227, set at:303and:322, awaited at:486 - VERIFIED — "A step that throws costs that step for that tick, not the steps after it" — per-step try/catch at
:498-522, cancellation rethrown at:514 - VERIFIED — "Shutdown awaits all four loops" —
:327and:338 - VERIFIED — "The backfill's 'sequence is the concurrency bound' comment is rewritten;
QueryStoreServerGateis the bound" —RemoteCollectorService.QueryStoreBackfill.cs:37-43, gate real at:103-113 - VERIFIED — "No UPDATE of
analysis_findingsanywhere underLite/" — grep returns nothing; the overlap is INSERT plus DELETE - VERIFIED — "Retention returns
nullon timeout andRunRetentionIfDueAsyncreturns without stamping" —RetentionService.cs:162-192,CollectionBackgroundService.cs:770-779 - VERIFIED — "The hold watchdog logs one Warn past
WriteHoldWarnAfter; reads pay one timestamp" —DuckDbInitializer.cs:388-411,_acquiredAta field initializer at:376,_write &&guard - VERIFIED — "
ArchiveOldDataAsync/CompactParquetFilesreturnbooland the ~30 statement call sites compile unchanged" — every site is statement-position or asserts the bool (ArchiveLockBudgetTests.cs:228/317/343/357; discarded atArchiveService.cs:2034) - VERIFIED — "The reset's export and restore locks stay exclusive" — the only remaining
AcquireWriteLock()calls are:1690,:1809,:1884 - VERIFIED — "
ArchiveStampAfterRun: complete a full interval away, incomplete due in exactly 5 minutes" —:719-720againstHousekeepingIsDueat:622, pinned byACompletePass_StampsTheClock_AnIncompleteOneStampsAFiveMinuteRetry - VERIFIED — "The reset still runs the recovery and post-reset compaction through the same bounded code" —
ArchiveService.cs:1793and:2034; correct, and it is the doc comment at:115-121that is wrong - UNVERIFIED — every test count, revert check and "0 warnings" —
Lite/Lite.Teststargetnet10.0-windows, so nothing builds here; I read the new assertions instead of running them - UNVERIFIED — "a reader that keeps asking gets in within budget plus slack" —
ArchiveLockBudgetTests.cs:198is a live timing measurement - UNVERIFIED — "the 4 GB COPY limit against live collectors is plan risk 1" — needs the dogfood run the body says was not done
- REFUTED — "Every
RunXIfDueAsynckeeps its name, signature, due check, stamp and text" —RunArchivalIfDueAsyncgained aCancellationToken(:722) and a changed stamp (:752);RunRetentionIfDueAsync's stamp is now conditional (:774). Lanes B1/B2 state both, so this is a stale body sentence, not a code defect
Riskiest lines
Lite/Services/ArchiveService.cs:717— the new timeout clause inRecoverPendingArchivesAsyncis safe for the scheduled pass, which re-checks the journal at:299, and not forArchiveAllAndResetAsync, which runs the same recovery at:1793with no journal check before exporting every table: an unresolved journal's rows land in a second parquet and are double-counted byv_*— UNPINNEDLite/Services/CollectionBackgroundService.cs:288-291— the size-triggered reset, archival, retention and analysis now run concurrently with a collection sweep rather than strictly after it — pinned for structure byHousekeepingLoopTests.SlowMaintenanceStep_DoesNotDelayTheNextCollectionandTheRealLoops_HoldTheSixSteps_InTheRulesOrder_AndAreStartedBeforeTheCollectionLoopAndAwaitedAtShutdown; the reset-versus-sweep overlap is UNPINNED (acknowledged as #5733)Lite/Services/LocalDataService.SkippedCycles.cs:103-106— four independentarg_maxcalls whereLocalDataService.CollectionHealth.cs:355documents thatarg_maxskips a NULL argument and packs the columns into one struct for it —SkippedCycleSurfaceTests.TheSummary_CountsCyclesAndSlots_AndTheNewestRowSuppliesTheLastFieldsseeds non-null values everywhere, so UNPINNED for this risk
Traps near this diff
- "Lite and Darling ship in parity: a fix to one SKU's reader, tool, alert or description without the counterpart's is a finding, not a nit (#3594)." — does not apply — the mechanism is Lite-only:
CollectionBackgroundService.cs:45is one shared 1-minute grid,DarlingCollectorRunnerschedules per collector and writes nocollection_cyclerow; the description is unchanged and no census comparesget_collection_health's key set across SKUs. The weaker field-shape precedent atMcpHealthTools.cs:656-664is in the top-level comment - "A
total_*assigned from a.Countis a count of the page unless the set is provably whole (#3699)." — applies —run_count = rows.CountatMcpHealthTools.cs:1094now countscollection_cycle/SKIPPEDrows as runs, becauseReadRecentCollectionLogAsync(LocalDataService.CollectionHealth.cs:781) is the one read left outside the nine-predicate set - "Two copies of one definition pinned EQUAL to each other fail only after BOTH have drifted (#3684). Alias, do not copy." — applies — the swap-journal regex is written character-for-character twice in one method,
ArchiveService.cs:851and:891, unpinned - "A roster-based census does not see the file it does not roster; an anti-pattern census sweeps the class (#3699)." — does not apply —
NoFileUnderLite_CopiesTheCollectorNameAsASingleQuotedSqlLiteralsweeps every.cs/.xaml/.sqlunderLite/; only the positive count is a roster - "
CASE WHEN interval > 0 THEN delta / interval ELSE 0 ENDspells 'unknowable' as 0 (#3707)." — does not apply —SkippedCycleSummary.NoneisCount = 0with every other field null (LocalDataService.SkippedCycles.cs:79), stated atMcpHealthTools.cs:725and pinned byTheMcpBlock_CarriesTheContractFields_AndZeroIsARealAnswer - "An alert evaluated every ~30 s against an observation that changes once per COLLECTION re-fires on every cooldown expiry (#3579 → #3628)." — does not apply — not an alert; written once per late wait from the single site at
CollectionBackgroundService.cs:312, withACycleOnTime_AndABackwardClockStep_LogNothingAndWriteNothingpinning the quiet case - "A test that reads the clock twice measures the CI scheduler: one anchor owns both ends of an interval (#3676)." — does not apply —
ArchiveLockBudgetTestsis on the timing-census allow list with its reason, the 20 s bound at:197is the hang guard that reason names, and:307/:313bracket one interval from one anchor
Divergence (blind read vs PR body)
Three places, none a disagreement about what the code does.
The body does not account for roughly half the diff. It opens with "Builds on #5730; merge after it," then grades only #5731's four lanes. The branch carries #5730 too, so the diff against main includes CollectionCycleSkip.cs, LocalDataService.SkippedCycles.cs, the nine collector_name <> 'collection_cycle' predicates across seven LocalDataService.*.cs files, LogSkippedCycleAsync, the skipped_cycles MCP block, the McpInstructions sentence and the new SkippedCyclesSummary TextBlock — none of it mentioned. On a full pass that is my scope whatever its provenance, so I reviewed it, and two of my three riskiest lines sit in it.
Lane A1's RunXIfDueAsync sentence is corrected by lanes B1/B2 rather than updated in place. B1 says the token is not passed yet, and the next bullet says it is and names the test that fails without it. The tree agrees with the later text; nothing to fix in the code.
The body's double-check is more accurate than the code comment it describes. ArchiveService.cs:118 says the reset "keeps its unbounded wait on purpose," true of :1690/:1809/:1884 and false of :1793 and :2034. That gap is where the data-integrity finding lives: the body reasoned about whether a deferred recovery fails the reset (it does not), not about what the reset then exports — the journal's rows, a second time.
Reviewer checklist
- Build
Liteand runLite.Testson Windows; nothing here compiles on Linux, includingint→int?againstArchiveViewRefreshAfterDeleteTests.cs:97/:115whereAssert.Equalnow infersint? - Reproduce the reset-with-surviving-journal path: kill between
WritePendingArchive(ArchiveService.cs:365) and the DELETE, hold a read lock pastWriteLockBudget, force a size-triggered reset, then count rows inv_<table> - Call
get_collection_logagainst a store holding acollection_cycle/SKIPPEDrow and decide whether that row belongs on the page, readingrun_countandoldest_returned_collection_time - Dogfood plan risk 1: watch
LogProcessMemorywhile an archiveCOPYunder the 4 GB limit overlaps a live sweep, which could not happen before this PR - Watch a live run for the hold-watchdog Warning: five seconds against a promote plus DELETE on a large store may be loud enough to want a different number
- Confirm the first housekeeping tick landing one minute after round one delays nothing that mattered at startup, the backfill's cycle-1 due check in particular
…the reset, one swap-journal regex, one struct for the skipped-cycle summary, cycle rows counted apart on the log page (#5731)
Claude posting for Erik Darling. Addressed in c9b6167 and 0a910ad: the reset runs its recovery with an unbounded wait and abandons the reset while an unfinished journal remains; the comment names the three unbounded reset locks; one shared journal-name regex; the skipped-cycle summary reads one row; get_collection_log counts collector rows only and reports skipped_cycle_rows.
Claude posting for Erik Darling.
Fixes #5731. Builds on #5730; merge after it.
Why
Lite's collection loop awaited six housekeeping calls after the collectors (archival, Query Store backfill, retention, findings cleanup, dismissed-alert cleanup, analysis). A long archive, compaction or analysis pass kept the loop from its next slot, so collection slots were skipped (the skips #5730 now reports).
What changes (lane A1: the loops)
CollectionBackgroundService.ExecuteAsyncno longer awaits any housekeeping call. Three loops run through the existingRunPileupCadenceAsync(no new loop type), each ticking every minute (HousekeepingInterval), behind the same gate as before (!IsPaused && !_collectorService.LocalDatabaseIsDown()):TaskCompletionSourceset after round one, and again after thewhileso a stop cannot strand a loop). Nothing used to run before the first round, and the backfill is due on cycle 1.await pileupLoop;kept, thenTask.WhenAll(housekeepingLoops)).RunDueCollectorsAsync,LogProcessMemory, the wait and the Lite logs and records collection cycles it skipped (#5643) #5730 report. ItsLaps shrink toconnection_checkandcollectors.RunXIfDueAsynckeeps its name, signature, due check, stamp and text; the constructor is unchanged.QueryStoreServerGateis the bound now.StartHousekeepingLoops(loops, interval, canRun, firstRound, delay, onFault, token),BuildHousekeepingLoops(), recordsHousekeepingLoop/HousekeepingStep.Not fixed by this PR: the 512 MB size-triggered reset (
ArchiveAllAndResetAsync) still pauses collection for its whole length (#5733). The bounded write-lock waits are lane B's; the housekeeping ledger and theCollectionCycleSkip.Buildmessage change are lane A2.FindingStore: does anything update old rows?
No.
Lite/Analysis/FindingStore.csonly INSERTs intoanalysis_findings(batch insert) and DELETEs (CleanupOldFindingsAsync, rows older than the retention horizon); no UPDATE ofanalysis_findingsexists anywhere underLite/. Cleanup (read-lock DELETE of old rows) overlapping an analysis append (disjoint new rows) is the case the class note andDuckDbInitializer.s_dbLockdoc say was measured safe, so letting the two overlap is fine.Test plan
HousekeepingLoopTests(5): a blocked maintenance step does not delay 5 collection slots (CountSkippedSlots0 each, realWaitForNextCycleAsyncon a fake clock); step order; no tick before the first round; cancel ends all loops, including one still waiting for the first round; a throwing step costs only itself and a gated tick runs nothing; source pin on the real step order, loops started beforewhile,Task.WhenAllat shutdown.CollectionCycleSkipTests.TheLoop_TimesEveryAwaitedStep_AndReportsAfterTheWait: twoLap(calls, noawait Run*IfDueAsyncin the loop body, the loops start beforewhile.await RunRetentionIfDueAsync();put back afterLap("collectors"), the changed Lite logs and records collection cycles it skipped (#5643) #5730 test and the source pin inHousekeepingLoopTestsboth fail (2 of 30 failed); restored and green.*Census*67,-trait Stage=Guard496, 0 failed.*Census*545 (1 skipped),*Ratchet*15, 0 failed.-trait Stage=Guard502 of 502, 0 failed.Housekeeping ledger (lane A2)
Lite/Services/HousekeepingLedger.cs.Time(name, fn)runs a step and records its name, start and length when it ran 1 s or more; the newest 32 are kept. One lock guards every access (three loops write, the collection loop reads). A step that throws is recorded as failed (a stop is not) and the exception goes on unchanged. A step still running is listed too, with its length so far, because the step holding the store when the report is written has not ended yet.RunHousekeepingLoopAsyncruns each step through the ledger (the oneforeachthat knows loop and step names).StartHousekeepingLoopsgained a trailing optionalledgerparameter, so the A1 tests and the seam are unchanged; the service passes its own.CollectionCycleSkip.Buildgained optionalhousekeepingandstoreLockHolderparameters.ReportSkippedSlotsAsyncpassesledger.Overlapping(previousCycleStart, nextCycleStart)and the firstStoreLockHolders.Snapshot()write holder held 1 s or more. With no overlapping entries the message is the old text, byte for byte (pinned), so the earlier pins keep their text; the holder rides with the housekeeping sentence and is never added alone. With entries the message gains:Housekeeping in that window: archival 252s, retention 3s.(the longest four, a repeated name summed, marked "(failed)" or "(still running)"), thenThe store write lock was held by ArchiveService.ExportAsync for 252s when this was written.Durations use the existingFormat(252s, not 4m12s).slowest_itemis unchanged: still the cycle's own slowest step.StoreLockHolders.Snapshot()shows who holds the lock now, not who held it during the gap, so the sentence says "when this was written".Test plan (lane A2)
HousekeepingLedgerTests(11): overlap math at each edge, under 1 s ignored and exactly 1 s kept, failed step recorded and exception rethrown, running step listed, newest 32 kept, three writers plus a timed writer and a reader with no exception and exactly 32 kept, the loop records slow and failed steps,Buildtext with and without entries (and a holder alone adds nothing), longest four / summed name / failed / running / holder, source pin on the wiring. AddedCollectionCycleSkipTests.ASkippedCycle_NamesTheHousekeepingStepsThatOverlappedTheGap_InTheLogAndTheRow(log line, row text,slowest_itemunchanged).ledger.Time(...)taken out of the loop and the entries ignored inBuild, 5 of 37 failed (the new report test, the loop test, the wiring pin, and the twoBuildsentence tests); restored and green.*Census*67,-trait Stage=Guard496, 0 failed.*Census*,*Ratchet*: 569 run, 1 skipped, 0 failed.Double-check
LogProcessMemoryin a dogfood run).SECTION: Fixed
ENTRY: - Lite housekeeping no longer holds up the collection loop ([#5740]) - archival, retention, the findings and dismissed-alert purges, the Query Store backfill and the scheduled analysis ran as steps of the collection loop, so a long one skipped collection slots. They now run on their own loops, once a minute, and start after the first collection round. A size-triggered database reset (#5733) still pauses collection while it runs. When slots are skipped anyway, the warning and the collection_log row now also name the housekeeping steps that overlapped the gap.
Retention budget and hold watchdog
Claude posting for Erik Darling.
RetentionService.CleanupOldArchivesAndRefreshViewsAsyncnow waits for the write lock withLocalDataService.WriteLockBudget(5 s, no second number) through aRetentionService.WriteLockBudgetproperty that a test can shorten. OnStoreLockTimeoutExceptionit logs at Information and returnsnull("not done"; the return type is nowTask<int?>).RunRetentionIfDueAsyncreturns without stamping onnull, so_lastRetentionTimestays due and the next housekeeping tick retries. Only the stamp lines ofRunRetentionIfDueAsyncchanged.LockReleaser.Disposelogs oneAppLogger.Warn("DuckDb", ...)when a WRITE hold lasted overDuckDbInitializer.WriteHoldWarnAfter(5 s default, an instance property so a test can lower it). It names the holder fromStoreLockHolders(newHolderName(id)) and the hold length. Reads pay one timestamp and nothing else.Lite.Tests/HousekeepingLockBudgetTests.cs, 4): a retention timeout leaves the stamp unset and the archive undeleted, and the retry stamps and deletes (plan test 6); the cleanup returns null on timeout and the count otherwise; a write hold past the threshold logs one warning with the holder; a short write hold and a long read hold log nothing. Budgets and thresholds are 30-500 ms; nothing sleeps 5 s.HousekeepingLockBudgetTests4,ArchiveViewRefreshAfterDeleteTests4,HousekeepingClockStepTests20,HousekeepingLoopTests5,HousekeepingLedgerTests11,WriteLockBudgetTests25,TimingTestCensusTests3,GuardStageCensusTests9, all green. Before the merge: the 73 Lite.Tests classes that read the touched files or are Census/Ratchet classes, plus-trait Stage=Guard(496), 0 failures.DuckDbInitializer.cs/CollectionBackgroundService.cs(they read other methods; not built on this laptop), and the full suite.Bounded lock waits (lane B1)
Claude posting for Erik Darling.
The archive's write-lock waits now use a budget:
ArchiveService.WriteLockBudget, which defaults toLocalDataService.WriteLockBudget(5 s shipped, the number the #5371 CHECKPOINT uses). A test sets a shorter one. On a timeout the unit is skipped and retried on a later pass, so a collector is held off by about the budget plus one chunk's hold, not by a read's whole length.ArchiveOldDataAsync) and the recovery of an interrupted export (DeleteArchivedRowsAsync): onStoreLockTimeoutExceptionthe temp parquet is deleted, the table is skipped and the line is Information. The clause sits ahead of the per-tableFailed to archive tableError catch. The rows stay in the table and the next pass takes them. A recovery timeout leaves the journal, so the export skips that table (incomplete pass) until the next pass.ArchiveOldDataAsyncreturnsTask<bool>, true when every table was done. It is false for a skipped unit, a table whose earlier journal could not be finished, or a stop on the token.CompactParquetFilesreturns bool too (false when a lock timed out); both existing call sites and the roughly 30 test call sites that call it as a statement compile unchanged.RunArchivalIfDueAsyncstamps_lastArchiveTimethroughArchiveStampAfterRun(complete, now). True stamps the clock; false stamps 5 minutes (ArchiveRetryDelay) short of due, read by the sameHousekeepingIsDuerule. Only the stamp lines of that method changed.CancellationTokenonArchiveOldDataAsync, checked between tables and before compaction. A stop returns false (not an exception, so shutdown does not log an Error), and the archive views are still rebuilt for tables already promoted. A COPY in flight is not interrupted, and the zero-wait probe of the archive lock and the read connection open takeCancellationToken.Noneon purpose.RunArchivalIfDueAsyncdoes not pass a token yet; its signature is lane A1's, so wiring the loop's stopping token is one argument there.RunArchivalIfDueAsync(CancellationToken cancellationToken = default)hands the housekeeping loop's stop token toArchiveOldDataAsync, so a service stop ends an archive pass between tables and leaves_lastArchiveTimeon the retry stamp. Pinned byRunArchivalIfDueAsync_AStopDuringThePass_EndsItBetweenTables_AndStampsTheRetry, which fails when the token is not passed through.ArchiveAllAndResetAsync's export and restore locks) stay exclusive, and so doesDeleteCollectorStateKeyAsync.Test plan (lane B1)
New
Lite.Tests/ArchiveLockBudgetTests.cs(named so, because lane B2 already hasHousekeepingLockBudgetTests; one timing-census allow-list entry added, the 20 s bound is a hang guard):ArchiveStampAfterRun: complete is a full interval away, incomplete is due in exactly 5 minutesRunArchivalIfDueAsyncthrough reflection, behind a held read: stamp is about 5 minutes from due; then with the lock free, the stamp is the clockAcquireWriteLock(WriteLockBudget)changed back toAcquireWriteLock()everywhere inArchiveService.cs, the promote test fails ("still waiting for the write lock after 30 s: the wait has no budget"), so does the next-pass test, and so does the compaction test. Restored afterwards.ArchiveService.csorArchiveOldDataAsyncplus the housekeeping, store-lock and cycle classes (22 class filters) on the pre-merge commitHousekeepingLockBudgetTests,HousekeepingLoop,HousekeepingLedger,HousekeepingClockStep,CollectionCycle*,ArchiveViewRefreshAfterDelete,ArchiveResetRestoresState,WriteLockBudget, every*Census*and*Ratchet*class and-trait Stage=Guard; one failure there (TimingTestCensusTests, my new class was not on the allow list) was fixed andTimingTestCensusTestsplus both budget classes pass: 16 of 16Double-check
_lastArchiveTimeunstamped (as before), so it retries every tick.ArchiveAllAndResetAsyncruns the interrupted-work recovery unbounded (see Review round 1); only the post-reset compaction stays on the bounded budget, and a timeout there defers it to the next pass without failing the reset.Review round 1
Fixes for review 5498955923 (commits c9b6167, 0a910ad). The ENTRY line above stays true as written.
RecoverInterruptedArchiveWorkAsync,RecoverPendingArchivesAsync,DeleteArchivedRowsAsyncandReplayCompactionSwapJournalstakeunbounded;ArchiveAllAndResetAsyncpasses true, so its recovery waits for the write lock like the reset's other three locks and cannot leave a journal behind because a reader held the lock. The scheduled pass keepsWriteLockBudgetand its journal check. The post-reset compaction stays bounded (a deferred compaction is idempotent; the next hourly pass merges the group), and the comment says so..archive-pendingjournal (the recovery failed for a reason other than the lock) is logged and the attempt is abandoned like a failed export: no file is written, every row stays in the database, and the retry backoff applies. The brief said to skip the table; skipping it and then resetting would have dropped its rows, which is the hazard the reviewer named, so the whole reset waits instead.WriteLockBudgetnames the three unbounded reset locks (unfinished-reset-export cleanup, export of every table, promote/preserve/reset) and says the recovery inside the reset is unbounded too.private static readonly Regex s_swapJournalNameon the class, used at both sites.GetSkippedCycleSummaryAsync: the fourarg_maxcolumns are onestruct_pack, so every "last" field comes from the same newest row.get_collection_log(one server and fleet) still showcollection_cycle/SKIPPEDrows; the two reads carry a comment saying so, and the sentence inCollectionCycleSkip.csnow admits it.run_countcounts collector rows only and the payload addsskipped_cycle_rows. The returned-time span fields stay over the page's rows, cycle rows included (comment in the payload). The tool description is NOT changed here: it is shared byte-for-byte with Darling, so a sentence about the new field and the cycle rows needs a matching Darling change.Tests:
ArchiveLockBudgetTests+2: a journal left by a killed promote, a read held past the budget during the reset: the reset waits, and each row is in the archive files exactly once (50/50 and 20/20 rows/distinct); a journal the recovery cannot delete (read-only) abandons the reset with no new file and a backoff.SkippedCycleSurfaceTests+1: the newest row with NULL step, step time and note returns NULL, not the older row's values.CollectionLogToolTests+2 (one server, fleet): 2 collector rows and 1 cycle row giverun_count2,skipped_cycle_rows1, 3runs, and the oldest-returned time is the cycle row's.ArchiveWatermarkCacheTestsreflection call updated for the new parameter.Revert checks (each restored): reset recovery back to bounded: the held-read test fails; guard disabled: the unfinishable-journal test fails; four
arg_maxcalls: the NULL test fails;run_count = rows.Count: both log-page cases fail.Run (0 warnings, Lite.Tests): 903 tests over 71 classes (every Lite.Tests class that reads a changed file,
ArchiveLockBudgetTests,HousekeepingLockBudgetTests,ArchiveResetRestoresStateTests,SkippedCycleSurfaceTests, the Census classes), two failures found and fixed (a single-quoted collector name in my comment trippedNoFileUnderLite_CopiesTheCollectorNameAsASingleQuotedSqlLiteral; the reflection call above), then re-run green;-trait Stage=Guard496 of 496. Darling.Tests (reads Lite source):McpPayloadContractCensusTestsplus every Census/Ratchet class, 568 run, 0 failed, 1 skipped;skipped_cycle_rowsneeded no roster entry (the census has none forrun_counteither). Not run: the full suite and CI.