How the Tool Ledger Scanner in Apache Maka Detects Corruption and Rejections
The Tool Ledger scanner detects corruption by scanning RuntimeEvent sequences for duplicate IDs, identity conflicts, and invalid lanes, then validates transitions before persistence to reject invalid candidate events.
Apache Maka's Tool Ledger provides an immutable audit trail of every tool-related fact—function calls, dispatches, responses, and recovery decisions. The tool-ledger-scanner.ts module implements a two-stage validation pipeline that detects corruption in existing ledgers and rejects invalid write attempts before they reach persistent storage.
Two-Stage Validation Architecture
The scanner operates through distinct but complementary mechanisms:
scanToolLedger– Performs a full ledger scan to build operational maps and flag corruptionvalidateToolLedgerTransition– Validates candidate events against ledger rules before persistence
This separation allows Apache Maka to distinguish between ledger-level corruption (data store damage) and application-level rejection (invalid writes from buggy producers).
Stage 1: Detecting Corruption with scanToolLedger
The scanToolLedger function in packages/core/src/tool-ledger-scanner.ts (lines 41–250) walks the entire event sequence once, maintains tracking structures, and records any integrity violations.
Core Detection Mechanisms
| Check | Issue Code | Location |
|---|---|---|
| Duplicate event IDs | duplicate_event_id |
L55–58 |
| Invocation spine conflicts | invocation_identity_conflict |
L60–66 |
| Invalid semantic lanes | Lane-specific codes | L70–73 |
| Duplicate tool calls | duplicate_call |
L95–103 |
| Duplicate dispatches | duplicate_dispatch |
L133–141 |
| Identity mismatches | identity_conflict |
L149–156 |
Corruption Flag Semantics
The function returns a ToolLedgerScanResult where hasCorruption: issues.length > 0 serves as the definitive indicator:
return { operations, issues, hasCorruption: issues.length > 0 };
When hasCorruption is true, the ledger is considered unusable for subsequent writes. The SqliteRuntimeStore will throw ToolLedgerCorruptionError rather than attempt further modifications.
Key Tracking Structures
The scanner maintains several in-memory collections during traversal:
seenEventIds– Detects duplicateevent.idvaluesinvocationSpines– MapsinvocationIdto(sessionId, runId, turnId)triplesbyToolCallandbyOperation– Indexes events by tool call ID and operation IDoperations– Builds the logical view of completed tool operations
Stage 2: Detecting Rejections with validateToolLedgerTransition
The validateToolLedgerTransition function validates prospective writes before persistence. It resides in the same source file (lines 10–35) and implements four defensive checks.
1. Existing Corruption Fast-Path
If the ledger already contains corruption, the function immediately rejects:
if (existing.hasCorruption) return issueValidation(existing.issues[0]!);
This prevents writes to fundamentally compromised storage.
2. Candidate Deduplication
Candidate events sharing IDs with existing events trigger rejection if content differs:
if (!nodeUtil.isDeepStrictEqual(prior, candidate)) {
return { ok: false, code: 'duplicate_event_id', eventId: candidate.id };
}
3. Transition Shape Validation
The scanner validates that candidate events match expected semantic patterns:
| Transition Type | Required Lane Sequence |
|---|---|
generic_append |
Single valid event |
t1_prepare |
function_call → tool_dispatch |
t2_outcome |
Response or decision following prepared call |
recovery_bundle |
Reconciliation-specific sequence |
Violations return transition_shape_conflict.
4. Prospective Re-Scanning
After shape validation, the scanner runs:
const prospective = scanToolLedger([...input.existingEvents, ...candidates]);
if (prospective.hasCorruption) return issueValidation(prospective.issues[0]!);
This catches issues introduced only by the candidate events—orphan responses, hash mismatches, or cross-event conflicts.
Error Types and Calling Conventions
The scanner distinguishes two failure modes through distinct error classes:
ToolLedgerRejectionError
Thrown when a healthy ledger receives an invalid candidate:
- Indicates producer-side bugs
- Embedded
codeandeventIdenable precise diagnosis - Source:
SqliteRuntimeStore.assertToolLedgerTransition(L318–329)
ToolLedgerCorruptionError
Thrown when operating on a pre-corrupted ledger:
- Indicates storage damage or prior crash
- Requires administrative intervention
- Prevents further writes that could compound damage
Both error classes are defined in packages/core/src/tool-ledger-scanner.ts and include the offending issue code and event ID.
Practical Implementation Example
import {
scanToolLedger,
validateToolLedgerTransition,
ToolLedgerRejectionError
} from '@maka/core/tool-ledger-scanner';
import type { RuntimeEvent } from '@maka/core/runtime-event';
// Stage 1: Validate ledger health on load
const ledgerEvents: RuntimeEvent[] = await store.fetchLedger(sessionId, runId);
const healthCheck = scanToolLedger(ledgerEvents);
if (healthCheck.hasCorruption) {
// Log all issues for forensic analysis
console.error('Corruption detected:', healthCheck.issues);
// Runtime will throw ToolLedgerCorruptionError on write attempts
}
// Stage 2: Validate before appending new events
const newDispatch: RuntimeEvent = {
id: generateUUID(),
lane: 'tool_dispatch',
invocationId: currentInvocation,
// ... additional fields
};
const validation = validateToolLedgerTransition({
existingEvents: ledgerEvents,
candidateEvents: [newDispatch],
expectedTransition: 't1_prepare'
});
if (!validation.ok) {
throw new ToolLedgerRejectionError(validation.code, validation.eventId);
}
// Safe to persist
await store.appendRuntimeEvent(sessionId, runId, newDispatch);
Summary
- Corruption detection occurs via
scanToolLedger, which scans the full event sequence and setshasCorruptionwhen any integrity violation is found - Rejection detection occurs via
validateToolLedgerTransition, which validates candidate events against ledger rules before persistence - Fast-fail behavior prevents writes to corrupted ledgers immediately, avoiding data compounding
- Precise diagnostics through embedded issue codes and event IDs enable rapid debugging
- Two error types—
ToolLedgerRejectionErrorfor invalid candidates,ToolLedgerCorruptionErrorfor damaged storage—provide clear operational signals
Frequently Asked Questions
What triggers an invocation_identity_conflict issue?
The invocation_identity_conflict issue fires when the same invocationId appears with different (sessionId, runId, turnId) triples across events. According to the source in packages/core/src/tool-ledger-scanner.ts, the scanner maintains a spine string representation of this triple and compares it against any prior occurrence for the same invocation ID.
Can a candidate event be rejected even if the ledger is healthy?
Yes. The validateToolLedgerTransition function performs four independent checks: existing corruption status, candidate deduplication, transition shape validation, and prospective re-scanning. Any of the last three checks can reject a candidate against a healthy ledger—typically indicating producer-side bugs or race conditions in event generation.
How does t1_prepare transition validation work?
The t1_prepare transition requires a specific two-event sequence: a function_call followed by a tool_dispatch for the same tool call. The validateTransitionShape helper checks that candidate events conform to this pattern; violations return transition_shape_conflict. This enforces causal ordering between call preparation and actual dispatch.
Where does the runtime actually throw these errors?
The SqliteRuntimeStore class in packages/storage/src/sqlite-runtime-store.ts (around L318–329) orchestrates the validation calls. It invokes validateToolLedgerTransition within assertToolLedgerTransition and throws ToolLedgerRejectionError or ToolLedgerCorruptionError based on the validation result returned from the scanner module.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →