fix: repair four breaks in the autonomic memory loop - #2032
Open
abseg wants to merge 1 commit into
Open
Conversation
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.
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.
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 shapeThe 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.resultreadundefined, and every programmatic caller died onmissing result field.Fix selects the terminal
resultelement instead of assuming either shape.2.
MemoryRetriever.ts— the evidence file nothing wroteCortexHealthandMemoryStatushave always readmemory-retrievals.jsonl, andMemoryHealthCheckwarns 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.validRetrievalRowexactly —ts,query_hash,returned_count,duration_ms, optionaltop_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.ts—last_review_atonly stamped on spawnThe 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_betweenis 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 orderThe 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.