Both bugs make a status line wrong while the underlying state is right. I searched open and closed issues for renderSystemLine, system-surfaces, LastResponseCache and DriftReminder and found no report of either.
Part A: renderSystemLine() keeps the first detail per label, so the ISA delta is stale
hooks/lib/system-surfaces.ts renderSystemLine() deduplicates entries by label with:
if (!prior || (!prior.detail && e.detail)) byLabel.set(e.label, e);
The comment says it keeps "the richest detail", but the test keeps the first entry that has any detail. Entries arrive in write order, and SystemChangeSurface.hook.ts gives every ISA write a detail, either N closed or first→closed. So when a turn writes the same ISA more than once, the first write wins. An ISA scaffolded with 0 of 6 closed and finished at 6 of 6 in the same turn renders as 0 closed.
Repro: in one turn, write an ISA with no claims closed, then write it again with all claims closed. The ⚙️ SYSTEM line reports 0 closed.
Fix that works here: let the latest detail win.
if (!prior || e.detail) byLabel.set(e.label, e);
An entry without a detail still cannot displace one that has it.
Caveat: covered by a unit test on renderSystemLine() here. I have not yet seen the corrected line render live on a multi-write ISA turn.
Part B: LastResponseCache keeps only the first 2000 characters, so the closer is cut off
hooks/LastResponseCache.hook.ts writes lastResponse.slice(0, 2000) to MEMORY/STATE/last-response.txt. hooks/DriftReminder.hook.ts reads that file and sets closer: /🗣️/.test(text). The closer is always the last line of a reply, so any reply over 2000 characters loses it in the cache, and the next turn is told the last response had "no closer" when it had one. Long replies are the ones most likely to carry real work, so the false flag lands where it costs the most trust.
Repro: send a reply longer than 2000 characters that ends with the 🗣️ closer. The next turn's format reminder reports "no closer".
Fix that works here: raise the cap, and past it keep the head and the tail so both the banner and the closer survive.
const CAP = 20000;
const cached = lastResponse.length <= CAP
? lastResponse
: `${lastResponse.slice(0, CAP / 2)}\n[…]\n${lastResponse.slice(-CAP / 2)}`;
writeFileSync(cachePath, cached, 'utf-8');
Caveat: covered by two unit tests here (a reply over 2000 characters keeps its closer; a reply over the cap keeps banner and closer). I have not yet confirmed it live on a reply over 2000 characters. The only other reader is hooks/SatisfactionCapture.hook.ts. It slices its own 500-character preview, and it passes the full cached text as context when it writes a low-rating learning, so those files can grow with the larger cap. Doctor.ts only names the file as a cache.
Environment: Linux (WSL2), Bun, Claude Code 2.1.278, LifeOS 7.40.4.
Both bugs make a status line wrong while the underlying state is right. I searched open and closed issues for
renderSystemLine,system-surfaces,LastResponseCacheandDriftReminderand found no report of either.Part A:
renderSystemLine()keeps the first detail per label, so the ISA delta is stalehooks/lib/system-surfaces.tsrenderSystemLine()deduplicates entries by label with:The comment says it keeps "the richest detail", but the test keeps the first entry that has any detail. Entries arrive in write order, and
SystemChangeSurface.hook.tsgives every ISA write a detail, eitherN closedorfirst→closed. So when a turn writes the same ISA more than once, the first write wins. An ISA scaffolded with 0 of 6 closed and finished at 6 of 6 in the same turn renders as0 closed.Repro: in one turn, write an ISA with no claims closed, then write it again with all claims closed. The
⚙️ SYSTEMline reports0 closed.Fix that works here: let the latest detail win.
An entry without a detail still cannot displace one that has it.
Caveat: covered by a unit test on
renderSystemLine()here. I have not yet seen the corrected line render live on a multi-write ISA turn.Part B:
LastResponseCachekeeps only the first 2000 characters, so the closer is cut offhooks/LastResponseCache.hook.tswriteslastResponse.slice(0, 2000)toMEMORY/STATE/last-response.txt.hooks/DriftReminder.hook.tsreads that file and setscloser: /🗣️/.test(text). The closer is always the last line of a reply, so any reply over 2000 characters loses it in the cache, and the next turn is told the last response had "no closer" when it had one. Long replies are the ones most likely to carry real work, so the false flag lands where it costs the most trust.Repro: send a reply longer than 2000 characters that ends with the
🗣️closer. The next turn's format reminder reports "no closer".Fix that works here: raise the cap, and past it keep the head and the tail so both the banner and the closer survive.
Caveat: covered by two unit tests here (a reply over 2000 characters keeps its closer; a reply over the cap keeps banner and closer). I have not yet confirmed it live on a reply over 2000 characters. The only other reader is
hooks/SatisfactionCapture.hook.ts. It slices its own 500-character preview, and it passes the full cached text as context when it writes a low-rating learning, so those files can grow with the larger cap.Doctor.tsonly names the file as a cache.Environment: Linux (WSL2), Bun, Claude Code 2.1.278, LifeOS 7.40.4.