Structured agent_events audit log replaces 60+ scattered console.log calls

nwhitehouse replaced the only debug trail for agent activity - scattered `console.log` calls across `chatTools.ts` and `llm/olava.ts` - with an append-only `agent_events` table. The fire-and-forget write pattern and the metadata-only payload rule are worth stealing if your fork has any multi-step agent flow you need to diagnose after the fact.

complianceanalytics

Migration 004_agent_events.sql creates agent_events(id, chat_id, type, payload jsonb, created_at) with RLS via can_access_chat() - the same predicate as chat_messages, so the audit log is automatically scoped to whoever can see the chat. No new access model to maintain.

lib/agentEvents.ts (65 LOC) exports a single recordEvent() function. It's fire-and-forget: the return type is void, failures are caught, logged, and swallowed. An audit write failure must never interrupt a streaming chat response. The function also skips silently when chatId is null or undefined, covering tabular-review chats that don't have a chats row.

The event taxonomy covers the turn lifecycle: turn.started, model.first_token (with latency_ms), tool.call_started (with name and args_keys), tool.call_succeeded (with latency_ms and result_length), tool.call_failed (reserved for feat-016), loop.escalated (reserved for feat-014), turn.completed (with total_steps and total_latency_ms). The hard rule on payload content is explicit in the migration comment and the JSDoc: metadata only - never user prompts, document text, or tool result bodies. Chat messages hold the content; duplicating it here would bloat the table and risk leaking PII to anyone querying the audit log.

So what Worth importing if your fork runs any kind of multi-step agent flow. The `select * from agent_events where chat_id = ? order by created_at` query becomes your single command for reconstructing what happened in a turn. The fire-and-forget pattern and the metadata-only rule are good defaults to copy wholesale. One dependency to note: feat-015 stacks on feat-017's `chatId` parameter threading - port that first so the plumbing already exists.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

1 commit from nwhitehouse/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
afeb5d8c [feat-015] Structured agent_events audit log Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Replaces the 60+ console.log calls in chatTools.ts + llm/olava.ts as the
only debug trail for agent activity per chat turn. Now you can run
`select * from agent_events where chat_id = ? order by created_at` and
reconstruct the agent's path: when each tool fired, how long the batch
took, total steps + total latency for the whole turn.

Schema (migration 004):
- agent_events(id, chat_id, type, payload jsonb, created_at)
- RLS via can_access_chat() - same predicate as chat_messages, so the
  audit log inherits chat permissions.
- Append-only.

Helper (agentEvents.ts):
- recordEvent({db, chatId, type, payload}) - fire-and-forget. Failures
  log + swallow; an audit hiccup must never break a chat stream.
- AgentEventType union: turn.started, model.first_token,
  tool.call_started, tool.call_succeeded, tool.call_failed,
  loop.escalated (feat-014), turn.completed.

Wired in runLLMStream:
- turn.started at top with model name
- tool.call_started before each runToolCalls dispatch (args_keys, no values)
- tool.call_succeeded after each call (name, batch latency, result_length)
- turn.completed at end (total_steps, total_latency_ms, text_length)

Hard PII rule: payload carries metadata only. NEVER user prompts, doc
text, or tool result bodies - chat_messages already holds those.

tool.call_failed is reserved for feat-016 (error envelope) - until then,
all tools dispatch as succeeded. loop.escalated is reserved for feat-014.

Stacked on feat-017-memory-hierarchy (uses the chatId param introduced
there). Will rebase cleanly onto main alongside feat-017.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-108.md from inside the repo you want the changes in.

⬇ Download capture-thread-108.md