fix(hooks): ISASync refreshes the ISA body hash on Read - #2130
Open
pai-scaffolde wants to merge 1 commit into
Open
pai-scaffolde wants to merge 1 commit into
pai-scaffolde wants to merge 1 commit into
Conversation
Finished ISAs (`phase: complete`) were rewound to `learn` by edits that changed only frontmatter. syncToWorkJson() rewinds when the registry's `bodyHash` differs from the file's body, but that hash is written only by the same sync, which fires on Write/Edit/MultiEdit. Any write that bypasses those tools — a Bash heredoc, a subagent, git, or the hook's own appended Decisions row — leaves the hash stale, so the next edit of any kind reads as a body change, including a frontmatter-only one that hashBody() excludes. ISASync's Read branch now calls the new refreshBodyHashBySlug(), so the registry's view of the body matches the file the model just read and the following Edit measures only its own delta. Read still never writes the ISA, only the registry's observation of it, keeping the v6.9.0 contract. ISASync is now registered under the PostToolUse Read matcher, which its own header already documented as a trigger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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.
Reproduced on a fresh LifeOS 7.40.4 install (macOS, Claude Code in the Claude desktop app); the fix was applied to that install and run there before filing. Related: #2079 / open PR #2082 describe the same missing observation from the Bash side; this change is independent (different files) and closes the rewind-specific consequence even where a Bash write is never relayed.
Observed
Finished ISAs (
phase: complete) rewound themselves tolearnafter edits that changed only frontmatter.LIFEOS/MEMORY/OBSERVABILITY/isa-rework.jsonlon the install, 2026-09-14 (fields trimmed):body_delta_bytes: 0is the tell: the file did not change length at all and the auto-rewind still fired, bumpediteration, wroteresumed_at/resumed_from_phaseback into the frontmatter and appended aD-auto-…Decisions row to a finished artifact.had_legacy_bodyhash: falserules out the "no hash yet" path — a hash was present and simply did not match.Root cause
syncToWorkJson()inhooks/lib/isa-utils.tsgates the v6.9.0 Resume After Complete rewind onexisting.bodyHashis only ever written by that same sync, and the sync only runs fromISASync.hook.tsunder the Write, Edit and MultiEdit PostToolUse matchers. Anything that writes an ISA outside those tools — a Bash heredoc, a subagent, agit checkout, or the rewind's own appended Decisions row — leaves the registry's hash describing a body that is no longer on disk.From then on the hash is stale for good, so the next edit of any kind compares a fresh body hash against a stale one and reads as a body change. That includes a frontmatter-only edit, which
hashBody()deliberately excludes from the hash: it strips the frontmatter block before hashing, so bumping anupdated:stamp cannot change the body hash and still rewound the ISA, because the stored hash was wrong to begin with.Fix
Three parts, no change to the rewind rule itself:
hooks/lib/isa-utils.tsgainsrefreshBodyHashBySlug(slug, isaPath)— reads the ISA from disk, rehashes the body with the samehashBody(), and updatesbodyHash/lastBodySizein the registry. It writes nothing if the hash already matches, and it never touches the ISA file or its phase.hooks/ISASync.hook.tscalls it from the existingReadbranch, before the heartbeat bump. The Edit tool requires a Read first, so after a Read the following Edit'sbodyChangedmeasures only that edit's own delta.hooks/hooks.jsonregistersISASync.hook.tsunder the existing PostToolUseReadmatcher, alongsideISAStaleWriteGuard.hook.ts. The hook's header already documentedReadas a trigger and the branch already existed; only the registration was missing.Read never mutates the ISA file. The v6.9.0 contract ("Read NEVER writes back to the file — only Write/Edit/MultiEdit do") is kept exactly. What Read now updates is the registry's observation of the body, which is bookkeeping about the file, not the file.
How tested
Probe in a throwaway tree,
HOMEandLIFEOS_DIRpointed at a temp dir. Fixture: an ISA withphase: completeplus awork.jsonrow for its slug carrying a deliberately stalebodyHash(all-zeroes), which is what an unobserved write leaves behind. The hook is fed PostToolUse JSON on stdin:Three cases, run against upstream/main's files and against this branch's files:
phase=learnphase=learnphase=learnphase=completephase=learnphase=learnB is the defect and is fixed. C shows the rewind contract still holds for a genuine body change. A is unchanged by design: nothing has observed the body, so nothing can tell a real change from a stale hash — in the harness an Edit is always preceded by a Read, which is case B.
Case B's observability log agrees: upstream/main emits
prev=complete new=learn trigger=edit body_delta_bytes=171; this branch emits no row at all.Read-only invariant, same probe:
The file is byte-identical; only the registry moved.
git diffshows nowriteFileSyncon the Read path.Both changed files transpile:
bun build --target=bun --no-bundle hooks/ISASync.hook.tsand… hooks/lib/isa-utils.ts, exit 0.hooks.jsonparses.Against open PR #2082, which also edits
hooks.json(it appendsBashWriteRelay.hook.tsto theBashmatcher at a different block):git apply --checksucceeds in both orders — #2082 on top of this branch, and this branch on top of #2082.Repro on a clean tree: check out
upstream/main, create a tempHOMEwithLIFEOS/MEMORY/WORK/<slug>/ISA.mdatphase: completeandLIFEOS/MEMORY/STATE/work.jsonholding that slug with anybodyHashthat does not match the file. Fire the Read event above, then change only theupdated:line, then fire the same JSON with"tool_name":"Edit". The ISA comes back asphase: learnwithiterationbumped and aD-auto-…row appended. Apply this patch and repeat: it stayscomplete.🤖 Generated with Claude Code