---
title: So funktioniert es
description: >-
  Der Signaturalgorithmus, warum der Abgleich exakt ist und was das Ledger
  tatsächlich protokolliert.
sidebar:
  order: 2
---
## Signaturen

Jede Entscheidung wird in eine stabile Signatur normalisiert, bevor sie als Cache-Schlüssel verwendet wird:

```ts
export function signature(decision: Decision): string {
  const normalized = {
    intent: decision.intent,
    params: sortKeysDeep(decision.params),
  };
  return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
}
```

Die Normalisierung ist bewusst oberflächlich — Objektschlüssel werden rekursiv sortiert, sodass `{ a: 1, b: 2 }` und `{ b: 2, a: 1 }` identisch gehasht werden, aber weder `intent` noch die Parameter-*Werte* werden verändert. Es gibt kein Stemming, keine Normalisierung der Groß-/Kleinschreibung und keine semantische Kanonisierung.

```txt
{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 482 } }
{ intent: "summarize_pr", params: { pr: 482, repo: "acme/widgets" } }
        │                                       │
        └──────────────── same signature ──────┘

{ intent: "summarize_pr", params: { repo: "acme/widgets", pr: 483 } }
        │
        └── different signature (different pr) ──┘
```

## Warum exakte Übereinstimmung statt semantischer Übereinstimmung

Ein semantischer/unscharfer Cache würde mehr Wiederholungen erfassen — „Fasse diesen PR zusammen“ und „Gib mir eine Zusammenfassung dieses Pull Requests“ würden beide einen Treffer liefern. Das würde aber auch bedeuten, dass Runic beurteilen muss, ob zwei unterschiedlich formulierte Anfragen ähnlich genug sind, um eine gecachte Antwort zu teilen — genau die Art nicht deterministischer Ermessensentscheidung, die Runic vermeiden soll.

Exakte Übereinstimmung bedeutet, dass ein Cache-Treffer nachweislich dieselbe Entscheidung ist, nicht nur vermutlich dieselbe Entscheidung. Der Preis dafür ist, dass Aufrufer bewusst festlegen müssen, was in `params` aufgenommen wird — siehe [Schnellstart](/quickstart#choosing-intent-and-params).

## Was das Ledger protokolliert

Das Ledger ist ein Append-only-Log, keine laufende Summe, die abweichen kann:

```ts
interface LedgerEvent {
  signature: string;
  kind: "hit" | "miss";
  tokensSpent: number;
  timestamp: number;
}
```

- **Bei einem Miss** protokolliert Runic die `tokensSpent`, die dein Code für die Erstellung des Artefakts gemeldet hat.
- **Bei einem Hit** sucht Runic den ursprünglichen Miss für diese Signatur und protokolliert denselben gespeicherten `tokensSpent`-Wert — niemals eine Schätzung, niemals eine Vermutung. Falls für eine getroffene Signatur wider Erwarten kein vorheriger Miss existiert, protokolliert es `0`, statt eine Zahl zu erfinden.

```txt
summary.totalSpent = sum of all miss events
summary.totalSaved = sum of all hit events
tokensWithoutRunic = totalSpent + totalSaved   (what it would've cost with no cache at all)
savingsPercent     = totalSaved / tokensWithoutRunic
```

Runic misst Token-Kosten niemals selbst. Es gibt nur wieder, was der aufrufende Code ihm über `storeResult(decision, artifact, tokensSpent)` mitgeteilt hat — siehe [Benchmarks](/benchmarks), um zu erfahren, wie `openrouter-savings` diese Zahl aus einer echten API-Antwort statt aus einer Schätzung erhält.

## Speicher-Backends

Sowohl `@runic-labs/cache` als auch `@runic-labs/ledger` basieren auf einer kleinen Speicher-Schnittstelle, mit zwei in v1 ausgelieferten Implementierungen:

| Backend | Lebensdauer | Anwendungsfall |
| --- | --- | --- |
| `MemoryCacheStore` / `MemoryLedgerStore` | Nur während der Prozesslaufzeit | Tests, kurzlebige Skripte, Benchmarks |
| `FileCacheStore` / `FileLedgerStore` | Auf Festplatte gespeichert (`.runic/` standardmäßig) | Ein CLI-Prozess und ein Agent-Prozess, die ohne laufenden Server denselben Zustand lesen |

Beide haben in v1 einen Gültigkeitsbereich pro Sitzung — siehe [Gültigkeitsbereich](/scope), warum sitzungsübergreifendes Teilen bewusst eine spätere Phase ist, statt jetzt nachträglich angebaut zu werden.

## Veralterung ist nur ein Hinweis

Cache-Einträge enthalten ein `stale`-Flag, das anhand eines konfigurierbaren Zeitfensters berechnet wird (standardmäßig 24 Stunden). Runic entfernt einen veralteten Eintrag niemals automatisch und verweigert ihn auch nicht — es sind Informationen, auf die der Aufrufer bei Bedarf reagieren kann, kein Durchsetzungsmechanismus. Wenn dein Anwendungsfall einen harten Ablauf erfordert, prüfe `entry.stale` selbst und entscheide, was damit geschehen soll.
