Data
Conversation records require decoding before you can query their messages, tool calls, approvals, and payments. Data exposes read-only SQL tables over the signed log, restricted to your authorized scope.
The signed, append-only log records conversation events. Data decodes those events into tables you can query with SQL. Queries cannot write to the log or create a separate durable copy.
conv-aIn scope- Turn starts
- Turn completes
conv-bIn scope- Turn starts
- No completion
conv-cOutside your scope- Turn starts
- Turn completes
Shared decoders and folds turn log records into typed, in-memory tables. The query layer adds the partition, position, and turn id.
partition · position · turn_idThe log stays unchanged. Queries do not create a second durable store.
SELECT partition, position
FROM events
WHERE kind LIKE 'output_msg:%'
ORDER BY partition, position;| partition | position |
|---|---|
conv-a | 1 |
conv-b is in scope, but its turn is unfinished. conv-c is outside your scope. Only the message from conv-a reaches this result.
Shared event interpretation
Decoding reads an event's payload. A fold interprets events to derive a fact, such as a settled payment. Separate implementations can disagree about which events count.
A decoded payment receipt does not establish that its signature is valid. If a dashboard skips verification, it can display a forged receipt as settled. A spending check that verifies signatures would reject the same receipt.
polyc-facts provides shared folds for readers. Spending checks call a fold directly, and the query layer uses the same fold to build tables. Both therefore apply the same payment rules.
The tables
The query layer adds three identifying columns. Event records contain kind, trust, and payload; they do not contain these row identifiers:
partition— the conversation the row belongs to, writtenconv-{id}.position— the row's position in append-only order.turn_id— the turn the row belongs to. The log records it three different ways. The query layer normalizes them into one column.
These columns identify rows derived from individual log events. Reference tables and aggregate views, such as dashboard, use their own schemas. Start with events, which is available in every scope:
-- Inspect event rows
SELECT * FROM events LIMIT 5;
-- Count input and output tokens per turn
SELECT turn_id,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens
FROM usage
GROUP BY turn_id
ORDER BY turn_id;
-- Find tool calls without a recorded result
SELECT c.name, c.turn_id, c.position
FROM tool_calls c
LEFT JOIN (SELECT tool_call_id FROM tool_calls WHERE block_type = 'result') r
ON r.tool_call_id = c.tool_call_id
WHERE c.block_type = 'call'
AND r.tool_call_id IS NULL
ORDER BY c.position DESC;A tool call, result, approval, and receipt share a call id. tool_calls and payments name it tool_call_id. approvals names it request_id. Join these columns to inspect the transaction.
-- List settled payments with their approvals
SELECT p.reference, p.amount, p.currency, a.tool_name, a.approved
FROM payments p
JOIN approvals a ON a.request_id = p.tool_call_id
WHERE a.phase = 'response'
ORDER BY p.position DESC;The catalog lists queryable tables. Your scope determines which tables register in your session. Names outside that scope fail to resolve.
Any scope 5
Available in every session. Rows still follow your conversation scope.
events 4 key columnsOne committed event, with its kind, turn id, and log position.
partitionpositionkindturn_id
usage 3 key columnsToken usage for one committed turn.
turn_idinput_tokensoutput_tokens
model_call 4 key columnsThe model configuration recorded for one committed turn.
turn_idprovidermodelcaptured_clock_unix_ms
turn_failed 3 key columnsOne turn that ends in a failure, and the kind of failure.
turn_idfailure_kindmessage
turn_dispatch 2 key columnsOne turn that the control plane records before it connects to the harness.
turn_idoccurrence
Narrowed by scope 11
Your scope determines the available tables, rows, and columns.
messages 5 key columnsOne text block of the transcript.
turn_idroletexttrustinternal_only
An admin session reads the messages that no client receives.
tool_calls 6 key columnsOne tool call, or the result that it records. Join them on tool_call_id.
tool_call_idblock_typenameargumentsresulttrust
An admin session reads the calls on the messages that no client receives.
approvals 10 key columnsOne approval request or decision.
phaserequest_idtool_nameargs_jsonapprovedsignature_status
signer_public_keyapprovercallermodified_args_json
Admin-only columns are absent from other sessions. Naming one in a query fails.
payments 7 key columnsOne settled payment. Its signature is valid.
directionreferenceamountcurrencytool_call_idsubject
signer_public_key
Admin-only columns are absent from other sessions. Naming one in a query fails.
refusals 7 key columnsOne payment refusal, with the requested and permitted amounts. You can query a refusal even if its turn never commits.
reasonmerchant_hostrequested_base_unitspermitted_base_unitstool_call_idsubject
signer_public_key
Admin-only columns are absent from other sessions. Naming one in a query fails.
wallet_link_lifecycle 7 key columnsOne wallet linking or unlinking step, with its delegation limits. A ceremony runs outside any turn, so turn_id is always empty.
transitioncurrencylimit_humanexpiry_unixrecipients
wallet_addresssigner_public_key
Admin-only columns are absent from other sessions. Naming one in a query fails.
handoffs 9 key columnsOne transfer to another agent, or one refusal to transfer.
phasechild_conversation_idchild_agent_idcarried_countreasonparent_agent_iddenial_reasonallowedsignature_status
A column the row's phase does not carry reads null: `carried_count` and `child_conversation_id` on a refusal, `allowed`, `parent_agent_id` and `denial_reason` on a transfer.
grant_replays 5 key columnsOne capability grant that authorizes one tool call.
toolgrant_refcovered_capabilitiessignature_status
signer_public_key
Admin-only columns are absent from other sessions. Naming one in a query fails.
attribution 3 key columnsThe persona that a turn runs as, and each persona that takes part.
turn_idpersona_idrole
An admin session reads the identity of each participant on the edge.
fires 4 key columnsOne firing of a routine. It holds the scheduled time and the actual time.
routinescheduled_at_msfired_at_msoutcome
You read the firings of the routines that you publish.
routines 5 key columnsOne published routine and its live schedule.
namereadyphasenext_fire_time_mscreator_persona
You read the routines that you publish.
Admin only 7
These table names are registered only in admin sessions.
handoff_signers 4 key columnsThe key that signed one transfer record.
partitionpositionsigned_bysigner_key_id
The whole table. `handoffs` carries the verdict, never the key, so a narrowed session reads `signature_status` and cannot reach this table at all.
summary 2 key columnsOne compaction of the older part of the transcript.
textcovers_through_position
persona_wallets 5 key columnsOne linked wallet and its delegation limits.
persona_idwallet_addresscurrencyexpiry_unixrevoked
persona_spend_policies 4 key columnsThe spend cap and the host allowlist of one persona.
persona_idlimitperiod_secsallowed_hosts
persona_credentials 5 key columnsOne passkey that a persona signs in with. No scope exposes key material through this table.
persona_idrp_idorigincreated_at_msrevoked
persona_usage 5 key columnsOne persona's running totals. usage counts a single turn; this counts a whole persona.
persona_idcommitted_turnsinput_tokensoutput_tokenslast_active_ms
dashboard 4 key columnsTurn, token, and settlement totals for one conversation.
conversation_idcommitted_turnsinput_tokenssettlements_json
Only committed turns
The control plane records dispatch intent through the state plane before calling the harness. It records the commit marker after the harness returns. A crash between these writes can leave records for an unfinished turn, including one that caused an external effect.
The events table includes a row only when its conversation and turn have turn_start and turn_complete markers. Most event-derived tables apply the same filter, excluding payments and messages from unfinished turns. refusals remains queryable even when a turn never commits. wallet_link_lifecycle records occur outside turns, and summary uses synthetic turn tags. Neither uses the committed-turn filter.
The filter matches both conversation and turn because clients supply turn ids. A committed turn in one conversation cannot make another conversation's turn eligible. A real-journal test checks that an unfinished turn has three raw rows and zero rows in the view.
Admin sessions can inspect unfinished-turn records through events_raw. Other session catalogs do not register that table.
Scope applies before query planning
Session creation registers only the tables and columns your scope permits. Queries that reference other names fail before execution.
| Queryable data | In a conversation | Your own sessions | Admin |
|---|---|---|---|
| This conversation's committed turns | Reads | Reads | Reads |
| Message text and tool arguments | Reads | Reads | Reads |
| Every conversation you take part in | Hidden | Reads | Reads |
| Someone else's conversation | Hidden | Hidden | Reads |
| Messages never sent to a client | Hidden | Hidden | Reads |
| Raw signer keys and edge identities | Hidden | Hidden | Reads |
| Undecoded event bytes (events_raw) | Hidden | Hidden | Reads |
| Every persona's profile and wallet | Hidden | Hidden | Reads |
Your scope limits which conversations, tables, and columns you can query. A query fails if it names a table or column that your session's catalog does not register.
The verified session determines scope. Admin sessions can query every conversation the control plane discovers. Persona sessions can query conversations associated with that person's participation records.
Tools running within a conversation use its trusted dispatch attribution to determine scope. Their arguments cannot select another conversation.
Non-admin queries expose the content already available to participants through transcripts and approval interfaces. These five categories require admin scope:
- Raw signer keys.
- Every other participant's identity on the edge.
- Messages the wire format marks as never sent to a client.
- Undecoded payload bytes.
- Deployment-wide persona tables.
Queries include full tool arguments and results already available to participants, without display truncation.
How an agent reads its own conversation
Two conversation-history tools use fixed SQL statements and do not require approval. conversation_recent_turns lists committed turns, newest first: 20 by default, at most 50. conversation_list_tool_calls lists recorded calls and argument previews: 20 by default, at most 100. Full-detail mode adds turn-failure information or human-approval status, respectively.
Agents cannot submit raw SQL from within a conversation. Model-authored SQL can combine rows with different trust levels, preventing the result from retaining each row's provenance verdict.
Outside conversations, POST /api/query and an MCP tool expose the query engine. Each request requires a verified session. Both apply the same scope rules and return the same response format.
curl -sS https://<control-plane>/api/query \
-H 'content-type: application/json' \
-H "authorization: Bearer $POLYCHROME_SESSION" \
-d '{"sql":"SELECT model, COUNT(*) AS turns FROM model_call GROUP BY model"}'
# → {"columns":["model","turns"],"rows":[["…",42]],"truncated":false,"skipped_partitions":0}Check truncated before interpreting a result. A true value means the response reached its row limit. Narrow the query or use COUNT(*) to obtain a total.
Query limits
The engine parses each statement and checks its type, not its prefix. It accepts exactly one statement, and that statement has to be a query. Data definition, data modification, SHOW, COPY, SET, transaction control, and multi-statement batches are refused before planning. EXPLAIN ANALYZE runs the query, so it is refused everywhere. Plain EXPLAIN is admin-only. Ad hoc external-table references are off for every scope.
| Limit | Default | When the limit is reached |
|---|---|---|
| Wall clock | 30 seconds | The query returns a timeout rather than holding a request open. |
| Rows returned | 10,000 | The result is cut to the cap and flagged with truncated. |
| Source events replayed | 500,000 | The scope is refused as too broad, before any decode runs. |
| Memory pool | 192 MiB, shared | Queries share it fairly, and larger queries spill intermediate data to disk. |
| Calls, per persona | 2 at once, 20 a minute | Calls exceeding either limit are rejected without queueing. Retry later. An admin session is not metered this way. |
Each query records an intent before execution and an outcome afterward in a dedicated audit partition. If recording the intent fails, the query does not run. An intent without an outcome indicates that no result was recorded. Fixed-statement conversation tools follow the same audit requirement.
Where the rows come from
A query reads its authorized log partitions and decodes their events through shared folds into in-memory tables. Most tables therefore require no separate ingestion process.
Replay decodes events rather than scanning columnar files, so file statistics cannot skip unread blocks. The planner pushes projections and filters into the scan. A LIMIT can bound a simple plan. Sorting, grouping, and joins build intermediate results before applying the output limit. Narrow the conversation scope to reduce replay work; reducing returned rows alone may not avoid the replay limit.
A decode cache reuses replayed partitions across queries and extends them when the log grows. Rewriting a partition invalidates its cache entry.
The dashboard table uses a maintained projection from the durable commit feed. At-least-once delivery can leave it temporarily behind during redelivery or resubscription. Other catalog tables use query-time replay, with cached decoding where available.
The query fact model defines the responsibilities of decoding, shared folds, and consumer projections. It also documents table classes, scope, and redaction rules.