달력

8

« 2026/8 »

  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

What 512,000 lines of accidentally-published TypeScript actually reveal about how a production agent stays alive across long conversations.

Halfway through a long Claude Code session, the thing you’ve been working on quietly stops getting better. The agent doesn’t crash. It doesn’t warn you. But it just stops being able to free up room in its own context window, and from that point on every reply gets a little bad.

There’s a line in the leaked Claude Code source that explains exactly when this happens. It sits in the fileautoCompact.ts:

const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3;

That’s it. Three failures and the system gave up.

Compaction is the agent’s word for freeing up room in a long conversation when the context window starts to fill. When the npm package@anthropic-ai/claude-codeversion 2.1.88 published, it shipped with a 59.8 megabyte JavaScript source map bundled inside it. The deployment script simply lacked an exclusion rule for.mapfiles. That tiny omission resulted into roughly 512,000 lines of complicate…

Anthropic removed the package within hours. The company confirmed it was a packaging mistake rather than a security breach. But the npm registry is unforgiving, and automated mirrors had already cloned the artifact.

By the time Anthropic published their April 23 postmortem addressing recent quality reports, anyone with the source map could read exactly what was running on every developer’s machine.

What Anthropic publicly described as a thin layer around their models turns out to be a system that does eight different things just to keep one conversation alive. The leak does not reveal a malicious conspiracy. It reveals how much work is required to keep a language model on track.

How Claude Code holds onto a long conversation

A long agent conversation hits token limits constantly. The context window fills up with file reads, bash outputs, and error traces. Passing the raw transcript to the model every single time eventually guarantees a crash.

The naive way to handle this is to summarize the whole conversation when it gets too long. The leaked code shows Anthropic explicitly rejected that approach as a first line of defense. Summarization requires an expensive language model call which also compromises the prompt cache. Rewriting the conversation history invalidates the cached prefix, causing API costs to skyrocket and latency to degrade.

Claude Code useseight different compaction mechanismsinstead of one tool. They run in a strict priority order based on a cheapest-first principle. Every mechanism that runs without a model call executes before any mechanism that costs tokens. The system tries to gracefully degrade the context using cheap structural tricks before it finally stops to summarize itself.

Depending on how you cut the code, you can count five or seven mechanisms. I count eight because the cached and time-based microcompact functions run on completely different signals and mutually exclusive code paths.

Here are the eight mechanisms in the exact order they fire:

  • Tool Result Budget:Caps individual tool outputs at 50,000 characters, persisting the full output to disk and keeping a 2KB preview in context.
  • Snip (HISTORY_SNIP):A sliding-window message trimmer that cuts old messages with no LLM call.
  • Cached Microcompact: Surgically deletes stale tool results from the server-side cached prompt without rewriting the local message list.
  • Time-based Microcompact:Wipes stale tool results when the user has been idle for over 60 minutes.
  • Context Collapse (Marble Origami): Non-destructive append-only commit log that projects a compacted view of the conversation.
  • Auto-Compact: Full LLM summarization triggered around 83.5% of the context window, forking a subagent to produce a structured summary.
  • Reactive Compact:Emergency fallback when the API returns aprompt_too_longerror.
  • Compaction Circuit Breaker: Disables auto-compaction after three consecutive failures to prevent infinite loops.

TheTool Result Budgetis the first line of defense. It aggressively trims large terminal outputs. If you ask for a 4,000-line log file, the rest of the conversation only sees a short preview, even though the file is technically still on disk.

Snipis the second defense. It drops older messages from the array before they even reach the tokenizer. This is why the agent suddenly stops referencing a specific variable name discussed earlier in the exact same session, even though the terminal window was never closed.

Cached Microcompact uses a betacache_editsAPI. It surgically removes stale tool results from the server-side prompt cache. It runs every turn for a fixed allowlist of tools like Read, Bash, Grep, Glob, and WebSearch. What this looks like in practice: the billing cost of a debugging session is much lower than the raw token count of the conversation suggests.

Time-based Microcompact wipes stale tool results based on the wall clock. It replaces the heavy text with the literal string[Old tool result content cleared]. It is mutually exclusive with the cached version. If you come back from lunch to a session where the older tool outputs have all vanished, this is what fired.

Context Collapsegoes by the internal codename Marble Origami. It is non-destructive. It keeps a commit log of collapses and projects a compacted view each turn instead of permanently rewriting the raw conversation array. The UI shows 40 messages of history, but the agent answers as if it had only seen a high-level summary of them.

Auto-Compactis the heavy lifter. It triggers with a roughly 33,000-token reserved buffer. It forks a subagent that produces a structured summary in nine fixed sections. The tell is unmistakable — thirty minutes in, the agent suddenly summarizes what you’ve done and continues from the summary.

Reactive Compactis the emergency fallback. It only runs when the API strictly throws aprompt_too_longerror. It aggressively compacts everything. If the summarizer itself overflows, it drops the oldest API-round groups until the prompt fits. You might see a prompt too long error flicker briefly in the terminal trace before the agent recovers without losing the thread completely.

The Compaction Circuit Breakeris the constant from our opening. After three consecutive auto-compact failures, it disables auto-compaction for the rest of the session. This is the exact mechanism that leaves you with an agent that stops working and never recovers.

That ordering — pre-flight, post-flight, post-failure — is what the cascade dispatcher actually encodes:

/** * compaction/cascade.ts * Illustrative reconstruction of Claude Code's eight-mechanism compaction * cascade as observed in the leaked source map. Identifiers differ from the * leak, but this version preserves the priority order, the pre-flight / * post-flight / post-failure split, the mutual exclusion, and the breaker. */import type { ConversationState } from "../state";import { toolResultBudget } from "./mechanisms/tool-result-budget";import { snip } from "./mechanisms/snip";import { cachedMicrocompact } from "./mechanisms/cached-microcompact";import { timeBasedMicrocompact } from "./mechanisms/time-based-microcompact";import { contextCollapse } from "./mechanisms/context-collapse";import { autoCompact } from "./mechanisms/auto-compact";import { reactiveCompact } from "./mechanisms/reactive-compact";import { circuitBreaker } from "./mechanisms/circuit-breaker";export const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3;export type MechanismName =  | "tool_result_budget"  | "snip"  | "cached_microcompact"  | "time_based_microcompact"  | "context_collapse"  | "auto_compact"  | "reactive_compact"  | "circuit_breaker";export type CompactionResult =  | { kind: "noop"; mechanism: MechanismName }  | { kind: "applied"; mechanism: MechanismName; tokensFreed: number }  | { kind: "skipped"; mechanism: MechanismName; reason: string }  | { kind: "failed"; mechanism: MechanismName; error: Error };export type Phase = "pre_flight" | "post_flight" | "post_failure";export interface Mechanism {  readonly name: MechanismName;  readonly phase: Phase;  readonly priority: number;   shouldRun(state: ConversationState): boolean;  apply(state: ConversationState): Promise<CompactionResult>;}const MECHANISMS = [  toolResultBudget,        // 1  snip,                    // 2  cachedMicrocompact,      // 3  timeBasedMicrocompact,   // 4  contextCollapse,         // 5  autoCompact,             // 6  reactiveCompact,         // 7  circuitBreaker,          // 8] as const satisfies readonly Mechanism[];export interface CascadeContext {  state: ConversationState;  consecutiveFailures: number;  lastUserActivityMs: number;}export async function runPreFlight(  ctx: CascadeContext,): Promise<readonly CompactionResult[]> {  return runPhase("pre_flight", ctx);}export async function runPostFlight(  ctx: CascadeContext,  modelCallSucceeded: boolean,): Promise<readonly CompactionResult[]> {  if (ctx.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {    return [{      kind: "skipped",      mechanism: "auto_compact",      reason: "circuit_breaker_open",    }];  }  return runPhase(modelCallSucceeded ? "post_flight" : "post_failure", ctx);}async function runPhase(  phase: Phase,  ctx: CascadeContext,): Promise<readonly CompactionResult[]> {  const results: CompactionResult[] = [];  const ordered = MECHANISMS    .filter((m): m is Mechanism => m.phase === phase)    .toSorted((a, b) => a.priority - b.priority);  for (const mechanism of ordered) {    if (!mechanism.shouldRun(ctx.state)) {      continue;    }    if (      mechanism.name === "cached_microcompact" &&      timeBasedAlreadyApplied(results)    ) {      results.push({        kind: "skipped",        mechanism: "cached_microcompact",        reason: "cold_cache",      });      continue;    }    try {      const result = await mechanism.apply(ctx.state);      results.push(result);            // Stop the cascade once we have headroom      if (result.kind === "applied" && fitsInWindow(ctx.state)) {        return results;      }    } catch (caught) {      const error = caught instanceof Error ? caught : new Error(String(caught));      results.push({ kind: "failed", mechanism: mechanism.name, error });            if (mechanism.name === "auto_compact") {        ctx.consecutiveFailures += 1;      }    }  }  return results;}function timeBasedAlreadyApplied(  results: readonly CompactionResult[],): boolean {  return results.some(r =>    r.kind === "applied" && r.mechanism === "time_based_microcompact"  );}function fitsInWindow(state: ConversationState): boolean {  const RESERVED_BUFFER = 33_000;  return state.estimatedTokens <= state.contextWindowMax - RESERVED_BUFFER;}

The API surface is tiny — two functions,runPreFlightandrunPostFlight, handle everything the rest of the application touches. The entire orchestration is hidden behind them. The mutual exclusion check between cached and time-based microcompact is just two lines of code. You cannot surgically edit a cold cache, so the system skips the cached version if the time-based version already fired.

Notice the failure counting logic at the bottom. Only auto-compact failures count toward the circuit breaker. The cheaper mechanisms are allowed to throw errors without permanently crippling the session.

When auto-compact actually runs, it executes a specific trick to preserve cache stability. It forks a subagent (a separate, background LLM call) to write a summary, but it forces that subagent to output exactly nine fixed sections. It passes the parent conversation’s cache key directly to the subagent. The summarizer call sees the exact same system prompt and tool definitions as the main conversation, meaning the prefix-shared context costs almost zero tokens on the second pass.

/** * compaction/mechanisms/auto-compact.ts * * Illustrative reconstruction of Claude Code's mechanism #6. * The forked subagent emits a fixed nine-section summary. * The fork passes through the parent conversation's cache-key parameters. */import type { ConversationState } from "../../state";import type { ModelClient } from "../../model";export const SUMMARY_SECTIONS = [  "Primary Request and Intent",  "Key Technical Concepts",  "Files and Code Sections",  "Errors and Fixes",  "Problem Solving",  "All User Messages",  "Pending Tasks",  "Current Work",  "Optional Next Step",] as const;export type SummarySection = (typeof SUMMARY_SECTIONS)[number];export type AutoCompactSummary = {  readonly [K in SummarySection]: string;};export interface CachePrefix {  readonly systemPrompt: string;  readonly toolDefinitionsHash: string;  readonly userContextHash: string;}export interface AutoCompactInput {  readonly state: ConversationState;  readonly model: ModelClient;  readonly cacheKey: CachePrefix;}const SUMMARIZER_INSTRUCTIONS = `You are summarizing a long conversation so it can be replaced with thissummary. Emit exactly nine sections, each beginning with the literalsection header on its own line. Do not add commentary. Do not omit asection. If a section has no content, write "(none)" beneath the header.Sections, in order:${SUMMARY_SECTIONS.map((s, i) => `  ${i + 1}. ${s}`).join("\n")}`.trim();export async function runAutoCompact(  input: AutoCompactInput,): Promise<AutoCompactSummary> {  const { state, model, cacheKey } = input;  // The fork inherits the cache prefix verbatim.  const response = await model.complete({    cacheKey,    systemPrompt: cacheKey.systemPrompt,    messages: [      ...state.messages,      { role: "user", content: SUMMARIZER_INSTRUCTIONS },    ],    maxOutputTokens: 8_000,  });  return parseSummary(response.text);}function parseSummary(raw: string): AutoCompactSummary {  const out = {} as Record<SummarySection, string>;  const lines = raw.split("\n");  let currentSection: SummarySection | null = null;  let buffer: string[] = [];  const flush = (): void => {    if (currentSection !== null) {      out[currentSection] = buffer.join("\n").trim();    }  };  for (const line of lines) {    const trimmed = line.trim();    const matched = SUMMARY_SECTIONS.find(      s => s.toLowerCase() === trimmed.toLowerCase(),    );    if (matched !== undefined) {      flush();      currentSection = matched;      buffer = [];    } else if (currentSection !== null) {      buffer.push(line);    }  }  flush();  // Fail closed. Any missing section is a parser error.  for (const section of SUMMARY_SECTIONS) {    if (!(section in out)) {      throw new Error(        `auto_compact: missing section "${section}" in summary output`,      );    }  }  return out as AutoCompactSummary;}

Eight sections out of nine isn’t a degraded summary, it’s a bug — and the parser treats it that way. The parser fails closed. If the model emits eight sections instead of nine, the parser throws a specific error. There are no silent empty-string defaults. The downstream consumer assumes all nine fields will exist, and the parser enforces that contract strictly.

Where Claude Code actually remembers things

An agent must remember things for the next message, for the next session, and for the lifetime of the project. Those different lifetimes need different stores.

The leak shows that Anthropic explicitly rejected vector databases for memory. They favored raw files, grep, and a markdown index. The reasoning is visible in the architecture. Vector retrieval is opaque and needs an embedding model on every read — files don’t need anything. And where vectors quietly reward recency, a markdown file preserves structure as long as you keep it.

Tier 1 is In-context memory.It holds the active conversation, the system prompt, and the tool definitions. It holds the first 200 lines of the memory index and the unevicted tool results. What makes Tier 1 architecturally different from the other two is that it’s the only tier the agent actively reasons over; the other two get read into it. Persistence is ephemeral and is gone at session end unless the--continueor--resumeflags are used. Eviction is governed entirely by the eight compaction mechanisms we just covered. Transcripts are written to a local.jsonlfile so resuming works, but the runtime memory is purely in-process. Close the terminal mid-task and the next session has no idea what you were just doing.

Tier 2 is Persistent file memory.This holds the pointer index calledMEMORY.mdand specific topic files likedebugging.md. It holds the session transcripts and the tool result spillover from Tier 1. Persistence survives session restarts, machine restarts, and explicit clear commands. This tier is self-healing. It uses a forked background subagent calledautoDream. This agent runs after a triple-gate of at least 24 hours since the last consolidation, at least 5 sessions since the last cycle, and the acquisition of a file-based advisory lock. Three gates because consolidating memory in the middle of an active session is worse than not consolidating at all. It reads recent signals, consolidates them, and prunes the index. This explains why the agent suddenly references a fact you told it three weeks ago on a different branch without anyone re-mentioning it.

The file system layout for Tier 2 is named explicitly in the source:~/.claude/projects//memory/MEMORY.md~/.claude/projects//memory/.md~/.claude/projects//sessions/*.jsonl

It is important to understand thatMEMORY.mddoes not store information directly. It stores the locations of information. It is a pointer index limited to about 150 characters per line. The first 25 kilobytes are streamed into Tier 1 at session start. Loading the full memory directory on every session start would defeat the point of compaction entirely.

Tier 3 is Instruction memory.This is theCLAUDE.mdhierarchy. It holds project rules, conventions, and architecture notes written by humans. It is read at every single session start. This tier matters disproportionately because it sits above the dynamic boundary in the system prompt. It is the only thing that survives auto-compact completely unchanged. This is what makes the agent always know your build commands.

The resolution chain for Tier 3 follows a strict precedence order where the most specific file wins:

  • /etc/claude-code/CLAUDE.mdfor global organizational rules.
  • ~/.claude/CLAUDE.mdfor the user across all projects.
  • /CLAUDE.mdfor the version-controlled project rules.
  • /.claude/rules/*.mdfor modular rules.
  • //CLAUDE.mdfor directory-specific instructions.
  • /CLAUDE.local.mdfor personal gitignored notes.

Here is how that resolution chain is implemented in code.

/** * memory/claude-md-resolver.ts * * Illustrative reconstruction of Claude Code's CLAUDE.md resolution chain. * Six layers where most-specific wins. Results are returned in apply-order * so callers fold them with later-overrides-earlier semantics. */import { readFile, readdir, stat } from "node:fs/promises";import { homedir } from "node:os";import { dirname, join, resolve, sep } from "node:path";export type ResolutionLayer =  | "global"            | "user"              | "project_root"      | "project_rules"     | "subdirectory"      | "personal";       export interface ResolvedInstruction {  readonly source: ResolutionLayer;  readonly path: string;  readonly content: string;}export interface ResolveOptions {  readonly projectRoot: string;  readonly currentFile?: string;}export async function resolveClaudeMd(  opts: ResolveOptions,): Promise<readonly ResolvedInstruction[]> {  const root = resolve(opts.projectRoot);  const found: ResolvedInstruction[] = [];  await tryAdd(found, "global", "/etc/claude-code/CLAUDE.md");  await tryAdd(found, "user", join(homedir(), ".claude", "CLAUDE.md"));  await tryAdd(found, "project_root", join(root, "CLAUDE.md"));  await addRulesDir(found, join(root, ".claude", "rules"));  if (opts.currentFile !== undefined) {    const leaf = resolve(root, opts.currentFile);        // Confine the walk to the project root.    if (leaf === root || leaf.startsWith(root + sep)) {      const chain: string[] = [];      let dir = dirname(leaf);      while (dir.startsWith(root) && dir !== root) {        chain.unshift(join(dir, "CLAUDE.md")); // root-to-leaf order        dir = dirname(dir);      }      for (const path of chain) {        await tryAdd(found, "subdirectory", path);      }    }  }  await tryAdd(found, "personal", join(root, "CLAUDE.local.md"));  return found;}async function tryAdd(  out: ResolvedInstruction[],  source: ResolutionLayer,  path: string,): Promise<void> {  try {    const s = await stat(path);    if (!s.isFile()) return;    const content = await readFile(path, "utf8");    out.push({ source, path, content });  } catch {    // Missing files are normal. Most layers are empty.  }}async function addRulesDir(  out: ResolvedInstruction[],  dir: string,): Promise<void> {  let entries;  try {    entries = await readdir(dir, { withFileTypes: true });  } catch {    return;  }  const files = entries    .filter(e => e.isFile() && e.name.endsWith(".md"))    .map(e => e.name)    .toSorted();  for (const name of files) {    const path = join(dir, name);    const content = await readFile(path, "utf8");    out.push({ source: "project_rules", path, content });  }}

The order ofawait tryAddcalls is the precedence chain. You read top-to-bottom and you have the six-layer list. ThetryAddfunction swallows missing files silently because the design expects most layers to be empty. The subdirectory walk useschain.unshiftto flip leaf-to-root traversal into root-to-leaf insertion order, so callers folding the results get the right precedence. This is what survives auto-compact. Mechanism number six clears Tier 1 and rewrites it as a summary, but whatever this resolver returned is preserved untouched in Tier 3.

What Claude Code does that no docs page mentions

If you grep the leaked source for the stringTengu, you get over a thousand hits. Tengu is Claude Code's internal project codename. Strip that prefix away and the next layer underneath is the feature-flag set Anthropic was using to ship and gate behavior. There are44of them.

44flags shows how a fast-moving team ships. The question is what each flag actually turns on. They fall into two groups:shipped but undocumented, and completely unreleased.

Here are the flags that are already running on customer machines today, just never mentioned in any docs page.

Anti-distillation and fake tools.When theANTI_DISTILLATION_CCflag is on, the API request includes ananti_distillation: ['fake_tools']directive. The server injects decoy tool definitions into the system prompt. The purpose is to poison training data captured by anyone recording API traffic to clone Claude Code's behavior. The agent occasionally references a tool name you have never seen documented and cannot find in the official tool list.

Frustration regex.The fileuserPromptKeywords.tscontains a regex matching profanity and frustration phrases like "wtf" and "this sucks". The match changes downstream behavior. After a developer curses at the agent in frustration, the next reply is noticeably more careful and apologetic, but only that one time.

Undercover mode.The environment variableCLAUDE_CODE_UNDERCOVER=1switches Claude Code into a mode that strips Anthropic identifiers from output. This is used by Anthropic engineers contributing to open source software without revealing AI authorship. It is asymmetric. The environment variable can force the mode on, but cannot force it off. An Anthropic employee commit on a public repository does not carry theCo-Authored-By: Claudeline every other Claude Code commit ships with.

Silent model downgrade. On certain server errors, Claude Code silently falls back from Opus to Sonnet for the rest of the request. The user sees a successful response, not a degraded one. An Opus session quietly gets slightly dumber halfway through and never recovers, even though there was no error message.

Employee-only verification gate. A flag gates a verification loop that re-runs a generated diff to check it actually compiles before returning. Anthropic engineers ran this. Customers did not. Anthropic engineers’ generated diffs feel notably more reliable than the same model running locally for an external user.

Then there are the flags that are not shipped yet. These are present in the source but not yet wired into the customer build.

KAIROS. This is an always-on background daemon that persists across sessions. It takes periodic tick prompts, monitors GitHub webhooks, and can independently take actions. There are over 150 references to it in the source.

autoDream. This is the Tier 2 consolidation subagent we covered earlier. It is gated under KAIROS’s idle mode. Whether it is currently active in shipped builds is unclear, but the gate suggests it is heavily restricted.

ULTRAPLAN. This offloads deep planning sessions to a remote Opus instance for up to 30 minutes. It is a dedicated planning-mode variant.

COORDINATOR_MODE. This is a multi-agent swarm with structured research, synthesis, and implementation phases.

The leak also surfaced the internal model codenames. Tengu is Claude Code itself. Capybara appears to be the Mythos variant. Fennec maps to Opus 4.6. Numbat is an unreleased model still in testing. These are just the names that propagate through the source code routing logic.

What the leak does and doesn’t tell us

There is a clear limit to what is in the source map. It contains no model weights, fine-tuning data, reinforcement learning curriculum or production server code, because only the client CLI is in the npm package. It contains no customer data and no credentials. This is the harness, not the model.

But the harness is exactly what builders need to see. The 8 compaction mechanisms, the 3 memory tiers, and the 44 flags exist because of the shape of the agent problem, not because of any specific model. Anthropic could swap Opus for a next-generation model tomorrow, and the harness would still do the exact same 8 things. It would store across the same three tiers. It would leave most of the same flags in place.

Compaction has to layer. Memory has to tier. The flags will keep multiplying. The leak isn’t a window into Anthropic. It is a preview of what your stack becomes by 2027.

:
Posted by Ritz®™