fix: recover from corrupt metadata.json on disk full (ENOSPC) - #101
Open
Brian Krabach (bkrabach) wants to merge 1 commit into
Open
fix: recover from corrupt metadata.json on disk full (ENOSPC)#101Brian Krabach (bkrabach) wants to merge 1 commit into
Brian Krabach (bkrabach) wants to merge 1 commit into
Conversation
Problem: Sessions running when the disk filled up logged endless repeating errors
(json.decoder.JSONDecodeError on empty metadata.json). Root cause is two bugs:
1. Non-atomic metadata writes. All four metadata writers used Path.write_text(),
which truncates the file to 0 bytes THEN writes. When the disk was full (ENOSPC),
the truncate succeeded but the content write failed, leaving metadata.json
permanently empty.
2. No tolerance for corrupt metadata. _touch_last_event_at reads-then-writes:
json.loads('') throws every event before reaching the write that would repair
it, so the error repeats forever. _ensure_metadata only recreates *missing*
files, not 0-byte ones that still "exist".
Solution (logging_handler.py):
- Added import os.
- New helper _read_metadata() returns None for missing/empty/corrupt/non-dict JSON
instead of raising, so callers rebuild from defaults.
- New helper _atomic_write_text() writes to a temp file then os.replace(); on
failure it leaves the existing file untouched and cleans up the temp file.
- Rewrote all four metadata functions to read tolerantly and write atomically.
_touch_last_event_at now self-heals: corrupt metadata.json is rebuilt from
defaults in-place, stopping the error loop.
Testing: 10 new tests in test_logging_handler_metadata_recovery.py covering:
- _read_metadata tolerance for all error cases
- atomic-write-leaves-original-intact-on-simulated-ENOSPC
- end-to-end self-heal of empty/corrupt metadata.json mid-session and after restart
All tests pass (639 + 10 new). Dogfooded against 4 real corrupted sessions on
this machine — all now valid JSON, 0 zero-byte files remaining.
Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Diego Colombo (colombod)
added a commit
that referenced
this pull request
Aug 26, 2026
…isible fail-loud alerts Stacked on #101 (atomic metadata writes + self-heal). Adds the resilience/UX layer on top: - ENOSPC/EDQUOT circuit breaker: skip disk writes for a capped exponential-backoff cooldown (5s..300s), then probe for recovery, instead of hammering a full disk on every event. Composes with #101 because its atomic writer re-raises OSError so this layer can classify ENOSPC. - Fail loud to the user via HookResult.user_message (bypasses the unwritable log file), severity matched to reality: PERMANENT DATA LOSS (error) when the event reached no sink (disk full and no destination / queue also full), a milder warning when still delivered to the server, and a one-shot info on recovery. - enqueue() now returns whether the event was queued so the handler distinguishes delivered from lost; network dispatch stays independent of disk state. Related: microsoft-amplifier/amplifier-support#492. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Diego Colombo (colombod)
added a commit
that referenced
this pull request
Aug 27, 2026
…isible fail-loud alerts Stacked on #101 (atomic metadata writes + self-heal). Adds the resilience/UX layer on top: - ENOSPC/EDQUOT circuit breaker: skip disk writes for a capped exponential-backoff cooldown (5s..300s), then probe for recovery, instead of hammering a full disk on every event. Composes with #101 because its atomic writer re-raises OSError so this layer can classify ENOSPC. - Fail loud to the user via HookResult.user_message (bypasses the unwritable log file), severity matched to reality: PERMANENT DATA LOSS (error) when the event reached no sink (disk full and no destination / queue also full), a milder warning when still delivered to the server, and a one-shot info on recovery. - enqueue() now returns whether the event was queued so the handler distinguishes delivered from lost; network dispatch stays independent of disk state. Related: microsoft-amplifier/amplifier-support#492. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: microsoft-amplifier/amplifier-support#492
Summary
On sessions that were running when the disk filled up (ENOSPC), logging_handler.py produced endless repeating errors:
Root cause: Non-atomic metadata writes combined with no tolerance for corruption.
Path.write_text()truncates to 0 bytes THEN writes. When disk was full, truncate succeeded but content write failed, leavingmetadata.jsonpermanently empty._touch_last_event_at()reads-then-writes:json.loads("")throws every event before reaching the write that would repair it, so the error repeats forever._ensure_metadata()only recreates missing files; a 0-byte file "exists" so it's never regenerated.The fix:
_read_metadata()helper that returnsNonefor missing/empty/corrupt/non-dict JSON instead of raising, allowing callers to rebuild from defaults._atomic_write_text()helper that writes to a temp file thenos.replace(); on failure leaves the original untouched and cleans up the temp file (never truncates to 0 bytes)._touch_last_event_at()now self-heals: a corruptmetadata.jsonis rebuilt from defaults in-place, stopping the error loop.Dogfooded against 4 real corrupted sessions on this machine — all now valid JSON, 0 zero-byte files remaining.
Scope / guardrails
This change is pure internal logic to
logging_handler.py(file I/O and error handling). No seams crossed:The existing logging_handler test suite runs against the same code paths and proves existing contracts are unbroken.
Verification
modules/hook-context-intelligence/handlers: 639 passedtests/: 0 failures (integration tests do not exercise this code path, which is expected)ruff check+ruff format --checkclean — ranuv run ruff checkanduv run ruff format --checkon changed files, no issuespyrightclean — ranuv run pyrighton changed files, 0 errorsscripts/validate-full.sh, validation_mode: full, overall PASS (the lone mode "error" is the documented false positive in AGENTS.md, confirmed)Evidence:
Real evidence on seams (not mock-only)
N/A — no seam crossed. All changes are internal to
logging_handler.py; the module's public contract (the four metadata functions that the rest of the bundle calls) remains unchanged. Existing unit tests prove the contract unbroken.Docs & diagrams
bundle.dot/bundle.pngunchanged (no bundle structure change)Notes / follow-ups
The root cause analysis in the parent session confirmed this is the only disk-full corruption case. However, this fix also hardens the code against any other source of empty/corrupt
metadata.json(e.g. filesystem errors, interrupted writes in other tools). The self-heal path means future corruptions will repair automatically on the next event.