Reference for every CloudEvent emitted by Honcho and the on-disk shape of the Parquet archive that Xatu writes to S3.

Honcho instances emit telemetry as CloudEvents to a configurable endpoint (TELEMETRY.ENDPOINT). In the managed-SaaS deployment that endpoint is Xatu’s POST /v1/events. Xatu enriches each event with tenant_id (resolved by Fly app name → HonchoInstance.app_name), publishes to Redpanda, and the consumer archives batches to S3 as zstd-compressed Parquet partitioned by date.

Source of truth:

  • Event classes: repos/honcho/src/telemetry/events/
  • Emitter (envelope construction): repos/honcho/src/telemetry/emitter.py
  • Ingestion: repos/groudon/xatu/ingestion/main.py
  • Parquet writer: repos/groudon/xatu/consumer/parquet_writer.py

Related: [[cloudevents-enrichment|cloudevents-enrichment.md]] spec for the design history and rev notes.

CloudEvent envelope

All Honcho events ship with the standard CloudEvents v1.0 attributes:

AttributeValueExample
specversionAlways "1.0""1.0"
idDeterministic SHA-256-derived ID (evt_{base64url}) — same (event_type, timestamp, resource_id) always produces the same ID, enabling idempotent retriesevt_AbCd...
source/honcho/{namespace}/{category} where namespace = TELEMETRY.NAMESPACE (Fly app name in SaaS) and category is one of representation, dream, dialectic, agent, deletion, reconciliation, llm, api, trace/honcho/honcho-acme-prod/dialectic
typeThe event type string (see table below)dialectic.completed
timeISO-8601 UTC timestamp2026-05-14T03:21:09.482Z
datacontenttype"application/json"
dataschemahttps://honcho.dev/schemas/{type}/v{schema_version}https://honcho.dev/schemas/dialectic.completed/v2
dataThe event payload (per-event fields below)

Xatu adds two enrichment fields on top of the envelope before publishing to Redpanda and writing to Parquet:

  • namespace — extracted from the source field (Fly app name)
  • tenant_id — looked up from namespace against the Groudon database

The Parquet schema (see Querying) stores data as a JSON string, not a typed struct, so payload fields are accessed via json_extract / -> in DuckDB/Trino.

The emitter also injects honcho_version into the event body (so events from different deploys don’t dedupe).

Volume & sampling

Each event declares a volume class. Ground-truth events (the aggregate envelopes like representation.completed, dialectic.completed, dream.run) always emit. High-volume per-call events (llm.call.completed, embedding.call.completed, agent.tool.call.completed) are subject to TELEMETRY.HIGH_VOLUME_SAMPLE_RATE, sampled deterministically on run_id so a whole agentic trace is either fully kept or fully dropped. At a sample rate < 1.0, aggregates still carry totals but per-call children are undercounted — rebuild cost from the aggregates, not the sampled children.

Trace payloads (full-fidelity stream)

A separate, opt-in stream carries replay-grade payloads — the exact context each model saw — content-addressed so payload size stays O(N). These trace-category events (llm.call.traced, embedding.call.traced, trace.content) go through a distinct emitter (emit_trace() / _trace_emitter) gated by TELEMETRY.TRACE_PAYLOADS_ENABLED, and are ground-truth (never sampled). llm.call.traced deliberately does not join to llm.call.completed — cost is computable from the trace alone, keeping the billing and audit streams decoupled.

Event catalog

representation.completed

Category: representation  ·  Schema: v2  ·  Source file: src/telemetry/events/representation.py

Emitted when the deriver finishes processing a batch of messages and writes the resulting conclusions for a peer’s representation. This is the workhorse event in the memory-formation pipeline — one per processed batch per observed peer.

FieldTypeDescription
workspace_namestrWorkspace
session_namestrSession
observedstrPeer being observed
queue_items_processedintNumber of QueueItem rows dequeued
earliest_message_idstrFirst message in the batch
latest_message_idstrLast message in the batch
message_countintMessages processed
explicit_conclusion_countintExplicit conclusions extracted
context_preparation_msfloatTime preparing context
llm_call_msfloatTime in the LLM call
total_duration_msfloatEnd-to-end duration
input_tokensintQueued-message tokens (the messages actually reasoned about); downstream metering key — do not rename
total_input_tokensintTotal tokens sent to the LLM (queued + extra context + scaffold)
output_tokensintLLM output tokens
queued_message_countintMessages in the batch that were the actual queue items
prompt_message_countintTotal messages in the prompt (queued + extra interleaving context)
prompt_message_tokensintSum of token_count across all prompt messages
extra_context_message_countintprompt_message_count - queued_message_count
extra_context_tokensintprompt_message_tokens - input_tokens — token cost of extra context
prompt_scaffold_tokensintEstimated tokens for the system/scaffold portion of the prompt
batch_max_tokensintDERIVER.REPRESENTATION_BATCH_TARGET_INPUT_TOKENS at fetch time
max_input_tokensintDERIVER.MAX_INPUT_TOKENS at call time
was_flush_enabledboolDERIVER.FLUSH_ENABLED snapshot at batch time
hit_batch_token_capboolTrue when the batcher clamped the batch to fit batch_max_tokens
hit_input_token_capboolTrue when the LLM call truncated input to fit max_input_tokens
observer_countintNumber of observers this representation was saved against

Resource ID: {workspace_name}:{session_name}:{latest_message_id}

dream.run

Category: dream  ·  Schema: v2  ·  Source file: src/telemetry/events/dream.py

Top-level event for a single dream orchestration. Aggregates totals across all specialists. Correlate with dream.specialist and agent.iteration events via run_id.

FieldTypeDescription
run_idstrNanoid for cross-event correlation
workspace_namestrWorkspace
session_namestr | nullSession if scoped to one
observerstrObserver peer
observedstrObserved peer
specialists_runlist[str]E.g. ["deduction", "induction"]
deduction_successbool
induction_successbool
surprisal_enabledboolWhether surprisal sampling ran
surprisal_conclusion_countintHigh-surprisal conclusions found
total_iterationsintLLM iterations across all specialists
total_input_tokensint
total_output_tokensint
total_duration_msfloat
dream_typestr | nullDreamType slug (currently omni; future deductive/inductive)
enabled_types_countintlen(DREAM.ENABLED_TYPES) at run start
trigger_reasonstr | nullWhat tripped the schedule: document_threshold / manual / surprisal
delay_reasonstr | nullWhat governed when it fired: idle_timeout / immediate / min_hours_gate
documents_since_last_dream_at_scheduleint | nullDocument count snapshot at schedule time
document_thresholdint | nullDREAM.DOCUMENT_THRESHOLD snapshot at schedule time

Resource ID: {run_id}

dream.specialist

Category: dream  ·  Schema: v2

One per specialist (deduction, induction) within a dream run. Shares run_id with the parent dream.run.

FieldTypeDescription
run_idstrSame as parent dream.run
specialist_typestr"deduction" or "induction"
workspace_namestr
observerstr
observedstr
iterationsintLLM iterations
tool_calls_countintTool calls made
input_tokensint
output_tokensint
duration_msfloat
successbool
created_observation_countintActual observations created (from ToolResult.metadata.created_count, not tool-name counting)
deleted_observation_countintActual observations deleted (from ToolResult.metadata.deleted_count)
created_counts_by_leveldict[str, int]Created observations per level (explicit/deductive/inductive/contradiction); treat missing keys as 0
deleted_counts_by_leveldict[str, int]Deleted observations per level
peer_card_updatedboolTrue when ≥1 update_peer_card call succeeded
search_tool_calls_countintsearch_memory / search_messages / search_messages_temporal invocations
error_classstr | nullException class when success=False; null on success

Resource ID: {run_id}:{specialist_type}

dialectic.completed

Category: dialectic  ·  Schema: v2  ·  Source file: src/telemetry/events/dialectic.py

Emitted when a /peers/{peer_id}/chat query finishes. User-facing latency and cost surface — most billing analytics start here.

FieldTypeDescription
run_idstrCorrelates with agent.iteration and agent.tool.*
workspace_namestr
peer_namestrPeer being queried about
session_namestr | nullIf session-scoped
reasoning_levelstrminimal / low / medium / high / max
total_iterationsintLLM iterations (defaults 1)
prefetched_conclusion_countintConclusions prefetched before agent loop
tool_calls_countintTool calls made
total_duration_msfloat
input_tokensint
output_tokensint
cache_read_tokensintPrompt-cache read tokens
cache_creation_tokensintPrompt-cache write tokens
hit_input_token_capboolTrue when an iteration’s input exceeded DIALECTIC.MAX_INPUT_TOKENS

Resource ID: {run_id}

agent.iteration

Category: agent  ·  Schema: v2  ·  Source file: src/telemetry/events/agent.py

One per LLM call inside an agentic loop (dream specialists and dialectic agents). Use for per-iteration cost analysis and tool-call attribution.

FieldTypeDescription
run_idstrCorrelates with parent dream.run / dream.specialist / dialectic.completed
parent_categorystr"dream" or "dialectic"
agent_typestr"deduction", "induction", or "dialectic"
workspace_namestr
observerstr | nullSet for dream agents
observedstr | nullSet for dream agents
peer_namestr | nullSet for dialectic agent
iterationint1-indexed iteration number
tool_callslist[str]Tool names called this iteration (can be empty)
input_tokensint
output_tokensint
cache_read_tokensint
cache_creation_tokensint

Resource ID: {run_id}:{iteration}

agent.tool.conclusions.created

Category: agent  ·  Schema: v2

Emitted when the create_conclusions agent tool executes. Includes a per-conclusion level breakdown (explicit vs. deductive).

FieldTypeDescription
run_idstr
iterationint
parent_categorystr
agent_typestr
workspace_namestr
observerstr
observedstr
conclusion_countint
levelslist[str]E.g. ["explicit", "deductive", "deductive"]

Resource ID: {run_id}:{iteration}:conclusions_created

agent.tool.conclusions.deleted

Category: agent  ·  Schema: v3

Emitted when the delete_conclusions agent tool executes — typically during dream consolidation.

FieldTypeDescription
run_idstr
iterationint
parent_categorystrTypically "dream"
agent_typestrTypically "deduction"
workspace_namestr
observerstr
observedstr
conclusion_countint
levelslist[str]

Resource ID: {run_id}:{iteration}:conclusions_deleted

agent.tool.peer_card.updated

Category: agent  ·  Schema: v2

Emitted when the update_peer_card agent tool executes during dream consolidation.

FieldTypeDescription
run_idstr
iterationint
parent_categorystrTypically "dream"
agent_typestr"deduction" or "induction"
workspace_namestr
observerstr
observedstr
facts_countintNumber of facts in the updated peer card

Resource ID: {run_id}:{iteration}:peer_card_updated

agent.tool.summary.created

Category: agent  ·  Schema: v3

Emitted when a session summary is created. run_id / iteration are null when summarization runs outside an agentic loop.

FieldTypeDescription
run_idstr | nullNull when not in an agentic run
iterationint | nullNull when not in an agentic run
parent_categorystr
agent_typestr
workspace_namestr
session_namestr
message_idstrMessage the summary covers up to
message_countintMessages summarized
message_seq_in_sessionintSequence number of the base message
summary_typestr"short" or "long"
input_tokensintProvider-side input tokens for the summary LLM call
output_tokensintSummary token count
previous_summary_tokensintTokens of the previous summary fed back as context (0 if first)
message_tokensintSum of Message.token_count across summarized messages
prompt_scaffold_tokensintEstimated tokens for the static scaffold portion of the prompt

Resource ID: {message_id}:{summary_type}:summary_created

agent.tool.call.completed

Category: agent  ·  Schema: v1  ·  Volume: high (sampled)  ·  Source file: src/telemetry/events/agent.py

Lightweight per-call record for every tool the Dialectic/Dreamer agents invoke — including read-only tools (search_memory, get_recent_history, …) that have no dedicated event. One per tool call.

FieldTypeDescription
run_idstrRun id for correlation
iterationintIteration number (1-indexed)
tool_call_seqint0-indexed position in the iteration’s tool batch
provider_tool_call_idstr | nullProvider-supplied tool call id (e.g. Anthropic toolu_*)
parent_categorystr"dream" or "dialectic"
agent_typestr"deduction", "induction", or "dialectic"
workspace_namestr
tool_namestrTool as invoked
duration_msfloatHandler wall-clock duration
is_errorboolTrue if the handler raised
result_charsintLength of the result string returned to the LLM
result_chars_before_truncationint | nullOriginal size when truncated; null otherwise
result_tokens_estimateinttiktoken-based size proxy (estimate)
was_truncatedboolTrue when the result was clamped to a size budget
query_tokensint | nullSearch tools only — tiktoken estimate of the query
top_kint | nullSearch tools only
results_countint | nullSearch tools only — results returned
used_embeddingbool | nullSearch tools only — true on vector lookup vs. metadata filter
embedding_query_countintEmbedding API calls the handler made

Resource ID: {run_id}:{iteration}:{tool_call_seq}

deletion.completed

Category: deletion  ·  Schema: v1  ·  Source file: src/telemetry/events/deletion.py

Emitted when an async deletion task completes (workspace, session, or conclusions). For workspace/session deletions, cascade counts are populated.

FieldTypeDescription
workspace_namestr
deletion_typestr"workspace", "session", or "conclusions"
resource_idstrID of the deleted resource
successbool
peers_deletedintWorkspace deletions only
sessions_deletedintWorkspace deletions only
messages_deletedintWorkspace or session deletions
conclusions_deletedintWorkspace or session deletions
error_messagestr | nullPopulated on failure

Resource ID: {workspace_name}:{deletion_type}:{resource_id}

reconciliation.sync_vectors.completed

Category: reconciliation  ·  Schema: v1  ·  Source file: src/telemetry/events/reconciliation.py

Periodic global maintenance — syncs documents and message embeddings to external vector stores. No workspace context (operates across all workspaces in the instance).

FieldTypeDescription
documents_syncedint
documents_failedint
documents_cleanedintSoft-deleted docs cleaned during sync
message_embeddings_syncedint
message_embeddings_failedint
total_duration_msfloat

Resource ID: sync_vectors (fixed)

reconciliation.cleanup_stale_items.completed

Category: reconciliation  ·  Schema: v1

Periodic global cleanup of soft-deleted documents and expired queue items. No workspace context.

FieldTypeDescription
documents_cleanedint
queue_items_cleanedint
total_duration_msfloat

Resource ID: cleanup_stale_items (fixed)

message.created

Category: api  ·  Schema: v1  ·  Source file: src/telemetry/events/api.py

Canonical event for counting created messages (including messages created from file uploads). One per create batch.

FieldTypeDescription
workspace_namestr
session_namestr
message_countintMessages created
total_tokensintTotal tokens across the batch
sourcestr"api" or "file_upload"
last_message_idstrpublic_id of the trailing message (stable batch key)

Resource ID: {workspace_name}:{session_name}:{source}:{last_message_id}

file.uploaded

Category: api  ·  Schema: v1  ·  Source file: src/telemetry/events/api.py

Emitted when an uploaded file is converted into messages — captures file-side metadata. Use message.created for message counts (avoids double-counting).

FieldTypeDescription
workspace_namestr
session_namestr
peer_namestrPeer that uploaded the file
file_idstrGenerated file identifier
filenamestr | null
content_typestr | null
file_size_bytesint | null
message_countintMessages created from the file
total_tokensintTokens across those messages

Resource ID: {workspace_name}:{session_name}:{file_id}

context.retrieved

Category: api  ·  Schema: v1  ·  Source file: src/telemetry/events/api.py

Emitted when context is retrieved for a session or peer (the get_context endpoints). Captures what was requested and what was returned.

FieldTypeDescription
workspace_namestr
context_scopestr"session" or "peer"
session_namestr | nullSession-scoped only
peer_namestr | nullObserver peer
target_namestr | nullObserved peer
tokens_requestedint | nullCaller’s tokens param (null = endpoint default)
message_countintMessages returned
has_summarybool
has_representationbool
has_peer_cardbool
search_query_providedbool
search_top_kint | null
search_max_distancefloat | null
include_most_frequentbool | null
max_conclusionsint | null
include_summarybool | null
limit_to_sessionboolRepresentation retrieval was session-limited
peer_perspective_providedboolpeer_perspective supplied for session context
total_duration_msfloat

Resource ID: session → {workspace}:session:{session}:{peer}:{target} · peer → {workspace}:peer:{peer}:{target}

llm.call.completed

Category: llm  ·  Schema: v1  ·  Volume: high (sampled)  ·  Source file: src/telemetry/events/llm.py

One per provider hit (success, error, or cancellation), with full cost-attribution context. Streaming calls report token counts as 0 placeholders — use the aggregate envelopes for streamed-call accuracy.

FieldTypeDescription
workspace_namestr | nullNull for system calls without workspace context
call_purposestr | nullClosed enum: deriver.representation, dialectic.answer, dream.deduction, dream.induction, summary.short, summary.long
parent_categorystr | nullrepresentation | dialectic | dream | summary
transportstrSDK transport: anthropic | openai | gemini
provider_labelstr | nullBest-effort vendor for relay setups (e.g. OpenRouter); null when unclear
modelstrModel id as sent to the provider
effective_max_output_tokensintmax_tokens used
provider_input_tokensint0 on stream placeholder
provider_output_tokensint0 on stream placeholder
cache_read_tokensintTokens read from prompt cache
cache_creation_tokensintTokens written to prompt cache
finish_reasonstr | null
outcomestrsuccess | error | cancelled (exclude cancelled from error alerts)
is_final_attemptboolTrue on the last allowed attempt; pair with outcome='error' for exhausted
error_classstr | nullException class on error/cancellation
attemptint1-indexed attempt number
retry_attemptsintTotal attempts allowed
was_fallbackboolTrue when this attempt used the fallback model
duration_msfloatProvider call wall-clock
has_toolsbool
tool_call_countintTool calls the model requested
was_streamboolTrue for the streaming path (token counts are placeholders)
run_idstr | nullAgent run id; null for non-agent calls
iterationint | nullIteration within an agentic loop

Resource ID: {run_id|none}:{iteration|0}:{attempt}:{transport}:{model}

embedding.call.completed

Category: llm  ·  Schema: v1  ·  Volume: high (sampled)  ·  Source file: src/telemetry/events/llm.py

One per embedding-provider call (real per-token spend). Search tools, observation creation, embedding sync, and deriver/summarizer paths all hit this.

FieldTypeDescription
workspace_namestr | null
call_purposestr | nullClosed enum: search_memory, search_messages, create_observations, vector_sync, summary, …
parent_categorystr | nulle.g. dialectic, representation
providerstropenai | gemini
modelstr
input_countintTexts embedded in this call (batch size)
input_tokens_estimateinttiktoken-based proxy (estimate only)
duration_msfloat
outcomestrsuccess | error | cancelled
is_final_attemptbool
error_classstr | null
run_idstr | nullAgent run id; null for sync/CRUD paths

Resource ID: {run_id|none}:{call_purpose|unknown}:{provider}:{model}:{input_count}

llm.call.traced

Category: trace  ·  Schema: v1  ·  Volume: ground-truth (never sampled)  ·  Source file: src/telemetry/events/trace.py

Opt-in replay-grade record, one per LLM call (gated by TELEMETRY.TRACE_PAYLOADS_ENABLED). Content fields are references (content hashes into the trace.content store), never inline bytes. Deliberately not joined to llm.call.completed — cost is computable from the trace alone.

FieldTypeDescription
trace_idstr | nullSpan-tree correlation
span_idstr | null
parent_span_idstr | null
iterationint | null
step_seqintDefault 0
attemptint1-indexed attempt
was_fallbackbool
parent_event_idstr | null
call_purposestr | null
parent_categorystr | null
session_idstr | nullUsed for grouping traces
transportstranthropic / openai / gemini
provider_labelstr | null
modelstr
input_message_refslist[str]Content hashes for the context window
system_prompt_refstr | nullContent hash
tool_schema_refslist[str]Content hashes
tool_choiceany
output_content_refstr | nullContent hash of replay-grade output
output_tool_callslist[dict]
output_thinking_refstr | nullContent hash
output_signatureslist[str]
raw_response_refstr | nullReserved
finish_reasonstr | null
provider_input_tokensintSelf-contained accounting copy
provider_output_tokensint
cache_read_tokensint
cache_creation_tokensint
was_truncatedbool

Resource ID: {span_id}:{iteration}:{attempt}:{step_seq}

embedding.call.traced

Category: trace  ·  Schema: v1  ·  Volume: ground-truth (never sampled)  ·  Source file: src/telemetry/events/trace.py

Trace-stream record, one per embedding-provider call (gated by TELEMETRY.TRACE_PAYLOADS_ENABLED).

FieldTypeDescription
trace_idstr | nullSpan-tree correlation
span_idstr | null
parent_span_idstr | null
iterationint | null
step_seqintDefault 0
attemptint1-indexed attempt
session_idstr | null
call_purposestr | null
parent_categorystr | null
providerstr
modelstr
provider_input_tokensintv1: tiktoken estimate (no authoritative count plumbed)
provider_output_tokensintAlways 0 (embeddings produce none)
input_countintTexts embedded in this call
was_truncatedbool

Resource ID: {span_id}:embedding:{call_purpose}:{input_count}

trace.content

Category: trace  ·  Schema: v1  ·  Volume: ground-truth (never sampled)  ·  Source file: src/telemetry/events/trace.py

One unique message in the content store, emitted once per run and referenced by content_hash. The CloudEvent id is content-addressed with no timestamp (content_{digest}), so re-sends of identical content dedupe at the transport layer.

FieldTypeDescription
content_hashstrCovers full message identity {role, content, tool_call_id}
rolestr
contentany | nullMessage text, normalized across providers
tool_call_idstr | null
tool_callslist[dict]Unified {id, name, input} shape (provider-agnostic)
honcho_authoredboolTags Honcho-authored content (system prompts, scaffold) for access control

Resource ID: {content_hash}

Quick reference

Event typeCategorySchemaResource ID pattern
representation.completedrepresentationv2{workspace}:{session}:{latest_message_id}
dream.rundreamv2{run_id}
dream.specialistdreamv2{run_id}:{specialist_type}
dialectic.completeddialecticv2{run_id}
agent.iterationagentv2{run_id}:{iteration}
agent.tool.conclusions.createdagentv2{run_id}:{iteration}:conclusions_created
agent.tool.conclusions.deletedagentv3{run_id}:{iteration}:conclusions_deleted
agent.tool.peer_card.updatedagentv2{run_id}:{iteration}:peer_card_updated
agent.tool.summary.createdagentv3{message_id}:{summary_type}:summary_created
agent.tool.call.completedagentv1{run_id}:{iteration}:{tool_call_seq}
deletion.completeddeletionv1{workspace}:{deletion_type}:{resource_id}
reconciliation.sync_vectors.completedreconciliationv1sync_vectors
reconciliation.cleanup_stale_items.completedreconciliationv1cleanup_stale_items
message.createdapiv1{workspace}:{session}:{source}:{last_message_id}
file.uploadedapiv1{workspace}:{session}:{file_id}
context.retrievedapiv1{workspace}:{scope}:…:{peer}:{target}
llm.call.completedllmv1{run_id}:{iteration}:{attempt}:{transport}:{model}
embedding.call.completedllmv1{run_id}:{call_purpose}:{provider}:{model}:{input_count}
llm.call.tracedtracev1{span_id}:{iteration}:{attempt}:{step_seq}
embedding.call.tracedtracev1{span_id}:embedding:{call_purpose}:{input_count}
trace.contenttracev1{content_hash}

Querying the Parquet archive

Storage layout

Xatu’s consumer writes batches to S3 with the following key layout (see xatu/consumer/parquet_writer.py):

s3://{XATU_S3_BUCKET}/year={YYYY}/month={MM}/day={DD}/{content_hash}.parquet
  • content_hash is a 12-char SHA-256 prefix over all event IDs in the batch — same batch always overwrites the same key (idempotent within a batch; cross-batch dupes are still possible and should be deduplicated in queries via DISTINCT id).
  • Compression: zstd. Statistics are written.

Parquet schema

All columns are strings — data is a serialized JSON string, not a typed struct.

ColumnTypeNotes
specversionstringAlways "1.0"
idstringDeterministic; use DISTINCT id to dedupe
sourcestring/honcho/{namespace}/{category}
typestringEvent type — filter on this
timestringISO-8601 UTC; cast with CAST(time AS TIMESTAMP)
datacontenttypestring
dataschemastringhttps://honcho.dev/schemas/{type}/v{n} — parse for schema version
datastringJSON payload; use json_extract / data->>
namespacestringFly app name (== HonchoInstance.app_name)
tenant_idstringGroudon tenant — primary partition key for billing queries

Querying with DuckDB

Best for local ad-hoc analysis from a laptop or notebook. Install DuckDB (brew install duckdb), configure S3 credentials, and read the Hive-partitioned Parquet directly.

Setup

INSTALL httpfs; LOAD httpfs;
SET s3_region='us-east-1';
SET s3_access_key_id='...';
SET s3_secret_access_key='...';
 
-- Define a reusable view across all partitions.
CREATE OR REPLACE VIEW events AS
SELECT *
FROM read_parquet(
    's3://xatu-events/year=*/month=*/day=*/*.parquet',
    hive_partitioning = true
);

With hive_partitioning=true, the year, month, day partition keys become first-class columns — push them down in WHERE clauses to prune scans:

WHERE year = '2026' AND month IN ('04', '05')

Recipes

1. Distinct event count for a tenant in May 2026

SELECT COUNT(DISTINCT id) AS events
FROM events
WHERE year = '2026' AND month = '05'
  AND tenant_id = 'tnt_abc123';

2. Token totals by tenant, last 7 days

SELECT
    tenant_id,
    SUM(CAST(json_extract_string(data, '$.input_tokens')  AS BIGINT)) AS input_tokens,
    SUM(CAST(json_extract_string(data, '$.output_tokens') AS BIGINT)) AS output_tokens
FROM events
WHERE year = '2026' AND month = '05'
  AND type IN ('representation.completed', 'dialectic.completed', 'dream.specialist')
  AND CAST(time AS TIMESTAMP) >= now() - INTERVAL 7 DAY
GROUP BY tenant_id
ORDER BY input_tokens DESC;

3. Dialectic latency percentiles by reasoning level

SELECT
    json_extract_string(data, '$.reasoning_level') AS reasoning_level,
    COUNT(*) AS calls,
    QUANTILE_CONT(CAST(json_extract_string(data, '$.total_duration_ms') AS DOUBLE), 0.50) AS p50_ms,
    QUANTILE_CONT(CAST(json_extract_string(data, '$.total_duration_ms') AS DOUBLE), 0.95) AS p95_ms,
    QUANTILE_CONT(CAST(json_extract_string(data, '$.total_duration_ms') AS DOUBLE), 0.99) AS p99_ms
FROM events
WHERE type = 'dialectic.completed'
  AND year = '2026' AND month = '05'
GROUP BY reasoning_level
ORDER BY p95_ms DESC;

4. Dream success rate per tenant

SELECT
    tenant_id,
    COUNT(*) AS runs,
    SUM(CASE WHEN json_extract_string(data, '$.deduction_success') = 'true' THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS deduction_rate,
    SUM(CASE WHEN json_extract_string(data, '$.induction_success') = 'true' THEN 1 ELSE 0 END) * 1.0 / COUNT(*) AS induction_rate
FROM events
WHERE type = 'dream.run'
  AND year = '2026' AND month = '05'
GROUP BY tenant_id
ORDER BY runs DESC;

5. Cross-event correlation by run_id — full dialectic breakdown

WITH dialectic AS (
    SELECT
        json_extract_string(data, '$.run_id')         AS run_id,
        tenant_id,
        json_extract_string(data, '$.peer_name')      AS peer_name,
        CAST(json_extract_string(data, '$.total_duration_ms') AS DOUBLE) AS total_ms
    FROM events
    WHERE type = 'dialectic.completed' AND year = '2026' AND month = '05'
),
iterations AS (
    SELECT
        json_extract_string(data, '$.run_id')        AS run_id,
        COUNT(*)                                     AS iters,
        SUM(CAST(json_extract_string(data, '$.input_tokens')  AS BIGINT)) AS in_tokens,
        SUM(CAST(json_extract_string(data, '$.output_tokens') AS BIGINT)) AS out_tokens
    FROM events
    WHERE type = 'agent.iteration' AND year = '2026' AND month = '05'
      AND json_extract_string(data, '$.parent_category') = 'dialectic'
    GROUP BY 1
)
SELECT d.*, i.iters, i.in_tokens, i.out_tokens
FROM dialectic d
LEFT JOIN iterations i USING (run_id)
ORDER BY d.total_ms DESC
LIMIT 50;

6. Deletion audit — which workspaces have been removed and what cascaded

SELECT
    CAST(time AS TIMESTAMP)                                            AS at,
    tenant_id,
    json_extract_string(data, '$.workspace_name')                      AS workspace,
    json_extract_string(data, '$.deletion_type')                       AS deletion_type,
    json_extract_string(data, '$.success')                             AS success,
    CAST(json_extract_string(data, '$.peers_deleted')       AS BIGINT) AS peers,
    CAST(json_extract_string(data, '$.sessions_deleted')    AS BIGINT) AS sessions,
    CAST(json_extract_string(data, '$.messages_deleted')    AS BIGINT) AS messages,
    CAST(json_extract_string(data, '$.conclusions_deleted') AS BIGINT) AS conclusions,
    json_extract_string(data, '$.error_message')                       AS error
FROM events
WHERE type = 'deletion.completed' AND year = '2026'
ORDER BY at DESC;

7. Reconciliation health — failed vector syncs over time

SELECT
    DATE_TRUNC('day', CAST(time AS TIMESTAMP)) AS day,
    namespace,
    SUM(CAST(json_extract_string(data, '$.documents_failed')           AS BIGINT)) AS doc_failures,
    SUM(CAST(json_extract_string(data, '$.message_embeddings_failed')  AS BIGINT)) AS msg_failures
FROM events
WHERE type = 'reconciliation.sync_vectors.completed'
  AND year = '2026'
GROUP BY 1, 2
HAVING doc_failures + msg_failures > 0
ORDER BY day DESC, doc_failures + msg_failures DESC;

DuckDB tips

  • Always filter by partition keys (year, month, day) first — they prune entire S3 prefixes from the scan.
  • Always DISTINCT id when computing counts — cross-batch retries can produce duplicates.
  • Cast JSON-extracted numericsjson_extract_string returns text; wrap with CAST(... AS BIGINT) / CAST(... AS DOUBLE).
  • Booleans round-trip as the strings "true" / "false" because data is a JSON-serialized string column.

Querying with ClickHouse

Best when you want the archive available to dashboards alongside the rest of the analytics stack. Two ergonomic options:

  1. s3() table function for ad-hoc reads (no schema migration required).
  2. S3 engine table for repeatable dashboard queries, optionally fronted by a MergeTree materialization for low-latency joins.

ClickHouse’s JSON functions are typed at the call site (JSONExtractString, JSONExtractInt, JSONExtractFloat, JSONExtractBool) — no CAST needed after extraction.

Setup

Ad-hoc reads via s3() — requires use_hive_partitioning=1 (ClickHouse 24.x+) so year/month/day become first-class columns:

SET use_hive_partitioning = 1;
 
SELECT count()
FROM s3(
    'https://xatu-events.s3.us-east-1.amazonaws.com/year=*/month=*/day=*/*.parquet',
    '<aws_access_key_id>',
    '<aws_secret_access_key>',
    'Parquet'
)
WHERE year = '2026' AND month = '05';

Reusable external table:

CREATE TABLE events_s3
(
    specversion      String,
    id               String,
    source           String,
    type             String,
    time             String,
    datacontenttype  String,
    dataschema       Nullable(String),
    data             String,
    namespace        String,
    tenant_id        String,
    year             String,
    month            String,
    day              String
)
ENGINE = S3(
    'https://xatu-events.s3.us-east-1.amazonaws.com/year=*/month=*/day=*/*.parquet',
    '<aws_access_key_id>',
    '<aws_secret_access_key>',
    'Parquet'
)
SETTINGS use_hive_partitioning = 1;
 
CREATE VIEW events AS SELECT * FROM events_s3;

Optional MergeTree materialization for fast repeat queries (project the JSON fields you actually use as typed columns):

CREATE TABLE events_mt
(
    event_time     DateTime64(3) DEFAULT parseDateTime64BestEffort(time, 3),
    id             String,
    type           LowCardinality(String),
    tenant_id      LowCardinality(String),
    namespace      LowCardinality(String),
    source         String,
    dataschema     Nullable(String),
    data           String,
    -- projected for common filters:
    workspace_name LowCardinality(String) MATERIALIZED JSONExtractString(data, 'workspace_name'),
    run_id         String                  MATERIALIZED JSONExtractString(data, 'run_id'),
    input_tokens   UInt64                  MATERIALIZED JSONExtractUInt(data, 'input_tokens'),
    output_tokens  UInt64                  MATERIALIZED JSONExtractUInt(data, 'output_tokens')
)
ENGINE = ReplacingMergeTree(event_time)
PARTITION BY toYYYYMM(event_time)
ORDER BY (tenant_id, type, event_time, id);
 
-- Backfill or incremental load
INSERT INTO events_mt (event_time, id, type, tenant_id, namespace, source, dataschema, data)
SELECT parseDateTime64BestEffort(time, 3), id, type, tenant_id, namespace, source, dataschema, data
FROM events_s3
WHERE year = '2026' AND month = '05';

ReplacingMergeTree(event_time) keyed on (tenant_id, type, event_time, id) collapses cross-batch duplicates on merge. Use FINAL or SELECT DISTINCT id for exact counts before merges complete.

Recipes

These assume events is the view defined above. If you materialized to events_mt, drop the JSONExtract* calls in favor of the typed projected columns.

1. Distinct event count for a tenant in May 2026

SELECT uniqExact(id) AS events
FROM events
WHERE year = '2026' AND month = '05'
  AND tenant_id = 'tnt_abc123';

2. Token totals by tenant, last 7 days

SELECT
    tenant_id,
    sum(JSONExtractUInt(data, 'input_tokens'))  AS input_tokens,
    sum(JSONExtractUInt(data, 'output_tokens')) AS output_tokens
FROM events
WHERE year = '2026' AND month = '05'
  AND type IN ('representation.completed', 'dialectic.completed', 'dream.specialist')
  AND parseDateTime64BestEffort(time, 3) >= now() - INTERVAL 7 DAY
GROUP BY tenant_id
ORDER BY input_tokens DESC;

3. Dialectic latency percentiles by reasoning level

SELECT
    JSONExtractString(data, 'reasoning_level') AS reasoning_level,
    count() AS calls,
    quantile(0.50)(JSONExtractFloat(data, 'total_duration_ms')) AS p50_ms,
    quantile(0.95)(JSONExtractFloat(data, 'total_duration_ms')) AS p95_ms,
    quantile(0.99)(JSONExtractFloat(data, 'total_duration_ms')) AS p99_ms
FROM events
WHERE type = 'dialectic.completed'
  AND year = '2026' AND month = '05'
GROUP BY reasoning_level
ORDER BY p95_ms DESC;

4. Dream success rate per tenant

SELECT
    tenant_id,
    count() AS runs,
    avg(JSONExtractBool(data, 'deduction_success')) AS deduction_rate,
    avg(JSONExtractBool(data, 'induction_success')) AS induction_rate
FROM events
WHERE type = 'dream.run'
  AND year = '2026' AND month = '05'
GROUP BY tenant_id
ORDER BY runs DESC;

JSONExtractBool returns 0/1, so avg(...) is the success rate directly.

5. Cross-event correlation by run_id — full dialectic breakdown

WITH
    dialectic AS (
        SELECT
            JSONExtractString(data, 'run_id')              AS run_id,
            tenant_id,
            JSONExtractString(data, 'peer_name')           AS peer_name,
            JSONExtractFloat(data, 'total_duration_ms')    AS total_ms
        FROM events
        WHERE type = 'dialectic.completed' AND year = '2026' AND month = '05'
    ),
    iterations AS (
        SELECT
            JSONExtractString(data, 'run_id')          AS run_id,
            count()                                    AS iters,
            sum(JSONExtractUInt(data, 'input_tokens'))  AS in_tokens,
            sum(JSONExtractUInt(data, 'output_tokens')) AS out_tokens
        FROM events
        WHERE type = 'agent.iteration' AND year = '2026' AND month = '05'
          AND JSONExtractString(data, 'parent_category') = 'dialectic'
        GROUP BY run_id
    )
SELECT d.*, i.iters, i.in_tokens, i.out_tokens
FROM dialectic AS d
LEFT JOIN iterations AS i USING (run_id)
ORDER BY total_ms DESC
LIMIT 50;

6. Deletion audit — which workspaces have been removed and what cascaded

SELECT
    parseDateTime64BestEffort(time, 3)                     AS at,
    tenant_id,
    JSONExtractString(data, 'workspace_name')              AS workspace,
    JSONExtractString(data, 'deletion_type')               AS deletion_type,
    JSONExtractBool(data, 'success')                       AS success,
    JSONExtractUInt(data, 'peers_deleted')                 AS peers,
    JSONExtractUInt(data, 'sessions_deleted')              AS sessions,
    JSONExtractUInt(data, 'messages_deleted')              AS messages,
    JSONExtractUInt(data, 'conclusions_deleted')           AS conclusions,
    JSONExtractString(data, 'error_message')               AS error
FROM events
WHERE type = 'deletion.completed' AND year = '2026'
ORDER BY at DESC;

7. Reconciliation health — failed vector syncs over time

SELECT
    toStartOfDay(parseDateTime64BestEffort(time, 3)) AS day,
    namespace,
    sum(JSONExtractUInt(data, 'documents_failed'))          AS doc_failures,
    sum(JSONExtractUInt(data, 'message_embeddings_failed')) AS msg_failures
FROM events
WHERE type = 'reconciliation.sync_vectors.completed'
  AND year = '2026'
GROUP BY day, namespace
HAVING doc_failures + msg_failures > 0
ORDER BY day DESC, doc_failures + msg_failures DESC;

ClickHouse tips

  • use_hive_partitioning = 1 is required to get year/month/day as columns. Without it, partition values are buried in the file path and you’d be glob-only.
  • uniqExact(id) is the safe count when reading directly from s3() (cross-batch dedupe). count() is fine on events_mt after OPTIMIZE TABLE ... FINAL or with SELECT count() FROM events_mt FINAL.
  • JSONExtract* is typed at call site — no follow-up CAST. Use JSONExtractInt / JSONExtractUInt / JSONExtractFloat / JSONExtractBool / JSONExtractString.
  • JSONExtractBool returns 0/1, so avg(...) directly gives the rate.
  • For dashboards, project hot JSON fields as MATERIALIZED columns on events_mt. The MergeTree ordering key (tenant_id, type, event_time, id) makes per-tenant filters effectively a primary-key scan.
  • Timestamp parsing: parseDateTime64BestEffort(time, 3) for millisecond precision; persist as DateTime64(3) if materializing.

General tips (both engines)

  • Always filter by year/month/day first — these are partition keys, not data columns; filtering on them prunes whole S3 prefixes.
  • Dedupe by id when counting. Same-batch retries hit the same Parquet key (idempotent), but cross-batch retries can produce duplicate rows across files.
  • Booleans live as the JSON strings "true" / "false" inside data. DuckDB needs string comparison; ClickHouse JSONExtractBool handles it natively.
  • Schema versions are encoded in the dataschema column (https://honcho.dev/schemas/{type}/v{n}). Filter on it when a field’s semantics changed across versions.
  • Trino / Athena equivalents: json_extract_scalar(data, '$.foo') mirrors DuckDB’s json_extract_string; external tables stand in for the events_s3 pattern.
  • Local dev: Xatu’s xatu/scripts/load_test.py seeds synthetic events — useful for iterating on query shapes without hitting prod.