Skip to content

Latest commit

 

History

History
1167 lines (975 loc) · 65.7 KB

File metadata and controls

1167 lines (975 loc) · 65.7 KB

← Back to Docs

Search & Query Reference

Polylogue uses a query-first grammar over archive units. Bare tokens are full-text terms, field clauses narrow the selected unit, explicit sessions/messages/actions/blocks/files/assertions/runs/observed-events/context-snapshots/delegations where ... forms opt into Boolean predicates, and trailing CLI verbs render or mutate the selected session set. The same query semantics — filters, retrieval lanes, ranking policy, and typed response payloads — apply across the CLI, MCP, Python API, and daemon HTTP surfaces.

Quick links:

Grammar

The canonical parser is the Lark grammar in polylogue/archive/query/expression.py. It has two entry shapes that lower through the same typed AST and query planner:

compact-query      ::= compact-clause*
boolean-query      ::= ["sessions" "where"] predicate
projection-query   ::= (compact-query | boolean-query) "with" projection-list
projection-list    ::= projection-item ("," projection-item)*
projection-item    ::= unit ["(" field ("," field)* ")"] ["[" bracket-clause ("," bracket-clause)* "]"]
bracket-clause     ::= field ":" value | ("first" | "last") ":" integer
unit-query         ::= ("messages" | "actions" | "blocks" | "files" | "assertions" | "runs" | "observed-events" | "context-snapshots" | "delegations") "where" predicate
pipeline-query     ::= unit-query ("|" pipeline-stage)+
                     | "sessions" "where" predicate "|" unit-query ("|" pipeline-stage)*

compact-clause     ::= field-clause
                     | quoted-text | bare-text
                     | "-" quoted-text | "-" bare-text
                     | count-comparison | count-range
                     | date-comparison | date-range

predicate          ::= predicate "OR" predicate
                     | predicate "AND" predicate
                     | "NOT" predicate
                     | "(" predicate ")"
                     | field-clause
                     | count-comparison | count-range
                     | date-comparison | date-range
                     | fts-leaf | semantic-leaf
                     | "exists" structural-unit "(" predicate ")"
                     | "seq" "(" sequence-step "->" sequence-step+ ")"

structural-unit    ::= "message" | "action" | "block" | "assertion" | "file"
                     | "run" | "observed-event" | "context-snapshot" | "delegation"
sequence-step      ::= action-field-clause ("AND" action-field-clause)*
action-field-clause ::= "action:" value | "tool:" value | "command:" value
                      | "path:" value | "output:" value | "text:" value
pipeline-stage     ::= "sort" "by" "time" ["asc" | "desc"]
                     | "group" "by" field ["," field]*
                     | "count"
                     | "agg" agg-metric ["," agg-metric]*
                     | "sort" "by" ("count" | "key") ["asc" | "desc"]
                     | "limit" integer
                     | "offset" integer

agg-metric         ::= "count"
                     | ("sum" | "avg" | "min" | "max" | "p" digit+) ":" field

Executable discovery corpus

Generated by devtools render query-discovery from polylogue/archive/query/discovery.py. The corpus contains 106 positive and 18 negative declarations. Every positive expression is parsed by the production Lark/compiler route in tests; every negative declaration pins the production diagnostic and a parser-valid correction.

Executable forms projected from the declarations:

Form Syntax
compact_form <field>:<value> [AND|OR|NOT ...]
session_form sessions where <session-predicate>
terminal_form <terminal-source> where <predicate>
pipeline_form <terminal-source> where <predicate> | group by <field> | count
scope_form sessions where <predicate> | <terminal-source> where <predicate>
projection_form <session-expression> with <unit>(<columns>)

Terminal sources: action / actions / assertion / assertions / block / blocks / context-snapshot / context-snapshots / delegation / delegations / file / files / message / messages / observed-event / observed-events / run / runs. Structural exists units: action / assertion / block / context-snapshot / delegation / file / message / observed-event / run.

Result coverage and total must be read with the declared semantics:

Class Total Continuation Discovery wording
exhaustive qualified cursor-or-offset Exhaustive relation is paged: total is page-local; follow continuation until absent.
top-k qualified ranked-frontier Top-k by relevance, not exhaustive; total is qualified and must not be read as all matching archive rows.
sample qualified none Sample, not exhaustive; total is qualified and does not establish archive-wide coverage.
aggregate aggregate cursor-or-offset Aggregate over the declared input relation; totals describe aggregate buckets, while input coverage is reported separately.
bounded-context qualified none Bounded context for orientation, not exhaustive; omitted evidence is expected and total is qualified.
recursive-page qualified recursive-cursor Recursive relation, physically paged; node and edge totals remain qualified until every continuation ends.

Featured parser-gated examples:

Semantics Source Expression What it answers Projection Cost
exhaustive sessions repo:example-repo Finds sessions associated with one repository. id, origin, title, title_source, title_ref, title_confidence, target_ref, anchor, actions, created_at, updated_at, message_count, tags, summary, words, repo, cwd_display, flags, parent_refs, child_refs, continuation selective
exhaustive sessions sessions where (repo:example-repo OR repo:example-library) AND NOT tag:stale Finds non-stale sessions from either of two repositories. id, origin, title, title_source, title_ref, title_confidence, target_ref, anchor, actions, created_at, updated_at, message_count, tags, summary, words, repo, cwd_display, flags, parent_refs, child_refs, continuation selective
exhaustive sessions sessions where exists message(role:assistant AND text:timeout) Finds sessions with an assistant message mentioning a timeout. id, origin, title, title_source, title_ref, title_confidence, target_ref, anchor, actions, created_at, updated_at, message_count, tags, summary, words, repo, cwd_display, flags, parent_refs, child_refs, continuation corpus-scale
exhaustive sessions sessions where seq(action:file_edit -> action:shell) Finds sessions where a file edit precedes a shell action. id, origin, title, title_source, title_ref, title_confidence, target_ref, anchor, actions, created_at, updated_at, message_count, tags, summary, words, repo, cwd_display, flags, parent_refs, child_refs, continuation corpus-scale
top-k sessions near:"semantic search" Ranks the sessions most relevant to a semantic-search phrase. session.id, session.origin, session.title, session.message_count, session.created_at, session.updated_at, match.rank, match.retrieval_lane, match.match_surface, match.target_ref, match.message_id, match.snippet, match.score, match.score_kind, match.matched_terms corpus-scale
sample sessions repo:example-repo since:30d Samples recent sessions from one repository. id, origin, title, title_source, title_ref, title_confidence, target_ref, anchor, actions, created_at, updated_at, message_count, tags, summary, words, repo, cwd_display, flags, parent_refs, child_refs, continuation selective
exhaustive messages messages where role:assistant AND text:timeout Returns assistant messages mentioning a timeout. unit, message_id, session_id, origin, title, role, message_type, material_origin, occurred_at_ms, position, word_count, text corpus-scale
exhaustive actions actions where session.repo:example-repo AND action:file_edit AND path:src/query Returns file-edit actions under the query source path for one repository. unit, session_id, message_id, origin, title, tool_use_block_id, tool_result_block_id, tool_name, semantic_type, tool_command, tool_path, occurred_at_ms, output_text, is_error, exit_code, followup_class, followup_message_ref selective
exhaustive actions actions where tool:shell AND command:pytest Returns shell actions whose command mentions pytest. unit, session_id, message_id, origin, title, tool_use_block_id, tool_result_block_id, tool_name, semantic_type, tool_command, tool_path, occurred_at_ms, output_text, is_error, exit_code, followup_class, followup_message_ref corpus-scale
exhaustive actions actions where session.repo:example-repo AND session.since:7d AND output:failed Returns recent failed action evidence for one repository. unit, session_id, message_id, origin, title, tool_use_block_id, tool_result_block_id, tool_name, semantic_type, tool_command, tool_path, occurred_at_ms, output_text, is_error, exit_code, followup_class, followup_message_ref corpus-scale
exhaustive files files where path:src/query/parser.py Returns file evidence for the query parser path. unit, session_id, origin, title, path, action_count, first_message_id, first_tool_use_block_id, last_tool_use_block_id, first_seen_ms, last_seen_ms selective
exhaustive files files where session.repo:example-repo AND path:src/mcp/server.py Returns file evidence for one path within one repository. unit, session_id, origin, title, path, action_count, first_message_id, first_tool_use_block_id, last_tool_use_block_id, first_seen_ms, last_seen_ms selective
exhaustive assertions assertions where kind:decision AND text:"schema migration" Returns decision assertions about a named topic. unit, assertion_id, target_ref, scope_ref, kind, key, body_text, value, author_ref, author_kind, status, visibility, evidence_refs, staleness, context_policy, created_at_ms, updated_at_ms corpus-scale
exhaustive messages sessions where repo:example-repo AND origin:unknown-export | messages where role:assistant Returns assistant messages scoped to repository sessions from one origin. unit, message_id, session_id, origin, title, role, message_type, material_origin, occurred_at_ms, position, word_count, text selective
aggregate messages messages where text:timeout | group by role | count Counts timeout-matching messages by role. unit, group_by, group_key, count, metrics corpus-scale
aggregate actions actions where is_error:true | group by tool | count Counts error-marked actions by tool. unit, group_by, group_key, count, metrics corpus-scale
bounded-context sessions repo:example-repo with messages(message_id,role,text) Builds bounded session orientation with selected message columns. session_id, message_id, role, text selective
bounded-context sessions sessions where title:"query compiler" with messages(message_id,role), actions(tool_name,semantic_type), files(path) Builds bounded mixed evidence for sessions whose title mentions the query compiler. session_id, message_id, role, tool_name, semantic_type, path selective
recursive-page sessions lineage:id:example-origin:session-child Seeds a recursive lineage walk from one session. session_id, parent_refs, child_refs, continuation selective

Invalid examples found in shipped teaching surfaces at the snapshot boundary:

Shipped at Rejected expression Actual diagnostic Corrected form
polylogue/mcp/server_prompts.py:509 actions where session.repo:example-repo since:7d AND output:failed invalid query expression near column 27 actions where session.repo:example-repo AND session.since:7d AND output:failed
polylogue/mcp/server_prompts.py:524 files where repo:example-repo AND path:src/mcp/server.py field 'repo' is not supported for file predicates files where session.repo:example-repo AND path:src/mcp/server.py
docs/search.md:924 text:css {session_id example}: refactor unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, messages, near, origin, paste_messages, path, project, repo, root, session, since, system_messages, tag, thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words contains:"css refactor"

Machine clients can request parser-gated positives with MCP/CLI query_completions(kind="example") and real diagnostics/corrections with query_completions(kind="error"). The query capability resource carries corpus counts and the six shared semantics contracts.

When a pipeline starts with sessions where ..., the left stage is lowered into predicates on the terminal unit query, so it uses the same row executor as direct messages/actions/files/... pipelines. Field/count/date predicates become session.<field> row predicates. FTS (~"..."), exists ..., sequence (seq(...)), and lineage:id:<session-id> stages lower through the existing session Boolean lowerers against the terminal row's owning session. Semantic vector predicates still reject in a session pipeline stage until terminal row queries have explicit ranked-result semantics. Terminal pipeline stages currently support sort by time [asc|desc], group by FIELD[,FIELD...] | count, group by FIELD[,FIELD...] | agg METRIC[,METRIC...], aggregate sort by count|key [asc|desc], limit N, and offset N for SQL-backed terminal rows (messages, actions, blocks, files, assertions). Query-string limits narrow the surface limit instead of expanding caller/API caps, and query-string offsets are added to the caller offset.

Named aggregate metrics (| agg ...)

| agg ... extends the count-only aggregate rollup with named, per-group reducer metrics (polylogue-fnm.1). It follows an optional group by stage (or reduces the whole matched row set with no group by) and precedes limit/offset:

polylogue --format json messages where session.repo:polylogue | group by role | agg count, avg:word_count, p90:word_count
polylogue --format json actions where session.repo:polylogue | group by tool | agg count, avg:is_error, sum:is_error

Each metric is count (field-less) or FN:FIELD, where FN is sum, avg, min, max, or a nearest-rank percentile pNN (p1..p99), and FIELD is one of the unit's declared numeric metric fields (message: word_count; action: is_error, exit_code). An unsupported function or field raises a typed error naming the unit, the metric, and the supported field set. Metric labels are stable output keys: count for the count metric, otherwise f"{fn}_{field}" (e.g. avg_word_count).

avg/sum/min/max/percentile reducers are not pushed down to SQL today — the count-only aggregate lowerer (ArchiveStore.query_unit_counts) computes exact grouped counts directly in SQL, but named-metric reduction fetches up to 50,000 predicate-matching rows through the unit's existing row query and reduces them in Python. The pipeline result payload reports this explicitly: result.exact is true when every matching row was fetched (the aggregate is exact), or false plus result.sampled_rows when the match set was larger than the cap (the aggregate is a bounded sample of the first 50,000 rows in time order, not the full population). Narrow the query with session/time/repo filters before trusting an agg result on a large archive; a count-only | group by ... | count stage stays exact at any scale because it is fully SQL-pushed.

runs, observed-events, context-snapshots, and delegations are SQL-backed terminal rows over source-derived archive relations (polylogue-dab): main runs, session_started events, tool-finished events, and session-start context snapshots are computed directly from sessions and blocks, with no separate materialized cache table. These units are usable as exists run(...) / exists observed-event(...) / exists context-snapshot(...) session selectors. runs and context-snapshots support time sorting and paging but have no aggregate lowerer. observed-events supports time sorting, paging, and declared group by | count fields. delegations supports paging and declared aggregation fields but deliberately does not advertise sort by time. Pipeline syntax is terminal-row syntax: session-selector surfaces reject piped queries, including aggregate stages like group by role | count, instead of dropping those stages and widening the query to exists message(...).

--explain --format json reports terminal pipelines as a unit_source AST with ordered pipeline_stages. Session-scoped pipelines include a session_scope stage carrying the original session predicate, followed by any terminal sort/limit/offset stages. Consumers should use that stage list rather than inferring pipeline behavior from incidental limit, offset, or sort fields.

Unsupported forms raise typed ExpressionCompileErrors and must not broaden into looser full-text search. In particular, reserved unit prefixes such as messages where are errors when malformed; they are not treated as ordinary text terms.

Bracket predicates/windows on attached units (with unit[...])

The with <units> projection clause (session-selecting queries, not terminal pipelines) accepts an optional bracket after each unit item, narrowing what gets attached to every selected session (polylogue-fnm.2):

polylogue --format json 'repo:polylogue with messages[role:user, last:20]'
polylogue --format json 'repo:polylogue with actions[tool:Bash, first:5]'
polylogue --format json 'repo:polylogue with messages(message_id,role,text)[role:user, last:10]'

A bracket is a comma-separated list of field:value equality predicates and/or a single first:N/last:N window, combinable with the existing unit(field, field) payload-field selector in either order. Bracket predicate fields are a small, explicit vocabulary per unit — the fields already present on that unit's attached row payload, not the full <unit> where ... structural predicate field set:

Unit Bracket predicate fields
message role, type
action tool, action, type, is_error, exit_code, followup_class
file path
assertion kind, status, visibility, author_kind

An unsupported bracket field or malformed clause raises a typed error naming the unit, the field, and the supported set. first:N/last:N trims each session's attached rows to the first/last N after predicates are applied, by fetch order — last:N fetches that unit in descending time order (then restores ascending order) precisely so the per-session/per-page row cap lands on the session's actual tail instead of silently landing on its head whenever a session has more matching rows than the cap.

Bracket predicates/windows apply to the already-fetched, capped attached-row set (attached_units.py's existing _MAX_ROWS_PER_SESSION/ _MAX_ROWS_PER_PAGE caps) — they are not pushed down to the SQL fetch itself. A predicate combined with last:N on a session whose matching rows are sparser than the fetch cap can still under-fetch; this is the same bounded-fetch tradeoff every other post-fetch filter on this path already has, not a window-specific gap.

DSL Fields

Field Meaning Example
repo Repository substring repo:polylogue
origin Source origin origin:claude-code-session
tag User/session tag tag:review
path Referenced file path substring path:polylogue/cli
cwd Working-directory prefix cwd:/realm/project
tool Tool name used in the session tool:bash
action Semantic action category action:file_edit
has Content/evidence presence (paste evidence, tools, thinking, or stored type) has:paste
id Session id or prefix id:codex-session:abc
session Exact session ref alias for id session:codex-session:abc
title Session title substring title:refactor
since / until Session time bounds, ISO or relative since:7d
contains Exact content substring filter contains:sqlite
near Vector similarity from text or a session id near:"semantic search" / near:id:<session>
lane Retrieval lane lane:dialogue
lineage Sessions sharing topology with a seed lineage:id:<session>

Field values support quoted strings and in-field alternatives:

polylogue 'origin:(codex-session|claude-code-session) title:"query DSL"'
polylogue 'tool:bash AND NOT tag:stale'

Negation is supported for fields that are semantically safe to negate, such as origin, tag, tool, and action.

Comparisons And Ranges

Readable comparisons are supported for message counts, word counts, numeric durations/tokens, and dates:

polylogue 'messages >= 5 AND messages <= 20'
polylogue 'words between 100 and 500'
polylogue 'sessions where duration_ms >= 60000'
polylogue 'messages where input_tokens >= 1000 AND duration_ms between 2000 and 3000'
polylogue 'date between 2026-06-01 and 2026-06-17'

Explicit Boolean queries can also compare the session aggregate columns already maintained by the archive:

polylogue 'sessions where user_messages >= 2 AND assistant_words between 500 and 2000'
polylogue 'sessions where authored_user_messages >= 2 AND authored_user_words >= 100'
polylogue 'sessions where system_messages = 0 AND tool_messages = 0'
polylogue 'sessions where tool_use_messages >= 1 AND paste_messages = 0'

Supported aggregate count fields are messages, words, user_messages, authored_user_messages, assistant_messages, system_messages, tool_messages, user_words, authored_user_words, assistant_words, tool_use_messages, thinking_messages, and paste_messages. user_messages and user_words are provider-role counts: they preserve source envelope truth and may include runtime protocol/context rows from providers such as Claude Code. Use authored_user_messages and authored_user_words when the question is human-authored prompt material. The tool_use_messages, thinking_messages, and paste_messages fields count messages carrying those signals, not raw block rows. These fields are SQL-backed session predicates; use them in sessions where ... Boolean queries or as session.<field> predicates inside terminal unit queries.

Supported non-count numeric fields are duration_ms on sessions and messages, plus message-scoped input_tokens, output_tokens, cache_read_tokens, and cache_write_tokens. Session duration_ms uses the archive's reported session duration; message duration_ms uses the stored per-message duration. Message numeric fields are accepted in messages where ... terminal queries and exists message(...) structural predicates.

Compact count syntax is equivalent where available:

polylogue messages:>=5 words:<=500

Structural Predicates

exists <unit>(...) keeps the selected unit as sessions but requires at least one child row matching the nested predicate.

Unit Accepted fields
message action, cache_read_tokens, cache_write_tokens, command, duration_ms, input_tokens, output, output_tokens, path, role, text, time, tool, type, words
action action, command, output, path, text, time, tool, type
block action, command, path, text, time, tool, type
file action, command, path, text, time, tool, type
assertion author, author_kind, author_ref, body, context, evidence, key, kind, scope, scope_ref, status, target, target_ref, text, value, visibility
run agent, branch, confidence, context_snapshot, cwd, evidence, harness, lineage, native_session_id, native_parent_session_id, origin, parent, provider_origin, role, run, status, text, title, transcript
observed-event delivery_state, evidence, kind, object, subject, summary, text
context-snapshot boundary, evidence, inheritance_mode, metadata, run, segment, text

Structural units also accept session.<field> predicates for the owning session fields that their lowerer can evaluate. SQL-backed units can use the full session filter surface, including action/tool/path/feature predicates. runs, observed-events, context-snapshots, and delegations are SQL-backed over the materialized run-projection tables, so they too accept the full session filter surface (e.g. session.repo, session.tool, session.path) alongside their own fields above. Count and date session fields accept compact comparison prefixes such as session.messages:>=2, session.words:<=500, and session.date:>=2026-01-02. This lets a unit query carry its session scope inline instead of splitting selection between the query string and parallel parameters.

Boolean and terminal predicates preserve comparison semantics: >, >=, <, <=, and = are distinct for count and numeric fields, while date and terminal time accept the four ordered comparisons plus between. The legacy flat root filters (messages > 10, words < 500) still compile into inclusive SessionQuerySpec min/max bounds because that older request object has no exclusive-bound fields.

Examples:

polylogue sessions where 'exists action(session.repo:polylogue AND tool:bash AND text:pytest)'
polylogue sessions where 'exists file(session.repo:polylogue AND action:file_edit AND path:archive/query)'
polylogue sessions where 'exists block(type:code AND text:timeout)'

FTS And Semantic Leaves

Inside Boolean predicates, ~ marks an explicit FTS leaf and semantic: / near:text: mark semantic-vector leaves:

polylogue sessions where '~"null pointer" AND repo:polylogue'
polylogue sessions where 'semantic:"query compiler failure"'
polylogue sessions where 'near:text:timeout'

Semantic leaves require embeddings to be configured and available. When vectors are unavailable, the query fails with a typed semantic/vector availability error instead of falling back to broad lexical search.

Terminal Unit Queries

Most query expressions select sessions. Explicit unit sources select terminal rows instead:

polylogue --format json messages where role:assistant AND text:timeout
polylogue --format ndjson actions where session.repo:polylogue AND action:file_edit AND path:polylogue/archive
polylogue --format yaml blocks where type:code AND text:sqlite
polylogue --format json files where action:file_edit AND path:polylogue/archive/query
polylogue --format json assertions where kind:decision AND status:active AND text:review
polylogue --format json runs where role:subagent AND status:completed AND agent:Explore
polylogue --format json observed-events where delivery_state:acted_on AND text:#2100
polylogue --format json context-snapshots where boundary:session_start AND session.repo:polylogue
polylogue --format json delegations where mapping_state:resolved AND instruction:review
polylogue --format json messages where text:timeout | group by role | count

The row shape is the shared QueryUnitEnvelope used by CLI JSON/NDJSON/YAML, Python Polylogue.query_units(), MCP query_units, and daemon GET /api/query-units?expression=.... Plain and CSV CLI output are transport-specific renderings of the same message/action/block/file/assertion/run/ observed-event/context-snapshot row payloads. Aggregate pipelines over SQL-backed terminal rows use the sibling QueryUnitAggregateEnvelope and currently support group by FIELD | count over closed unit fields such as message role, action tool/action, block type, file path, assertion kind/status, and owning-session origin/repo. Aggregate rows can be ordered with sort by count [asc|desc] or sort by key [asc|desc] before limit/offset; the default order is count descending, then key. Those surfaces share the same session-scoping filters for the row source where applicable, such as origin, tag, repo, title, date bounds, message-type and tool/paste/thinking feature filters.

Use session.<field> inside the expression when the unit rows should be scoped by their owning session:

polylogue messages where session.origin:claude-code-session AND role:assistant
polylogue actions where session.repo:polylogue AND action:file_edit
polylogue blocks where session.since:7d AND session.words:<=500 AND type:code
polylogue files where session.repo:polylogue AND action:file_edit AND path:archive/query
polylogue assertions where session.repo:polylogue AND kind:caveat
polylogue runs where session.repo:polylogue AND role:subagent AND status:completed
polylogue observed-events where session.origin:codex-session AND object_ref:github-review
polylogue context-snapshots where session.messages:>=2 AND session.date:>=2026-01-02 AND boundary:subagent_start

files where ... is an affected-path evidence projection over action rows with non-empty tool_path. It returns one row per owning session and normalized path, with action counts and first/last tool-use refs. It is not a global filesystem inventory and does not claim that a file still exists on disk.

runs, observed-events, context-snapshots, and delegations are SQL-backed row sources over source-derived archive relations, with materialized projection tables used only for richer non-duplicate rows that are not cheap local projections of sessions and blocks. They are both terminal unit sources (runs where ...) and exists run(...) / exists observed-event(...) / exists context-snapshot(...) session selectors, and they accept the full SQL-backed session filter surface (session.action, session.tool, session.path, session.has, …). runs and context-snapshots support sort by time, limit, and offset but reject aggregate stages because no aggregate lowerer exists. observed-events supports time sorting, paging, and aggregation. delegations supports paging and aggregation but rejects time sorting because no stable event-time column is declared.

Session filters such as --origin, --tag, --repo, --since, and --until still narrow the owning sessions before rows are returned. Session-only actions and result-shaping modes that do not have row semantics yet, such as delete, open, stats, count, --cursor, and custom sort/reverse modes, are rejected instead of silently coercing row queries back to session queries.

Assertion Candidate Judgment

Any non-user-authored assertion write (agent, transform, detector, blackboard post, or any future automated writer) lands with status:candidate by default -- enforced once, inside upsert_assertion itself, not per-writer (37t.15). Two narrow exceptions: an assertion_id that already carries a terminal judgment outcome (accepted/rejected/deferred/superseded/deleted) keeps that outcome instead of being resurrected to candidate, and session tags (AssertionKind.TAG) opt out entirely via require_promotion=False since tags are categorization, not epistemic claims, and have no judgment-queue path today. Candidate rows are private, carry context_policy.inject=false, and keep promotion_required=true until an operator makes an explicit judgment. They can be inspected like any other assertion row, or through the review list that keeps pending candidates separate from already judged lifecycle rows:

polylogue assertions where 'status:candidate AND target:session:codex-session:abc123' --format json
polylogue judge --target-ref session:codex-session:abc123 --list --format json
polylogue judge --target-ref session:codex-session:abc123 --review --format json
polylogue judge --status --format json

The sole public judgment workflow is root polylogue judge; it writes through the existing lifecycle authority into the same user.db assertion substrate:

polylogue judge --accept assertion:candidate-id --reason "confirmed by transcript" --format json
polylogue judge --reject assertion:candidate-id --reason "unsupported by evidence" --format json
polylogue judge --defer assertion:candidate-id --reason "needs another source" --format json
polylogue judge --supersede assertion:candidate-id --replacement-kind summary --body "replacement claim" --format json

The closed assertion status vocabulary is active, candidate, accepted, rejected, deferred, superseded, deleted, and inactive; unknown values fail at the typed boundary instead of being rendered as generic strings. accept and supersede create an active assertion whose evidence includes the candidate assertion ref and whose supersedes lineage points at the candidate. reject writes status:rejected, and defer writes status:deferred; both preserve a judgment assertion with the reason and leave a durable lifecycle record. Judged candidates remain in polylogue judge --review with disabled action reasons such as candidate_already_accepted, candidate_already_rejected, candidate_deferred, or candidate_superseded, while polylogue judge --list only shows candidates still awaiting judgment. Review rows disclose the claim summary, source identity, candidate age, total evidence count, and up to five bounded evidence previews; unresolved or failed references are represented as explicit per-reference states rather than failing the whole review. No candidate assertion is injected into compiled context unless a later surface asks for candidates explicitly.

polylogue judge --status is a non-destructive queue-health query. It reports pending counts and age, lifecycle/kind/source breakdowns, producer-run state, scheduler heartbeat, convergence debt, and retention outcome. An empty queue is healthy only when a recent successful producer run and fresh heartbeat are both observable; otherwise it remains empty-unverified. Candidates older than 60 days remain durable and visible as retained backlog.

Pending candidate judgments also appear in the operator debt cockpit:

polylogue ops debt list --kind assertion-candidate --only-actionable --format json

Public Ref Resolution

Query/read payloads carry public object and evidence refs so agents and the web shell can jump from a row, context image, or assertion back to the exact archive object it cites. Resolve refs through the shared resolver rather than turning them into broad text search:

polylogue read session:codex-session:abc123 --format json
polylogue read message:codex-session:abc123:m1 --format json
polylogue read block:codex-session:abc123:m1:0 --format json
polylogue read assertion:assertion-id --format json
polylogue read delegation:claude-code-session:parent:message:0 --format json

The same PublicRefResolutionPayload is exposed by Polylogue.resolve_ref(), MCP resolve_ref, and daemon GET /api/refs/resolve?ref=.... The resolver supports session, message, block, assertion, delegation, and runtime projection refs (run, observed-event, context-snapshot) when the addressed object exists. Unsupported or missing refs return a bounded unresolved payload with caveats; they never widen into a session search.

Delegation refs resolve to an explicit evidence card. The card preserves the complete recorded instruction while separately bounding the parent-side dispatch result, actual child-session excerpt, parent context, and parent follow-up with per-window/per-excerpt truncation markers. Ordinary delegations where ... rows expose only previews, SHA-256 hashes, structural mapping/result states, and evidence refs; they do not claim child success, utility, or parent use.

Outbound OTel Projection

Polylogue exposes an outbound OTel projection that exports archive evidence as an observability-shaped view.

Outbound projection is not archive authority. Trace/span ids are stable export ids, while Polylogue refs remain the navigation surface back to canonical sessions, messages, runs, context snapshots, observed events, assertions, and evidence refs.

The Python API exposes the first bounded projection surface:

payload = await archive.export_otel(
    source_ref="session:codex-session:abc123",
    expressions=(
        "runs where session.id:codex-session:abc123",
        "observed-events where session.id:codex-session:abc123",
        "context-snapshots where session.id:codex-session:abc123",
    ),
)

OtelProjectionPayload currently emits OTLP-like JSON (format="otlp-json") over existing query-unit row payloads. Runs and actions become spans; messages, observed events, and context snapshots become log/event records. Tool outputs and absolute local paths are omitted by default; the payload instead carries output length/presence, redaction flags, and refs that clients can resolve deliberately when the operator wants to inspect source evidence.

Filters

Identity and content

Flag Description
--id, -i Session ID (exact or prefix match)
--contains, -c FTS term (repeatable = AND)
--exclude-text Exclude sessions matching this term
--title Title contains substring
--origin, -o Include origins (comma = OR)
--exclude-origin Exclude origins
--repo, -r Filter by repository name
--referenced-path File path contains substring (repeatable = AND)
--cwd-prefix Working directory starts with this prefix

Content type

Flag Description
--has-tool-use Only sessions with tool calls
--has-thinking Only sessions with reasoning/thinking blocks
--has-paste Only sessions with paste evidence
--typed-only Only sessions without paste evidence
--has, --has-type Filter by content type: thinking, tools, summary, attachments

Message stats

Flag Description
--min-messages Minimum message count
--max-messages Maximum message count
--min-words Minimum word count

Semantic actions

Flag Description
--action Require semantic action: file_read, file_write, file_edit, shell, search, web, agent, subagent, git (repeatable = AND)
--exclude-action Exclude semantic action (repeatable = AND)
--action-sequence Require ordered action subsequence (comma-separated)
--action-text Text match within action evidence (repeatable = AND)
--tool Require normalized tool name (repeatable = AND)
--exclude-tool Exclude normalized tool name (repeatable = AND)

Time and scope

Flag Description
--since Only sessions on or after this date/time
--until Only sessions on or before this date/time
--limit, -n Maximum results
--offset Start offset
--latest Newest-first sort
--sort Sort order
--reverse Reverse sort direction
--sample Random sample of N sessions

Tags

Flag Description
--tag, -t Include tags (comma = OR, supports key:value)
--exclude-tag Exclude tags

Retrieval

Flag Description
--retrieval-lane Query lane: auto, dialogue, actions, hybrid
--similar Semantic similarity query (requires embeddings)

Verbs

Verbs determine the action applied to the matched session set.

Verb Description
read --all Read every matched session with metadata
analyze --count Print count of matched sessions
analyze --by ... Grouped statistics (origin, month, year, day, action, tool, repo, work-kind)
read Display session content through read views
read --to browser Open session in browser
read --all --format ... Render every matched session in the selected format
read --view messages Show individual messages
read --view raw Show raw (unparsed) session data
select Select and print a single field
delete Delete matched sessions (requires --dry-run confirmation)

Retrieval Lanes

Lane selection lives on the query as retrieval_lane. The resolved value appears in the SearchEnvelope.retrieval_lane field on every response so consumers can tell what actually ran (which matters because auto resolves to a concrete lane — see below).

Lane Description Score kind
auto Default lexical planner lane. Resolves to dialogue for ordinary text queries; vector work is explicit through --semantic, --similar, or --retrieval-lane hybrid. bm25
dialogue FTS5 over message text (messages_fts virtual table, unicode61 tokenizer). Default lexical lane. bm25
actions FTS5 over tool-use/tool-result block text in messages_fts. Targets tool/file/shell evidence rather than prose. Public ranked-hit payloads currently carry action rank/evidence without a numeric action BM25 score. null
hybrid Reciprocal Rank Fusion combining FTS5 and vector similarity (requires embeddings). rrf
semantic Pure vector similarity over Voyage-4 embeddings via sqlite-vec. Triggered by --similar or --semantic. vector_distance

Implementation: polylogue/storage/search/query_builders.py, polylogue/storage/search_providers/hybrid.py, polylogue/storage/search_providers/sqlite_vec_support.py.

Lane Semantics

dialogue (FTS5 lexical)

  • Backed by SQLite FTS5's BM25 implementation against messages_fts.
  • Tokenizer is unicode61 remove_diacritics 2 — case-insensitive for ASCII, Unicode-aware tokenization, and folds ordinary combining-mark diacritics (ó->o, ż->z, ą->a, ...) so a plain-ASCII query finds accented content and vice versa. Porter stemming is not available in this SQLite build, so refactor and refactoring are distinct tokens. Use prefix queries (refactor*) when you want morphological breadth.
  • ł/Ł (Latin L with stroke) has no Unicode decomposition, so remove_diacritics cannot reach it. pl_fold (polylogue/storage/fts/pl_fold.py) closes that specific gap: it folds ł/Ł into indexed text before FTS insertion and into MATCH query text in escape_fts5_query, so latwo/zrobilem finds seeded łatwo/zrobiłem (polylogue-9jsi). The same tokenizer and fold apply to the threads_fts and session_work_events_fts insight-search surfaces, including full rebuild, missing-row repair, and dangling-row repair paths. A trigram fallback lane for further recall (beyond word-boundary tokens) is deliberately not part of this fold — see polylogue-xul7 (tracked follow-up) for a measured, benchmarked trigram lane before any such lane ships or is defaulted on.
  • Raw score is BM25: lower is better in SQLite FTS5, values are typically negative, and they are not comparable across queries.
  • Match evidence: matched_terms, snippet, match_surface="message", message_id, and target_ref point at the hit message.

actions (FTS5 over action blocks)

  • Same FTS5 mechanics as dialogue, but the query is restricted to tool_use and tool_result blocks inside messages_fts. The normalized actions view remains the structured action surface for filters and analytics.
  • Current public action-lane hits preserve rank and action match surface but do not expose the underlying action FTS BM25 score in the shared SearchEnvelope; consumers should treat score_kind=null as the contract for action-only hits until the action evidence path is widened.
  • Useful when you remember an action ("the session where I edited connection_profile.py") rather than its prose.

hybrid (RRF fusion)

  • Runs both dialogue (FTS5) and semantic (vector) lanes, then fuses with Reciprocal Rank Fusion at k=60: fused_score = Σ 1 / (k + rank_in_lane).
  • Tie-breaking is deterministic: descending fused score, then ascending session_id. This makes cursor and offset pagination stable across runs even when scores tie.
  • Reported score_kind is "rrf". Higher fused scores indicate stronger cross-lane consensus.
  • Lane contributions (per-lane rank and per-lane RRF contribution) are preserved end-to-end on each hit's score_components (#1267): every lane that contributed adds a <lane>_rank (1-based rank within that lane) and a matching <lane>_rrf (the 1 / (k + rank) contribution that was summed into the fused score). Lane names are text (FTS5 dialogue), action (FTS5 action blocks), and vector (semantic). A hit that appeared only in the lexical lane carries {text_rank, text_rrf} and nothing else; a hit that survived both lanes carries the full (text|action|vector)_(rank|rrf) set, so consumers can show "ranked high in both lexical and semantic lanes" without re-running the search.

semantic (vector-only)

  • Pure k-nearest-neighbor over Voyage-4 1024-dim embeddings via sqlite-vec's vec0 virtual table.
  • Triggered by --similar <text> or --semantic (which promotes the positional query string into similar_text; no FTS leg runs).
  • Score kind is "vector_distance" — lower means closer in embedding space. Like BM25, distances are not directly comparable across different query embeddings.
  • Requires embeddings to be enabled and populated; see docs/architecture.md § Embedding Pipeline.
  • Use polylogue ops embed status to check whether vector retrieval is disabled, missing an API key, pending backlog catch-up, partially usable, or complete. polylogue ops embed status --detail performs exact pending-message and retrieval-band accounting; the default status path stays cheap and reports the latest persisted catch-up run.

Embedding Activation And Catch-Up

Semantic search stays unavailable until embeddings are both enabled and materialized. The activation path is deliberately bounded:

  1. polylogue ops embed status shows config state, key presence, coverage, configured model/dimension, monthly cost cap, backlog, latest catch-up progress, and next_action (code, reason, command) for automation.
  2. polylogue ops embed preflight --max-sessions 10 estimates the next bounded window without contacting Voyage. Use --format json for the scriptable form: it reports the exact window, pricing assumptions, effective cost cap, and a ready-to-run backfill_args list for the same bounded catch-up slice.
  3. polylogue ops embed enable --yes enables the daemon stage when a Voyage key is already configured, or polylogue ops embed enable --voyage-api-key ... records the key and enables the stage.
  4. polylogue ops embed backfill --max-sessions 10 runs an explicit bounded catch-up batch; after enablement, polylogued also processes bounded daemon batches for new or stale sessions.

--max-messages is a hard message-count window and uses live message counts rather than potentially stale session_stats. --max-sessions may use materialized session stats so small first batches remain fast on large archives.

MCP clients should use embedding_status for readiness/next-action state and embedding_preflight for the same no-provider-call cost window. Both tools are read-only and return the canonical JSON payloads used by the CLI, so agents do not need to scrape terminal output before deciding whether semantic search is actually usable.

Python API clients use the same contracts through Polylogue.embedding_status(detail=False) and Polylogue.embedding_preflight(...).

Auto Lane

When retrieval_lane=auto (the default), ordinary text queries run on the lexical dialogue lane. The planner does not inspect embeddings.db before a default search, so keyword lookup stays fast and predictable on large archives.

Condition Resolved lane
--lexical flag set dialogue (forced FTS-only)
--semantic flag set, or --similar <text> given semantic (vector-only)
--retrieval-lane hybrid with an FTS query hybrid
Otherwise dialogue

The resolved lane is always echoed back in SearchEnvelope.retrieval_lane so callers do not have to re-derive what ran.

Two ergonomic overrides on the root CLI surface (#1217):

  • --lexical — force the FTS-only lane; useful when you want deterministic keyword matches regardless of embedding state.
  • --semantic — promote the query string into a vector-only similarity probe; no FTS leg, no boolean operators applied.

Ranking Policy

Every SearchEnvelope declares its ranking_policy and ranking_policy_version. The current policy identifier is mixed-bm25-rrf-vector (version 1):

  • dialogue orders hits by FTS5 BM25 (lower is better; raw scores are usually negative).
  • actions orders through the action FTS read model, but the public action-lane hit payload does not currently expose a numeric action score.
  • hybrid fuses dialogue + action + semantic lanes with RRF at k=60 and orders by fused score, breaking ties on (−fused_score, session_id).
  • semantic orders by ascending vector distance.

Consumers should pin the ranking_policy_version they validate against and treat any change as a contract event. The version is intentionally exposed so external pipelines can detect ordering shifts without diffing raw scores. See docs/openapi/search.yaml (x-polylogue-ranking-policy) for the machine-readable declaration.

SearchEnvelope Contract

All ranked surfaces (CLI --format json, MCP search/list_sessions, daemon GET /api/sessions?query=…, Python API) return the typed SearchEnvelope defined in polylogue/surfaces/payloads.py and emitted via the daemon under docs/openapi/search.yaml (#1266).

Field Meaning
hits Ordered list of SessionSearchHitPayload. Each hit carries a session summary plus a match evidence block.
total Total matching sessions, or null when the lane cannot compute it cheaply.
limit / offset Applied page size and row offset. Offset-based pagination is best-effort for ranked results.
next_offset Convenience offset pointer; only set when more results are likely.
next_cursor Opaque keyset cursor encoding rank, score, session id, and resolved retrieval lane. Preferred for stable rank-first pagination across pages — pass it back unchanged in the next request.
query The FTS query text actually applied after CLI/MCP/HTTP coercion. Empty when no FTS query was given.
sort Applied explicit sort field ("date", "messages", "words", etc.) or null to preserve the lane's natural rank order. Ranked search will not silently fall back to date sort.
retrieval_lane Resolved lane that actually ran (dialogue / actions / hybrid / semantic / auto).
ranking_policy / ranking_policy_version Declared ordering semantics; see above.
diagnostics Optional QueryMissDiagnosticsPayload when the query produced zero hits but filters were applied.

Each hit's match (a SessionSearchMatchPayload) carries:

Field Meaning
rank 1-based position in the result list.
retrieval_lane Lane that produced this hit (matches envelope, unless future per-hit attribution differs).
match_surface Indexed surface that matched — e.g. message, action, hybrid, semantic, or attachment.
score Raw lane score (semantics depend on score_kind).
score_kind One of "bm25", "rrf", "vector_distance", or null for identity-only lanes. Always check this before comparing or ordering by score directly.
score_components Map of per-component contributions explaining the rank. Dialogue (FTS5) hits carry {"bm25_raw": <relevance>}; hybrid hits carry per-lane <lane>_rank and <lane>_rrf entries summed into score (#1267). Identity-only lanes (e.g. attachment) carry {}.
lane_rank / lane_contribution Primary lane rank and contribution when the backend can identify one. For hybrid, this is the strongest contributing RRF lane; all lane details still live in score_components.
raw_score Backend-native score before public interpretation. For FTS this is BM25 relevance; for hybrid it is the fused RRF score.
matched_terms FTS terms that triggered the match.
snippet Highlighted excerpt around the match (FTS5 snippet).
message_id / target_ref / anchor Stable identifiers pointing the reader at the matching message or sub-block.
actions Per-target reader action availability (open, copy-link, etc.).

Score-kind cheatsheet

  • bm25 — lower = better match in SQLite FTS5. Values are typically negative. Never display raw BM25 as a percent or compare across queries; rank position is the durable signal.
  • rrf — higher = better; bounded by Σ 1/(k+1) over contributing lanes. Per-lane decomposition lives in score_components as <lane>_rank / <lane>_rrf pairs, so consumers can explain "this hit appeared in both lexical and semantic lanes" without re-running the query (#1267).
  • vector_distance — lower = closer in embedding space. Not comparable across different query embeddings.
  • null — identity-bearing match (e.g. attachment identity lane); no numeric score, only rank.

Per-hit explanations (#1267)

Every ranked hit carries deterministic why-this-matched evidence on its match payload. The exact field set depends on the lane that produced the hit:

Lane matched_terms score_kind score_components
dialogue (FTS5 over messages) tokenized query terms (lowercased, FTS5 operators stripped) bm25 {"bm25_raw": <relevance>}
actions (FTS5 over action blocks) tokenized query terms null today {} today; action rank is preserved, but action BM25 is not part of the public hit evidence contract yet
hybrid (RRF fusion) tokenized query terms rrf <lane>_rank and <lane>_rrf for every contributing lane (text / action / vector); score equals the sum of *_rrf
semantic (vector-only) the query string passed to --similar / --semantic (single term) vector_distance {} (raw distance lives in score)
attachment (identity lookup) the matched identifier (single term) null {} (identity hits have no numeric rank)

Tokenization for matched_terms strips FTS5 boolean operators (AND / OR / NOT / NEAR), quote/colon/paren punctuation, and trailing * prefix markers, then deduplicates case-insensitively. The result is the literal set of tokens a reader can expect to see highlighted in the snippet.

Hybrid score_components are the load-bearing surface: each contributing lane adds two entries that explain its share of the fused score. For example, a hit at lexical rank 1 and vector rank 2 carries:

"score_components": {
  "text_rank": 1.0,
  "text_rrf": 0.0163934426,
  "vector_rank": 2.0,
  "vector_rrf": 0.0161290323
},
"score": 0.0325224749

Consumers can read score_components directly to render a "matched in both lanes" badge or to debug ranking drift without re-running the search.

Pagination

For ranked queries, prefer next_cursor over offset. Cursor pagination encodes the rank tie-breaker ((rank, score, session_id, retrieval_lane)) and is stable under archive growth between page fetches. Offset pagination is supported for non-ranked list paths and as a best-effort fallback for ranked paths.

The cursor is an opaque URL-safe base64 token (a versioned JSON envelope; see :class:polylogue.surfaces.payloads.SearchCursor). Consumers MUST treat it as opaque and pass it back unchanged.

# Page 1
polylogue "sqlite" read --all --format json --limit 25
# Read .next_cursor from the response, then ask for page 2:
polylogue "sqlite" read --all --format json --limit 25 \
    --cursor "$NEXT_CURSOR"

MCP search and the daemon /api/sessions endpoint accept the same cursor parameter; the Python API exposes Polylogue.search_envelope( query, cursor=...). The cursor carries the retrieval lane it was minted in, so a dialogue cursor passed back to a hybrid request is rejected up-front rather than silently changing ranking policy mid-walk.

Stability guarantees (#1268):

  • No duplicates: any hit returned on page N is filtered out on page N+1 even when new rows were ingested between requests.
  • No gaps: any hit that sorts strictly after the anchor (under the lane's natural ordering: BM25 lower-is-better, RRF higher-is-better, vector distance lower-is-better) survives the cursor trim.
  • Restart-stable: cursors are self-contained tokens with no server-side state; they survive daemon restart.

FTS5 Syntax

The messages_fts virtual table uses SQLite's FTS5 with the unicode61 tokenizer. Prefix queries use *:

polylogue "refactor*"

Phrase queries use quotes:

polylogue '"null pointer exception"'

Boolean operators combine terms:

polylogue "refactor AND schema NOT test"

The strict Polylogue command floor does not expose raw FTS5 column-filter syntax. A token such as text:css is parsed as a query-DSL field and rejected. Use a parser-valid content predicate instead:

polylogue 'contains:"css refactor"'

Searchable Content Coverage

blocks.search_text — the generated column FTS5 indexes — concatenates a fixed subset of block fields, not the whole block. Anything outside that subset is invisible to the ranked dialogue/actions/hybrid lanes, --contains, contains:, and bare-text queries, even though the underlying data is stored and readable through other surfaces (read, select, direct block/action reads). The live definition lives in polylogue/storage/sqlite/archive_tiers/index.py (blocks.search_text GENERATED ALWAYS AS (...)); this table is drift-checked against that expression by tests/unit/pipeline/test_search_text_coverage_contract.py.

Source In search_text (FTS-searchable) Notes
blocks.text — message prose, thinking/reasoning block content, tool_result output Yes Every text-bearing block type stores its content in this column, so thinking/reasoning text and tool-result stdout/stderr/output are all searchable today.
blocks.tool_name Yes Tool identifier string (Write, Bash, Edit, ...).
tool_input.$.command Yes Shell/exec command lines (Bash, exec_command-style tools).
tool_input.$.file_path Yes Primary path argument for file-oriented tools.
tool_input.$.path Yes Alternate path key used by some tools (Grep, Glob).
tool_input.$.content — file bodies a Write tool call authored No The full text an agent wrote into a new/overwritten file is excluded from search_text. A distinctive string that only appears inside a Write body returns zero FTS hits.
tool_input.$.old_string / tool_input.$.new_stringEdit tool bodies No Same gap as Write: edited code is excluded unless it also happens to appear in prose, a tool_result echo, or one of the indexed fields above.
Any other tool_input key (pattern, description, url, ...) No Only the three json_extract paths above are concatenated into search_text; every other key is excluded regardless of tool.

Dedicated action-evidence lane: when you know an agent wrote or edited a specific string into a file body, use the query DSL's action text predicate. It searches the action's raw tool_input JSON (as well as normalized action fields and output), so it covers Write's content and Edit's old_string/new_string without putting those bodies in FTS:

polylogue 'actions where tool:write AND text:"needle"'
polylogue 'sessions where exists action(tool:edit AND text:"needle")'

This is an unindexed LIKE filter, not FTS; constrain it by action, path, session scope, or time on large archives. For direct index.db inspection, the equivalent JSON-aware SQL probe is:

SELECT block_id, session_id, tool_name,
       json_extract(tool_input, '$.file_path') AS file_path
FROM blocks
WHERE tool_name IN ('Write', 'Edit')
  AND (
    json_extract(tool_input, '$.content') LIKE '%needle%'
    OR json_extract(tool_input, '$.old_string') LIKE '%needle%'
    OR json_extract(tool_input, '$.new_string') LIKE '%needle%'
  );

Extending search_text itself to cover $.content was considered and deliberately deferred (see polylogue-013x) because Write bodies can be large enough to meaningfully bloat the FTS index, and that tradeoff needs a size probe against a live archive plus a derived-tier index rebuild before it is decided, not a silent schema bump.

Output Formats

Format Description
markdown Default -- formatted markdown with syntax-highlighted code blocks
json Full session as JSON
jsonl One JSON object per line (used by read --all --format ndjson)
yaml YAML representation
plaintext Plain text, no formatting
html HTML with Pygments syntax highlighting
obsidian YAML frontmatter + markdown body
org Org-mode format
csv Messages as rows

Set format with -f / --format on a verb:

polylogue "sqlite locking" read --all --format json
polylogue --since yesterday read --all --format ndjson

Facets (Scoped vs Global)

Facets summarize the archive as aggregate counts. Polylogue exposes the same shape across daemon HTTP (GET /api/facets), MCP (facets), CLI (polylogue facets), and the Python API (Polylogue.facets); see #1269 (slice D of #873).

A facets response carries both views explicitly:

  • scoped — counts rolled from the current query/filter set. Empty buckets if the filter chain narrows away every value.
  • global — counts over the unfiltered archive. Always populated.
  • scoped_to_querytrue whenever any filter narrowed the view.
  • complete_families / deferred_families / family_status — route-state metadata for each facet family. HTTP readers use this to distinguish an actually-empty bucket from a family intentionally not materialized in the current response.
  • generated_at, stale, stale_age_s, budget_exceeded — freshness and budget metadata surfaced to the web workbench.
  • idf — inverse-document-frequency per facet value, computed against the global universe. Higher = rarer = stronger signal; near zero = value appears in almost every session. Disable with --no-idf on the CLI.

Top-level fields (origins, tags, total_sessions etc.) mirror the active view (scoped when filtered, global otherwise) for backward compatibility with surfaces written before #1269. Consumers that need both views should read scoped and global directly.

Daemon HTTP keeps expensive archive-wide families lazy for workbench first paint. GET /api/facets includes origin/tag/count buckets and marks repos plus action_types as deferred_by_default. Clients that need the full set can call GET /api/facets?include_deferred=1&budget_ms=5000 or GET /api/facets?families=repos,action_types; a budget overrun returns the existing bucket fields plus budget_exceeded=true and per-family deferred reasons instead of blocking the page indefinitely. HTTP workbench requests attach request IDs and bounded AbortController timeouts; the daemon observes closed client sockets inside facet SQLite progress handlers so cancelled/timed-out browser requests can interrupt already-started scans.

polylogue facets                     # global only (no filters)
polylogue facets -o chatgpt-export   # scoped to ChatGPT exports + global side-by-side
polylogue facets -q "vector store"   # scoped to FTS hits
polylogue facets -f json --no-idf    # FacetsResponse, no IDF weighting

Empty Result Diagnostics

When a query returns no results:

  1. Check origin spelling: polylogue --origin claude-code-session read --all (not claude_code)
  2. Expand the time window: --since 2024-01 instead of --since yesterday
  3. Verify the archive has data: polylogue analyze --count (no filters)
  4. Check FTS index health: polylogued status shows fts_readiness
  5. Run polylogue ops doctor for schema and index integrity
  6. If using --similar, ensure embeddings are built (check polylogue ops embed status --detail for embedding readiness/coverage)
  7. If you know an agent wrote or edited a specific string into a file (a Write/Edit tool body), FTS will not find it. Use the action-evidence query path documented in Searchable Content Coverage, such as actions where tool:write AND text:"needle".