# The Secret Amnesia Failsafe: Emergency Truncation

> Deep inside engine.ts, when all summarization attempts fail, the plugin logs "FALLING BACK TO EMERGENCY TRUNCATION" and calls createEmergencyFallbackSummarize() — silently discarding history in the very system designed to prevent that. The infinite-loop bail in compaction.ts and the hard sweep iteration cap (default 12) added in v0.11.2 are further evidence that the "nothing is lost" promise has real escape hatches baked in.

- Repository: Martian-Engineering/lossless-claw
- GitHub: https://github.com/Martian-Engineering/lossless-claw
- Human wiki: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e
- Complete Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/llms-full.txt

## Source Files

- `src/engine.ts`
- `src/compaction.ts`
- `src/summarize.ts`
- `CHANGELOG.md`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/engine.ts](src/engine.ts)
- [src/compaction.ts](src/compaction.ts)
- [src/summarize.ts](src/summarize.ts)
- [CHANGELOG.md](CHANGELOG.md)
</details>

# The Secret Amnesia Failsafe: Emergency Truncation

Lossless Claw is built around a bold promise: conversations never lose history the way a context-window rollover does. Instead of silently cutting old messages, it compacts them into a tree of summaries using an LLM. The system is designed to always preserve meaning, not just recency.

But buried inside the engine are multiple explicit escape hatches where "nothing is lost" quietly breaks down. When every summarizer candidate fails, when a single compaction sweep runs too long, or when the round loop makes no progress, the plugin logs a terse error and falls back to blunt character-level truncation — the very thing it exists to prevent. This page excavates those escape hatches with exact source citations.

---

## The Emergency Truncation Backstop

The most dramatic failsafe lives at the bottom of `resolveSummarize()` in `engine.ts`. When the plugin tries to build an LLM-backed summarizer and that setup fails — either because `createLcmSummarizeFromLegacyParams` returned `undefined` (no model candidates resolved) or because the call threw — the engine logs an error and falls back to a pure truncation function:

```typescript
// src/engine.ts:3967–3973
this.deps.log.error(
  `[lcm] resolveSummarize failed, using emergency fallback: ${describeLogError(err)}`,
);
// ...
this.deps.log.error(`[lcm] resolveSummarize: FALLING BACK TO EMERGENCY TRUNCATION`);
return { summarize: createEmergencyFallbackSummarize(), summaryModel: "unknown" };
```

The function that actually performs this emergency operation (`createEmergencyFallbackSummarize`) is defined at the very end of engine.ts:

```typescript
// src/engine.ts:8995–9006
function createEmergencyFallbackSummarize(): (
  text: string,
  aggressive?: boolean,
) => Promise<string> {
  return async (text: string, aggressive?: boolean): Promise<string> => {
    const maxChars = aggressive ? 600 * 4 : 900 * 4;
    if (text.length <= maxChars) {
      return text;
    }
    return text.slice(0, maxChars) + "\n[Truncated for context management]";
  };
}
```

No LLM. No semantic compression. Just `text.slice(0, maxChars)`. Aggressive mode cuts to 2,400 characters; normal mode keeps 3,600. Everything after that is silently deleted and labeled with a bracket annotation. The comment in the code diplomatically calls this "a stable baseline summarize callback to keep compaction operable when runtime setup is unavailable."

Sources: [src/engine.ts:3966-3973](), [src/engine.ts:8985-9006]()

---

## The Summarize Escalation Chain (and Where It Falls Off)

Before the emergency truncation is ever reached from `CompactionEngine`, there is a three-level escalation inside `summarizeWithEscalation()` in `compaction.ts`. This is the inner summarizer used during actual compaction passes:

1. **Normal**: call the LLM summarizer; if the output is non-empty, use it.
2. **Aggressive**: call again with `aggressive=true`; this mode targets a tighter word budget.
3. **Deterministic fallback**: if the LLM produces output larger than the input, or returns empty, truncate to `FALLBACK_MAX_TOKENS` (512 tokens) with a `[Truncated from N tokens]` suffix.

```typescript
// src/compaction.ts:1758–1768
const buildDeterministicFallback = (): { content: string; level: CompactionLevel } => {
  const suffix = `\n[Truncated from ${inputTokens} tokens]`;
  const truncated = truncateTextToEstimatedTokens(
    sourceText,
    Math.max(0, FALLBACK_MAX_TOKENS - estimateTokens(suffix)),
  );
  return {
    content: `${truncated}${suffix}`,
    level: "fallback",
  };
};
```

A provider auth failure at any stage returns `null` instead of content, which the caller treats as a non-compacting skip — the pass is abandoned so truncation artifacts don't persist into the summary DAG. But empty provider output (the LLM answered but said nothing) still compacts deterministically at 512 tokens.

Additionally, in `summarize.ts`, after all provider candidates are exhausted, the outer summarize function falls back to `buildDeterministicFallbackSummary`:

```typescript
// src/summarize.ts:1673–1679
params.deps.log.error(
  `[lcm] ALL PROVIDERS EXHAUSTED: ${resolvedCandidates.length} candidate(s) tried, none succeeded.
   Compaction falling back to deterministic truncation. Check provider keys and quotas.`,
);
if (lastAuthError) {
  throw lastAuthError;
}
return buildDeterministicFallbackSummary(text, targetTokens);
```

`buildDeterministicFallbackSummary` in `summarize.ts` preserves up to `targetTokens * 4` characters and appends `[LCM fallback summary; truncated for context management]`.

Sources: [src/compaction.ts:1743-1831](), [src/compaction.ts:214-216](), [src/summarize.ts:1118-1131](), [src/summarize.ts:1673-1679]()

---

## The Iteration Cap: Hard-Stop at 12 Passes

A subtler escape hatch was introduced in v0.11.2: a hard per-sweep iteration cap. The motivation was observed in the wild — a 308K-token conversation triggered 16 compaction passes in a single sweep, each potentially running a full summarizer timeout on the turn-critical path.

The constant is defined in `compaction.ts`:

```typescript
// src/compaction.ts:225
const DEFAULT_MAX_SWEEP_ITERATIONS = 12;
```

During `compactFullSweep`, both the leaf phase and the condensed phase share a single counter. A closure checks the budget before each pass and logs a warning the first time a limit is hit:

```typescript
// src/compaction.ts:879–904
const sweepBudgetExhausted = (phase: "leaf" | "condensed"): boolean => {
  if (stoppedAtBudget) return true;
  const hitIterationCap = sweepIterations >= maxSweepIterations;
  const hitDeadline = Date.now() >= sweepDeadlineAt;
  if (hitIterationCap || hitDeadline) {
    stoppedAtBudget = true;
    // ...
    this.log.warn(
      `[lcm] compactFullSweep stopped at ${limit} in ${phase} phase: ` +
      `conversation=${conversationId} passes=${sweepIterations} ... (returning partial result)`,
    );
    return true;
  }
  return false;
};
```

When the cap fires, the sweep exits cleanly with whatever partial compression it achieved. History that would have been summarized in passes 13+ stays in raw form — it gets another chance on the next turn, but it may pile up if each sweep keeps hitting the cap.

The CHANGELOG entry for v0.11.2 (PR #712) explicitly acknowledges the scenario that prompted this: "large conversations would otherwise drive an unbounded number of passes (observed: 16 passes on a 308K-token conversation), each potentially burning a full summarizer timeout on the turn-critical path."

Sources: [src/compaction.ts:218-245](), [src/compaction.ts:858-904](), [CHANGELOG.md:9-11]()

---

## The Infinite-Loop Bail in compactUntilUnder

The `compactUntilUnder` function runs multiple full sweeps in a loop until the conversation fits inside the token budget. Without a progress guard, a degenerate case — where each sweep makes no progress — would loop forever.

The bail is explicit and commented:

```typescript
// src/compaction.ts:1187–1194
// No progress -- bail to avoid infinite loop
if (!result.actionTaken || result.tokensAfter >= lastTokens) {
  return {
    success: false,
    rounds: round,
    finalTokens: result.tokensAfter,
  };
}
```

If a sweep doesn't reduce the token count at all, the function returns `success: false` immediately, abandoning further attempts. The conversation stays over budget; the host's overflow handler (or the emergency truncation path) takes over.

Sources: [src/compaction.ts:1186-1195]()

---

## The Failure Mode Map

```text
resolveSummarize() called
│
├─ LLM model candidates available?
│   ├─ YES → createLcmSummarizeFromLegacyParams() → normal LLM path
│   │
│   └─ NO / exception thrown
│       └─ createEmergencyFallbackSummarize()
│           → text.slice(0, 3600) + "[Truncated for context management]"
│           Level: ??? (not tracked in summary DAG)
│
summarizeWithEscalation() called (during compaction pass)
│
├─ Normal LLM call → output non-empty → use it
├─ Output >= input size → aggressive retry
│   ├─ Aggressive output < input → use it
│   └─ Still too large → buildDeterministicFallback()
│       → truncateTextToEstimatedTokens(source, 512) + "[Truncated from N tokens]"
│       Level: "fallback"
│
└─ Auth failure at any stage → return null (skip pass, no truncation stored)

compactFullSweep() (a single sweep)
│
├─ sweepIterations < 12 AND time < sweepDeadlineMs (120s) → continue passes
└─ Either limit hit → log warn, return partial result (remaining history un-compacted)

compactUntilUnder() (up to maxRounds=10 sweeps)
│
├─ tokensAfter < target → success
├─ No progress → bail (success=false, history still over budget)
└─ maxRounds exceeded → return success=(finalTokens <= targetTokens)
```

---

## Configuration Levers

| Parameter | Default | Effect |
|-----------|---------|--------|
| `maxSweepIterations` | 12 | Hard cap on leaf+condensed passes per full sweep |
| `sweepDeadlineMs` | 120,000 ms | Wall-clock budget for one full sweep |
| `compactUntilUnderDeadlineMs` | 300,000 ms | Wall-clock budget for the whole overflow-recovery loop |
| `maxRounds` | 10 | Max full sweeps in `compactUntilUnder` |
| `FALLBACK_MAX_TOKENS` | 512 (constant) | Max tokens for deterministic truncation in `compaction.ts` |
| `LCM_MAX_SWEEP_ITERATIONS` | (env var) | Override for `maxSweepIterations` |
| `LCM_SWEEP_DEADLINE_MS` | (env var) | Override for `sweepDeadlineMs` |
| `LCM_COMPACT_UNTIL_UNDER_DEADLINE_MS` | (env var) | Override for the operation-wide deadline |

Sources: [src/compaction.ts:44-97](), [src/compaction.ts:219-243](), [CHANGELOG.md:9-11]()

---

## The Uncomfortable Truth

The emergency truncation path in `engine.ts` produces summaries with no level tracking, no token-count telemetry, and a fixed character-based slice. It is invisible to the summary DAG and produces no `SummaryRecord` entry — the history simply disappears from the visible context without a proper node in the compaction tree.

The deterministic fallback in `compaction.ts` is slightly more honest: it persists a real `SummaryRecord` with `level: "fallback"` and appends a `[Truncated from N tokens]` annotation. At least there is an artifact.

The iteration cap and infinite-loop bail are the least dramatic of the four mechanisms — they stop work rather than discard it. History that didn't get summarized this turn is still in the raw-message store and will be attempted again. The real amnesia risk is when these caps fire repeatedly, context accumulates faster than the capped-sweep can clear it, and eventually the host's overflow handler or the emergency truncation callback takes the remainder.

The v0.10.0 CHANGELOG captures what happens when compaction evaluation itself is skipped: "leaving the host's emergency overflow truncation as the only safety net." The escape hatches form a layered defense-in-depth, but the deepest layer is the one that simply cuts.

Sources: [src/engine.ts:8985-9006](), [src/compaction.ts:1743-1831](), [CHANGELOG.md:72]()
