{"record":{"id":"a946557a916c7e51","repo":"actualbudget/actual","slug":"trienode-for-key-k-could-not-be-found","errorCode":null,"errorMessage":"TrieNode for key ${k} could not be found","messagePattern":"TrieNode for key (.+?) could not be found","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/crdt/src/crdt/merkle.ts","lineNumber":157,"sourceCode":"}\n\nexport function prune(trie: TrieNode, n = 2): TrieNode {\n  // Do nothing if empty\n  if (!trie.hash) {\n    return trie;\n  }\n\n  const keys = getKeys(trie);\n  keys.sort((a, b) => a.localeCompare(b));\n\n  const next: TrieNode = { hash: trie.hash };\n\n  // Prune child nodes.\n  for (const k of keys.slice(-n)) {\n    const node = trie[k];\n\n    if (!node) {\n      throw new Error(`TrieNode for key ${k} could not be found`);\n    }\n\n    next[k] = prune(node, n);\n  }\n\n  return next;\n}\n\nexport function debug(trie: TrieNode, k = '', indent = 0): string {\n  const str =\n    ' '.repeat(indent) +\n    (k !== '' ? `k: ${k} ` : '') +\n    `hash: ${trie.hash || '(empty)'}\\n`;\n  return (\n    str +\n    getKeys(trie)\n      .map(key => {\n        const node = trie[key];","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/crdt/src/crdt/merkle.ts#L139-L175","documentation":"The Merkle trie prune function walks the trie and, when pruning the last n keys, expects every child key in the trie map to exist. A missing node indicates trie corruption or an inconsistent key set, so it throws rather than silently producing a wrong hash.","triggerScenarios":"applyMessages/addMessages/pruned operate on a trie (e.g. deserialized from a sync server or stored clock) whose key map is missing entries for keys listed in the trie — corrupted or hand-edited merkle data, or a version mismatch producing incompatible trie shapes.","commonSituations":"Restoring a budget database where the clock/merkle trie was truncated or corrupted; syncing with a mismatched crdt package version; manually editing the messages/clock table.","solutions":["Rebuild the merkle trie by re-syncing the budget from a peer or re-adding the messages (delete the stored clock and let it be recomputed).","Restore the budget file/database from a backup taken before corruption.","Check that client and server use compatible versions of the crt package.","Never hand-edit the merkle trie or messages tables."],"exampleFix":"// before\nconst trie = data.clock.merkle; // possibly truncated\nprune(trie, n);\n// after\nif (!trie || Object.keys(trie).length === 0) {\n  trie = {}; // rebuild from full message history via addMessages\n}\ntrie = addMessages({}, allMessages);","handlingStrategy":"try-catch","validationCode":"function trieIsConsistent(trie: Record<string, unknown> | undefined): boolean {\n  return !!trie && Object.keys(trie).length > 0;\n}","typeGuard":"function hasValidTrie(clock: unknown): clock is { timestamp: string; merkle: Record<string, object> } {\n  const c = clock as { timestamp?: unknown; merkle?: unknown };\n  return typeof c?.timestamp === 'string' && typeof c?.merkle === 'object' && c.merkle !== null;\n}","tryCatchPattern":"let trie;\ntry {\n  trie = prune(storedTrie, n);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('TrieNode')) {\n    // rebuild from full message history\n    trie = addMessages({}, allMessages);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Never manually edit the messages or clock/merkle tables in the budget database.","Keep client and server crdt package versions in sync.","Take regular backups of budget data so a corrupt trie can be restored.","Re-sync the budget from a healthy peer if sync errors indicate hash mismatches."],"tags":["crdt","merkle","data-corruption","sync"],"backgroundTag":"merkle-trie-corruption","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}