Skip to main content
Patter can persist every call to a directory tree on disk so you can replay transcripts, audit tool calls, and track latency/cost trends without running a hosted dashboard. Persistence is on by default since 0.6.2 (writes land under the platform default location described below) so the dashboard rebuilds across process restarts without extra wiring. Pass persist: false to keep the prior ephemeral-RAM-only behaviour. The same on-disk layout also backs the local dashboard’s call history: when persistence is enabled, phone.serve() rebuilds the in-memory dashboard from disk on startup so call history survives process restarts without an external database.

Enable

You can turn persistence on either in code (recommended for application defaults) or via an environment variable (recommended for ops overrides). Both reach the same on-disk layout — see the Configuration reference for the full precedence rules.

In code

import { Patter, Twilio } from "getpatter";

// Platform default location
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15555550100", persist: true });

// Explicit path
const phone = new Patter({
  carrier: new Twilio(),
  phoneNumber: "+15555550100",
  persist: "/var/log/patter",
});
persist valueBehaviour
omitted / undefined (default)On. Falls back to PATTER_LOG_DIR when set; otherwise writes under the platform default location. (Changed from opt-in to default-on in 0.6.2 for dashboard hydrate.)
falseForce-off, even if PATTER_LOG_DIR is set.
truePlatform default location (see below).
"<path>"Use the supplied path (~ expanded).

Via env var

Set PATTER_LOG_DIR before starting the server:
# Explicit path
export PATTER_LOG_DIR=/var/log/patter

# Or use the platform-idiomatic default directory
export PATTER_LOG_DIR=auto
Platform defaults for auto (and for persist: true):
  • macOS: ~/Library/Application Support/patter
  • Linux: $XDG_DATA_HOME/patter (falls back to ~/.local/share/patter)
  • Windows: %LOCALAPPDATA%\patter
When persist is set explicitly the env var is ignored. When persist is left unset (the default) the logger uses PATTER_LOG_DIR if set, otherwise the platform default location. Pass persist: false to disable disk writes entirely.

Layout

<PATTER_LOG_DIR>/calls/YYYY/MM/DD/<call_id>/
├── metadata.json     // envelope; written at call start, updated at call end
├── transcript.jsonl  // one JSON object per turn (role/text/ts/latency/cost)
└── events.jsonl      // operational events (tool_call, barge_in, error, ...)
metadata.json is written atomically (tmp file + rename) so a reader never sees a half-written file. JSONL files are append-only.

Metadata schema

{
  "schema_version": "1.0",
  "call_id": "CA9a2b...",
  "trace_id": null,
  "started_at": "2026-04-23T18:02:12.413Z",
  "ended_at":   "2026-04-23T18:03:47.892Z",
  "duration_ms": 95479,
  "status": "completed",
  "caller": "***4567",
  "callee": "***7890",
  "telephony_provider": "twilio",
  "provider_mode": "openai_realtime",
  "agent": { "provider": "openai_realtime", "voice": "nova" },
  "turns": 8,
  "cost":    { "total": 0.1234, "stt": 0.01, "tts": 0.02, "llm": 0.08 },
  "latency": { "p50_ms": 412, "p95_ms": 870, "p99_ms": 1240 },
  "error": null
}

Phone redaction

Caller / callee numbers in metadata.json are masked by default (last 4 digits). Change via:
# Mask last 4 digits (default): "***4567"
export PATTER_LOG_REDACT_PHONE=mask

# Store the full E.164 number (disables redaction)
export PATTER_LOG_REDACT_PHONE=full

# Replace with a sha256 prefix for correlation without storing the number
export PATTER_LOG_REDACT_PHONE=hash_only
transcript.jsonl is not redacted — it can contain customer PII spoken during the call. Gate access to the log root and/or wire up your own redaction pipeline before exporting.

Retention

Old day directories are cleaned up automatically. The sweep runs on ~2% of calls (sampled; no daemon) so a long-running server doesn’t accumulate indefinitely.
# Default: 30 days
export PATTER_LOG_RETENTION_DAYS=30

# Disable cleanup (keep forever)
export PATTER_LOG_RETENTION_DAYS=0
Retention defaults to 30 days. Set PATTER_LOG_RETENTION_DAYS=0 if you need to keep call history indefinitely (the default is intentionally conservative to avoid runaway disk use). Phone redaction defaults to last-4 masking; review the Phone redaction section before storing customer numbers.

Dashboard hydration

When persistence is enabled, phone.serve() calls MetricsStore.hydrate(logRoot) once at startup so the local dashboard repopulates its 500-call ring buffer from the on-disk envelopes before the first new call lands. There’s nothing to wire up — it just happens. If you instantiate the store yourself (custom dashboard host, separate process), call hydrate() directly:
import { MetricsStore } from "getpatter/dashboard/store";

const store = new MetricsStore();
const restored = store.hydrate("/var/log/patter");
console.log(`Restored ${restored} call(s) from disk`);
hydrate() is idempotent: callIds already in the store are skipped, and unparseable records are logged at debug level rather than aborting. The on-disk JSON/JSONL files are the source of truth — the in-memory store is a cache on top.

Reading a call

import * as fs from 'node:fs';
import * as path from 'node:path';

const callDir = '/var/log/patter/calls/2026/04/23/CA9a2b...';

const metadata = JSON.parse(fs.readFileSync(path.join(callDir, 'metadata.json'), 'utf8'));
console.log(`${metadata.call_id}: ${metadata.duration_ms}ms, $${metadata.cost.total.toFixed(4)}`);

const transcript = fs.readFileSync(path.join(callDir, 'transcript.jsonl'), 'utf8');
for (const line of transcript.split('\n').filter(Boolean)) {
  const turn = JSON.parse(line);
  console.log(`[${turn.ts}] ${turn.role}: ${turn.text}`);
}

Safety guarantees

  • File-write errors never raise into the call path — a full disk or a permissions hiccup logs a warning and the call continues uninterrupted.
  • When persistence is disabled (persist: false), CallLogger.enabled is false and every method returns immediately.

Interop

Patter session reports include per-turn STT/LLM/TTS spans, tool invocations, latency metrics, and barge-in events. transcript.jsonl rows slot into OpenTelemetry gen_ai.* turn spans. The Python SDK writes the same schema, so a multi-runtime deployment produces a single coherent directory tree.