# The "Never Forgets" Plugin That Has an Amnesia Button

> lossless-claw markets itself as the agent that "never forgets" — but hidden inside the engine is an emergency truncation escape hatch that can silently throw away conversation history when summarization fails. This opening brief exposes what the README doesn't advertise.

- 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

- `README.md`
- `src/engine.ts`
- `src/summarize.ts`
- `package.json`

---

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

- [README.md](README.md)
- [src/engine.ts](src/engine.ts)
- [src/summarize.ts](src/summarize.ts)
- [src/compaction.ts](src/compaction.ts)
- [src/plugin/lcm-doctor-shared.ts](src/plugin/lcm-doctor-shared.ts)
</details>

# The "Never Forgets" Plugin That Has an Amnesia Button

The README for `lossless-claw` makes a bold promise: *"It feels like talking to an agent that never forgets. Because it doesn't."* The entire pitch rests on replacing the normal sliding-window truncation with a DAG-based summarization system that keeps every raw message in a SQLite database. What the marketing copy omits is that there is a hard-coded escape hatch buried near the end of `engine.ts` — a function called `createEmergencyFallbackSummarize` — that silently throws away conversation content whenever the real summarization pipeline cannot be established. Raw messages survive in the database, but the *summaries* that get injected into the model's live context window may be silent character-sliced truncations, not intelligent condensations.

This matters because the entire LCM design depends on the quality of its DAG summaries. If those summaries are garbage, the model's active context is garbage too — even though the raw messages sit safely on disk, unreachable without an explicit `lcm_expand` call.

---

## The "Never Forgets" Claim, Precisely Read

The README says:

> **Nothing is lost. Raw messages stay in the database. Summaries link back to their source messages. Agents can drill into any summary to recover the original detail.**

This is technically accurate — raw messages are always persisted first, before any compaction. The "never forgets" guarantee is specifically about the *storage layer*. What the claim does **not** cover is what the model *sees in context* when summaries are broken or missing. A summary that has been silently truncated to 3,600 characters with `"\n[Truncated for context management]"` appended is not the same as a thoughtful LLM-generated distillation.

Sources: [README.md:29](), [src/engine.ts:8987-9005]()

---

## The Two Amnesia Paths

There are actually two separate truncation fallbacks at different layers of the stack. They activate under different conditions and leave different fingerprints.

### Path 1: `buildDeterministicFallbackSummary` (per-call fallback in `summarize.ts`)

This fires inside the summarizer loop when a single LLM call fails to produce any text — after a primary attempt, an envelope-recovery attempt, and one retry with conservative settings have all failed.

```typescript
// src/summarize.ts:1118-1131
function buildDeterministicFallbackSummary(text: string, targetTokens: number): string {
  if (typeof text !== "string") return "";
  const trimmed = text.trim();
  if (!trimmed) {
    return "";
  }

  const maxChars = Math.max(256, targetTokens * 4);
  if (trimmed.length <= maxChars) {
    return trimmed;
  }

  return `${trimmed.slice(0, maxChars)}\n[LCM fallback summary; truncated for context management]`;
}
```

The token-to-character multiplier is `4` (a rough estimate), meaning a `targetTokens` of `1200` allows up to 4,800 characters — and anything beyond that is silently discarded. The marker string `[LCM fallback summary; truncated for context management]` is the canonical fingerprint, exported as a constant for the doctor command to detect:

```typescript
// src/plugin/lcm-doctor-shared.ts:3
export const FALLBACK_SUMMARY_MARKER = "[LCM fallback summary; truncated for context management]";
```

This path triggers when:
- All configured providers return empty content (after retry)
- All providers are exhausted from auth failures
- A `SummarizerTimeoutError` fires on the last candidate provider

Sources: [src/summarize.ts:1118-1131](), [src/summarize.ts:1672-1679](), [src/plugin/lcm-doctor-shared.ts:3-6]()

---

### Path 2: `createEmergencyFallbackSummarize` (engine-level fallback in `engine.ts`)

This is the deeper, less-obvious path. It fires not when a single call fails, but when the entire summarizer *cannot even be built* — meaning the plugin starts every compaction pass for this session using a dumb character-slicer instead of an LLM.

```typescript
// src/engine.ts:8995-9005
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]";
  };
}
```

- Normal mode: hard cap at **3,600 characters** (`900 * 4`)
- Aggressive mode: hard cap at **2,400 characters** (`600 * 4`)
- Marker: `[Truncated for context management]` (distinct from the per-call fallback marker)

This fallback is invoked by `resolveSummarize`:

```typescript
// src/engine.ts:3967-3973
    } catch (err) {
      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" };
```

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

---

## When Do These Paths Actually Fire?

The emergency path is a last resort, but the trigger conditions are real and common enough to deserve scrutiny:

| Trigger | Which fallback fires |
|---|---|
| `LcmRuntimeLlmUnavailableError` (old OpenClaw host, pre-2026.5.12) | Emergency path (re-thrown, aborts compaction) |
| `LcmRuntimeLlmPolicyError` (missing `llm.allowModelOverride` config) | Emergency path (re-thrown) |
| All provider candidates return 401 auth failure | Per-call fallback (`buildDeterministicFallbackSummary`) |
| All provider candidates time out (60s default, last candidate) | Per-call fallback |
| `createLcmSummarizeFromLegacyParams` returns `undefined` (no model candidates at all) | Emergency path |
| Any unhandled exception in `resolveSummarize` | Emergency path |
| Retry also returns empty, no next candidate | "falling back to truncation" log + per-call fallback |

The key distinction: `LcmRuntimeLlmPolicyError` and `LcmRuntimeLlmUnavailableError` are *re-thrown* — they abort compaction entirely. Most other failures are swallowed and activate one of the truncation paths. A user running lossless-claw on an old OpenClaw build (pre-`2026.5.12`) will see the runtime unavailable error propagated up rather than silently truncated, which is the correct behavior. But a misconfigured provider, expired API key, or quota exhaustion scenario can slide into silent character-slicing with no user-facing notification.

Sources: [src/summarize.ts:1466-1520](), [src/engine.ts:3967-3973](), [README.md:68]()

---

## The Retry Gauntlet Before Truncation Fires

It is worth being fair: lossless-claw does *not* give up easily. The `createLcmSummarizeFromLegacyParams` loop in `summarize.ts` goes through multiple recovery attempts before accepting defeat:

```text
For each provider candidate:
  1. Initial call to deps.complete()
  2. Check for policy/runtime/auth/response failures
  3. If empty summary → envelope re-normalization attempt
  4. If still empty or incomplete → single retry with "low" reasoning budget
  5. If retry fails → try next provider candidate (with exponential backoff)

After all candidates exhausted:
  → log.error "ALL PROVIDERS EXHAUSTED"
  → buildDeterministicFallbackSummary()
```

The provider fallback list itself can be extended via `config.fallbackProviders`, and the retry delay uses exponential backoff capped at 8 seconds (`Math.min(500 * Math.pow(2, index), 8000)`).

Sources: [src/summarize.ts:1380-1679](), [src/summarize.ts:1253-1263]()

---

## How You Can Tell It Happened

The two truncation markers are distinct and detectable:

| Marker | Source | Detected by |
|---|---|---|
| `[LCM fallback summary; truncated for context management]` | `buildDeterministicFallbackSummary` in `summarize.ts` | `FALLBACK_SUMMARY_MARKER` constant; `/lcm doctor` |
| `[Truncated for context management]` | `createEmergencyFallbackSummarize` in `engine.ts` | `/lcm doctor`; `TRUNCATED_SUMMARY_PREFIX` constant |

The `/lcm doctor` command scans for broken or truncated summaries. If you run it and see hits on these markers, you know compaction fell back to dumb truncation at some point. `/lcm doctor clean` can then identify high-confidence junk summaries from orphaned subagent runs.

Sources: [src/plugin/lcm-doctor-shared.ts:3-6](), [README.md:39]()

---

## The Ownership Escape Hatch

There is one additional fail-safe that may be invisible to users: the engine only claims `ownsCompaction: true` when the SQLite schema migration succeeded. If the database is broken, `ownsCompaction` is set to `false`, which re-enables OpenClaw's built-in sliding-window compaction as a backstop.

```typescript
// src/engine.ts:2890-2897
    this.info = {
      id: "lossless-claw",
      name: "Lossless Context Management Engine",
      version: "0.1.0",
      ownsCompaction: migrationOk,
      turnMaintenanceMode: "background",
    } as ContextEngineInfo;
```

When `ownsCompaction` is false, compaction and maintenance methods exit early with `skip("engine-unhealthy")`. In that state, the "never forgets" guarantee is completely suspended and OpenClaw reverts to its normal truncation behavior.

Sources: [src/engine.ts:2890-2897](), [src/engine.ts:7885-7889](), [src/engine.ts:8394-8397]()

---

## Summary

The "never forgets" claim is honest about the storage layer — raw messages really do persist in SQLite regardless of what happens to summaries. But the active model context, which is what the agent actually *uses* during a conversation, depends entirely on the quality of the DAG summaries. When the summarization pipeline fails — misconfigured provider, expired API key, quota exhaustion, or a provider returning empty content through three attempts — lossless-claw silently falls back to one of two deterministic character-slicers that truncate conversation chunks to approximately 2,400–4,800 characters and stamp them with a `[Truncated for context management]` or `[LCM fallback summary; truncated for context management]` marker. The `/lcm doctor` command is the only built-in mechanism for auditing whether this has happened — and it is not run automatically. The README advertises that in normal operation "you'll never need to think about compaction again," which is true until provider credentials expire or quota runs out, at which point compaction silently downgrades to the same kind of lossy truncation it was designed to replace.

Sources: [src/engine.ts:8985-9005](), [src/summarize.ts:1118-1131](), [README.md:29]()
