511aa4899c
The library only ever SELECTs/INSERTs into llm_calls (D15), so expiring rows has to live outside it — holding DELETE would contradict the REVOKE UPDATE, DELETE the deployment template recommends. tools/telemetry_retention.py is dry-run by default and prints the row count, the created_at window and the tenant_id spread so an operator can tell whether the rows about to go are the intended ones. The Postgres branch refuses partitioned targets with exit code 3 (DETACH/DROP PARTITION is O(1); DELETE is not) and otherwise deletes in per-batch transactions. Missing asyncpg exits 2 rather than degrading quietly: this is an ops tool, and a silent "0 rows" reads as "already clean". Exit codes are the contract with the scheduler, so argparse errors were moved off 2 (now 1) to keep "bad flags" distinguishable from "cannot reach the database". The Postgres cases run against the real instance in throwaway schemas — never public.llm_calls — and the batch case asserts the shared table's row count is unchanged, so a search_path that failed to apply lands as a red test instead of a deletion.