fix(memory): recover stale write locks instead of jamming a note forever - #1646
Open
elhoim wants to merge 1 commit into
Open
fix(memory): recover stale write locks instead of jamming a note forever#1646elhoim wants to merge 1 commit into
elhoim wants to merge 1 commit into
Conversation
MemorySystem.appendToTierBFile took a `<note>.lock` with O_EXCL and removed it in a `finally`, with no staleness check. Memory writes run in hook-spawned subprocesses; one killed after the lockfile was created but before the finally ran left the lock on disk permanently, so every later write to that note returned EWRITE_FAILED "Lock held" forever. Nothing reported it, so the note silently stopped accepting memory. Add MemoryLock.ts, a crash-safe lock that decides staleness on evidence: the holder stamps pid + host into the lockfile, and a contender that finds the lock held probes the holder with kill(pid, 0). A holder the kernel says is gone is stale immediately, no TTL wait. When liveness cannot be established -- empty or unparseable stamp, or a holder recorded on another host -- it falls back to the same LOCK_STALE_MS age rule DerivedSync.ts already uses. Every ambiguous probe resolves toward "alive", so a live holder is respected rather than trampled by a second concurrent writer. Recovery breaks the lock by renaming it aside and re-creating with O_EXCL, re-checking the inode it judged stale, so concurrent recoverers cannot both end up holding it. Recovery and refusal are both appended to MEMORY/OBSERVABILITY/memory-locks.jsonl and echoed to stderr, so a jammed note is now visible and distinguishable from a clean run. Tests: test/tools/MemorySystem.test.ts -- 6 of the 8 fail against the previous implementation.
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.
The problem
The per-note write lock in
MemorySystem.tswasopenSync(lockPath, "wx")with removal in afinally, and no staleness check, TTL, or liveness test.Memory writes run inside hook-spawned subprocesses. If the harness times one out, or the machine loses power, after the lock file is created but before the
finallyruns, the.locksurvives on disk. Every later write to that note then returns:Forever. There is no self-heal, and nothing surfaces that error, so the outcome is silent permanent memory loss for that note. The operator is never told their memory system stopped accepting writes for that entry.
Two files in this tree already solve exactly this —
DerivedSync.tswith aLOCK_STALE_MSconstant andrmSyncrecovery, andPromptProcessing.hook.tswith aLOCK_STALEwindow. This site simply did not use the pattern.The fix
Liveness first, age as fallback. The lock file now records the holder's pid, host and timestamp. On contention:
kill(pid, 0)says it is gone → recover immediately, no waiting out a TTL.LOCK_STALE_MS.Every unknown resolves toward "assume alive", so the failure mode is a delayed write rather than two writers corrupting one note.
LOCK_STALE_MSmatchesDerivedSync.ts's constant rather than introducing a third number.Failures stop being silent. Every lock failure and every recovery is written to
MEMORY/OBSERVABILITY/memory-locks.jsonland to stderr, with a reason code:holder-process-gone,holder-alive,unverifiable-holder-within-ttl,expired-unverifiable-holder. An operator can now distinguish "recovered from a crash" from "nothing ever went wrong" from "still jammed".Tests
test/tools/MemorySystem.test.ts, 8 cases:Separately verified with an independent probe. The dangerous direction here is not failing to recover a dead lock — it is breaking a live one, which would put two writers on the same note — so that case got equal weight:
Note for review
A holder that is alive but has held the lock longer than
LOCK_STALE_MSis still broken, on the reasoning that a five-minute append is itself a failure. That is a deliberate trade-off rather than an oversight, and it is the one case where this can interrupt a genuinely running writer. Worth a second opinion on the window.