actualbudget/actual · critical
TrieNode for key ${k} could not be found
Error message
TrieNode for key ${k} could not be found What it means
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.
Source
Thrown at packages/crdt/src/crdt/merkle.ts:157
}
export function prune(trie: TrieNode, n = 2): TrieNode {
// Do nothing if empty
if (!trie.hash) {
return trie;
}
const keys = getKeys(trie);
keys.sort((a, b) => a.localeCompare(b));
const next: TrieNode = { hash: trie.hash };
// Prune child nodes.
for (const k of keys.slice(-n)) {
const node = trie[k];
if (!node) {
throw new Error(`TrieNode for key ${k} could not be found`);
}
next[k] = prune(node, n);
}
return next;
}
export function debug(trie: TrieNode, k = '', indent = 0): string {
const str =
' '.repeat(indent) +
(k !== '' ? `k: ${k} ` : '') +
`hash: ${trie.hash || '(empty)'}\n`;
return (
str +
getKeys(trie)
.map(key => {
const node = trie[key];View on GitHub (pinned to d4334cb6e6)
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.
Example fix
// before
const trie = data.clock.merkle; // possibly truncated
prune(trie, n);
// after
if (!trie || Object.keys(trie).length === 0) {
trie = {}; // rebuild from full message history via addMessages
}
trie = addMessages({}, allMessages); Defensive patterns
Strategy: try-catch
Validate before calling
function trieIsConsistent(trie: Record<string, unknown> | undefined): boolean {
return !!trie && Object.keys(trie).length > 0;
} Type guard
function hasValidTrie(clock: unknown): clock is { timestamp: string; merkle: Record<string, object> } {
const c = clock as { timestamp?: unknown; merkle?: unknown };
return typeof c?.timestamp === 'string' && typeof c?.merkle === 'object' && c.merkle !== null;
} Try / catch
let trie;
try {
trie = prune(storedTrie, n);
} catch (err) {
if (err instanceof Error && err.message.includes('TrieNode')) {
// rebuild from full message history
trie = addMessages({}, allMessages);
} else {
throw err;
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Timestamp.InvalidError: ${data.timestamp}
- Timestamp.ClockDriftError
- out-of-sync
- Sync ID is required for sync ${flag}. Set --sync-id or ACTUA
- Could not resolve on-disk budget id for syncId ${syncId} aft
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/a946557a916c7e51.
Report an issue: GitHub.