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.

From signed events to SQLFollow a message from the log to a row.Three fictional conversations. One query. Change the scope to see what reaches the result.
Query as
Read the authorized log partitions
conv-aIn scope
Your completed conversation
  1. Turn starts
  2. Message
  3. Turn completes
conv-bIn scope
Your unfinished conversation
  1. Turn starts
  2. Message
  3. No completion
conv-cOutside your scope
Another person's conversation
  1. Turn starts
  2. Message
  3. Turn completes
Replay 2 authorized partitions
Decode events and derive facts

Shared decoders and folds turn log records into typed, in-memory tables. The query layer adds the partition, position, and turn id.

Signed log eventpartition · position · turn_id

The log stays unchanged. Queries do not create a second durable store.

Register the tables and views this scope permits
Query the rows you can read
Try a table
SQL
SELECT partition, position
FROM events
WHERE kind LIKE 'output_msg:%'
ORDER BY partition, position;
1 message row returned from events.
Illustrative query result
partitionposition
conv-a1

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.

Read-only throughout. This illustration uses sample rows, not a live query.

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, written conv-{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:

SQL
-- 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.

SQL
-- 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.

Browse by access
23 of 23 tables

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.
Key columns
  • partition
  • position
  • kind
  • turn_id
usage 3 key columnsToken usage for one committed turn.
Key columns
  • turn_id
  • input_tokens
  • output_tokens
model_call 4 key columnsThe model configuration recorded for one committed turn.
Key columns
  • turn_id
  • provider
  • model
  • captured_clock_unix_ms
turn_failed 3 key columnsOne turn that ends in a failure, and the kind of failure.
Key columns
  • turn_id
  • failure_kind
  • message
turn_dispatch 2 key columnsOne turn that the control plane records before it connects to the harness.
Key columns
  • turn_id
  • occurrence

Narrowed by scope 11

Your scope determines the available tables, rows, and columns.

messages 5 key columnsOne text block of the transcript.
Key columns
  • turn_id
  • role
  • text
  • trust
  • internal_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.
Key columns
  • tool_call_id
  • block_type
  • name
  • arguments
  • result
  • trust

An admin session reads the calls on the messages that no client receives.

approvals 10 key columnsOne approval request or decision.
Key columns
  • phase
  • request_id
  • tool_name
  • args_json
  • approved
  • signature_status
Admin-only columns
  • signer_public_key
  • approver
  • caller
  • modified_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.
Key columns
  • direction
  • reference
  • amount
  • currency
  • tool_call_id
  • subject
Admin-only columns
  • 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.
Key columns
  • reason
  • merchant_host
  • requested_base_units
  • permitted_base_units
  • tool_call_id
  • subject
Admin-only columns
  • 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.
Key columns
  • transition
  • currency
  • limit_human
  • expiry_unix
  • recipients
Admin-only columns
  • wallet_address
  • signer_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.
Key columns
  • phase
  • child_conversation_id
  • child_agent_id
  • carried_count
  • reason
  • parent_agent_id
  • denial_reason
  • allowed
  • signature_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.
Key columns
  • tool
  • grant_ref
  • covered_capabilities
  • signature_status
Admin-only columns
  • 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.
Key columns
  • turn_id
  • persona_id
  • role

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.
Key columns
  • routine
  • scheduled_at_ms
  • fired_at_ms
  • outcome

You read the firings of the routines that you publish.

routines 5 key columnsOne published routine and its live schedule.
Key columns
  • name
  • ready
  • phase
  • next_fire_time_ms
  • creator_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.
Key columns
  • partition
  • position
  • signed_by
  • signer_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.
Key columns
  • text
  • covers_through_position
persona_wallets 5 key columnsOne linked wallet and its delegation limits.
Key columns
  • persona_id
  • wallet_address
  • currency
  • expiry_unix
  • revoked
persona_spend_policies 4 key columnsThe spend cap and the host allowlist of one persona.
Key columns
  • persona_id
  • limit
  • period_secs
  • allowed_hosts
persona_credentials 5 key columnsOne passkey that a persona signs in with. No scope exposes key material through this table.
Key columns
  • persona_id
  • rp_id
  • origin
  • created_at_ms
  • revoked
persona_usage 5 key columnsOne persona's running totals. usage counts a single turn; this counts a whole persona.
Key columns
  • persona_id
  • committed_turns
  • input_tokens
  • output_tokens
  • last_active_ms
dashboard 4 key columnsTurn, token, and settlement totals for one conversation.
Key columns
  • conversation_id
  • committed_turns
  • input_tokens
  • settlements_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.

What each query scope can read
Queryable dataIn a conversationYour own sessionsAdmin
This conversation's committed turnsReadsReadsReads
Message text and tool argumentsReadsReadsReads
Every conversation you take part inHiddenReadsReads
Someone else's conversationHiddenHiddenReads
Messages never sent to a clientHiddenHiddenReads
Raw signer keys and edge identitiesHiddenHiddenReads
Undecoded event bytes (events_raw)HiddenHiddenReads
Every persona's profile and walletHiddenHiddenReads
ReadsReadsHiddenHidden

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.

Shell
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.

LimitDefaultWhen the limit is reached
Wall clock30 secondsThe query returns a timeout rather than holding a request open.
Rows returned10,000The result is cut to the cap and flagged with truncated.
Source events replayed500,000The scope is refused as too broad, before any decode runs.
Memory pool192 MiB, sharedQueries share it fairly, and larger queries spill intermediate data to disk.
Calls, per persona2 at once, 20 a minuteCalls 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.