Skip to content

feat: store Recall history in ClickHouse and add replacement sinks - #1163

Merged
kvlonge merged 21 commits into
mainfrom
feat/recall-clickhouse-store
Oct 19, 2026
Merged

kvlonge merged 21 commits into
mainfrom
feat/recall-clickhouse-store

Conversation

@kvlonge

@kvlonge kvlonge commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Why

Teams that run many commands across machines want one shared Recall history, and some need to decide what Recall stores (for example, redacting secrets from output) without losing other sinks.

Changes

  • ClickHouse store: [recall]/targets.*.recall connection may name a ClickHouse connection. MergeTree tables with TTLs and FINAL-read views are created on first use; the DDL (with its schema-version row) is printed when the user may not create them, and a SELECT+INSERT-only user can write a store created from it. Batched idempotent inserts with conflict detection, per-table failure accounting, retention-conflict notices (TTLs are never altered), and sqb recall query/schema/output support. Output is not capped by default.
  • Replacement sinks: a sink taking a RecallWriter replaces Recall's default for its stream. Both replacements run on Recall's delivery thread within shutdown_timeout; output is held until discovery decides; filtered/failed counts and filter_active are recorded; exit codes and stdout are unaffected.
  • Additive schema at version 1: the first heartbeat records the command, so runs killed during discovery stay listed; SQLite and ClickHouse stores from the first Recall release gain the new columns or are written without them.
  • Docs and the bundled docs skill.

Verification

  • ClickHouse Recall E2E suite on a local ClickHouse 26.1 server, including limited-privilege users, a first-release store, retention, duplicates, concurrency, killed runs, byte-exact and uncapped output.
  • Recall integration and replacement-sink E2E tests, discovery unit tests, sqlbuild-recall cargo tests.
  • make check-ci (fmt, clippy, ruff, ty, fensu, layering, boundary).

A [recall] or targets.<name>.recall connection on the clickhouse adapter now
sends history to MergeTree tables in an existing ClickHouse database through
the adapter's connection. Recall creates and additively upgrades its tables and
views there, prints the DDL when the user may not, and batches RowBinary
inserts with deterministic deduplication tokens. Lookups before each insert
keep the first copy of a key, record conflicting reuse and refresh artifacts
older than a week. Table TTLs follow the configured retention; a store with
another retention is reported once a day with the statements that would
change it. sqb recall query, schema and output read the ClickHouse store.

Also fix the ClickHouse view definitions so aggregate aliases no longer
shadow the columns they filter on, and take queued records when a heartbeat
falls due before a batch fills.
…bles

Every Recall table the views read is now a ReplacingMergeTree ordered by its
key, and the views read it with FINAL instead of deduplicating with LIMIT BY.
Filters on invocation_id now reach the primary key: on a store of about 3,000
invocations and 2.2 million events, a per-invocation artifact lookup reads
185 thousand rows instead of 507 thousand, and sqb recall output drops from
about 2.8 s to 0.5 s.
…rded yet

A reader that ran while the first writer was still creating the views failed
with an unknown-table query error; it now gets the coded store error that
says no history is recorded yet.
A @lifecycle_event_sink or @command_output_sink with a parameter annotated
sqlbuild.recall.RecallWriter replaces Recall's default for its stream: Recall
stores only what it publishes. Discovery allows one replacement per stream and
reports D015 naming every path:function otherwise, rejects a sink with two
writers or with providers, and reserves the provider name recall_writer
(D012). Replacements are not run as ordinary sinks; other sinks still see
every record.

Lifecycle events reach the replacement as they are published, filtered by its
event kinds and minimum severity. Output chunks reach it on the delivery
thread as schema version 2 CommandOutputRecords carrying the exact bytes in
data; record.replace() keeps the ID and sequence. An unpublished record is
counted as filtered, a raising sink as failed, never falling back to the
unfiltered record and never changing the exit code or output. The invocation
row now waits for discovery so filter_active records whether a replacement
ran.
@kvlonge
kvlonge enabled auto-merge (squash) October 19, 2026 17:43
@kvlonge
kvlonge merged commit 41e183e into main Oct 19, 2026
46 checks passed
@kvlonge
kvlonge deleted the feat/recall-clickhouse-store branch October 19, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant