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 corruption
  • validateToolLedgerTransition – 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 duplicate event.id values
  • invocationSpines – Maps invocationId to (sessionId, runId, turnId) triples
  • byToolCall and byOperation – Indexes events by tool call ID and operation ID
  • operations – 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 code and eventId enable 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 sets hasCorruption when 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—ToolLedgerRejectionError for invalid candidates, ToolLedgerCorruptionError for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →