Skip to content

fix: repair four breaks in the autonomic memory loop - #2032

Open
abseg wants to merge 1 commit into
danielmiessler:mainfrom
abseg:fix/memory-loop-evidence
Open

fix: repair four breaks in the autonomic memory loop#2032
abseg wants to merge 1 commit into
danielmiessler:mainfrom
abseg:fix/memory-loop-evidence

Conversation

@abseg

@abseg abseg commented Aug 30, 2026

Copy link
Copy Markdown

Summary

Four independent defects in the path between "memory ran" and "the health check can see that it ran". Each is small. Together they made the autonomic loop report itself broken while it was, in part, genuinely not running.

They are grouped in one PR because they were found as one investigation and two of them mask each other: the missing retrieval writer and the frozen review stamp both surface as the same "loop is stuck" reading. Happy to split if you'd rather review them separately.

1. Inference.ts — envelope shape

The CLI emits either a single result object or a JSON array of stream events whose last type:"result" element carries the envelope. Only the object shape was handled. An array fell through the object check (arrays are objects), envelope.result read undefined, and every programmatic caller died on missing result field.

Fix selects the terminal result element instead of assuming either shape.

2. MemoryRetriever.ts — the evidence file nothing wrote

CortexHealth and MemoryStatus have always read memory-retrievals.jsonl, and MemoryHealthCheck warns when no row is fresher than its window. Nothing has ever written it. A repo-wide grep found three references, all readers, and the path did not exist on disk. So the check was correct and the writer was simply absent: retrieval fired every turn and left no trace.

Fix adds the writer, honouring CortexHealth.validRetrievalRow exactly — ts, query_hash, returned_count, duration_ms, optional top_score. Extra keys make a row invalid, which is a louder failure than a missing one, so the schema is deliberately not extended. Only a hash of the query is stored, never the query text: this is freshness evidence, not a query history.

Rows are written on every call including cache hits, because the question the health check asks is "did retrieval run this turn?" and a cache hit is still a turn that ran it.

3. MemoryReviewer.tslast_review_at only stamped on spawn

The field was written only by MemoryReviewFire.hook.ts, and only at the moment it decided to spawn. A review that ran by any other path — a manual run, a retry, a recovery after the hook was suppressed — left it frozen.

Two things were wrong, not one. The health report was misleading, and min_minutes_between is computed from that same field, so a completed review did not count against the rate cap and the hook could immediately spawn a duplicate.

The event the field names is "a review finished", so the reviewer is where it belongs. Skipped runs are not reviews and do not stamp.

4. MemoryTurnStart.hook.ts — emission order

The composed payload runs ~12 KB, past the harness inline budget, so the whole thing is persisted to a file and only a ~2 KB preview is seen. The two blocks whose entire contract is render this verbatim were emitted last, landing at byte ~9,776 of 12,226 and outside every preview.

They are short and bounded (one line each), so they are now emitted first and the unbounded hot layer follows. No content change, order only.

Verification

All four parse-check clean (bun build --target=bun) against a tree with dependencies installed.

Four independent defects, all in the path between "memory ran" and "the
health check can see that it ran". Each is small; together they made the
loop report itself broken while it was partly not running at all.

1. Inference.ts — the CLI emits either a single result object or a JSON
   array of stream events whose last type:"result" element carries the
   envelope. Only the object shape was handled; an array fell through the
   object check (arrays are objects), envelope.result read undefined, and
   every programmatic caller died on "missing result field". Now the
   terminal result element is selected rather than either shape assumed.

2. MemoryRetriever.ts — CortexHealth and MemoryStatus have always READ
   memory-retrievals.jsonl, and MemoryHealthCheck warns when no row is
   fresher than its window. Nothing ever wrote it: a repo-wide grep found
   three references, all readers, and the path did not exist on disk. The
   check was right and the writer was simply absent. Added, honouring
   CortexHealth's validRetrievalRow schema exactly (extra keys make a row
   invalid, which is louder than a missing one). Only a hash of the query
   is stored, never the query.

3. MemoryReviewer.ts — last_review_at was written only by the hook, and
   only at the moment it decided to SPAWN. A review that ran by any other
   path left the field frozen, so the health report claimed the loop was
   stuck. Worse, min_minutes_between is computed from that same field, so
   a completed review did not count against the rate cap and the hook
   could immediately spawn a duplicate. The reviewer now stamps on actual
   completion. Skipped runs are not reviews and do not stamp.

4. MemoryTurnStart.hook.ts — emission order. The composed payload runs
   ~12 KB, past the harness inline budget, so it is persisted and only a
   ~2 KB preview is seen. The two blocks whose entire contract is "render
   this verbatim" were emitted last, landing at byte ~9,776 of 12,226 and
   outside every preview. They are short and bounded, so they are now
   emitted first and the unbounded hot layer follows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant