Skip to content

MemoryReviewer kills healthy runs at a stale 240s wall and never retries a timeout; CortexHealth then calls one dropped run critical (9% of runs, measured) #1907

Description

@xmasyx

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: 1overall: warn, message "Reviewer timed-out once; the next scheduled run repairs it."
  • consecutiveFailures: 2 and 3overall: 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions