Skip to content

Memory janitor: condense only when it can shrink, parse the reply it gets, name a trust-dialog block - #33

Merged
aaroncoville merged 2 commits into
theme/sixth-historyfrom
fix/md-condense-fail
Sep 30, 2026
Merged

aaroncoville merged 2 commits into
theme/sixth-historyfrom
fix/md-condense-fail

Conversation

@aaroncoville

Copy link
Copy Markdown
Owner

Stops the memory janitor from spending tokens with nothing to show.

Since 08-25 the condense step aborted 855 times against 67 successes. Four failure classes, each reproduced against real or captured fixtures and fixed at its root:

  • not-smaller (~271). recentKeep was a fixed section count with no byte cap, so a few untouched recent sections outweighed anything a summary could save; on real files the old rule evicted 0 and 1 sections. selectKeep() is now byte-budgeted (keepBudgetPct, 25%), and a file that just failed not-smaller is not retried until it has regrown 10%.
  • no parseable JSON (~195). The parser only accepted a fence anchored to the whole reply; extraction is now a balanced-brace scan that honours quoted strings.
  • recent-count-mismatch (~4). A model-echoed ## line was re-parsed as a phantom section on the next load; embedded headings are demoted.
  • timeouts. The hidden-session timeout scales with the eviction payload instead of a fixed 180 s; one retry only for the specific transcript-race message, never for a genuine timeout.

no assistant response found in transcript (~284) reproduced deterministically: a headless PTY session in a directory Claude Code has not trust-accepted blocks on the first-run workspace-trust dialog, which renders with cursor-positioning escapes so a naive substring check never matches. looksLikeTrustDialog() (ANSI-strip, whitespace-strip, substring) now reports it as a distinct, non-retried error, so the log names the real cause going forward. Whether the production hidden-spawn directory is trusted for headless sessions is a separate question the new log line will answer.

Verification: 26 new tests (test/reflect.test.cjs, test/hidden-claude-trust-dialog.test.cjs); full suite and both typecheck targets green; the pre-existing failing set is identical to a clean checkout of the base.

Aaron Coville added 2 commits September 30, 2026 14:14
…ing to show

Production evidence: the memory-reflection condense pass aborted far more
often than it succeeded, and every attempt still spent a full Haiku
hidden-session call for zero shrink. Five root causes, fixed at the source
(the retry/prompt/JSON fixes are defense-in-depth; the parser and keep-budget
fixes are the structural root causes):

- not-smaller (the largest class): the verbatim "keep" region was bounded by
  a fixed section COUNT only, so a handful of untouched recent sections could
  outweigh anything a summary of the rest could ever save. selectKeep() now
  also bounds the keep region by bytes (keepBudgetPct), guaranteeing eviction
  headroom. A file that still fails not-smaller is backed off (must regrow by
  10% before the autonomous scan retries it) instead of being retried every
  tick for the same zero-shrink outcome.

- no parseable JSON: parseSummary()/extractJsonObject() now finds the JSON
  object under preamble, trailing prose, or a fence not anchored to the whole
  string, via a real balanced-brace scan (honors quoted strings) instead of a
  string-anchored regex.

- recent-count-mismatch: a model-echoed "## " sub-heading inside the summary
  re-parsed as a new section boundary on the next load, corrupting the
  3-region structure. demoteEmbeddedHeadings() sanitizes both the condensed
  text and hoisted pinned lines before they're ever written.

- no assistant response found in transcript: retried once (the likely
  transcript-flush race) before aborting.

- hidden session timed out: NOT retried (replaying the same oversized prompt
  buys nothing); instead the timeout budget now scales with the eviction
  payload size instead of a fixed 180s regardless of how much text is being
  summarized.

Tightened the prompt contract (no "## " lines, no prose outside the JSON) as
an additional line of defense alongside the parser/sanitizer fixes.

test/reflect.test.cjs reproduces each abort class against a real memory.md
shape and proves the fix; full suite + both typecheck targets green with the
pre-existing failure set byte-for-byte unchanged (diffed against a clean
clone of the same base commit).
…ent no-response race

Live verification of the condense-retry fix (against a real, previously
untrusted cwd) reproduced 'no assistant response found in transcript'
deterministically, not as a rare race: a hidden headless session spawned in
a cwd Claude Code has never been trusted in blocks forever on the first-run
workspace-trust dialog. The boot-quiet heuristic reads the rendered dialog as
"ready for input" and sends the condense prompt into a menu that never
consumes it, so no transcript is ever written — every attempt hits the
identical dialog and fails identically.

Confirmed with a raw node-pty capture against this exact repo checkout: the
dialog's own CLI flags (--permission-mode bypassPermissions,
--dangerously-skip-permissions) do not suppress it, and it renders with
absolute-column cursor moves between words rather than spaces, so a naive
substring match against the raw PTY bytes never fires either — the detector
strips ANSI/CSI sequences and all whitespace before matching.

hiddenClaude.ts now distinguishes the two: a genuine no-response case keeps
its retryable message; a detected trust-dialog block gets a distinct,
non-retried one (reflect.ts's retry loop only matches the original phrase),
so a guaranteed-to-repeat failure no longer burns a second hidden-session
call. Verified against three independent live spawns: two unmodified showed
the generic message in ~3.5-6.5s (too fast for genuine model latency, and
unaffected by widening idleMs/bootCapMs to 12s — ruling out a timing race),
one post-fix correctly returned the new trust-dialog message.

Full suite + both typecheck targets green; failure set byte-for-byte
unchanged against the pre-existing baseline.
@aaroncoville
aaroncoville merged commit 77f8fc7 into theme/sixth-history Sep 30, 2026
@github-actions

Copy link
Copy Markdown

🚫 This PR is missing its before/after evidence

Every pull request here has to show its work. Screenshots or a short screen recording, before the change and after it.

  • Before — no image or video under that heading
  • After — no image or video under that heading

How to fix it: edit the description, keep the ### Before and ### After headings from the template, and drag an image or video under each. GitHub uploads it inline. This check re-runs the moment you save.

A bug fix with no visible surface still needs it: show the failing behaviour, then the same steps passing. A terminal recording is fine.

Genuinely nothing to show — a CI tweak, a typo, a dependency bump? A maintainer can apply the no-visual-change label. Please don't ask unless it truly has no observable effect.

📖 CONTRIBUTING.md → Evidence is mandatory

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