A volume simulation through the real store showed two costs worth cutting.
Rows: hermes.task_run.finished carried duration, retry, model-call and
tool-call buckets on top of outcome/end reason/surface, so almost every task
became its own row; hermes.tool_call.count did the same with latency and
retry buckets. The terminal rows now keep only the fields they are filtered
by, and the same end events feed two small split counters:
hermes.task_run.duration (surface, outcome, duration, retries) and
hermes.tool_call.latency (tool category, latency, retries). Per-task call
counts already ride on hermes.task_cost.count. v3 still accepts the v2 field
sets of both counters so rows recorded before an upgrade drain.
Local store: every package lived three times for 30 days (counter rows, the
payload column, a pretty-printed outbox file). Outbox files are now compact
JSON, and once the ingest has accepted or refused a package the database
drops its copy of the body; the file stays as the local history the docs
promise, and pending packages keep their body for resends.
Measured (simulated day, skewed draws, before -> after):
heavy 2,788 -> 2,347 rows, 21.0 -> 17.7 KB on the wire, 102 -> 44 MB local over 30 days
extreme 6,561 -> 5,047 rows, 46.0 -> 33.6 KB, 272 -> 102 MB
typical 663 -> 650 rows, 7.1 -> 6.8 KB, 24 -> 13 MB