Polylogue Architecture
For the target shape, guardrails, and architectural decision log, see
Architecture Spine. Current sequencing and active
workstreams live in the Beads backlog (bd ready, bd list --status open) —
the durable directive substrate; the former docs/execution-plan.md was
superseded (its GitHub-issue map was re-encoded as Beads issues) and retired
(Ref polylogue-3tl.13). For the vocabulary split between
provider-wire identity, public origin, source roots, capture mode, parser
provenance, and refs, see docs/provider-origin-identity.md.
Polylogue is a local archive for AI sessions. The system has four rings:
- archive substrate
- derived read models
- user and machine surfaces
- verification and maintenance
flowchart TD
SRC["Source files<br/>ChatGPT · Claude · Codex · Gemini · Drive · Antigravity"]
subgraph R1["Ring 1 · Archive substrate"]
DETECT["detect_provider()<br/>shape-based dispatch"]
PARSE["provider parsers<br/>normalize → content hash (NFC)"]
STORE[("split-tier SQLite<br/>source · index · embeddings · user · ops<br/>+ content-addressed blob store")]
DETECT --> PARSE --> STORE
end
subgraph R2["Ring 2 · Derived read models"]
INSIGHTS["session profiles · work events<br/>phases · threads · cost rollups<br/>FTS5 + vector indexes"]
end
subgraph R3["Ring 3 · Surfaces"]
CLI["CLI<br/>find QUERY then VERB"]
MCP["MCP bridge"]
API["Python API"]
DAEMON["daemon HTTP reader<br/>+ /metrics"]
end
subgraph R4["Ring 4 · Verification & maintenance"]
VERIFY["schemas · demo fixtures<br/>devtools lanes · tests"]
end
SRC --> DETECT
STORE --> INSIGHTS
INSIGHTS --> R3
STORE --> R3
R4 -.audits.-> R1
R4 -.audits.-> R2
Source evidence and user-authored state (rings 1 and the user.db tier) are
durable; indexes, insight read models, embeddings, and daemon telemetry are
rebuildable. Surfaces (ring 3) are leaf adapters — they read through the same
archive/query substrate rather than owning their own stores.
Rings
1. Archive Substrate
Owns stored meaning:
- source acquisition and provider detection
- provider parsing and normalization
- SQLite persistence and search indexes
- archive-level query and runtime operations
Primary modules:
polylogue/sources/polylogue/pipeline/polylogue/storage/polylogue/archive/polylogue/operations/archive.py
2. Derived Read Models
Stored insights computed over the archive:
- session profiles
- work events, phases, threads
- day and week summaries
- provider-level analytics and tag rollups
Primary modules:
polylogue/insights/polylogue/storage/insights/session/polylogue/storage/repository/insight/
3. Surfaces
These expose the archive and its insights:
- CLI:
polylogue/cli/ - Python API:
polylogue/api/__init__.py - MCP server:
polylogue/mcp/— exposes the distilled, agent-preferred surfaces (get_postmortem_bundle) alongside search/list/insight tools. - daemon web reader:
polylogue/daemon/web_shell.py - dashboard and TUI:
polylogue/ui/ - renderers:
polylogue/rendering/
Leaf adapters over archive operations and derived insights.
4. Verification and Maintenance
- schema inference and verification
- synthetic corpus generation
- deterministic demo fixtures and behavior-backed archive/reader smoke checks
- optional validation lanes, mutation campaigns, and benchmark campaigns that dispatch executable commands
Primary modules:
polylogue/schemas/polylogue/demo/devtools/tests/
Data Flow
source files (JSON/JSONL/ZIP)
→ detect_provider() # dispatch.py — shape-based, not filename
→ provider parser # parsers/{chatgpt,claude,codex,drive}.py
→ content hash (NFC) # pipeline/ids.py — SHA-256 over normalized payload
→ store (upsert-if-changed) # storage/ — idempotent by content hash
→ session insights # storage/insights/session/ — profiles, work events, phases, threads
→ FTS index # search_providers/fts5.py — unicode61 tokenizer
CLI / MCP / Python API
↑
filter chain → query → storage
The daemon-owned ingest path acquires source payloads, parses provider records, writes archive rows, and refreshes derived read models through explicit convergence stages.
Provider Detection
| Provider | Detected by | Parser |
|---|---|---|
| ChatGPT | mapping dict with message graph |
sources/parsers/chatgpt.py |
| Claude web | chat_messages list |
sources/parsers/claude/ai_parser.py |
| Claude Code | parentUuid/sessionId in record array |
sources/parsers/claude/code_parser.py (code path) |
| Codex | Session envelope structure | sources/parsers/codex.py |
| Gemini | chunkedPrompt.chunks structure |
sources/parsers/drive.py |
| Drive | Google Takeout format with OAuth | sources/parsers/drive.py |
| Gemini CLI | local_agent.looks_like_gemini_cli() document shape |
sources/parsers/local_agent.py |
| Hermes | state-db payload or hermes agent document | sources/parsers/hermes_state.py, sources/parsers/local_agent.py |
| Antigravity | Brain artifact metadata (*.metadata.json) or language-server Markdown export envelope |
sources/parsers/antigravity.py |
| Browser capture | capture envelope wrapping a native provider payload | sources/parsers/browser_capture.py |
detect_provider() (in sources/dispatch.py) runs looks_like() checks in a
tightness order, not filename order: structural/document detectors first
(browser-capture, gemini-cli, hermes, antigravity), then Pydantic-validated
record checks (Codex, Claude Code), then loose dict-key checks (ChatGPT,
Claude web, Gemini). Insert a new check at the tightness level it deserves or an
earlier parser will claim its records. Provider.GROK/grok-export is a
reserved origin token with no wired parser yet.
Provider and Origin Vocabulary
The codebase has origin-related vocabularies with different scopes. See
docs/provider-origin-identity.md for the full classification table and
feature guidance.
Provider(polylogue/core/enums.py): the provider-wire enum used by provider-wire parsing, provider schema packages, and some internal storage adapters. It previously leaked into public surfaces such as the CLI--providerfilter, MCPproviderparameter, and daemon facet labels. Mixes lab identity (OpenAI, Anthropic, Google), product/runtime identity (Claude Code, Codex), and source-family identity (claude-code-session vs claude-ai-export) into one token. See the vocabulary table inpolylogue/core/provider_identity.pyfor the detailed conflation surface.Origin(polylogue/core/enums.py): the public source-origin token carried by query surfaces and archive read payloads, such asclaude-code-session,claude-ai-export, andchatgpt-export. Public filters useorigin; internal bridges still map origins to canonical provider tokens where older storage/insight contracts have not been renamed yet.Source(polylogue/core/sources.py): the richer source-centered identity used for runtime roots and lab attribution. ASourceis an immutable dataclass withfamily,runtime_root, andoriginating_lab.
The primary CLI/MCP/API/daemon read surfaces use origin vocabulary. Provider
remains only for raw export, schema-package, and provider-metadata boundaries.
The remaining internal provider-token adapters are cleanup targets, not an
alternate archive mode.
Cleanup sequencing:
- Internal callers switch from provider-token filters to origin filters at boundaries where lab identity and runtime identity need to be distinguished (e.g. analytics, cost rollups, source-discovery).
- Storage columns that still carry provider-token names are either renamed in the canonical DDL or kept only where their contract is provider-wire.
- Provider-wire schemas under
schemas/providers/are retained as lab/provider-scope artifacts — they describe raw export shapes and stay keyed by lab/product, not by source family.
Anti-goal: provider wording on source-origin public filters or payloads. Provider remains valid only where the contract is truly provider-wire, schema-package, provider metadata, or embedding-provider terminology.
Antigravity Language-Server Export Path
Antigravity persists its session transcripts as opaque non-protobuf
conversations/*.pb and implicit/*.pb blobs that cannot be statically
decoded. The installed Antigravity language server binary
(language_server_linux_x64) exposes two endpoints over a local HTTP loopback
port that together form the supported export surface:
| Endpoint | Purpose |
|---|---|
/exa.language_server_pb.LanguageServerService/SearchConversations |
Returns cascade IDs, titles, workspace names, snippets, and lastModifiedTime for stored sessions. |
/exa.language_server_pb.LanguageServerService/ConvertTrajectoryToMarkdown |
Returns a complete Markdown export for a given cascade ID — user inputs, planner responses, tool/command events. |
The adapter lives in polylogue/sources/parsers/antigravity.py:
AntigravityLanguageServerClientspawns the binary with-standalone -persistent_mode -http_server_port=<port>against the user's Antigravity data root, waits until the search endpoint answers, and tears the process down on close.discover_language_server()resolves the binary in this order:POLYLOGUE_ANTIGRAVITY_LANGUAGE_SERVERenv var,$PATH, then the highest matching/nix/store/*-antigravity-*extension bundle.iter_language_server_exports(root)drivesSearchConversationsandConvertTrajectoryToMarkdownand yieldsParsedSessionobjects throughparse_markdown_export().
The ingest path is layered in polylogue/sources/source_parsing.py: when the
source is antigravity and a conversations/ subdirectory exists, the
language-server export runs first; any AntigravityExportError (binary not
found, connection failure, malformed response) is logged and the source falls
back to the existing brain-artifact metadata walk. Both paths emit normalized
Provider.ANTIGRAVITY sessions.
Key Abstractions
| Abstraction | Location | Role |
|---|---|---|
Polylogue |
api/__init__.py |
Async entry point. Wraps storage + search + pipeline. |
SessionRepository |
storage/repository/__init__.py |
Mixin-composed async repository (10 mixins: archive reads, archive writes, raw, vectors, and six insight readers — profile, run-projection, timeline, thread, summary, topology). |
SearchProvider protocol |
protocols.py |
FTS5 and Hybrid (RRF fusion) implementations. |
SessionFilter |
archive/filter/filters.py |
Fluent filter chain used by CLI, MCP, and facade. |
Session Insights |
storage/insights/session/ |
Materialized read models: profiles, work events, phases, threads, aggregates. |
ContentHash |
pipeline/ids.py |
SHA-256 over NFC-normalized session payload. Title, timestamps, messages, attachments are hashed. User metadata (tags, summaries) is excluded — editable metadata doesn't trigger re-import. |
Provider enum |
core/enums.py |
Legacy/provider-wire identifier used by parsers, schemas, provider metadata, and compatibility bridges. |
Origin enum |
core/enums.py |
Public source-origin identity used by query/read surfaces and archive payloads. |
Source dataclass |
core/sources.py |
Source-centered identity (family, runtime_root, originating_lab) used for runtime roots and lab attribution. |
TopologyEdgeRecord |
archive/topology/edge.py |
Typed cross-session parent reference. Persisted in session_links even when the parent has not yet been ingested (#1258) so out-of-order ingest and sidechain/subagent edges are durable. Closed TopologyEdgeType (a LinkType alias) / TopologyEdgeStatus enums (both in core/enums.py) centralize the vocabulary. |
| Logical Session ID | session_profiles.logical_session_id |
Materialized root session for continuation/fork/subagent lineages. Rollups expose logical_session_count alongside physical session_count, and get_logical_session() returns the compact read-pull lineage envelope. |
Artifact Taxonomy
Acquired files are classified by ArtifactKind before ingestion:
| Kind | Description |
|---|---|
session_document |
A single session (Claude Code JSONL, ChatGPT JSON) |
session_record_stream |
Stream of session events |
subagent_session_stream |
Sidechain sub-agent session |
agent_sidecar_meta |
Session metadata (history.jsonl, sessions-index.json) |
session_index |
Provider-level session index |
bridge_pointer |
Pointer from a parent session to a sub-agent session |
metadata_document |
Supplementary metadata |
unknown |
Unclassified artifact |
classify_artifact() in sources/artifact_taxonomy/ assigns each acquired file
a kind. The daemon uses artifact classification to route files to the correct
ingestion path.
Hook Integration
Polylogue integrates with AI coding agents via hook scripts that fire on session lifecycle events. See docs/hooks.md for the full event catalog, per-event use cases, sidecar layout, and recommended starter set.
- Claude Code: 16 hook events available (SessionStart, Setup, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, Notification, Elicitation, ElicitationResult, CwdChanged, FileChanged, WorktreeCreate, SubagentStart, Stop). See #802 and #1213.
- Codex: 6 hook events available (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PermissionRequest, Stop).
Hook scripts call polylogue-hook which ingests session data at event
granularity, providing 100% data coverage vs. ~79% from post-hoc JSONL
discovery. Hooks are the enabling infrastructure for real-time context injection
(SessionStart), accurate paste detection (UserPromptSubmit fires before
clipboard expansion so [Pasted text #N] markers are observable), tool
execution metadata that never lands in JSONL (PreToolUse/PostToolUse), and a
full permission audit trail (PermissionRequest/PermissionDenied).
Embedding Pipeline
Vector embeddings for semantic search, powered by Voyage AI (voyage-4,
1024 dimensions) via SQLite-vec (vec0 virtual table):
- Storage:
message_embeddings(vec0),message_embeddings_meta,embedding_status - Search:
--similarflag triggers pure vector search; hybrid mode combines FTS5 + vector via Reciprocal Rank Fusion - Integration: Daemon-side post-ingest and ambient catch-up embedding is
opt-in via
embedding_enabled = trueinpolylogue.tomlwith a validvoyage_api_key. When enabled, the daemon drains pending sessions in bounded windows and records catch-up progress; when disabled, no embedding provider calls are made (#1503).
Activation flow (#1217)
The polylogue ops embed group is the operator-facing onboarding surface:
| Command | Purpose |
|---|---|
polylogue ops embed preflight |
Count pending messages + Voyage cost estimate without contacting the provider. |
polylogue ops embed enable |
Verify sqlite-vec, capture the Voyage key, print the cost preflight, and on confirmation persist [embedding] enabled = true (and the API key unless --no-store-key) into the user polylogue.toml. |
polylogue ops embed backfill |
Run a bounded, resumable embedding batch with per-session cost feedback; honours embedding_max_cost_usd as a soft cap and persists run progress. |
polylogue ops embed disable |
Flip embedding.enabled = false without dropping existing embeddings — previously-embedded messages remain queryable via --similar. |
polylogue ops embed status |
Coverage / freshness / configured model+cap / latest catch-up / next-action snapshot via embedding_status_payload. Use --detail for exact pending-message and retrieval-band accounting. |
The CLI orchestrates substrate primitives under
polylogue.storage.embeddings (iter_pending_sessions,
embed_session_sync, embedding_catchup_runs) and the cost constants
ESTIMATED_TOKENS_PER_MESSAGE / VOYAGE_4_COST_PER_1M_TOKENS from
polylogue.storage.search_providers.sqlite_vec_support.
Search defaults (#1217)
Default polylogue searches stay lexical: retrieval_lane=auto resolves to
dialogue for ordinary FTS queries and does not probe embeddings.db before
returning keyword results. Vector retrieval is explicit on the root query
surface:
--lexical— forceretrieval_lane=dialogue(FTS-only).--semantic— promote the query string intosimilar_textso the request runs as a vector-only similarity probe (no FTS leg).--retrieval-lane hybrid— combine FTS5 and vector similarity via RRF when embeddings are configured and populated.
See docs/search.md § Retrieval Lanes for the
full lane semantics, ranking policy, and SearchEnvelope contract.
polylogue ops status includes an Embeddings: line whenever any
messages are embedded, so the operator can see coverage at a glance.
Blob Store
Content-addressed storage for large binary artifacts (message content, attachments, exports):
- Addressing: SHA-256 hash over content, stored in 256 prefix-sharded
subdirectories (
blob/ab/cdef...) - Dedup: Identical content produces identical hashes — automatic deduplication
- Linking:
raw_artifacts.link_group_keygroups blobs by session for lifecycle management (there is no separateblob_linkstable; the name is a historical alias for this row-group view ofraw_artifacts) - Scale: ~24K blobs, ~42 GB in production archive
- GC: Blob garbage collection is tracked in #818
Database
- Archive SQLite file set, WAL mode.
- Schema is fresh-first: version mismatches are rejected and the affected tier
is rebuilt from source/user evidence. Tier DDL and versions live under
storage/sqlite/archive_tiers/. - FTS5 with
unicode61tokenizer (no porter stemmer in this SQLite build).
Placement Rules
Substrate (archive meaning)
lib/— domain types, invariants, shared primitives (no I/O, no storage)storage/— SQLite backends, repositories, FTS, search providerssources/— provider detection, parsing, acquisitionpipeline/— stage execution, daemon ingestion, validation, and indexinginsights/— derived read models, session insights, analyticsoperations/— operation specs, artifact graph, declared runtime contracts
Surfaces (presentation only)
cli/— Click commands, shared helpers, output formattingmcp/— MCP server toolsapi/— async library APIrendering/— markdown/HTML renderersui/— TUI, dashboarddaemon/— daemon convergence, HTTP API, and web reader
Verification (repo health)
devtools/— operator tooling, lints, campaigns, renderingdemo/— deterministic archive/demo workspace fixturestests/— pytest suite, property tests, integration tests
Cross-cutting
schemas/— provider schemas, schema inference, validationscenarios/— synthetic corpus, scenario families
Key rules
- Surfaces may not import substrate internals directly (see layering.yaml).
- New semantics go into substrate or insights first, then surfaces adapt.