ruvnet/ruflo · error · Error

KV cache file truncated

Error message

KV cache file truncated

What it means

Thrown by loadKvCache inside the entry loop when offset + 8 > data.length — i.e. the buffer cannot even fit the 8-byte per-entry header (key length u32 + value length u32) for the next entry. The declared entryCount says more entries remain, but the file ran out of bytes. Indicates truncation during the entry region (the second truncation check; the first is for the entry body).

Source

Thrown at v3/@claude-flow/cli/src/appliance/gguf-engine.ts:401

    if (this.config.verbose) console.log(`[gguf-engine] KV cache persisted: ${this.kvCache.size} entries`);
  }

  /** Restore KV cache from an RVF-compatible binary file. */
  async loadKvCache(inputPath: string): Promise<void> {
    const data = await readFile(inputPath);
    if (data.length < 44) throw new Error('KV cache file too small');

    const magic = data.readUInt32LE(0);
    if (magic !== RVKV_MAGIC) throw new Error(`Invalid KV cache magic: 0x${magic.toString(16)}`);
    const version = data.readUInt32LE(4);
    if (version !== RVKV_VERSION) throw new Error(`Unsupported KV cache version: ${version}`);

    const entryCount = data.readUInt32LE(40);
    let offset = 44;
    const entries = new Map<string, Buffer>();

    for (let i = 0; i < entryCount; i++) {
      if (offset + 8 > data.length) throw new Error('KV cache file truncated');
      const keyLen = data.readUInt32LE(offset);
      const valLen = data.readUInt32LE(offset + 4);
      offset += 8;
      if (offset + keyLen + valLen > data.length) throw new Error('KV cache file truncated');
      entries.set(data.toString('utf-8', offset, offset + keyLen), Buffer.from(data.subarray(offset + keyLen, offset + keyLen + valLen)));
      offset += keyLen + valLen;
    }

    // Verify footer hash (mandatory)
    if (offset + 32 > data.length) {
      throw new Error('KV cache file missing SHA256 footer');
    }
    const stored = data.subarray(offset, offset + 32);
    const computed = createHash('sha256').update(data.subarray(44, offset)).digest();
    if (!stored.equals(computed)) throw new Error('KV cache integrity check failed: hash mismatch');

    this.kvCache = entries;
    if (this.config.verbose) console.log(`[gguf-engine] KV cache loaded: ${entries.size} entries`);

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Make persistence atomic: write to a temp file then rename over the target, so a crash never leaves a half-written cache.
  2. Delete the truncated cache and regenerate from the current in-memory state.
  3. Before loading, sanity-check that file size is plausible for entryCount (rough lower bound) and purge if not.
  4. Wrap loadKvCache so truncation triggers a cold start instead of crashing inference.

Example fix

// before
// persistKvCache writes non-atomically; a crash truncates the file
await writeFile(path, Buffer.concat([header, entryData, footer]));

// after
const tmp = `${path}.tmp`;
await writeFile(tmp, Buffer.concat([header, entryData, footer]));
await rename(tmp, path); // atomic on same filesystem
Defensive patterns

Strategy: fallback

Validate before calling

// Atomic writes prevent truncated files; before loading, sanity-check size vs entryCount:
const data = await readFile(path);
if (data.length < 44) return; // error 110 path
const entryCount = data.readUInt32LE(40);
const minSize = 44 + entryCount * 8; // each entry needs at least its 8-byte header
if (data.length < minSize) {
  await fsUnlink(path).catch(() => {});
  return;
}
await engine.loadKvCache(path);

Type guard

function plausiblyComplete(data: Buffer): boolean {
  if (data.length < 44) return false;
  const entryCount = data.readUInt32LE(40);
  return data.length >= 44 + entryCount * 8;
}

Try / catch

try {
  await engine.loadKvCache(path);
} catch (e) {
  if (e instanceof Error && /truncated/.test(e.message)) {
    await fsUnlink(path).catch(() => {});
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A cache file whose header claims N entries but is truncated before all N entry headers are present — typically a persistKvCache that was interrupted (process killed, disk full) mid-write, since writeFile is not atomic.

Common situations: Process killed (SIGKILL/OOM) during persistKvCache; disk filled mid-write; a copy of the cache file was interrupted. Because writeFile overwrites in place non-atomically, a crash leaves a partial file with a valid header and entryCount but missing entries.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/a0b9eeff23a6ff20. Report an issue: GitHub.