An agent that acts on your behalf while you are not watching has to be answerable for it afterwards. Denkeeper records what happened at four levels: an audit log of decisions, telemetry on tool reliability, cost tracking per model and agent, and optional OpenTelemetry export.

Audit log

Every consequential decision emits an event. Enabled by default.

[audit]
enabled = true
retention_days = 30
buffer_size = 1000

Auditing is deliberately best-effort: emission never blocks an agent turn, so if a burst overruns the buffer the excess events are dropped with a warning log rather than stalling the agent.

Events are grouped into categories:

CategoryRecords
llmOne event per tool-loop round
tool_callTool invocations and their outcomes
approvalApprovals, denials, auto-approvals (with the deciding scope)
supervisorSupervisor decisions, with the raw response preserved
skillSkill lifecycle
scheduleSchedule firing
sessionSession and channel switches
channelChannel changes
configConfiguration changes
mcpTool-server health and lifecycle
safetyPanic and resume
evalDry-run and eval lifecycle

Query them via GET /api/v1/audit — filterable by category, agent, status, source, free text, and time range — or from the dashboard’s Audit Log page.

Health-check failures for remote (SSE/HTTP) tool servers only emit a health_fail event after health_fail_threshold consecutive failures, default 3. A remote server blipping once is not worth waking you for. Stdio servers emit on the first failure, because a local subprocess dying is unambiguous.

Tool-call telemetry

Every tool call is recorded with an outcome, and the distinction between them is the useful part:

OutcomeMeaning
okSucceeded
rejectedThe tool was healthy and refused the arguments
failedTransport or execution fault
deniedBlocked at approval
cachedServed from the within-turn memo cache

Summaries expose these as rejection_count, failure_count, denial_count, and cached_count. failure_count is the “broken tool” signal — a denial is a policy decision, and a rejection usually means the model passed bad arguments, so folding them together produces a number that cannot tell you anything. cached results are excluded from fault counts and from duration averages, since nothing executed.

Each call is attributed to at most one owning skill and version. Attribution is conservative: an explicit skill name wins, a single matched skill is used, and anything ambiguous is left blank rather than guessed. by_tool_skill then groups reliability per skill and version, so you can tell whether the edit you made to a skill last week made its tool use worse.

Per-session detail is available at GET /api/v1/sessions/{id}/stats, /tool-calls, and /skills; the aggregate at GET /api/v1/telemetry/summary.

Cost tracking

Denkeeper ships a pricing registry covering roughly 70 models, so most setups need no cost configuration at all.

[costs]
default_rate_per_1k_tokens = 0

[costs.model_prices."my-org/custom-model"]
input = 3.0          # USD per million tokens
output = 15.0
cached_input = 0.3

Lookup priority:

  1. Cost reported by the provider
  2. Registry exact match
  3. Registry longest-prefix match
  4. default_rate_per_1k_tokens
  5. $0, with a warning logged

Whichever source won is recorded as the pricing_source attribute, so a real price is always distinguishable from a fallback. An unknown model logs a warning rather than failing quietly — a cost of exactly zero in your dashboard should be treated as a missing price, not a free model.

Cached prompt tokens are tracked separately, read from the provider’s own cache-read counts.

Limits are enforced per session against the agent’s cost_limit_soft and cost_limit_hard; see Sessions & Permissions. Fallback rules can switch to a cheaper model when a limit is approached rather than simply stopping.

OpenTelemetry

[otel]
enabled = true
traces_endpoint = "http://localhost:4318"
service_name = "denkeeper"

When enabled, Prometheus metrics are served at GET /metricsno authentication, so do not expose that port publicly. Traces are exported only when traces_endpoint is set, so metrics alone need no collector.

Spans carry the attributes you would want when a turn behaves oddly: the model, the pricing source, and the effective tool-round cap including which skill imposed it.

Both settings have environment overrides — DENKEEPER_OTEL_ENABLED and DENKEEPER_OTEL_TRACES_ENDPOINT — for deployments that configure telemetry separately from the rest of the config.

Retention

DataSettingDefault
Conversations[memory] retention_days90
Conversations[memory] max_conversations10000
Audit events[audit] retention_days30

All three accept 0 for unlimited. Cleanup runs on cleanup_interval in each section.