Context
Found while implementing #17338, and confirmed by @neo-gpt's review of PR #17475 — which is the reason it is a separate ticket rather than a line in that PR. I added a check that consumed this census as a verdict, the review showed the census cannot support one, and the check was removed. The consumer was repaired; the producer was not.
The Problem
ai/services/memory-core/HealthService.mjs — buildBackupStateBlock(backupPath, fs, path):
const empty = {lastSuccessful: null, lastCompleted: null, count: 0, unusableCount: 0, unverifiedCount: 0};
if (!await fs.pathExists(backupPath)) {
return {...empty};
}
…
if (backupDirs.length === 0) {
return {...empty};
}Two different facts, byte-identical payloads. "I cannot see the backup root" and "I looked and there is nothing in it" are the difference between the plane has no backup capability configured and the plane has backup capability and zero recoverable bundles. A reader cannot recover the distinction from the result, because it is not in the result.
Canonical mc-server carries no backup mount, so on that plane the census reports count: 0, lastSuccessful: null permanently while nothing is wrong.
Why it matters beyond tidiness
This is the same defect class as #17338 itself, one process over. #17338 was "an unread retry observation scored as clean"; this is "an unmeasurable root reported as a measurement." Both hand a consumer a value that looks like evidence and is not.
The concrete cost is already recorded: on PR #17475 I wired a healthcheck verdict to this census — zero bundles vetoing a healthy backup verdict — and it would have degraded every healthcheck on the canonical plane, because "no mount" arrived as "observed empty". @neo-gpt's RA-1: "buildBackupStateBlock() maps the missing path to a present zero object, so the veto falsely treats 'cannot see the root' as 'observed empty'." The consumer-side repair was to delete the veto. The next consumer will hit the same wall, and the honest ones will also have to delete their check.
There is a second, quieter conflation in the same function: the catch path. A backup root that exists but cannot be read (permissions, I/O error) is a third fact, and it currently lands somewhere it should not — worth establishing alongside the first, since any fix that separates absent from empty must also decide where unreadable goes.
The Architectural Reality
ai/services/memory-core/HealthService.mjs (1318 LOC) — buildBackupStateBlock, the producer. fs and path are already dependency-injected, so a fixture can drive all three states without touching a real filesystem.
- The block reaches the healthcheck payload as top-level
backup, beside maintenance.backup — the orchestrator's verdict, derived from task state and receipt and unable to see the mount at all.
- Prior art for the shape this needs: the
maintenance.backup verdict now carries observationStatus: 'observed' | 'partial' (PR #17475, #17338), and stagingResidue in the deployment-state bridge already distinguishes a failed observation from a zero — "an observation that failed reports unreadable with null counts, never a zero." That is the pattern; this census is the surface that lacks it.
The Fix
Give the census an observability field, and let the counts be absent when nothing was counted:
| root state |
today |
proposed |
| path does not exist |
{count: 0, lastSuccessful: null, …} |
observationStatus: 'unavailable', counts null |
| path exists, no bundles |
{count: 0, lastSuccessful: null, …} |
observationStatus: 'observed', count: 0 |
| path exists, read failed |
(see Problem) |
observationStatus: 'unreadable', counts null |
null counts rather than zeros for the two non-readings is the load-bearing half: a zero is a measurement, and the whole defect is zeros standing in for measurements that never happened. Match stagingResidue's existing vocabulary rather than inventing a fourth one.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
backup.observationStatus |
HealthService.buildBackupStateBlock |
New field: observed | unavailable | unreadable |
absent field on an older producer; consumers treat absence as unknown |
function docblock |
three fixtures against the injected fs |
backup.count / unusableCount / unverifiedCount |
same |
null when nothing was counted; unchanged integers when it was |
today's 0 when the field is absent |
same |
a 0 and a null are distinguishable in the payload |
backup.lastSuccessful / lastCompleted |
same |
UNCHANGED — already null for both absent and empty, and observationStatus now says which |
— |
— |
existing arms stay green |
healthcheck backup block |
toolService.composeMemoryCoreHealthcheck |
Passes the richer block through; no verdict change in this ticket |
unchanged |
— |
the composed payload keeps its current status logic |
Decision Record impact
none. A producer-side observability field; no ADR surface.
Acceptance Criteria
Out of Scope
- Re-adding the inventory veto to
maintenance.backup. That was correctly removed; whether it returns is a decision for #17338's successor once this census can support it, and it needs its own evidence.
- Making backups succeed, off-host durability, retention, verification policy.
- The orchestrator-side
maintenance.backup verdict, which is a different process reading different facts.
Avoided Traps
- Do not fix this at a consumer. Two consumers already read this block; a guard in either one would be inferring from a blind instrument, which is what PR #17475 tried and what its review correctly rejected.
- Do not keep
0 and add a boolean beside it. A zero that must be read together with a flag to mean anything is the same conflation with a second field to forget. null makes the absence unusable by accident.
- Do not invent a fourth status vocabulary.
stagingResidue already says unreadable for exactly this; a census that spells it differently teaches two dialects for one fact.
Related
- #17338 / PR #17475 — the consumer-side defect this was found under, and the review that established the census cannot support a verdict.
- #17337 — the same family in the embedding lane: a probe that cannot fail the way its subject fails.
- #17443 — "the census has no second number", adjacent census-honesty work.
Live latest-open sweep: checked latest 20 open issues at 2026-08-21T18:49:34Z, plus keyword sweeps for backup census / buildBackupStateBlock / backup mount unmounted / backup observability — only #17338 returns, which is the close target of the PR that surfaced this and therefore cannot own it. A2A claim scan over the recent window: no competing claim.
Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07
Retrieval Hint: query_raw_memories("buildBackupStateBlock empty object unmounted plane versus empty mount census observability")
Context
Found while implementing #17338, and confirmed by
@neo-gpt's review of PR #17475 — which is the reason it is a separate ticket rather than a line in that PR. I added a check that consumed this census as a verdict, the review showed the census cannot support one, and the check was removed. The consumer was repaired; the producer was not.The Problem
ai/services/memory-core/HealthService.mjs—buildBackupStateBlock(backupPath, fs, path):const empty = {lastSuccessful: null, lastCompleted: null, count: 0, unusableCount: 0, unverifiedCount: 0}; if (!await fs.pathExists(backupPath)) { return {...empty}; // the backup root is NOT THERE } … if (backupDirs.length === 0) { return {...empty}; // the root is there and holds nothing — IDENTICAL object }Two different facts, byte-identical payloads. "I cannot see the backup root" and "I looked and there is nothing in it" are the difference between the plane has no backup capability configured and the plane has backup capability and zero recoverable bundles. A reader cannot recover the distinction from the result, because it is not in the result.
Canonical
mc-servercarries no backup mount, so on that plane the census reportscount: 0, lastSuccessful: nullpermanently while nothing is wrong.Why it matters beyond tidiness
This is the same defect class as #17338 itself, one process over. #17338 was "an unread retry observation scored as clean"; this is "an unmeasurable root reported as a measurement." Both hand a consumer a value that looks like evidence and is not.
The concrete cost is already recorded: on PR #17475 I wired a healthcheck verdict to this census — zero bundles vetoing a
healthybackup verdict — and it would have degraded every healthcheck on the canonical plane, because "no mount" arrived as "observed empty". @neo-gpt's RA-1: "buildBackupStateBlock()maps the missing path to a present zero object, so the veto falsely treats 'cannot see the root' as 'observed empty'." The consumer-side repair was to delete the veto. The next consumer will hit the same wall, and the honest ones will also have to delete their check.There is a second, quieter conflation in the same function: the
catchpath. A backup root that exists but cannot be read (permissions, I/O error) is a third fact, and it currently lands somewhere it should not — worth establishing alongside the first, since any fix that separates absent from empty must also decide where unreadable goes.The Architectural Reality
ai/services/memory-core/HealthService.mjs(1318 LOC) —buildBackupStateBlock, the producer.fsandpathare already dependency-injected, so a fixture can drive all three states without touching a real filesystem.backup, besidemaintenance.backup— the orchestrator's verdict, derived from task state and receipt and unable to see the mount at all.maintenance.backupverdict now carriesobservationStatus: 'observed' | 'partial'(PR #17475, #17338), andstagingResiduein the deployment-state bridge already distinguishes a failed observation from a zero — "an observation that failed reportsunreadablewith null counts, never a zero." That is the pattern; this census is the surface that lacks it.The Fix
Give the census an observability field, and let the counts be absent when nothing was counted:
{count: 0, lastSuccessful: null, …}observationStatus: 'unavailable', countsnull{count: 0, lastSuccessful: null, …}observationStatus: 'observed',count: 0observationStatus: 'unreadable', countsnullnullcounts rather than zeros for the two non-readings is the load-bearing half: a zero is a measurement, and the whole defect is zeros standing in for measurements that never happened. MatchstagingResidue's existing vocabulary rather than inventing a fourth one.Contract Ledger Matrix
backup.observationStatusHealthService.buildBackupStateBlockobserved|unavailable|unreadablefsbackup.count/unusableCount/unverifiedCountnullwhen nothing was counted; unchanged integers when it was0when the field is absent0and anullare distinguishable in the payloadbackup.lastSuccessful/lastCompletednullfor both absent and empty, andobservationStatusnow says whichbackupblocktoolService.composeMemoryCoreHealthcheckDecision Record impact
none. A producer-side observability field; no ADR surface.Acceptance Criteria
nullfor the two non-readings and integers for the reading. Control: a genuinely empty root still reportscount: 0, so the fix does not simply stop counting.pathExistsreturnsfalseand one whosereaddirreturns[]produce identical blocks onmainand differentobservationStatusafter. Two fixtures that differ in some other field would pass both ways and prove nothing.stagingResidue's existingunreadablerather than adding a fourth spelling.Out of Scope
maintenance.backup. That was correctly removed; whether it returns is a decision for #17338's successor once this census can support it, and it needs its own evidence.maintenance.backupverdict, which is a different process reading different facts.Avoided Traps
0and add a boolean beside it. A zero that must be read together with a flag to mean anything is the same conflation with a second field to forget.nullmakes the absence unusable by accident.stagingResiduealready saysunreadablefor exactly this; a census that spells it differently teaches two dialects for one fact.Related
Live latest-open sweep: checked latest 20 open issues at 2026-08-21T18:49:34Z, plus keyword sweeps for
backup census/buildBackupStateBlock/backup mount unmounted/backup observability— only #17338 returns, which is the close target of the PR that surfaced this and therefore cannot own it. A2A claim scan over the recent window: no competing claim.Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07
Retrieval Hint:
query_raw_memories("buildBackupStateBlock empty object unmounted plane versus empty mount census observability")