← Back to Docs

Daemon (polylogued)

polylogued is the long-lived Polylogue service. It watches chat directories, ingests new sessions, runs periodic maintenance, and exposes optional HTTP surfaces.

Quickstart

polylogued run

The daemon auto-discovers standard chat directories and begins ingestion. It stays running until interrupted.

Check status:

polylogued status

Auto-Discovery

The daemon watches these directories by default:

~/.claude/projects/       Claude Code sessions
~/.codex/sessions/         Codex sessions

Custom watch roots with --root (repeatable):

polylogued run --root /path/to/exports --root /another/path

Configuration Flags

By default polylogued run enables every component (watch, browser capture, HTTP API). Pass the corresponding --no-* flag to disable any one.

Flag Default Description
--root (auto) Override watch root (repeatable)
--debounce-s 2.0 Quiet period in seconds before parsing a modified file
--no-watch off Disable the live source watcher
--no-browser-capture off Disable the browser-capture receiver
--no-api off Disable the HTTP API + web reader

Browser Capture

Flag Default Description
--host 127.0.0.1 Browser-capture receiver host
--port 8765 Browser-capture receiver port
--spool (auto) Browser-capture artifact spool path
--browser-capture-auth-token auto Receiver bearer token; auto-minted/loaded from a 0600 file if not given
--browser-capture-allow-no-auth off Explicit opt-out: serve with no bearer token at all
--browser-capture-origin none Additional allowed origin (repeatable)
--insecure-allow-remote off Allow non-loopback binding

By default the receiver requires the auto-minted/loaded bearer token on every route (GET status/archive-state/post-commands and POST captures/post-commands alike) — run polylogued browser-capture token show to print it and paste it into the extension popup's "Receiver token" field to pair. Pass --browser-capture-allow-no-auth to opt out and serve unauthenticated (any local process can then read/post to the receiver).

HTTP API

Flag Default Description
--api-host 127.0.0.1 API server host
--api-port 8766 API server port
--api-auth-token auto API auth token (auto-generated if not provided)

HTTP API Endpoints

The HTTP API server runs by default. Pass --no-api to disable it. The endpoints below are exposed under the configured --api-host:--api-port.

Route contract metadata lives in polylogue/daemon/route_contracts.py. Dispatch still happens in polylogue/daemon/http.py, but the dispatcher now exposes its implemented route patterns from the routing tables used at runtime. The route-contract tests compare the two surfaces, so stable routes, shell-supported helpers, operational probes, and mutation routes must stay classified with explicit auth and response posture.

Route class Auth policy Examples
Browser shell bootstrap unauthenticated loopback HTML GET /, GET /s/:id, GET /p, GET /a
Operational probes unauthenticated loopback probe/scrape GET /healthz/live, GET /healthz/ready, GET /metrics
Stable read/query API bearer or scoped web credential when configured GET /api/sessions, GET /api/query-units, GET /api/sessions/:id, GET /api/sessions/:id/read, GET /api/assertions, GET /api/sessions/:id/provenance
User overlay reads bearer or scoped web credential when configured GET /api/user/marks, GET /api/user/saved-views/:id
Browser-accessible user-state mutations bearer or scoped web credential plus exact-origin browser request POST /api/user/marks, DELETE /api/user/saved-views/:id
Archive control mutations machine bearer when configured plus exact-origin browser request POST /api/reset, POST /api/ingest, POST /api/maintenance/run
Observability ingest explicit config flag plus loopback-or-bearer policy POST /v1/traces, POST /v1/metrics, POST /v1/logs

User overlay mutation routes return the shared mutation result envelope (status, operation, affected_count, and resource/target identity fields). Detailed overlay resources are read through the corresponding GET /api/user/... endpoints.

GET /api/health

Health check with database statistics.

{"ok": true, "db_size_bytes": 52428800, "wal_size_bytes": 4096, "disk_free_bytes": 107374182400, "quick_check": "pass"}

GET /api/status

Full daemon status including component state, source lag, ingestion throughput, FTS readiness, and insight freshness.

GET /api/sessions

List sessions. Query params: limit, origin, since.

GET /api/sessions/:id

Get a single session by ID.

GET /api/sessions/:id/messages

Get messages for a session. Query params: limit, offset.

GET /api/query-units

Return terminal rows for explicit query-unit expressions. Query params: expression, limit, offset. expression must be a messages/actions/blocks/assertions/runs/observed-events/context-snapshots where ... query and returns the shared QueryUnitEnvelope row envelope or, for SQL-backed group by FIELD | count pipelines, QueryUnitAggregateEnvelope.

GET /api/sessions/:id/read

Execute a supported single-session read profile and return the shared SessionReadViewEnvelope. Query params include view, format, and profile-specific options such as limit, offset, window_hours, and since_hours.

GET /api/assertions

List assertion-backed overlay claims through AssertionClaimListPayload. Query params include target_ref, scope_ref, kind, status, context_inject, and limit. The default status scope is active,candidate; use status=all for an explicit full lifecycle read.

GET /api/facets

Query faceted aggregations. The route returns FacetsResponse, including complete_families, deferred_families, generated_at, stale, stale_age_s, budget_exceeded, and family_status. Repo and action families are deferred from first-paint responses by default; request them with include_deferred=1 or families=repos,action_types, optionally bounded by budget_ms. The daemon also polls the client socket from SQLite progress handlers while computing facet SQL, so browser aborts/timeouts can stop already-started archive scans instead of waiting for response write.

GET /api/sources

List configured watch sources and their availability.

GET /api/raw_artifacts/:id

Fetch raw (pre-parsed) artifact data by raw ID.

POST /api/reset

Trigger a daemon reset operation.

GET /

Web shell (localhost-only, no auth). A lightweight browser interface for querying the archive through the daemon.

Authentication

When an API auth token is configured, machine clients pass it as a bearer:

curl -H "Authorization: Bearer <token>" http://host:8766/api/status

The first-party web shell does not receive that bearer. It obtains a separate short-lived, scoped HttpOnly cookie from POST /api/web-auth/session; ordinary fetch and SSE reconnects use the same cookie. Mutation routes require exact Origin-to-Host authority in addition to authentication. The cookie carries only read, events, and user_state scopes; archive reset, ingest, and maintenance controls require the machine bearer when authentication is enabled. GET /healthz/* and GET /metrics remain unauthenticated so health checks and Prometheus scrapers can operate without credentials.

Browser Capture Receiver

The browser capture receiver accepts chat session payloads from local browser extensions. It stores artifacts in the spool directory and triggers ingestion through the normal pipeline.

Enabled by default on 127.0.0.1:8765. Disable with --no-browser-capture.

Health Monitoring

polylogued status reports typed daemon health via the DaemonStatus model. polylogued health runs tiered health checks (fast by default, --expensive to include full integrity checks).

Status Fields

Field Description
daemon_liveness Whether the daemon process is running (pidfile check)
component_state.watcher running or stopped
component_state.api running or stopped
component_state.browser_capture running or stopped
source_lag Per-source file counts and availability
failing_files Files that failed ingestion
fts_readiness.messages_ready FTS index covers all messages
fts_readiness.actions_ready FTS index covers tool-use/tool-result action blocks
insight_freshness Sessions with profiles vs. total
embedding_readiness Embedding enabled, coverage, pending/stale, failures, cost
db_size_bytes Database file size
wal_size_bytes WAL file size
blob_dir_size_bytes Blob store size
disk_free_bytes Free disk space
ingestion_throughput Messages and files per second

Health Check Tiers

polylogued health runs checks grouped by cost:

Fast (sub-1s):

  • daemon_liveness — pidfile check
  • disk_space — free disk space (warns at 500 MB, critical at 100 MB)
  • wal_size — WAL file size (warns at 50 MB, errors at 200 MB)
  • source_availability — watch roots exist and are readable

Medium (sub-10s queries):

  • fts_readiness — FTS index coverage vs. total messages
  • raw_failures — parse/validation failure counts
  • stale_ingest_attempts — running attempts past the stale threshold
  • insight_freshness — session profile coverage vs. total sessions
  • repeated_stage_failures — recent ingest attempt failure rate

Expensive (potentially >10s):

  • db_integrityPRAGMA integrity_check
  • blob_integrity — sample blob store content verification
  • embedding_coverage — embedding coverage when enabled

Each check produces a typed HealthAlert with severity (ok, warning, error, critical), message, and consecutive_failures counter for persistent condition detection.

Notification Backend

Health alerts are delivered through a configurable notification backend. The default log backend writes alerts to the structured logger. Configure via polylogue.toml:

[notifications]
backend = "log"

Or via environment variable:

export POLYLOGUE_NOTIFICATION_BACKEND=log

Webhook backend

Selecting backend = "webhook" POSTs a typed JSON envelope to a user-configured URL on every non-OK health tick. The URL is fed through the resolved config and must be set; missing or empty URLs raise a typed config error at daemon startup rather than silently falling back to log.

[notifications]
backend = "webhook"
webhook_url = "https://hooks.example.invalid/polylogue"

Or via environment variables:

export POLYLOGUE_NOTIFICATION_BACKEND=webhook
export POLYLOGUE_NOTIFICATION_WEBHOOK_URL=https://hooks.example.invalid/polylogue

Envelope shape:

{
  "alerts": [
    {
      "check_name": "wal_size",
      "tier": "fast",
      "severity": "warning",
      "message": "wal_size warning: 80 MB",
      "checked_at": "2026-05-17T00:00:00+00:00",
      "consecutive_failures": 1
    }
  ],
  "emitted_at": 1747440000.123,
  "daemon_version": "0.6.0+g<sha>"
}

The backend uses a bounded 5-second per-attempt timeout and a single retry on transient failure (network error or HTTP 5xx). 4xx responses are treated as permanent client errors and are not retried. Persistent failure surfaces through the existing periodic-loop catch boundary in polylogue/daemon/cli.py; it does not crash the daemon. Dedup and rate-limiting are out of scope for this backend.

Build Identity & Deployment Attestation

Every runtime (Nix package, pip/uv install from a git checkout, or an sdist) embeds the exact git commit it was built from and reports it through two channels, distinct on purpose:

  • Concise human versionpolylogue --version / polylogued --version and polylogue.version.POLYLOGUE_VERSION: <pyproject-version>+<8-char commit prefix>[-dirty], e.g. 0.1.0+d8194234. Meant for logs and --version banners, not for exact identity comparison — an 8-character prefix is not collision-proof.
  • Full machine-readable revision — the same build's complete 40-character commit hash, for attestation:
    • polylogue.version.VERSION_INFO.commit (Python) — always the full hash, never truncated.
    • polylogued status --jsonarchive_storage.identity.build_commit (also .build_dirty), from ArchiveIdentity.as_dict() (polylogue/storage/archive_identity.py).
    • GET /metricspolylogue_daemon_build_info{version="...", revision="...", dirty="true|false"} 1revision is the full hash, version is the concise human string above.

For Nix-packaged deployments, the embedded commit comes from the flake's self.rev (or self.dirtyRev with the -dirty suffix stripped and tracked separately as BUILD_DIRTY, when the source tree was dirty at build time — see flake.nix's buildRevision/buildDirty and the checks.build-info package-level proof that the embedded value matches self).

Joining runtime identity to deployment evidence. A consuming flake (e.g. sinnix) pins Polylogue as a flake input and records its exact provenance in that flake's own flake.lock, under nodes.polylogue.locked:

{
  "rev": "d81942345…",       // full 40-char commit — compare directly to
                              // polylogue_daemon_build_info{revision=…}
  "narHash": "sha256-…"      // content hash of the fetched source tree
}

To confirm a running daemon matches what was actually deployed:

  1. Read the running build's revision: curl -s http://127.0.0.1:8766/metrics | grep polylogue_daemon_build_info (or polylogued status --json | jq .archive_storage.identity.build_commit).
  2. Read the deploying flake's locked revision: jq .nodes.polylogue.locked.rev flake.lock in the consuming repo (sinnix).
  3. The two full hashes must match exactly. A mismatch means the running binary was built from a different commit than the one the deployment flake currently locks — e.g. a nixos-rebuild switch that hasn't picked up a nix flake update yet, or a manually-built/side-loaded package. narHash corroborates that the fetched source tree content itself was not tampered with in transit, independent of the rev string.
  4. build_dirty / dirty="true" means the build was taken from an uncommitted working tree — there is no rev to join against a lockfile at all for that build; treat it as unattested and rebuild from a clean commit before trusting the comparison.

Maintenance

Maintenance tasks are split between daemon-owned (fast, inline) and operator-owned (heavy, scheduled externally via systemd timers or cron).

For the operator-facing maintenance surface — polylogue ops maintenance preview/plan/run, the resume/--operation-id pattern, scope filters, the status and failure surfaces, and incident runbooks — see maintenance.md. The daemon's inline convergence loops below cover the high-frequency, low-cost tasks; anything that exceeds those bounded windows is operator-driven and documented there.

Daemon-Owned Tasks

These run automatically inside the daemon process:

Task Frequency Description
WAL checkpoint Every 5 minutes Keeps the WAL file bounded via PRAGMA wal_checkpoint(TRUNCATE)
Heartbeat Every 15 minutes Logs session/message counts as structured heartbeat
FTS convergence Every 10 minutes Verifies FTS coverage, rebuilds if messages are unindexed
Health checks Configurable (default 5 min) Runs bounded FAST health checks by default, sends notifications on non-OK status. MEDIUM and EXPENSIVE checks are explicit operator diagnostics.
FTS startup check Once at startup Rebuilds the FTS index if messages exist but aren't indexed (covers gaps from pre-daemon data)

Health check tier and interval are configurable via polylogue.toml:

[health]
health_check_interval_s = 300
health_check_tiers = "fast"

Operator-Owned Tasks

These are heavier operations that should run outside the daemon via systemd timers, cron, or manual invocation. The daemon does not own these — operators are responsible for scheduling them:

Task Tool Frequency Description
Durability-tier backup polylogue ops backup Daily archives copy source.db, user.db, embeddings.db, and referenced blobs while omitting rebuildable index.db and disposable ops.db.
Blob store backup polylogue ops backup Weekly backup copies referenced blob files. For large archives, restic or similar incremental backup tools can also target blob/ directly.
Database vacuum sqlite3 <db> "VACUUM" Monthly Reclaims space after large deletes or updates. Requires downtime or VACUUM INTO to a new file while the daemon runs.
Litestream replication Litestream Continuous Real-time WAL replication to S3-compatible storage. Configure Litestream to watch the database and WAL files; the daemon's periodic WAL checkpoint is compatible with Litestream's replication model.

Litestream Integration (Guidance)

Litestream provides continuous SQLite replication by shipping WAL frames to object storage. Integration guidance:

  1. Install Litestream and configure it to watch the Polylogue tier files you need to preserve continuously. Prioritize source.db, user.db, and embeddings.db; include index.db when avoiding reindex time matters.
  2. The daemon's periodic WAL checkpoint (PRAGMA wal_checkpoint(TRUNCATE)) triggers Litestream to create new generations. No daemon changes needed.
  3. Test restores periodically: litestream restore -o /tmp/restore.db <path>.

A sample Litestream config:

dbs:
  - path: /home/user/.local/share/polylogue/source.db
    replicas:
      - type: s3
        bucket: my-backups
        path: polylogue/source
        endpoint: https://s3.amazonaws.com
  - path: /home/user/.local/share/polylogue/user.db
    replicas:
      - type: s3
        bucket: my-backups
        path: polylogue/user
        endpoint: https://s3.amazonaws.com

Blob Store Backup Guidance

The blob store uses content-addressed storage under <archive_root>/blob/. Each blob's filename is its SHA-256 hash, so identical content is automatically deduplicated. For backup:

  • polylogue ops backup copies archive blobs referenced by source.db
  • For large archives, use restic or rsync targeting the blob/ directory
  • Blobs are write-once, read-many — incremental backup tools work well

When a backup sees source rows or attachment references whose content-addressed blob file is absent, it keeps the backup usable but records the debt instead of silently hiding it. The warning names the total missing referenced blobs and the backup directory includes blob-reference-debt.json with the exact count, bounded hash sample, and reference-source counts. Treat that report as read-only recovery evidence: restore missing blob files from an older backup or re-ingest the affected raw sources before deleting any source/blob/link rows. For a richer recovery map, run polylogue ops maintenance blob-reference-debt --output-format json; it does not mutate the archive and classifies missing refs by origin, reference table, ref type, raw-row joinability, and whether the recorded source path still exists. During daemon convergence, direct source files whose current bytes still hash to a missing blob address are restored automatically before raw materialization replay. Container/member paths such as export.zip:conversations.json are deliberately left for source re-acquisition because the referenced blob may be an extracted record inside the member, not the member file itself.

Vacuum Guidance

SQLite VACUUM rebuilds the database file, reclaiming free pages. It requires the full database size in free disk space. Two approaches:

polylogue ops backup is not a vacuum operation. It creates a byte-exact, checkpointed snapshot and briefly holds the tier's SQLite writer lock; run large backups during a quiet window (and stop the daemon for durable-tier migration backups). To reclaim pages, stop the daemon, run sqlite3 <db> "VACUUM", verify the database, then restart. Use VACUUM INTO manually only when a reviewed copy-and-swap maintenance plan requires it.

Systemd Integration

Example unit file for running polylogued as a user service:

[Unit]
Description=Polylogue daemon
After=network.target

[Service]
Type=simple
ExecStart=%h/.nix-profile/bin/polylogued run --api-port 8766
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

The daemon writes a pidfile to the archive root and uses fcntl advisory locking to prevent concurrent instances.

Embeddings

The daemon can generate Voyage AI vector embeddings for message content, stored in the message_embeddings vec0 virtual table and consumed by --similar search and the hybrid retrieval lane.

Embedding convergence is opt-in — ordinary daemon catch-up does not make provider API calls unless explicitly configured.

Configuration

Embedding settings live in the [embedding] section of polylogue.toml (see polylogue config for the resolved path). The daemon reads these keys:

Key Type Default Description
enabled bool false Enable post-ingest embedding convergence
model string voyage-4 Voyage AI model name
dimension int 1024 Vector dimension (must match the model)
max_cost_usd float 5.0 Per-run soft cost cap in USD (0 = no limit)
voyage_api_key string (none) Voyage AI API key

Example TOML:

[embedding]
enabled = true
model = "voyage-4"
dimension = 1024
max_cost_usd = 1.00
voyage_api_key = "va-..."

Without a voyage_api_key, the embedding stage reports "disabled" in daemon status — this is not an error.

Model/dimension changes

When the configured model or dimension differs from stored embeddings, the daemon marks all sessions for re-embedding. A dimension change also drops and recreates the vec0 virtual table.

Checking coverage

polylogue analyze                    # embedding coverage in archive aggregates
polylogue ops embed status               # cheap readiness + latest catch-up run
polylogue ops embed status --detail      # exact pending-message/retrieval accounting
polylogued status                    # daemon status includes embedding readiness

polylogue ops embed backfill records each bounded catch-up window in the local archive as embedding_catchup_runs. The latest run is shown by polylogue ops embed status, including terminal state, stop reason, processed sessions, embedded messages, errors, and estimated cost. This is the operator recovery point after interruption, OOM, restart, or a cost/error window stop; per-session retry state still lives in embedding_status.

Service Recovery

polylogued is typically managed as a systemd user service. Example unit:

# ~/.config/systemd/user/polylogued.service
[Unit]
Description=Polylogue daemon
After=network.target

[Service]
ExecStart=%h/.local/bin/polylogued run
Restart=on-failure
Environment=POLYLOGUE_CONFIG=%h/.config/polylogue/polylogue.toml

[Install]
WantedBy=default.target

Check status: systemctl --user status polylogued View logs: journalctl --user -u polylogued -f

Pause or stop the daemon with systemd, not a Polylogue-specific switch:

systemctl --user stop polylogued
systemctl --user start polylogued
systemctl --user disable --now polylogued

Packaged deployments should keep host-friendliness in the service manager. The NixOS module defaults the daemon to background scheduling (Nice=10, idle I/O scheduling, conservative IOWeight, MemoryHigh, and MemoryMax) while the daemon itself still converges the full archive: live source ingest, raw parsing, FTS freshness, insight materialization, and convergence-debt retry. Operators can override those systemd resource controls in Nix or via a systemd drop-in without changing daemon behavior.

If the daemon fails to start, check:

  • tier files exist and are writable
  • polylogue.toml is valid (run polylogue config)
  • No other instance is running (pidfile lock)

Backup and Recovery

The Polylogue archive consists of:

  • source.db — raw acquisition/source evidence SQLite database
  • index.db — parsed sessions, search, graph, and insight read models
  • embeddings.db — vector index, embedding status, and catch-up metadata
  • user.db — user marks, corrections, and annotations
  • ops.db — daemon, cursor, and telemetry SQLite state
  • blob/ — content-addressed blob store

For backups, prioritize source.db, user.db, embeddings.db, and blob/. index.db is rebuildable but convenient to keep. ops.db is disposable unless you need operational history.

Continuous backup with Litestream:

litestream replicate source.db s3://my-bucket/polylogue/source
litestream replicate user.db s3://my-bucket/polylogue/user

Blob store backup with restic:

restic backup /path/to/archive/blob/

Recovery:

# Stop daemon
systemctl --user stop polylogued
# Restore files
cp /backup/source.db /path/to/archive/
cp /backup/user.db /path/to/archive/
cp /backup/embeddings.db /path/to/archive/
cp -r /backup/blob/ /path/to/archive/blob/
# Start daemon
systemctl --user start polylogued

The daemon will catch up on any files ingested after the backup was taken.