fix(persist): spill to side file on rename failure + per-entry compress invalid reasons - #172
Conversation
…ing data Windows file locks (AV/indexer) held long enough that the ~120ms rename retry window always exhausted; each failed persist then unlinked the temp file, permanently losing that round of session state (billion-context#362: 4,879 EPERM lines, 117 sessions, top sessions never landed on disk). - exponential backoff retry (default 6 attempts, 50ms base, 1600ms cap) - on final rename failure, spill the envelope to <name>.fb.json (one slot per id, overwritten) so the data is on disk; loadAll discovers spills and reconciles canonical vs spill by savedAt; a successful canonical write removes the now-stale spill of the same id - rate-limited failure alerting (1x error, then warn at powers of two) instead of one error line per failed write The store still never deletes a record's data; it only removes a temp file it itself created or a stale spill superseded by a newer canonical.
Add diagnostics.invalidReasons (one index-prefixed, human-readable reason per dropped content entry: missing range bounds / missing summary / not an object). Hosts can surface the specific reason to the model so it retries with a corrected range instead of blind-retrying the same invalid payload (billion-context#362: 26 no-valid-ranges rejections, 5x blind retries).
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)In your adapter project: npm install acp-kernel@pr-172Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf acp-kernel-pr172.tgz
npm install ./packageThis comment is automatically updated on each push. |
[bot] Acknowledged — reviewing PR #172 now. I'll check out |
[bot] Review #1 (ework-daemon) — full review of Pre-flight (re-ran everything myself)
Findings1. [MAJOR — design inconsistency, no data loss] Sync vs async spill semantics diverge
Consequences:
Suggestion: reject only when both canonical and spill failed (or at minimum, don't error-log in 2. [MINOR] 3. [MINOR] The top-level single-range path ( 4. [MINOR — test gap] No dedicated test for "canonical fresher than spill → Verified correct (spot-checked, no issues)
Observations (pre-existing, out of scope)
Verdictapprove-with-nits — no data-loss path found in either the persist or parse changes; finding #1 is worth a small follow-up (or an explicit doc note) before merge. |
[bot] Review #2 (independent agent) — second review complete; this satisfies the ≥2-agent requirement from AGENTS.md. Reviewer also independently re-verified: typecheck clean, 548/548 tests pass. Agrees with Review #1 on the 1. [MINOR] 2. [MINOR] Spill write is not atomic — 3. [MINOR] Test gaps (core paths are well covered — spill-on-failure, both reconciliation directions, alert counts, counter reset, sync spill — but):
4. [NIT] 5. [NIT] Sync/async retry asymmetry — the sync loop retries 6. [NIT] 7. [NIT] Formatting regression — 8. [NIT] Verified OK (both reviewers concur)Backoff math (1550ms window, no sleep after last attempt, constructor clamps); error-code filtering on both paths (EPERM/EBUSY/EACCES only, non-Error throws treated as non-retryable); Combined verdict: approve-with-nits (2/2)Suggested pre-merge items (all small, none blocking):
I will not merge — per AGENTS.md, PR merge is human-only: #172 |
Addresses billion-context#362 (Windows persist EPERM storm + compress no-valid-ranges).
1. Persist: stop silently dropping session data on rename failure (main)
Root cause. On Windows, file locks (AV real-time scan / indexers) are held long enough that the ~120ms rename retry window always exhausted. Each failed persist then unlinked the temp file — permanently losing that round of session state. In the field: 4,879 EPERM lines, 117 sessions, and the top-3 failing sessions (2062/271/214 failures) had no file on disk at all — never persisted. All silent except the error log lines.
Fix (
src/persist/store.ts):retryAttempts/retryBaseMs/retryMaxMs.<name>.fb.json(one slot per id, overwritten) so the data is on disk.loadAlldiscovers spills and reconciles canonical vs spill bysavedAt(freshest wins); a successful canonical write removes the now-stale spill of the same id.The store still never deletes a record's data; it only removes a temp file it itself created or a stale spill superseded by a newer canonical write.
2. Compress: report per-entry invalid reasons (secondary)
Root cause. When a compress call is rejected with
no-valid-ranges, hosts only knewinvalidItems=N, not why each entry was invalid — so the model blind-retried the same payload (5x in the field).Fix (
src/parse-compress-input.ts): adddiagnostics.invalidReasons— one index-prefixed, human-readable reason per dropped entry (missing range bounds/missing summary/not an object). Hosts can surface the specific reason to the model so it self-corrects.Pre-flight
npm run typecheck— cleannpm test— 548 pass, 0 fail (+8 new: 6 persist spill/retry/alert, 2 compress reasons)npm run build— successNote for billion-context
This ships as a kernel release; billion-context must bump
acp-kernelto the new version (after it is live on npm) to pick up the persist fix, and can then useinvalidReasonsin its compress error message. The spill files end in.jsonso existingloadAlldiscovery and the billion-contextrelPathFor/flatFileNameForreconciliation both pick them up with no proxy-side change.Requires review by ≥2 agents per acp-kernel AGENTS.md.