# Clavue v8.13.6 — Resume consistency surface: distinguish compacted from lost

Released: 2026-05-20

## Why this exists

A user upgrading to v8.13.5 hit `/resume` and saw:

```
Resume loaded 3 of 12 messages (-9)
```

Followed by a reasonable concern: nine messages just disappeared?

The concern was wrong, but the surface was indefensible. Nothing was
lost. The user had run `/compact` earlier in the session — by design,
that prunes older turns from the resumable chain while leaving the
on-disk transcript fully intact. The "12" was the writer's pre-compact
checkpoint counter (it counts every loggable message it ever saw); the
"3" was the post-compact reader's chain length. Two different units,
one scary minus sign.

This is the worst kind of bug. Karpathy's framing: a confidently wrong
signal is *worse* than silent loss, because users react to it. Yao
Shunyu's framing of trajectory rewards in agent training applies the
same way to telemetry — if your observation function returns ambiguous
labels, every downstream action built on it inherits the ambiguity.

v8.13.6 splits the single negative-delta surface into four explicit
classifications and tunes each one's voice.

## What changed

### 1. New module `src/utils/resumeConsistency.ts`

Pure analysis function `computeResumeConsistency(chain, { fullMessageMap })`
extracted from `sessionStorage.ts` so it can be unit-tested directly
(the parent file pulls in fs + analytics + a Bun-bundle stub that the
node test runner can't load).

It returns one of four `reason` values:

| Reason          | Trigger                                                      | Surface       |
|-----------------|--------------------------------------------------------------|---------------|
| `consistent`    | delta == 0 and no orphaned u/a uuids                         | (no message)  |
| `compacted`     | delta < 0, compact_boundary present, no orphaned u/a uuids   | info          |
| `lost`          | One or more user/assistant uuids on disk missing from chain  | warning       |
| `reconstructed` | delta > 0 (orphan-recovery added content back)               | info          |

Priority is hard-coded: **lost beats everything**. Even if a chain has
both a compact_boundary *and* a truly orphaned user/assistant uuid, the
result is `lost`. We never let a friendly "compacted" label mask a real
chain-walk bug.

`fullMessageMap` is the on-disk transcript map (the output of
`loadTranscriptFile`). The "lost" classification needs it — without the
full map we can't tell whether negative delta is /compact pruning or a
chain-walk dropping content. Legacy callers that pass only the chain
get the pre-D heuristic (delta sign + compact_boundary count) and never
see `lost`.

### 2. `src/utils/sessionStorage.ts`

`checkResumeConsistency` is now a thin wrapper that delegates to
`computeResumeConsistency` and adds the `tengu_resume_consistency_delta`
analytics event. Same signature, same return type. Existing callers
continue to import from here.

`loadFullLog` now passes the full transcript map into the consistency
check at chain-build time and attaches the result to the returned
`LogOption` so `/resume` UI can render the right surface text without
recomputing.

### 3. `src/utils/sessionRestore.ts`

Surface text now branches on `reason`:

- `lost` → warning, "Resume loaded N of M messages — K user/assistant
  messages on disk could not be reconstructed into the chain."
- `compacted` → info, "Resume loaded N of M messages — earlier turns
  were merged by /compact (transcript on disk is intact)."
- `reconstructed` → info, "Resume loaded N of M messages — recovered
  K orphaned tool results."
- `consistent` → no message.

The previous v8.13.5 surface was a single warning regardless of reason.

### 4. `src/utils/conversationRecovery.ts`

Reuses the `consistencyDelta` from the LogOption when present (avoids
the second pass) and falls back to recomputing only when called from a
path that didn't pre-compute.

### 5. `src/types/logs.ts`

`LogOption` gained an optional `consistencyDelta?: ResumeConsistencyResult`
so the precomputed analysis can ride along with the log entry.

## What we did NOT do

- **Did not change writer-side counting.** `recordTranscript`'s
  `messageCount` is still the count of loggable messages the writer has
  seen since session start; reader-side `actual` is still the chain
  index. They are different units and that is fine — what was wrong was
  surfacing their delta as a uniform warning.
- **Did not auto-suppress `compacted` warnings to silence.** A
  `compacted` line is still surfaced (info-level), so users who care
  can see that /compact happened in the resumed thread. We just don't
  call it data loss.
- **Did not retroactively classify pre-v8.13.6 logs.** The
  `lost_user_assistant_count` field is only computed when the full
  message map is available at resume time; we don't replay older
  sessions to attach it.

## Verification

- `tests/resume-consistency.test.mjs`: 9 new unit tests covering each
  reason, lost-dominates-compacted priority, sidechain exclusion,
  legacy-no-map fallback, and the two `null`-return paths.
- Full suite: 1559 tests / 1553 pass / 6 skipped / 0 fail.
- `dist/cli.js --version` → `8.13.6 (Clavue)`.
- `npm run check` full release gate green.

## What changes for users

Before:

```
⚠ Resume loaded 3 of 12 messages (-9)
```

After (when /compact had been used):

```
ℹ Resume loaded 3 of 12 messages — earlier turns were merged by /compact
  (transcript on disk is intact).
```

After (when content really was lost — the case the warning was
designed for):

```
⚠ Resume loaded 4 of 6 messages — 2 user/assistant messages on disk
  could not be reconstructed into the chain.
```

The warning still fires, but only when there is something to warn
about.

## Upgrade

```bash
npm i -g clavue@8.13.6
# or
npx -y clavue@8.13.6
# or with the v8.13.4+ self-healing installer
curl -fsSL https://unpkg.com/clavue@8.13.6/install.sh | bash -s -- 8.13.6
```
