Skip to content

Stop reporting stale compressed hashes as damage - #2

Merged
PEEKPerformer merged 2 commits into
mainfrom
fix/stale-compressed-hash
Sep 17, 2026
Merged

PEEKPerformer merged 2 commits into
mainfrom
fix/stale-compressed-hash

Conversation

@PEEKPerformer

Copy link
Copy Markdown
Owner

Stacked on #1. Review that one first; this PR's diff is only the second commit.

What this PR does

The rolling-sweep fsck no longer calls a deposit damaged when only its recorded sha256_compressed is stale and the content still matches the canonical sha256. A new scripts/repair_compressed_hashes.py rewrites the stale values, and pure migrations stop carrying them forward.

Why

On a real archive, 1,832 of 5,315 compressed deposits fail the fsck with ARCHIVE INTEGRITY FAILURE. Every one decompresses to exactly its canonical sha256, so the content is intact. All 1,832 were deposited on 2026-05-23 by v2.1.0 and migrated by v2.3.0.

The v2.3.0 pure-migration path copies sha256_compressed from the prior manifest instead of hashing the file, so a stale value survives. The fsck compares only the compressed hash. Each sweep slice therefore reported about 40 false failures, enough to hide a real one.

  • Migration: hash the .zst file on disk.
  • Fsck: when the compressed hash disagrees, decompress and compare the canonical hash. A match counts as stale-hash, not a failure. Real damage is reported as before.
  • Repair script: rewrites only the sha256_compressed string, only after the canonical hash matches, in one repair: commit. It takes the ingest lock. --dry-run reports without writing. Damaged deposits are listed, never rewritten, and make it exit 1.

Type of change

  • Bug fix (non-breaking change that fixes a documented or observed defect)
  • New feature (non-breaking change that adds capability)
  • Breaking change (alters existing behavior; requires major-version bump and CHANGELOG entry)
  • Documentation only
  • Performance improvement (no functional change)
  • Internal refactor (no user-visible change)
  • CI / tooling

Non-negotiables check

  • Forensic completeness (no filtering of transcript content)
  • Provenance via hash chain (SHA-256 per session, verifiable with stock Unix tools)
  • Git is the archive substrate
  • Environment capture (host CLI version, OS, holotype version, project-repo git state)
  • Append-only by design
  • No silent network behavior

Append-only, for discussion: the repair script rewrites a manifest in place without a manifest_version bump. It changes one derived field (the hash of the file as stored) and only when the canonical hash proves the content is unchanged. Transcripts and the ledger, which chains canonical sha256 values only, are untouched. The history keeps both values in a repair: commit. The alternative is bumping MANIFEST_VERSION, which migrates every manifest and has to read every source transcript, including ones iCloud has evicted.

Selftest

  • python scripts/selftest.py passes locally
  • If this PR adds a feature or fixes a bug, a new selftest assertion exercises the change
  • CI is green (GitHub Actions workflow on this PR)

New step: a migration over a stale hash records the real one, the fsck reports a stale hash as stale, the repair commits the real value, and a truly damaged .zst is still a failure that the repair refuses to touch.

Manifest / archive-format impact

  • No archive-format change
  • manifest_version bumped from N to N+1
  • index_schema_version bumped from N to N+1
  • On-disk archive layout changed (describe migration path)

Documentation

  • CHANGELOG.md updated (under the next-release section, 2.4.1)
  • README.md updated if user-facing behavior changed
  • SKILL.md updated if wizard flow changed
  • docs/ updated as appropriate

Cross-platform impact

  • Verified on macOS
  • Verified on Linux (CI)
  • Verified on Windows (not applicable: no Windows runner configured)
  • CI passes on all configured runners

AI assistance

Diagnosis, fix, and draft written by Claude Code, from the sweep logs and a full pass over the real archive's manifests. Selftest run locally.

Anything else reviewers should know

I haven't found the exact event that made the v5 manifests stale in the first place. The archive is a shallow clone that starts after May, so git history can't show it. This PR fixes the symptom and stops migrations from spreading it.

git runs `git maintenance run --auto --detach` after every commit.
Once the loose-object count passes gc.auto, each commit starts its own
detached full repack, and nothing stops a second one from starting
while the first is still running. An archive's pack is mostly zstd or
git-crypt blobs, so each repack holds gigabytes. One ingest tick makes
one commit per changed session, which stacked repacks until a 16 GB
Mac with a 7.3 GiB archive ran out of memory and had to be
force-restarted.

Set maintenance.auto=false and gc.auto=0 in the archive's local git
config. init.py sets them after `git init`. ingest.py sets them under
the ingest lock before the first commit, which migrates existing
archives on their next tick. Loose objects now wait for a manual
`git repack -d`.

The selftest checks both the init path and the migration path.
On a real archive 1,832 compressed deposits failed the sweep fsck,
yet every one decompressed to its canonical sha256. The v2.3.0 pure
migration copied sha256_compressed from the prior manifest instead of
hashing the file, so stale values survived, and the fsck compares only
the compressed hash. About 40 false failures per sweep slice buried
any real one.

Hash the file during a pure migration. In the fsck, fall back to the
canonical hash when the compressed one disagrees and count a match as
stale, not failed. Add scripts/repair_compressed_hashes.py to rewrite
the stale values in one commit, touching only that field and only
after the canonical hash matches.
@PEEKPerformer
PEEKPerformer changed the base branch from fix/disable-auto-maintenance to main September 17, 2026 12:42
@PEEKPerformer
PEEKPerformer merged commit f5b8b6f into main Sep 17, 2026
@PEEKPerformer
PEEKPerformer deleted the fix/stale-compressed-hash branch September 17, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant