Summary
MemoryReviewer.ts kills healthy reviewer runs at a wall clock fitted to a stale latency measurement, never retries a timeout (though it retries a parse failure), and CortexHealth.ts then reports the single dropped run as critical. The statusline paints PROBLEM · LATEST REVIEWER RUN IS TIMED-OUT. for up to an hour, until the next scheduled review happens to succeed. Nothing is actually broken.
Measured on one install over 45 consecutive reviewer runs (LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs.jsonl):
| metric |
value |
| duration of successful runs |
min 15s · p50 125s · p90 200s · max 250s |
| runs killed at the 240s wall |
4 / 45 ≈ 9% |
| comment justifying 240s |
"successful runs measure 50–115s" (dated 2026-08-03) |
Prompt size does not explain it: a 39,691-byte prompt finished in 57s while a 29,199-byte one hit the wall. This is reasoning-latency variance under --effort high, so the 240s wall sits inside the working distribution rather than beyond it.
Root cause
1. LIFEOS/TOOLS/MemoryReviewer.ts:72 — the budget is fitted to a measurement the distribution has since outgrown:
const DEFAULT_TIMEOUT_MS = 240_000; // 120s timed out repeatedly on heavy sessions (2026-08-03); successful runs measure 50–115s, so 240s bounds the tail without masking hangs
2. LIFEOS/TOOLS/MemoryReviewer.ts:681-696 — a timeout is terminal on the first attempt. The asymmetry is the tell: a parse failure a few lines below gets a deliberate corrective retry (with a comment explaining why), while a wall-clock timeout, which is pure transient latency, gets none.
const result = await inference({ ..., timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS });
inferenceDuration = Date.now() - startedAt;
if (!result.success) {
const failed: ReviewResult = { ok: false, ..., error: `inference failed: ${result.error}` };
logRunSummary({ ts: new Date().toISOString(), ...failed });
return failed; // <- no second attempt, ever
}
3. LIFEOS/TOOLS/CortexHealth.ts:87 — severity asks the weaker question ("did the last run fail?") instead of the real one ("is the memory loop broken?"). 44 successes around one dropped run do not move the verdict:
else if (["failed", "parse-failed", "timed-out", "invalid"].includes(input.reviewer.status))
add(..., "critical", `Latest reviewer run is ${input.reviewer.status}.`, input.reviewer);
The reviewer re-fires on the next quiet moment and repairs itself unattended, so a lone failure is warn-shaped, not critical-shaped. The evidence object already carries priorSuccesses but severity ignores it.
Suggested fix
a. Re-fit the budget to the measured tail (MemoryReviewer.ts:72) — 300s clears the observed max with headroom, and note in the comment that the tail beyond it is handled by the retry, not by a bigger number:
const DEFAULT_TIMEOUT_MS = 300_000;
b. One retry on a wall-clock timeout, mirroring the existing parse retry. Two independent draws beyond the wall are ~0.8% where one is ~9%; anything genuinely hung still fails both attempts and surfaces, now with the attempt count logged so a real hang cannot hide behind a silent second try:
const callInference = () => inference({ systemPrompt, userPrompt, level: "medium", expectJson: false, timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS });
const startedAt = Date.now();
let result = await callInference();
let attempts = 1;
if (!result.success && /timeout/i.test(String(result.error ?? ""))) { result = await callInference(); attempts = 2; }
Add attempts?: number to ReviewResult and log it on every terminal path, so reviewer-runs.jsonl distinguishes "succeeded on the second try" from "hung twice".
c. Reserve critical for a streak (CortexHealth.ts). Count the trailing non-ok, non-skipped rows (a skip is a healthy no-op and should neither break nor extend the streak), carry it on the evidence as consecutiveFailures, and branch:
else if (["failed", "parse-failed", "timed-out"].includes(input.reviewer.status)) {
const streak = input.reviewer.consecutiveFailures ?? 1;
const id = `reviewer-latest-${input.reviewer.status}`;
if (streak >= 2) add(id, "critical", `Reviewer has failed ${streak} runs in a row (latest is ${input.reviewer.status}).`, input.reviewer);
else add(id, "warn", `Reviewer ${input.reviewer.status} once; the next scheduled run repairs it.`, input.reviewer);
}
"invalid" should stay unconditionally critical — it is a contract violation, not latency. This is not a mute: there is no ack and no TTL, the count and age are carried forward, and a persistent failure escalates on its own at the second run.
The orphan-dir path (CortexHealth.ts:128, a run dir newer than any terminal row past reviewerRunGraceMs) is the current failure itself, so its streak is trailingFailures(rows) + 1.
Verification
Two poles against assessCortexEvidence, both observed after applying the above:
consecutiveFailures: 1 → overall: warn, message "Reviewer timed-out once; the next scheduled run repairs it."
consecutiveFailures: 2 and 3 → overall: critical, message naming the streak
status: "invalid" → still critical
Replaying the 4 historical timeouts through the new logic: each had a successful run immediately before it, so all four would have been warn — matching what actually happened, since each was repaired by the next scheduled run without intervention.
Summary
MemoryReviewer.tskills healthy reviewer runs at a wall clock fitted to a stale latency measurement, never retries a timeout (though it retries a parse failure), andCortexHealth.tsthen reports the single dropped run ascritical. The statusline paintsPROBLEM · LATEST REVIEWER RUN IS TIMED-OUT.for up to an hour, until the next scheduled review happens to succeed. Nothing is actually broken.Measured on one install over 45 consecutive reviewer runs (
LIFEOS/MEMORY/OBSERVABILITY/reviewer-runs.jsonl):Prompt size does not explain it: a 39,691-byte prompt finished in 57s while a 29,199-byte one hit the wall. This is reasoning-latency variance under
--effort high, so the 240s wall sits inside the working distribution rather than beyond it.Root cause
1.
LIFEOS/TOOLS/MemoryReviewer.ts:72— the budget is fitted to a measurement the distribution has since outgrown:2.
LIFEOS/TOOLS/MemoryReviewer.ts:681-696— a timeout is terminal on the first attempt. The asymmetry is the tell: a parse failure a few lines below gets a deliberate corrective retry (with a comment explaining why), while a wall-clock timeout, which is pure transient latency, gets none.3.
LIFEOS/TOOLS/CortexHealth.ts:87— severity asks the weaker question ("did the last run fail?") instead of the real one ("is the memory loop broken?"). 44 successes around one dropped run do not move the verdict:The reviewer re-fires on the next quiet moment and repairs itself unattended, so a lone failure is warn-shaped, not critical-shaped. The evidence object already carries
priorSuccessesbut severity ignores it.Suggested fix
a. Re-fit the budget to the measured tail (
MemoryReviewer.ts:72) — 300s clears the observed max with headroom, and note in the comment that the tail beyond it is handled by the retry, not by a bigger number:b. One retry on a wall-clock timeout, mirroring the existing parse retry. Two independent draws beyond the wall are ~0.8% where one is ~9%; anything genuinely hung still fails both attempts and surfaces, now with the attempt count logged so a real hang cannot hide behind a silent second try:
Add
attempts?: numbertoReviewResultand log it on every terminal path, soreviewer-runs.jsonldistinguishes "succeeded on the second try" from "hung twice".c. Reserve
criticalfor a streak (CortexHealth.ts). Count the trailing non-ok, non-skipped rows (a skip is a healthy no-op and should neither break nor extend the streak), carry it on the evidence asconsecutiveFailures, and branch:"invalid"should stay unconditionally critical — it is a contract violation, not latency. This is not a mute: there is no ack and no TTL, the count and age are carried forward, and a persistent failure escalates on its own at the second run.The orphan-dir path (
CortexHealth.ts:128, a run dir newer than any terminal row pastreviewerRunGraceMs) is the current failure itself, so its streak istrailingFailures(rows) + 1.Verification
Two poles against
assessCortexEvidence, both observed after applying the above:consecutiveFailures: 1→overall: warn, message "Reviewer timed-out once; the next scheduled run repairs it."consecutiveFailures: 2and3→overall: critical, message naming the streakstatus: "invalid"→ stillcriticalReplaying the 4 historical timeouts through the new logic: each had a successful run immediately before it, so all four would have been warn — matching what actually happened, since each was repaired by the next scheduled run without intervention.