Summary
In LifeOS/install/hooks/lib/isa-utils.ts, syncToWorkJson() computes incomingBodyHash from the body it read. When the Resume-After-Complete rewind fires, it appends a D-auto-* Decisions row to the body and writes that to disk — but it still persists the pre-append hash to work.json.
The stored bodyHash therefore describes a body that no longer exists on disk. A later sync reads that mismatch as a fresh bodyChanged — a change the rewind itself caused.
Invariant violated
After any sync, the persisted bodyHash must describe the body actually on disk.
Where
// ~726 — hash of the body as read
const incomingBodyHash = content ? hashBody(content) : (existing.bodyHash || "");
// ~750/751 — the rewind mutates the body and writes the mutated version
updated = appendDecisionRow(updated, timestamp, newIteration);
writeFileSync(isaPath, updated);
// ~931 — but this persists the PRE-append hash
...(incomingBodyHash ? { bodyHash: incomingBodyHash, lastBodySize: content ? content.length : 0 } : {}),
Fix (2 lines)
// 726: const -> let
let incomingBodyHash = content ? hashBody(content) : (existing.bodyHash || "");
// right after writeFileSync(isaPath, updated):
incomingBodyHash = hashBody(updated);
Test
Regression test asserting the invariant (there were no tests under hooks/lib yet):
RED (fix disabled): (fail) REGRESSION: after a rewind, the persisted bodyHash describes the body on disk
Expected: <sha256 of the body on disk>
Received: <sha256 of the pre-append body>
GREEN (fix applied): 3 pass, 0 fail
Scope, honestly
The broken invariant is proven by the test. What I could not reproduce is a full causal chain from it to a user-visible symptom: one of my ISAs bounced from complete back to learn on every close attempt, bumping iteration each time. A test replicating that exact sequence passes with and without the fix, so that symptom likely has an additional factor I have not isolated (multiple sessions / a restart in between are candidates).
What the evidence does support: the invariant is violated on every rewind, deterministically. In a single install I count 15 rewind events across 9 different slugs, so the path is well travelled — the damage is just usually invisible, because most ISAs re-sync their hash on the next body edit and close fine.
Happy to open a PR with the fix + test if useful.
Summary
In
LifeOS/install/hooks/lib/isa-utils.ts,syncToWorkJson()computesincomingBodyHashfrom the body it read. When the Resume-After-Complete rewind fires, it appends aD-auto-*Decisions row to the body and writes that to disk — but it still persists the pre-append hash towork.json.The stored
bodyHashtherefore describes a body that no longer exists on disk. A later sync reads that mismatch as a freshbodyChanged— a change the rewind itself caused.Invariant violated
After any sync, the persisted
bodyHashmust describe the body actually on disk.Where
Fix (2 lines)
Test
Regression test asserting the invariant (there were no tests under
hooks/libyet):Scope, honestly
The broken invariant is proven by the test. What I could not reproduce is a full causal chain from it to a user-visible symptom: one of my ISAs bounced from
completeback tolearnon every close attempt, bumpingiterationeach time. A test replicating that exact sequence passes with and without the fix, so that symptom likely has an additional factor I have not isolated (multiple sessions / a restart in between are candidates).What the evidence does support: the invariant is violated on every rewind, deterministically. In a single install I count 15 rewind events across 9 different slugs, so the path is well travelled — the damage is just usually invisible, because most ISAs re-sync their hash on the next body edit and close fine.
Happy to open a PR with the fix + test if useful.