# lossless-claw: Hidden Secrets & Paparazzi Files

> A gossip-column deep-dive into the surprising, embarrassing, and carefully buried truths inside @martian-engineering/lossless-claw — the OpenClaw plugin that promises perfect memory but ships its own amnesia failsafe.

## Context Links

- [Agent index](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/llms.txt)
- [Human interactive wiki](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e)
- [GitHub repository](https://github.com/Martian-Engineering/lossless-claw)

## Repository Metadata

- Repository: Martian-Engineering/lossless-claw

- Generated: 2026-05-22T15:56:03.333Z
- Updated: 2026-05-22T18:49:30.405Z
- Runtime: Claude Code
- Format: Custom
- Pages: 6

## Page Index

- 01. [The "Never Forgets" Plugin That Has an Amnesia Button](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/01-the-never-forgets-plugin-that-has-an-amnesia-button.md) - 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.
- 02. [The Secret Amnesia Failsafe: Emergency Truncation](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/02-the-secret-amnesia-failsafe-emergency-truncation.md) - 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.
- 03. [The Name Change Cover-Up: From openclaw-lcm to lossless-claw](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/03-the-name-change-cover-up-from-openclaw-lcm-to-lossless-claw.md) - The package was originally published as @martian-engineering/openclaw-lcm before a deliberate rebranding to @martian-engineering/lossless-claw. A full rename spec (specs/lossless-claw-rename-spec.md) survives in the repo, listing every line-by-line change needed — including the missing repository metadata that was never present in the original package.json. The rename also gave the project its own website at losslesscontext.ai.
- 04. [Born In-Tree: The Great Extraction from OpenClaw Core](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/04-born-in-tree-the-great-extraction-from-openclaw-core.md) - lossless-claw was not built as a standalone plugin — it was extracted from OpenClaw's own in-tree src/plugins/lcm/ directory. The extraction-plan.md spec reveals the code was simply copied with all imports still pointing at OpenClaw core internals, and the entire extraction was a post-hoc dependency-injection refactor. The plugin also quietly depends on three @earendil-works/pi-* packages whose source is not public in this repo.
- 05. [The Bug Hall of Shame: Deadlocks, Bedrock Bombs & Discord Disasters](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/05-the-bug-hall-of-shame-deadlocks-bedrock-bombs-discord-disasters.md) - The CHANGELOG reads like a confessional: a sub-agent deadlock guard was needed in the expansion recursion guard; AWS Bedrock silently rejected messages with empty content arrays until a fix in v0.10.0; the /lossless native command description was too long and Discord was silently truncating it during slash-command registration; a stat-fail loop could get a conversation permanently stuck in transparent passthrough mode (no compaction ever ran); and bootstrap replay floods could inject thousands of duplicate messages. Each bug was quietly fixed in a patch release.
- 06. [Who Actually Built This: The Contributor Dossier](https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/06-who-actually-built-this-the-contributor-dossier.md) - The repo's CHANGELOG attribution tells a richer story than the README credits. Core author @jalehman (Josh Lehman, Martian Engineering) owns the majority of commits, but notable patches came from @100yenadmin (compaction bounds, image externalization, session deduplication), @0xopaque (bootstrap replay hardening), @jetd1 (live-input preservation, stat-fail placeholder seeding), @castaples (Bedrock empty-content fix), and @holgergruenhagen (Discord description truncation). The project also ships a TUI component written in Go (go.mod path github.com/Martian-Engineering/lossless-claw/tui) that is referenced in specs but not present in this repo.

## Source File Index

- `CHANGELOG.md`
- `package.json`
- `README.md`
- `RELEASING.md`
- `specs/extraction-plan.md`
- `specs/historical-session-backfill.md`
- `specs/lossless-claw-rename-spec.md`
- `src/assembler.ts`
- `src/compaction.ts`
- `src/engine.ts`
- `src/openclaw-bridge.ts`
- `src/summarize.ts`
- `src/tools/lcm-expansion-recursion-guard.ts`
- `src/types.ts`
- `test/bootstrap-flood-regression.test.ts`
- `test/regression-2026-03-17.test.ts`

---

## 01. 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.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/01-the-never-forgets-plugin-that-has-an-amnesia-button.md
- Generated: 2026-05-22T15:54:57.774Z

### 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]()

---

## 02. 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.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/02-the-secret-amnesia-failsafe-emergency-truncation.md
- Generated: 2026-05-22T15:54:15.927Z

### 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]()

---

## 03. The Name Change Cover-Up: From openclaw-lcm to lossless-claw

> The package was originally published as @martian-engineering/openclaw-lcm before a deliberate rebranding to @martian-engineering/lossless-claw. A full rename spec (specs/lossless-claw-rename-spec.md) survives in the repo, listing every line-by-line change needed — including the missing repository metadata that was never present in the original package.json. The rename also gave the project its own website at losslesscontext.ai.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/03-the-name-change-cover-up-from-openclaw-lcm-to-lossless-claw.md
- Generated: 2026-05-22T15:54:06.060Z

### Source Files

- `specs/lossless-claw-rename-spec.md`
- `package.json`
- `README.md`

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

- [specs/lossless-claw-rename-spec.md](specs/lossless-claw-rename-spec.md)
- [package.json](package.json)
- [README.md](README.md)
- [openclaw.plugin.json](openclaw.plugin.json)
- [specs/extraction-plan.md](specs/extraction-plan.md)
- [specs/depth-aware-prompts-and-rewrite.md](specs/depth-aware-prompts-and-rewrite.md)
</details>

# The Name Change Cover-Up: From openclaw-lcm to lossless-claw

This page is the paper trail of a deliberate brand overhaul. The package once lived on npm as `@martian-engineering/openclaw-lcm` and carried a plugin id of `openclaw-lcm`. Someone sat down and wrote a meticulous line-by-line rename spec covering every file that needed touching — and then left the checklist inside the repo with every box still unchecked. Today's codebase carries the new name, but the spec (`specs/lossless-claw-rename-spec.md`) survives as a fossil record of both what the project used to be called and exactly where every incriminating reference was buried.

The rename is more than a cosmetic string swap. It introduced a dedicated domain (`losslesscontext.ai`, linked from the README), added repository/homepage/bugs metadata that was literally absent from the original `package.json`, and anchored a new plugin identity (`lossless-claw`) that users configure as a slot key in OpenClaw. Knowing the old names matters because external consumers who installed `@martian-engineering/openclaw-lcm` or imported `github.com/Martian-Engineering/openclaw-lcm/tui` need to migrate manually — the spec itself calls this out explicitly.

---

## What the Old Name Was (and Where It Still Haunts the Repo)

The original published package name was `@martian-engineering/openclaw-lcm`. The spec records the before/after for every significant identifier:

| Location | Old value | New value |
|---|---|---|
| `package.json:2` | `@martian-engineering/openclaw-lcm` | `@martian-engineering/lossless-claw` |
| `package-lock.json:2,8` | `@martian-engineering/openclaw-lcm` | `@martian-engineering/lossless-claw` |
| `index.ts:746` | `id: "openclaw-lcm"` | `id: "lossless-claw"` |
| `index.ts:771` | `api.registerContextEngine("openclaw-lcm", ...)` | `api.registerContextEngine("lossless-claw", ...)` |
| `openclaw.plugin.json:2` | `"id": "openclaw-lcm"` | `"id": "lossless-claw"` |
| `tui/go.mod:1` | `github.com/Martian-Engineering/openclaw-lcm/tui` | `github.com/Martian-Engineering/lossless-claw/tui` |
| `.goreleaser.yml:41` | `name: openclaw-lcm` | `name: lossless-claw` |
| `.pebbles/config.json:2` | `"prefix": "open-lcm"` | `"prefix": "lossless-claw"` |

Sources: [specs/lossless-claw-rename-spec.md:14-63]()

The old name itself evolved through at least three aliases before stabilising — the spec also references `open-lcm`, `@martian-engineering/open-lcm`, and `OpenClaw LCM (open-lcm plugin)` in the older spec documents it was updating:

> `specs/depth-aware-prompts-and-rewrite.md:5` used to read `OpenClaw LCM (open-lcm plugin) + lcm-tui`  
> `specs/extraction-plan.md:5` used to reference `` `@martian-engineering/open-lcm` ``

Sources: [specs/lossless-claw-rename-spec.md:152-183]()

---

## The Missing Metadata — A Detail Hiding in Plain Sight

The juiciest disclosure in the rename spec is that the original `package.json` contained **no repository, homepage, or bugs field at all**. The rename spec added them as net-new entries, not replacements:

```json
// specs/lossless-claw-rename-spec.md — the "Add repository metadata (currently missing)" block
"repository": {
  "type": "git",
  "url": "git+https://github.com/Martian-Engineering/lossless-claw.git"
},
"homepage": "https://github.com/Martian-Engineering/lossless-claw#readme",
"bugs": {
  "url": "https://github.com/Martian-Engineering/lossless-claw/issues"
}
```

Today's `package.json` confirms these fields are now present at lines 74–81 — the rename was completed for this part. But the spec notes them as `[ ]` (unchecked), a checklist artifact that makes it look like a migration in-progress even if the code has moved on.

Sources: [specs/lossless-claw-rename-spec.md:19-31](), [package.json:74-81]()

---

## The Unchecked Checklist

Every single change in `specs/lossless-claw-rename-spec.md` is marked with `- [ ]` — the standard Markdown unchecked checkbox. None are `- [x]`. This is either:

1. A spec that was written before execution and the checkboxes were never ticked off as changes landed, or
2. A spec that was checked in as reference material without ever being intended as a live tracking document

Given that the current `package.json` already reads `@martian-engineering/lossless-claw` and `openclaw.plugin.json` already reads `"id": "lossless-claw"`, the rename is complete in the codebase. The unchecked boxes are fossils.

Sources: [specs/lossless-claw-rename-spec.md:15-189](), [package.json:2](), [openclaw.plugin.json:2]()

---

## The Website Arrival

The rename also brought the project its own website. The README now includes this sentence in the "What it does" section:

> Two ways to learn: read the below, or [check out this super cool animated visualization](https://losslesscontext.ai).

The domain `losslesscontext.ai` does not appear anywhere in the rename spec's checklist — it was apparently not part of the original name-swap scope, suggesting the website launched concurrently with or after the rename rather than being a tracked migration target.

Sources: [README.md:17]()

---

## Historical Scars Left Intentionally

The rename spec explicitly protects one file from rewriting:

> `.pebbles/events.jsonl` contains historical issue IDs like `open-lcm-8a9` and `open-lcm-6fc`. These are append-only history records and should be preserved as-is.

Sources: [specs/lossless-claw-rename-spec.md:206-209]()

This means any future maintainer browsing the pebbles issue tracker will see tickets prefixed `open-lcm-` that predate the rename. The spec explicitly chose archaeology over false consistency.

---

## Test Temp Directory Names: The Sneakiest Renames

Hidden among the rename targets are test fixtures — the kind of thing that quietly breaks CI only when someone runs `grep` on the old name after a rename. The spec listed six test helper calls in `test/engine.test.ts` and one in `test/migration.test.ts` that used the old name as a prefix for temporary directories:

```
"openclaw-lcm-engine-"    →  "lossless-claw-engine-"   (lines 89, 107)
"openclaw-lcm-session-"   →  "lossless-claw-session-"  (line 101)
"openclaw-lcm-home-"      →  "lossless-claw-home-"     (line 118)
"openclaw-lcm-shared-db-" →  "lossless-claw-shared-db-"(line 363)
"openclaw-lcm-migration-" →  "lossless-claw-migration-"(migration test, line 19)
```

And in `src/transcript-repair.ts`, even a log prefix was scoped for rename:

```
[openclaw-lcm] missing tool result ...  →  [lossless-claw] missing tool result ...
```

Sources: [specs/lossless-claw-rename-spec.md:121-149]()

---

## Rename Scope Summary

```text
Package & registry    ──── npm name, package-lock
Plugin identity       ──── openclaw.plugin.json id, index.ts registration
Go module path        ──── tui/go.mod, .goreleaser.yml binary name
README                ──── clone URLs, cd path, config keys, install instructions
docs/*.md             ──── architecture.md, configuration.md
specs/*.md            ──── depth-aware, summary-presentation, extraction-plan
Tests                 ──── temp dir prefix strings in engine & migration tests
Source logs           ──── log prefix in transcript-repair.ts
Issue tracker prefix  ──── .pebbles/config.json (future issues only)
Missing metadata ADD  ──── repository/homepage/bugs fields (net-new, not rename)
Website (out-of-scope)──── losslesscontext.ai (not in checklist)
```

Sources: [specs/lossless-claw-rename-spec.md:1-210]()

---

The rename spec is the most complete record of what the project used to be. It documents three name generations (`open-lcm` → `openclaw-lcm` → `lossless-claw`), reveals the original `package.json` shipped without any repository metadata, and preserves the exact line numbers where every identity token was embedded — a precise archaeological map of a project that quietly changed its name and pointed users at a shiny new domain without making any noise about it.

Sources: [specs/lossless-claw-rename-spec.md:1-10]()

---

## 04. Born In-Tree: The Great Extraction from OpenClaw Core

> lossless-claw was not built as a standalone plugin — it was extracted from OpenClaw's own in-tree src/plugins/lcm/ directory. The extraction-plan.md spec reveals the code was simply copied with all imports still pointing at OpenClaw core internals, and the entire extraction was a post-hoc dependency-injection refactor. The plugin also quietly depends on three @earendil-works/pi-* packages whose source is not public in this repo.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/04-born-in-tree-the-great-extraction-from-openclaw-core.md
- Generated: 2026-05-22T15:54:48.075Z

### Source Files

- `specs/extraction-plan.md`
- `src/types.ts`
- `src/openclaw-bridge.ts`
- `package.json`

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

- [specs/extraction-plan.md](specs/extraction-plan.md)
- [specs/lossless-claw-rename-spec.md](specs/lossless-claw-rename-spec.md)
- [src/types.ts](src/types.ts)
- [src/openclaw-bridge.ts](src/openclaw-bridge.ts)
- [src/plugin/index.ts](src/plugin/index.ts)
- [src/plugin/shared-init.ts](src/plugin/shared-init.ts)
- [src/engine.ts](src/engine.ts)
- [package.json](package.json)
- [index.ts](index.ts)
</details>

# Born In-Tree: The Great Extraction from OpenClaw Core

`@martian-engineering/lossless-claw` was never designed as a standalone plugin from day one. It was born inside OpenClaw's own codebase as `src/plugins/lcm/` — a first-party feature living cheek-by-jowl with the runtime it consumed. The repo you're looking at is the *extracted* artifact, and several layers of its history, naming decisions, and dependency tricks tell that story in surprisingly candid detail.

This page surfaces the hidden paper trail: the copy-paste origin, the two name changes, the post-hoc dependency-injection surgery, and the three `@earendil-works/pi-*` packages whose source lives nowhere public in this repo.

---

## The Copy-Paste Genesis

The extraction plan is unusually frank about how this started:

> "The source code has already been copied into `src/` and `test/` directories but all imports still reference OpenClaw core internals. The main task is to refactor imports using dependency injection."

Sources: [specs/extraction-plan.md:1-6]()

This means the initial commit of the repo was not a fresh design. It was a verbatim dump of `src/plugins/lcm/` from the OpenClaw monorepo, with every `import` still pointing at paths like `../../context-engine/types.js`, `../../routing/session-key.js`, and `../../../memory/sqlite.js`. The extraction plan itself is a *to-do list written after the code already landed*, not a design document written before it.

---

## The Three Names (A Brief Identity Crisis)

By the time the extraction happened, this plugin had already been renamed once internally. The rename spec reveals a layered history:

| Era | npm package name | Plugin/engine id | Repo name |
|-----|-----------------|-----------------|-----------|
| Earliest | `@martian-engineering/open-lcm` | `open-lcm` | `openclaw-lcm` |
| Middle | `@martian-engineering/openclaw-lcm` | `openclaw-lcm` | `openclaw-lcm` |
| Current | `@martian-engineering/lossless-claw` | `lossless-claw` | `lossless-claw` |

Sources: [specs/lossless-claw-rename-spec.md:1-10, 45-50, 181-183]()

The `.pebbles/events.jsonl` issue-tracker history (mentioned in the rename spec) still carries IDs like `open-lcm-8a9` and `open-lcm-6fc` — the append-only event log that cannot be rewritten. The extraction plan itself references the intermediate name `@martian-engineering/open-lcm` as the original target package name before the final rename landed.

---

## The Import Rewrite Surgery

The `extraction-plan.md` doubles as a forensic map of every coupling that existed between LCM and OpenClaw core. Eight distinct import paths needed replacing:

```text
../../context-engine/types.js          → openclaw/plugin-sdk
../../context-engine/registry.js       → openclaw/plugin-sdk
../../config/config.js                 → openclaw/plugin-sdk
@mariozechner/pi-ai (completeSimple)   → LcmDependencies.complete (injected)
../../agents/pi-embedded-runner/model  → LcmDependencies.resolveModel
../../agents/agent-paths.js            → LcmDependencies.resolveAgentDir
../../routing/session-key.js           → LcmDependencies.parseAgentSessionKey et al.
../../gateway/call.js                  → LcmDependencies.callGateway
../../../memory/sqlite.js              → better-sqlite3 (direct)
```

Sources: [specs/extraction-plan.md:49-128]()

The strategy was dependency injection through a single `LcmDependencies` interface, constructed entirely in `src/plugin/index.ts` from the `OpenClawPluginApi` at registration time. Every function that previously reached into OpenClaw's guts now receives a closure.

---

## The `LcmDependencies` Contract: All the Secrets in One Interface

The `LcmDependencies` interface in `src/types.ts` is essentially a full confession of what LCM once imported directly. Its fifteen-plus fields read like a negative space drawing of OpenClaw's internal API surface:

```typescript
export interface LcmDependencies {
  config: LcmConfig;
  complete: CompleteFn;                            // was: @mariozechner/pi-ai completeSimple
  callGateway: CallGatewayFn;                      // was: ../../gateway/call.js
  resolveModel: ResolveModelFn;                    // was: ../../agents/.../model.js
  parseAgentSessionKey: ParseAgentSessionKeyFn;    // was: ../../routing/session-key.js
  isSubagentSessionKey: IsSubagentSessionKeyFn;
  normalizeAgentId: (id?: string) => string;
  buildSubagentSystemPrompt: (...) => string;
  readLatestAssistantReply: (...) => string | undefined;
  resolveAgentDir: () => string;                   // was: ../../agents/agent-paths.js
  resolveSessionIdFromSessionKey: ...;
  resolveSessionTranscriptFile: ...;
  listStartupSessionFileCandidates?: ...;
  agentLaneSubagent: string;
  log: { info, warn, error, debug };
}
```

Sources: [src/types.ts:115-178]()

One field even has an inline tombstone in the live source: `sanitizeToolUseResultPairing` was removed from `LcmDependencies` and its comment says "now imported directly in assembler from transcript-repair.ts" — the refactor was still in flux when this file settled.

---

## The `openclaw-bridge.ts` Admission

`src/openclaw-bridge.ts` exists because the `openclaw/plugin-sdk` package doesn't export the newer context-engine type symbols yet. Rather than waiting for the SDK to catch up, the plugin ships its own local redefinitions of `ContextEngine`, `AssembleResult`, `BootstrapResult`, and friends. The file comment says it plainly:

> "This module intentionally keeps the context-engine contract local because older OpenClaw SDK packages do not publish these newer type symbols yet."

Sources: [src/openclaw-bridge.ts:7-9]()

The `OpenClawPluginApi` type defined there is deliberately permissive (`[key: string]: any`), a defensive stance against an evolving host API.

---

## The `@earendil-works/pi-*` Packages: Private Lineage, Public Dependency

The three runtime dependencies are not hosted in this repository:

| Package | Role | Version range |
|---------|------|---------------|
| `@earendil-works/pi-agent-core` | Core agent message types | `>=0.74 <1` |
| `@earendil-works/pi-ai` | LLM completion primitives | `>=0.74 <1` |
| `@earendil-works/pi-coding-agent` | `SessionManager` used in `engine.ts` | `>=0.74 <1` |

Sources: [package.json:36-39]()

The extraction plan's **original** import map lists these as `@mariozechner/pi-agent-core` and `@mariozechner/pi-ai` — a personal npm scope for [Mario Zechner](https://github.com/badlogic). By the time the extraction landed in this repo, the packages had been published under the `@earendil-works` org scope, but the extraction plan spec still refers to `@mariozechner/pi-ai` as the old import that needed to be replaced.

Sources: [specs/extraction-plan.md:86-90, 132-133]()

The `engine.ts` file imports `SessionManager` directly from `@earendil-works/pi-coding-agent` with no local shim — this is the one coupling that was *kept* as a direct peer dependency rather than injected through `LcmDependencies`.

Sources: [src/engine.ts:8]()

---

## The Singleton Anti-DB-Lock-Storm Pattern

When OpenClaw v2026.4.5+ started calling `register()` per-agent-context (main agent, subagents, cron lanes), each call would have opened a fresh SQLite connection and run migrations on the same `~/.openclaw/lcm.db`. The result: lock storms on large databases.

The fix is a `globalThis`-keyed singleton map using `Symbol.for()`:

```typescript
const SHARED_KEY = Symbol.for("@martian-engineering/lossless-claw/shared-init");
```

Sources: [src/plugin/shared-init.ts:30-31]()

The first `register()` call wins and stores its `waitForEngine`/`waitForDatabase` closures in the global map under the DB path key. Every subsequent call reuses those closures without opening a new connection.

---

## The Build: Bundled, Minified, External-Punched

The esbuild command in `package.json` is worth reading closely:

```
esbuild index.ts --bundle --platform=node --target=node22 --format=esm
  --outfile=dist/index.js
  --external:openclaw
  --external:"@earendil-works/*"
  --minify-whitespace
```

Sources: [package.json:19]()

`openclaw` and the entire `@earendil-works/*` namespace are marked external — they are *not* bundled into `dist/index.js`. OpenClaw provides `openclaw` at runtime; the `@earendil-works` packages come from the host's `node_modules`. Everything else (TypeScript source, `better-sqlite3`, `@sinclair/typebox`) is bundled in. This means a consumer who installs the plugin with `--ignore-scripts` may fail to build the native `better-sqlite3` addon — a known risk the extraction plan explicitly flags.

Sources: [specs/extraction-plan.md:168-172]()

---

## Diagram: Before and After the Extraction

```text
BEFORE (in-tree)                     AFTER (standalone plugin)
─────────────────────────────────    ──────────────────────────────────────
OpenClaw monorepo                    @martian-engineering/lossless-claw
  src/
    plugins/
      lcm/  ←── LCM code            src/
        engine.ts  ─────── directly    engine.ts  ──── via LcmDependencies
        summarize.ts ──── imports       summarize.ts     (injected at register)
        tools/ ─────────── from         tools/
    context-engine/                  openclaw/plugin-sdk  (external)
    routing/session-key.js           @earendil-works/pi-*  (external peers)
    gateway/call.js                  better-sqlite3  (bundled direct dep)
    memory/sqlite.js
```

---

## Summary

`lossless-claw` is a post-hoc extraction of OpenClaw's in-tree LCM feature, dressed up as a standalone plugin through a systematic dependency-injection refactor. The extraction plan spec is still in the repo as `specs/extraction-plan.md` — a candid to-do list written *after* the code was already copied, not before. The package carries two ghost names (`open-lcm`, `openclaw-lcm`) in its rename spec and issue-tracker history, relies on three `@earendil-works/pi-*` packages whose source is not public here, and includes a compatibility shim (`src/openclaw-bridge.ts`) for SDK symbols the host hasn't published yet. The whole thing is held together by a single `LcmDependencies` interface that is, in effect, a type-safe record of everything LCM once stole directly from OpenClaw's internals.

Sources: [specs/extraction-plan.md:1-6](), [specs/lossless-claw-rename-spec.md:181-209]()

---

## 05. The Bug Hall of Shame: Deadlocks, Bedrock Bombs & Discord Disasters

> The CHANGELOG reads like a confessional: a sub-agent deadlock guard was needed in the expansion recursion guard; AWS Bedrock silently rejected messages with empty content arrays until a fix in v0.10.0; the /lossless native command description was too long and Discord was silently truncating it during slash-command registration; a stat-fail loop could get a conversation permanently stuck in transparent passthrough mode (no compaction ever ran); and bootstrap replay floods could inject thousands of duplicate messages. Each bug was quietly fixed in a patch release.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/05-the-bug-hall-of-shame-deadlocks-bedrock-bombs-discord-disasters.md
- Generated: 2026-05-22T15:56:03.329Z

### Source Files

- `CHANGELOG.md`
- `src/tools/lcm-expansion-recursion-guard.ts`
- `src/assembler.ts`
- `test/bootstrap-flood-regression.test.ts`
- `test/regression-2026-03-17.test.ts`

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

- [CHANGELOG.md](CHANGELOG.md)
- [src/tools/lcm-expansion-recursion-guard.ts](src/tools/lcm-expansion-recursion-guard.ts)
- [src/assembler.ts](src/assembler.ts)
- [src/plugin/lcm-command.ts](src/plugin/lcm-command.ts)
- [test/bootstrap-flood-regression.test.ts](test/bootstrap-flood-regression.test.ts)
- [test/regression-2026-03-17.test.ts](test/regression-2026-03-17.test.ts)
</details>

# The Bug Hall of Shame: Deadlocks, Bedrock Bombs & Discord Disasters

Every production system accumulates a rogues' gallery of bugs that only surface under real-world conditions — the ones that slip past every code review and unit test until a user hits them at the worst possible moment. Lossless Claw is no exception. Its CHANGELOG reads less like a feature log and more like a confessional: five separate patch releases each quietly fixed a different catastrophic failure mode, each one a small masterwork of "who could have predicted this."

This page digs into the juicy specifics — what actually went wrong, why it was subtle, and how each fix closed the gap without introducing new problems.

---

## Bug 1: The Sub-Agent Deadlock That Wasn't (Until It Was)

**Version fixed:** 0.11.x (via `lcm-expansion-recursion-guard.ts`)

### What happened

The `lcm_expand_query` tool lets an AI agent spawn sub-agents to handle expensive retrieval work. The recursion guard was supposed to prevent sub-agents from re-entering `lcm_expand_query`, but it only blocked on *depth*. It had no concept of *concurrency*.

The specific failure mode: two calls originating from the same origin session could simultaneously acquire what should have been a single expansion lane. Both would proceed. Both would stamp delegated contexts. Both would eventually wait for the other to finish. Classic deadlock.

### The fix

Two separate guard mechanisms were added:

**Recursion guard** (`evaluateExpansionRecursionGuard`): blocks any session whose stamped `expansionDepth >= EXPANSION_DELEGATION_DEPTH_CAP` (hardcoded to 1). A second-time block on the same `requestId` is classified as `"idempotent_reentry"` rather than `"depth_cap"` so telemetry can distinguish retries from genuine recursive calls.

**Concurrency guard** (`acquireExpansionConcurrencySlot`): tracks a single active `requestId` per origin session in `activeRequestIdByOriginSessionKey`. A second concurrent caller gets `EXPANSION_CONCURRENCY_BLOCKED` immediately:

```typescript
// src/tools/lcm-expansion-recursion-guard.ts:255-266
if (activeRequestId && activeRequestId !== requestId) {
  return {
    blocked: true,
    code: EXPANSION_CONCURRENCY_ERROR_CODE,
    reason: "origin_session_in_flight",
    message:
      `${EXPANSION_CONCURRENCY_ERROR_CODE}: Another lcm_expand_query delegation is already ` +
      `in flight for origin session (${originSessionKey}; activeRequestId=${activeRequestId}). ` +
      buildExpansionConcurrencyRecoveryGuidance(originSessionKey),
    ...
  };
}
```

The recovery message even tells the blocked sub-agent what to do: use `lcm_grep` or `lcm_describe` as immediate fallbacks instead of waiting.

### The sneaky part

The guard maintains three separate in-memory maps: one for delegated contexts, one for blocked request IDs (for idempotency detection), and one for the active slot. All three are module-level singletons, reset only in tests. This means a crashed gateway process that never called `releaseExpansionConcurrencySlot` would leave a ghost slot — a potential follow-up bug if sub-agents are long-lived.

Sources: [src/tools/lcm-expansion-recursion-guard.ts:61-63](src/tools/lcm-expansion-recursion-guard.ts), [src/tools/lcm-expansion-recursion-guard.ts:247-278](src/tools/lcm-expansion-recursion-guard.ts)

---

## Bug 2: AWS Bedrock's Invisible Content Rejection

**Version fixed:** v0.10.0 (PR #606)

### What happened

AWS Bedrock Converse API has strict opinions about message content: if `content` is an empty array `[]` for a `user` or `toolResult` message, it rejects the entire request with:

> `The content field in the Message object at messages.N is empty. Add a ContentBlock object to the content field and try again.`

The pre-existing empty-content filter in the assembler only checked the `assistant` role. User and toolResult messages with momentarily empty content arrays (possible during content transformation pipelines) sailed right through to Bedrock, which then silently refused them. The error surfaced as an API rejection downstream, far from its cause.

### The fix

A unified `isEmptyMessageContent` helper was added to `src/assembler.ts` that handles every role:

```typescript
// src/assembler.ts:151-170
export function isEmptyMessageContent(message: {
  role?: unknown;
  content?: unknown;
}): boolean {
  if (!message) return true;
  const content = message.content;
  if (content === undefined || content === null) return true;
  if (Array.isArray(content)) {
    if (content.length === 0) return true;          // ← the new universal guard
    if (message.role === "assistant") {
      if (isThinkingOnlyContent(content)) return true;
      if (isBlankContent(content)) return true;
    }
    return false;
  }
  if (typeof content === "string") {
    return content.trim() === "";
  }
  return false;
}
```

The comment in the source is unusually candid about the asymmetric gap: *"The pre-existing filter only protected the assistant role, leaving an asymmetric gap when an empty user/toolResult shape is momentarily produced upstream."*

### The sneaky part

Bedrock's rejection message uses the phrase *"Add a ContentBlock object"* — which means if you were watching logs, you'd see a provider error about a missing content block, with no indication that lossless-claw's assembly pipeline was the source. The bug was effectively anonymous in production traces.

Sources: [src/assembler.ts:129-170](src/assembler.ts), [CHANGELOG.md](CHANGELOG.md) (v0.10.0, PR #606)

---

## Bug 3: Discord Silently Ate the `/lossless` Command

**Version fixed:** v0.11.2 (PR #672)

### What happened

Discord enforces a hard limit on slash command description length during registration. If a command's description string exceeds this limit, Discord truncates or silently rejects the registration — no error, no warning. The command simply stops appearing in users' command menus, or worse, appears with a garbled description.

The `/lossless` command was registered with a description string that was too long. Discord quietly swallowed the registration failure.

### The fix

The description was shortened to fit within Discord's limit. The current value in the source:

```typescript
// src/plugin/lcm-command.ts:2232-2233
description:
  "Lossless Claw health, backups, compaction, junk review, and doctor tools.",
```

This is short enough to pass Discord's validator without truncation. The command also exposes the native name `"lossless"` (mapping to `/lossless` in Discord) via `nativeNames.default`.

### The sneaky part

There was no error, no log entry, no exception. Discord simply silently truncated the registration. Users would see the command appear to register successfully on the OpenClaw side, but the command would be broken or missing on the Discord side. This category of bug — provider-side silent failure with no feedback — is particularly nasty because the symptom (broken command) appears nowhere near the cause (description too long at registration time).

Sources: [src/plugin/lcm-command.ts:2225-2234](src/plugin/lcm-command.ts), [CHANGELOG.md](CHANGELOG.md) (v0.11.2, PR #672)

---

## Bug 4: The Stat-Fail Loop That Killed Compaction Forever

**Version fixed:** v0.11.0 (PR #685)

### What happened

This one has a chain-of-custody worthy of a true crime doc:

1. **PR #649** added a graceful fallback in `afterTurn`: when `stat(sessionFile)` fails, return `hasOverlap:true` to allow live persistence to continue. The expectation was that the `refreshAfterTurnBootstrapState` hook would then refresh the checkpoint on the next call.

2. **The bug**: that hook calls `refreshBootstrapState`, which also calls `stat(sessionFile)` — and also throws on failure. The catch block in the hook swallowed the error silently. So `conversation_bootstrap_state` remained `NULL`.

3. **The consequence**: every subsequent `afterTurn` re-entered the slow path with `reason="checkpoint-missing"`. Checkpoint-missing is explicitly excluded from `allowNoAnchorImport`. The conversation got **permanently stuck**.

4. **The stuck state**: once stuck, the assembler's safe-fallback returned `params.messages` verbatim — raw, uncompacted messages — because no DB anchor could be established. Compaction never ran again. The context window filled up. The host's emergency overflow truncation became the only safety net.

### The fix

When the stat-fail slow path is hit, a placeholder `conversation_bootstrap_state` row is now written directly via `summaryStore.upsertConversationBootstrapState` — bypassing `stat()` entirely — so the contract "permissive return ⟹ checkpoint exists" is restored. Subsequent turns recover from `offset=0` once the transcript becomes statable again, routing through the DB-anchor reconciliation path so already-persisted messages aren't replayed.

The CHANGELOG entry for this fix is the most detailed of the five — three dense paragraphs explaining the causal chain — because the original PR author clearly wanted no ambiguity about what went wrong.

Sources: [CHANGELOG.md](CHANGELOG.md) (v0.11.0, PR #685)

---

## Bug 5: Bootstrap Replay Floods

**Version fixed:** v0.10.0 (PR #640), tested in `test/bootstrap-flood-regression.test.ts`

### What happened

When a gateway restarts and reconnects to an existing conversation, LCM bootstraps by reading the session transcript and importing messages into its SQLite DB. The bug: if the stored checkpoint was stale (wrong `mtime`, wrong `size`, mismatched hash), the bootstrap `reconcileSessionTail` path would treat the entire transcript as "new" content to import.

On a long conversation with thousands of messages, this meant **thousands of duplicate rows** being injected into LCM's DB on every restart. Every row existed twice (or more). Compaction would process duplicates as real history. Expand queries would surface duplicate summaries. The DB bloated silently.

The two conditions that triggered this:
- The `maintain()` function rewrote the JSONL transcript but didn't update the bootstrap checkpoint (the PR #280 bug that the flood test covers).
- Any situation where checkpoint state diverged from actual file state (gateway crash, OS-level filesystem quirk, etc.).

### The fix

Two defenses were added:

**Checkpoint update**: `maintain()` now updates the bootstrap checkpoint after a successful transcript rewrite, so the next bootstrap sees the post-rewrite state as current and imports 0 messages.

**Import cap**: `reconcileSessionTail` now enforces a cap of `max(existingDbCount × 0.2, 50)` rows per bootstrap. If a reconcile would import more than the cap, it aborts with `reason: "reconcile import capped"` and imports 0 messages — protecting the DB even when the checkpoint is fully stale.

The regression test covers both defenses separately and together:

```typescript
// test/bootstrap-flood-regression.test.ts:323-325
expect(
  boot2.reason,
  "should report import cap was hit",
).toBe("reconcile import capped");
```

The test also verifies the combined case: a valid checkpoint update blocks the flood; a corrupted checkpoint triggers the cap; both together provide defense-in-depth.

Sources: [test/bootstrap-flood-regression.test.ts:212-447](test/bootstrap-flood-regression.test.ts), [CHANGELOG.md](CHANGELOG.md) (v0.10.0, PR #640)

---

## Pattern: Why These Bugs Are Interesting

All five bugs share a structural property: **the failure was silent and the symptom was distant from the cause**.

| Bug | Silent failure | Distant symptom |
|---|---|---|
| Sub-agent deadlock | No deadlock error; sub-agents just hang | Stalled user turn |
| Bedrock content rejection | API error with no pointer to LCM pipeline | Provider-level failure |
| Discord command truncation | Registration "succeeds" on LCM side | Command missing in Discord UI |
| Stat-fail loop | Compaction never runs; no error logged | Context window overflow later |
| Bootstrap flood | Duplicate rows silently inserted | DB bloat, wrong summaries |

Each fix added either an explicit guard (the concurrency slot, the import cap, the unified empty-content filter) or restored a broken contract (the checkpoint-after-maintain, the placeholder checkpoint-on-stat-fail). None of the fixes were large — most are under 20 lines — but each required understanding a subtle invariant that existed only in the original author's head until the bug surfaced.

The test suite now covers four of the five with dedicated regression files, ensuring that the same class of bug cannot re-enter silently.

Sources: [CHANGELOG.md](CHANGELOG.md) (v0.10.0–v0.11.2 entries), [test/bootstrap-flood-regression.test.ts](test/bootstrap-flood-regression.test.ts), [test/regression-2026-03-17.test.ts](test/regression-2026-03-17.test.ts)

---

## 06. Who Actually Built This: The Contributor Dossier

> The repo's CHANGELOG attribution tells a richer story than the README credits. Core author @jalehman (Josh Lehman, Martian Engineering) owns the majority of commits, but notable patches came from @100yenadmin (compaction bounds, image externalization, session deduplication), @0xopaque (bootstrap replay hardening), @jetd1 (live-input preservation, stat-fail placeholder seeding), @castaples (Bedrock empty-content fix), and @holgergruenhagen (Discord description truncation). The project also ships a TUI component written in Go (go.mod path github.com/Martian-Engineering/lossless-claw/tui) that is referenced in specs but not present in this repo.

- Page Markdown: https://grok-wiki.com/public/wiki/martian-engineering-lossless-claw-a94e8135853e/pages/06-who-actually-built-this-the-contributor-dossier.md
- Generated: 2026-05-22T15:54:55.513Z

### Source Files

- `CHANGELOG.md`
- `specs/historical-session-backfill.md`
- `specs/lossless-claw-rename-spec.md`
- `RELEASING.md`

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

- [CHANGELOG.md](CHANGELOG.md)
- [RELEASING.md](RELEASING.md)
- [specs/historical-session-backfill.md](specs/historical-session-backfill.md)
- [specs/lossless-claw-rename-spec.md](specs/lossless-claw-rename-spec.md)
- [tui/go.mod](tui/go.mod)
- [tui/main.go](tui/main.go)
- [tui/README.md](tui/README.md)
</details>

# Who Actually Built This: The Contributor Dossier

The CHANGELOG is a better biography of this project than the README. It names real people, real PRs, and real production bugs — and it reveals a tight inner circle, a handful of well-timed outside patches, and at least one identity mystery worth noting. What follows is the story the commit log actually tells.

One meta-quirk to know upfront: the project's Changesets workflow means changelog entries are attributed to *whoever opened the PR that includes the changeset file* — not necessarily who wrote the code. The maintainer [explicitly called this out in v0.9.4](#changelog-quirks), retroactively crediting `@jalehman` for a PR that had shipped without his name attached. The contributor table below reflects attributed PRs, not necessarily lines of code.

---

## @jalehman (Josh Lehman, Martian Engineering) — The Architect

**69 attributed PRs** across all versions, making him the dominant force in the CHANGELOG by a factor of 2.5x over the next contributor.

Josh owns the deep plumbing: the cache-aware compaction scheduler, the bootstrap reconcile loop, the `afterTurn` pipeline, the session-file rotation system, the `lcm_*` tool registrations, and the TUI's repair/rewrite/backfill/transplant subsystems. He also owns the embarrassing bugs — the stale `clearStableOrphanStrippingOrdinal` call in v0.11.2 was his, left over from an earlier refactor of the cache-state-dependent assembly path. His fix message is deadpan: "Remove a stale stable-orphan invalidation call."

A sampling of his defining contributions:

| Version | PR | What it did |
|---|---|---|
| v0.11.2 | [#714](https://github.com/Martian-Engineering/lossless-claw/pull/714) | Removed ghost `clearStableOrphanStrippingOrdinal` call that crashed transcript reconcile |
| v0.11.0 | [#692](https://github.com/Martian-Engineering/lossless-claw/pull/692) | Introduced `/lossless focus <prompt>`, focus brief generation, and the full unfocus/refocus lifecycle |
| v0.10.0 | [#661](https://github.com/Martian-Engineering/lossless-claw/pull/661) | Preserved hot prompt-cache deferral for OpenAI GPT models; raised critical budget pressure ratio to 0.90 |
| v0.9.0 | [#592](https://github.com/Martian-Engineering/lossless-claw/pull/592) | Automated session-JSONL rotation with backup-backed safety guardrails and startup scan limits |
| v0.8.1 | [#380](https://github.com/Martian-Engineering/lossless-claw/pull/380) | Stopped re-running startup backfills that had already completed |
| 0.7.x | [#400](https://github.com/Martian-Engineering/lossless-claw/pull/400) | Stripped JSDoc comments from `dist/index.js` so OpenClaw's install-time safety scanner stopped flagging "Fetch all context items" as an env-harvesting network pattern |

Sources: [CHANGELOG.md](CHANGELOG.md) — `Thanks [@jalehman]` lines throughout

---

## @100yenadmin — The Compaction Specialist

**28 attributed PRs**, all of them surgical corrections to the compaction engine and ingest pipeline. This contributor's fingerprints are on every place where "it worked, but only until a long session" — and they consistently shipped the fix before production burned down.

Their greatest hits:

- **Compaction livelock fix (v0.9.3, [#557](https://github.com/Martian-Engineering/lossless-claw/pull/557))** — Fixed a silent livelock where mutation-sensitive providers (Anthropic, Codex, Copilot) would keep refreshing `lastCacheTouchAt` so the cache TTL never expired, deferred compaction never fired, and the runtime emergency overflow handler was left to do everything. The fix adds a critical-pressure escape at 70% token budget.

- **Compaction-sweep bounds (v0.11.2, [#712](https://github.com/Martian-Engineering/lossless-claw/pull/712))** — Added `maxSweepIterations` (default 12) and `sweepDeadlineMs` (default 120,000 ms) to prevent `compactFullSweep` from hanging an agent turn indefinitely. Both are configurable via `LCM_MAX_SWEEP_ITERATIONS` / `LCM_SWEEP_DEADLINE_MS`.

- **Image externalization generalization (v0.11.0, [#573](https://github.com/Martian-Engineering/lossless-claw/pull/573))** — Extended the native-image-block externalizer from user-role-only to assistant, system, tool, and toolResult messages, closing a gap left by v0.9.3.

- **Deferred compaction as default (v0.8.x, [#408](https://github.com/Martian-Engineering/lossless-claw/pull/408))** — Introduced explicit maintenance debt tracking so foreground turns no longer run threshold compaction inline.

- **`afterTurn` empty-batch skip fix (v0.9.x, [#621](https://github.com/Martian-Engineering/lossless-claw/pull/621))** — Fixed a subtle early-return that meant conversations could grow well past `contextThreshold` when `deduplicateAfterTurnBatch` removed every new message. The fix threads compaction evaluation through even when the ingest batch is empty.

The changelog note on [#574](https://github.com/Martian-Engineering/lossless-claw/pull/574) is the most revealing: `@100yenadmin` retroactively added a changelog entry crediting `@jalehman` for a PR that had been attributed to `@100yenadmin`'s own changeset. That's an unusually honest attribution correction.

Sources: [CHANGELOG.md](CHANGELOG.md) — `Thanks [@100yenadmin]` lines; [RELEASING.md:26-32](RELEASING.md)

---

## @jetd1 — The Transcript Continuity Fixer

**6 attributed PRs**, all targeting the same narrow, painful failure mode: what happens when OpenClaw *changes* the JSONL file out from under LCM mid-session.

- **[#688](https://github.com/Martian-Engineering/lossless-claw/pull/688)** — Preserved unpersisted inter-session live input when assembling context from the durable DB frontier. The problem: if a user typed something before the session was fully persisted, that input vanished on context assembly.

- **[#685](https://github.com/Martian-Engineering/lossless-claw/pull/685)** — Seeded a placeholder `conversation_bootstrap_state` row in the `afterTurn` slow-path stat-fail branch so the *next* turn can recover instead of crashing blind.

- **[#659](https://github.com/Martian-Engineering/lossless-claw/pull/659)** — Handled the case where OpenClaw rewrites a session JSONL in-place and the bootstrap checkpoint now points past the new file end. LCM previously tried to read past EOF silently; the fix treats it as an epoch rollover.

- **[#403](https://github.com/Martian-Engineering/lossless-claw/pull/403)** — Moved bootstrap file I/O off the Node.js event loop entirely. Previously `readFileSegment` and `readLastJsonlEntryBeforeOffset` used synchronous `openSync`/`readSync`/`statSync`, which could block the gateway for minutes on multi-MB JSONL transcripts.

- **[#649](https://github.com/Martian-Engineering/lossless-claw/pull/649)** — Preserved continuity when OpenClaw switches to a new transcript file for the same session key, treating it as a new transcript epoch rather than a broken checkpoint.

Sources: [CHANGELOG.md](CHANGELOG.md) — `Thanks [@jetd1]` lines in v0.10.0, v0.11.0

---

## @0xopaque — One PR, One Real Problem

**1 attributed PR**, but it's a meaningful one.

- **[#640](https://github.com/Martian-Engineering/lossless-claw/pull/640) (v0.10.0)** — Prevented bootstrap from replaying prior transcript rows as fresh LCM messages. The fix adds replay-shaped-tail filtering in bootstrap append/reconcile, rejects same-timestamp prior-content floods at the message-write layer, and wraps ingest batches in transactions. This is exactly the kind of patch that prevents subtle database corruption in production sessions.

Sources: [CHANGELOG.md](CHANGELOG.md) — v0.10.0 section

---

## @castaples — The AWS Bedrock Bridge

**1 attributed PR**, fixing a production incompatibility with Amazon Bedrock.

- **[#606](https://github.com/Martian-Engineering/lossless-claw/pull/606)** — Fixed `messages.0 is empty` Bedrock Converse rejections by extending the empty-content filter from `assistant`-only to `user` and `toolResult` roles. The new `isEmptyMessageContent` helper handles empty arrays, empty strings, null, and undefined for any role. Without this, any user or tool-result message with a briefly-empty content array would reach Bedrock and trigger a hard rejection: *"The content field in the Message object at messages.N is empty."*

Sources: [CHANGELOG.md](CHANGELOG.md) — v0.9.4 / v0.10.0 boundary

---

## @holgergruenhagen — The Discord One-Liner

**2 attributed PRs**, both quality-of-life patches.

- **[#672](https://github.com/Martian-Engineering/lossless-claw/pull/672)** — Shortened the `/lossless` native command description because Discord truncates command descriptions at a character limit during slash-command registration, making the command unusable in Discord integrations.

Sources: [CHANGELOG.md](CHANGELOG.md) — v0.11.1 section

---

## The Wider Long Tail

Beyond the inner circle, the CHANGELOG names a dozen more one-off contributors — each fixing a specific production edge case they personally encountered:

| Handle | Contribution |
|---|---|
| @GodsBoy (4 PRs) | Bootstrap re-read avoidance, compaction cap fixes, prompt-aware eviction fallback, Codex OAuth docs |
| @mvanhorn (3 PRs) | Conversation prune function, LLM startup diagnostics to stderr, `lcm_expand_query` token accounting docs |
| @liu51115 (3 PRs) | CJK timestamp parsing, auth circuit-breaker isolation, defensive `.trim()` crash fix |
| @ryanngit (2 PRs) | Conversation creation race fix, SQLite busy-timeout increase to 30 seconds |
| @semiok (2 PRs) | CJK full-text search fallback via LIKE, 60-second LLM call timeout protection |
| @jeremyheslop | `contracts.tools` declaration in `openclaw.plugin.json` so OpenClaw 2026.5.2 accepts the plugin |
| @andyylin | Context-engine registration alignment after the `openclaw-lcm` → `lossless-claw` rename |
| @tingyiy | Explicit timezone offset preservation for stored timestamps |
| @catgodtwno4 | Auth-error detection fix for nested `data`/`body` envelopes |
| @vincentkoc | Single mention, version unknown |

Sources: [CHANGELOG.md](CHANGELOG.md) — minor-version sections throughout

---

## The Ghost Repo: The Go TUI That Lives Here

The rename spec planted a flag: `tui/go.mod:1` should read `module github.com/Martian-Engineering/lossless-claw/tui`. It does. The TUI is **present in this repo** at `tui/`, not absent — the spec description noting it is "not present in this repo" is out of date.

The TUI is a full Go application using [Bubble Tea](https://github.com/charmbracelet/bubbletea) and ships as the `lcm-tui` binary. It includes interactive screens for agents, sessions, conversations, summaries, files, context, focus briefs, and Codex context comparison. The `backfill.go` file implements the historical session import described in [specs/historical-session-backfill.md](specs/historical-session-backfill.md), which requires calling the same compaction DAG logic from Go that the TypeScript plugin runs at runtime.

```
tui/
├── main.go          ← Bubble Tea screens: agents, sessions, conversation, summaries, files, context, focus_briefs, codex_context_compare
├── backfill.go      ← Historical JSONL import + Go-side compaction loop
├── repair.go        ← Anthropic client + repair workflow
├── rewrite.go       ← Summary rewrite flow
├── transplant.go    ← DAG transplant between conversations
├── dissolve.go      ← Conversation dissolution
├── doctor.go        ← Diagnostic + apply workflow
├── data.go          ← Session parsing + normalization
├── prompts.go       ← Prompt rendering (shared with plugin)
└── go.mod           ← module github.com/Martian-Engineering/lossless-claw/tui
```

Sources: [tui/go.mod:1](tui/go.mod), [tui/main.go:22-29](tui/main.go), [tui/README.md:1-20](tui/README.md), [specs/historical-session-backfill.md:47-103](specs/historical-session-backfill.md)

---

## The Release Attribution Quirk

RELEASING.md makes a structural admission that shapes how to read any contributor count: *"For external PRs, do not expect the contributor to know or run the Changesets workflow. The reviewer or merge maintainer should add the changeset before merge."* This means the CHANGELOG headline attribution reflects who *merged* the changeset file, not always who wrote the code. The explicit retroactive note in v0.9.4 — where `@100yenadmin` credited `@jalehman` for a PR the changelog had silently swallowed — is the clearest example of the seams showing.

The practical upshot: the 69 `@jalehman` entries are a floor, not a ceiling. His actual code surface is likely larger. Similarly, one-time external contributors may have shipped the fix while a maintainer added the changelog entry.

Sources: [RELEASING.md:26-35](RELEASING.md), [CHANGELOG.md](CHANGELOG.md) v0.9.4 entry for PR [#574](https://github.com/Martian-Engineering/lossless-claw/pull/574)

---
