This page documents the compact, lossless format that list-shaped tool results use. Several tools return arrays of uniform records (sessions, events, tool calls, cost groups, search hits). Serializing those as plain arrays-of-objects repeats every JSON key on every row and re-serializes the same nested values (model, directory, agent) again and again. To cut payload size without losing information, list results are encoded as a columnar table with string interning.
Scalar, single-object responses (for example get-session or db-stats) are
returned as ordinary JSON objects and are not encoded this way.
An encoded list result is an object with three keys:
{
"cols": ["id", "title", "agent", "model"],
"dict": {
"agent": ["build", "executor-gpt"],
"model": [{ "id": "claude-opus", "provider": "anthropic" }]
},
"rows": [
["ses_a1", "Fix retry bug", 0, 0],
["ses_b2", "Cost audit", 1, 0]
]
}cols— the column order; each row is an array of cells in this order.dict— interning tables for selected columns. A column listed here stores its distinct values once; rows reference them by integer index. Columns absent fromdictstore their literal value inline.rows— one array per record.
There is a single decode rule:
A cell in a column whose name is a key in
dictis an integer index intodict[col]. Otherwise the cell is its literal value.
Decoding the example above yields:
[
{ "id": "ses_a1", "title": "Fix retry bug", "agent": "build", "model": { "id": "claude-opus", "provider": "anthropic" } },
{ "id": "ses_b2", "title": "Cost audit", "agent": "executor-gpt", "model": { "id": "claude-opus", "provider": "anthropic" } }
]Note that both rows reuse the same model object (index 0) instead of repeating
it — that is where the savings come from on large result sets.
The plugin ships the inverse function, decodeTable(), in
src/lib/table.ts. It is used by the test suite and is
safe for consumers to copy. Two behaviors make migration painless:
- A plain array passed to
decodeTable()is returned unchanged, so callers can treat encoded and unencoded results uniformly. - An out-of-range or non-integer cell in a
dictcolumn is passed through as its literal value rather than throwing.
A minimal equivalent decoder:
function decodeTable(t) {
if (t == null) return []
if (Array.isArray(t)) return t // already plain
const dict = t.dict ?? {}
return t.rows.map((row) => {
const o = {}
t.cols.forEach((c, i) => {
const v = row[i]
const d = dict[c]
o[c] = d && typeof v === "number" && v >= 0 && v < d.length ? d[v] : v
})
return o
})
}A list-sessions result interns the repetitive agent, model, directory, and
project_id columns:
{
"sessions": {
"cols": ["id", "title", "agent", "model", "directory", "cost"],
"dict": {
"agent": ["build"],
"model": [{ "id": "claude-opus" }],
"directory": ["/projects/app"],
"project_id": []
},
"rows": [
["ses_a1", "Fix retry bug", 0, 0, 0, 0.42]
]
},
"has_more": false
}Apply the decode rule to recover the records, then read cost (a literal column)
directly.
- Tool catalog: tools.md
- Search surfaces and channels: search-surfaces.md
- Four-layer architecture: architecture.md
- Configuration and environment overrides: configuration.md
- Troubleshooting: ../support/troubleshooting.md