{"record":{"id":"a0b9eeff23a6ff20","repo":"ruvnet/ruflo","slug":"kv-cache-file-truncated","errorCode":null,"errorMessage":"KV cache file truncated","messagePattern":"KV cache file truncated","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/appliance/gguf-engine.ts","lineNumber":401,"sourceCode":"    if (this.config.verbose) console.log(`[gguf-engine] KV cache persisted: ${this.kvCache.size} entries`);\n  }\n\n  /** Restore KV cache from an RVF-compatible binary file. */\n  async loadKvCache(inputPath: string): Promise<void> {\n    const data = await readFile(inputPath);\n    if (data.length < 44) throw new Error('KV cache file too small');\n\n    const magic = data.readUInt32LE(0);\n    if (magic !== RVKV_MAGIC) throw new Error(`Invalid KV cache magic: 0x${magic.toString(16)}`);\n    const version = data.readUInt32LE(4);\n    if (version !== RVKV_VERSION) throw new Error(`Unsupported KV cache version: ${version}`);\n\n    const entryCount = data.readUInt32LE(40);\n    let offset = 44;\n    const entries = new Map<string, Buffer>();\n\n    for (let i = 0; i < entryCount; i++) {\n      if (offset + 8 > data.length) throw new Error('KV cache file truncated');\n      const keyLen = data.readUInt32LE(offset);\n      const valLen = data.readUInt32LE(offset + 4);\n      offset += 8;\n      if (offset + keyLen + valLen > data.length) throw new Error('KV cache file truncated');\n      entries.set(data.toString('utf-8', offset, offset + keyLen), Buffer.from(data.subarray(offset + keyLen, offset + keyLen + valLen)));\n      offset += keyLen + valLen;\n    }\n\n    // Verify footer hash (mandatory)\n    if (offset + 32 > data.length) {\n      throw new Error('KV cache file missing SHA256 footer');\n    }\n    const stored = data.subarray(offset, offset + 32);\n    const computed = createHash('sha256').update(data.subarray(44, offset)).digest();\n    if (!stored.equals(computed)) throw new Error('KV cache integrity check failed: hash mismatch');\n\n    this.kvCache = entries;\n    if (this.config.verbose) console.log(`[gguf-engine] KV cache loaded: ${entries.size} entries`);","sourceCodeStart":383,"sourceCodeEnd":419,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/appliance/gguf-engine.ts#L383-L419","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Make persistence atomic: write to a temp file then rename over the target, so a crash never leaves a half-written cache.","Delete the truncated cache and regenerate from the current in-memory state.","Before loading, sanity-check that file size is plausible for entryCount (rough lower bound) and purge if not.","Wrap loadKvCache so truncation triggers a cold start instead of crashing inference."],"exampleFix":"// before\n// persistKvCache writes non-atomically; a crash truncates the file\nawait writeFile(path, Buffer.concat([header, entryData, footer]));\n\n// after\nconst tmp = `${path}.tmp`;\nawait writeFile(tmp, Buffer.concat([header, entryData, footer]));\nawait rename(tmp, path); // atomic on same filesystem","handlingStrategy":"fallback","validationCode":"// Atomic writes prevent truncated files; before loading, sanity-check size vs entryCount:\nconst data = await readFile(path);\nif (data.length < 44) return; // error 110 path\nconst entryCount = data.readUInt32LE(40);\nconst minSize = 44 + entryCount * 8; // each entry needs at least its 8-byte header\nif (data.length < minSize) {\n  await fsUnlink(path).catch(() => {});\n  return;\n}\nawait engine.loadKvCache(path);","typeGuard":"function plausiblyComplete(data: Buffer): boolean {\n  if (data.length < 44) return false;\n  const entryCount = data.readUInt32LE(40);\n  return data.length >= 44 + entryCount * 8;\n}","tryCatchPattern":"try {\n  await engine.loadKvCache(path);\n} catch (e) {\n  if (e instanceof Error && /truncated/.test(e.message)) {\n    await fsUnlink(path).catch(() => {});\n    return;\n  }\n  throw e;\n}","preventionTips":["Make persistKvCache atomic (write temp, then rename).","Cold-start on truncation rather than failing inference.","Ensure the process is not OOM/SIGKILLed mid-write (resource limits).","Validate size vs declared entryCount before parsing."],"tags":["gguf","kv-cache","truncation","integrity"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}