Skip to content

cr-sqlite retention + paced deletes, and a reproduced CRR rollout wedge - #1063

Merged
arul28 merged 4 commits into
mainfrom
ade/t3-crsqlite-retention
Aug 10, 2026
Merged

cr-sqlite retention + paced deletes, and a reproduced CRR rollout wedge#1063
arul28 merged 4 commits into
mainfrom
ade/t3-crsqlite-retention

Conversation

@arul28

@arul28 arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Item 3 of three hygiene fixes from the 2026-08-09 t3code research (§3.3). Stacked on #1061; shares no files with #1062.

This PR does not do what the research asked for, and the measurements are why. The ask was "make compactCrsqlTombstones run on paired machines." Investigating it produced the opposite conclusion.

Why the proposed fix was dropped

Tombstones are under 4% of the metadata. On the live 28.7 MB project DB, __crsql_clock/__crsql_pks really is 5.7 MB (20% — the research's number is right). But only ~1,950 of ~53,000 clock rows are tombstones. ai_usage_log, the single worst table at 0.93 MB metadata for 0.27 MB data (3.5×, exactly as reported), has zero. The bloat is structural — one clock row per live row per non-PK column — not garbage awaiting collection. A perfect, safe compaction would recover ~220 KB of 28.7 MB.

And it cannot be made safe. Reproduced against the repo's own vendored crsqlite.dylib:

  • rebuildCrrTableWithBackfill drops and recreates the clock table. Measured: col_version 3 → 1, cl 2 → 1, the col_name='-1' tombstone gone, db_version moved forward, site id unchanged.
  • Silent divergence: host at col_version=6, peer holding its own write at 4. Control run converges. With compaction the host pushes col_version=1, the peer rejects it as older, and the two never converge again. No error, no resync.
  • Deleted rows resurrect: the host deletes a row the peer hasn't heard about, compaction destroys the tombstone, the peer pushes the row back, and it returns on the machine that deleted it.

A watermark would not rescue it. There is no durable host-owned per-peer ack version — PeerState.lastKnownServerDbVersion is in-memory, initialized to 0, and the authoritative copy lives on the client (hello.peer.dbVersionBySite). And peer seeding replays crsql_changes from version 0 (mobileReplicaReseed.ts), so any clock row removed is permanently absent from every future seed — the watermark describes existing peers and says nothing about ones that don't exist yet.

So has_peers stays exactly as it is. It is correctly polarized (it means "ever paired", not "currently connected", and fails closed). The measured 4% figure is now in docs/features/storage-and-recovery/README.md so nobody re-litigates removing the guard for a 220 KB prize.

What this actually ships

A reproduced rollout wedge, fixed

applyChanges threw crsql - could not find the schema information for table X when a peer on an older build sent changes for a table this machine has since made local-only. The base table still exists, so the "unknown table" guard passes. That throw rolls back the entire BEGIN IMMEDIATE — and a batch is ordered by db_version, so one usage row took that peer's chats, lanes, and commits down with it. The peer's outbound cursor only advances on an ok:true ack, so it re-exported the same poisoned range forever; on restart the un-acked window was silently skipped instead.

Inbound changes for local-only tables are now skipped. This is latent for every future local-only move, not just this one — which is exactly why it lands here.

Paced deletes

Every existing prune is a single unbounded DELETE on a fully synchronous driver (node:sqlite DatabaseSync), so a large one stalls UI, IPC, and the sync pump for its full duration. pruneRowsInBatches does 2,000 rows per batch with a setImmediate yield between them, bounded at 200 batches so one sweep cannot run away.

Retention, and three tables deliberately left alone

Retained: ai_usage_log (90d), linear_sync_events, linear_workflow_run_events, worker_agent_cost_events, pack_events (30d). Registered in STORAGE_LEDGER + LEDGER_LABELS, so Settings shows a real policy chip rather than falling through to dev-shaped copy ("Ai usage log").

Not retained, each documented in code:

table why pruning it would be a bug
linear_ingress_events It is the webhook replay guard, not a log. persistRecord refuses to dispatch a delivery_id it has stored, and the cursor is explicitly reset on cursorExpired. Pruning it lets a backlog drain re-dispatch automations — re-running agent work and re-posting Linear comments.
cto_session_logs Two-way reconciled against an append-only .ade/cto/sessions.jsonl with no retention of its own. Every prune is undone by the next CTO read, and each cycle writes a tombstone plus a fresh set of clock rows — net negative on the exact metric this PR exists to improve.
worker_agent_runs A lifecycle table, not an event log. Keying on created_at would delete a still-pending run and orphan worker_agent_cost_events.run_id and automation_runs.worker_run_id.

worker_agent_cost_events keys on created_at, not occurred_at — the latter is event time and can be backdated, which would age a row out the moment it is written.

Timestamps are matched with like '____-__-__%'. On a text-affinity column an epoch number stores as its digits and '1767225600000' < '2026-…' is true as a string, and CRR repair appends default '' to NOT NULL text columns — so both a numeric and a defaulted timestamp would otherwise be born expired.

Mobile exclusion

All seven event logs added to MOBILE_CHANGESET_EXCLUDED_TABLES — verified zero Swift read paths (they exist only in DatabaseBootstrap.sql). This is an outbound filter only: it never touches CRR metadata, so unlike a local-only conversion it carries none of the apply hazard above.

Measured (copy of the real project DB, through ADE's own prune path)

before after
ai_usage_log rows 2,235 1,313
its __crsql_clock rows 20,115 12,739 (−37%)
CRR metadata 5.71 MB 5.38 MB
file after vacuum 28.66 MB 27.98 MB

The clock arithmetic confirms the model exactly: 922 rows × 9 non-PK columns = 8,298 clock rows removed, 922 tombstones added, net −7,376. Deletes on a CRR table are a net win, not "worse before better" — worth stating because the research assumed the opposite.

ai_usage_log stays synced — decision recorded

The local-only conversion was approved conditionally, paired with a slim synced daily aggregate so dailyLimit would stay account-wide. Scoping it against that condition, it falls back: the aggregate is not a small, low-risk change, and the forgone win is not worth loosening a cost control.

What syncs, what doesn't, what dailyLimit reads — unchanged by this PR:

behavior
ai_usage_log raw rows still replicate desktop↔desktop (and still never reach phones — already in MOBILE_CHANGESET_EXCLUDED_TABLES)
ai.budgets.<feature>.dailyLimit still reads raw rows: count(*) where feature = ? and success = 1 for today, summed across every machine on the account
account-wide cap preserved. Nothing in this PR makes it per-machine
what this PR does add 90-day retention on the table (safe on its own)

Why the aggregate didn't fit in one change. Three findings, in order of how much they cost:

  1. The specified shape wouldn't have served the cap. The aggregate was specified as spend + tokens per (day, provider). dailyLimit counts successful requests per feature — not spend, not tokens, not keyed on provider. Building it as specified would have preserved the wrong number.
  2. A shared counter is CRDT-unsafe. It has to be keyed (day, feature, site) and summed at read. cr-sqlite is last-writer-wins per column, so one machine upserting a shared (day, feature) total discards the other's — silently under-counting, which is the same loosening by a different route.
  3. It cannot roll out in one release. The moment raw rows stop replicating, a machine on the new build sees nothing from a peer still on the old build — inbound local-only changes are dropped by the applyChanges fix in this very PR — so it under-counts and overruns the account cap during exactly the window a rollout guarantees. There is no fail-stricter reading available: the data simply isn't there to be strict about. Safe sequencing is two releases (ship the aggregate while the table still replicates, then flip).

A fourth, smaller cost: a new CRR table must exist in every peer's schema or unknown_sync_table wedges apply — the same hazard this PR fixes.

Forgone win: ~1.2 MB of CRR metadata, ~2.8 MB off the file after vacuum, roughly 10% of the database. Recorded in code at both places someone would act on it (beside the exclusion set in kvDb.ts, and above countDailyUsage) and in docs/features/storage-and-recovery/README.md, with the corrected (day, feature, site) design and the two-release sequencing, so the planned cross-machine-usage initiative can pick it up properly rather than rediscover it.

Verification

  • /quality: 1 Blocker (reproduced), 1 High, 5 Medium, 2 Low. All fixed except the gated item above. The High and two Mediums were tables I had wrongly included in retention.
  • 186 tests pass across state/storage/settings-storage. Typecheck + lint clean on both apps. ade-cli sync suite: 551 pass.
  • Docs updated: storage-and-recovery README, crdt-model.md (the local-only skip is now a stated CRR invariant), sync README. validate-docs.mjs: 228 files.
  • iOS: no change required, verified rather than assumed. CLI: renders ledger steps generically, so the new steps appear automatically.

🤖 Generated with Claude Code

ADE   Open in ADE  ·  ade/t3-crsqlite-retention branch  ·  PR #1063

Summary by CodeRabbit

  • New Features

    • Added automatic retention cleanup for AI usage records and selected event logs.
    • Added storage maintenance entries and labels for these cleanup operations.
    • Maintenance now processes large cleanup tasks in bounded batches to keep the app responsive.
  • Bug Fixes

    • Prevented excluded local-only event data from causing sync insertion failures.
    • Improved handling and reporting of database maintenance errors.
  • Documentation

    • Clarified that AI usage limits are account-wide rather than device-specific.

Greptile Summary

The PR adds paced retention for selected replicated event logs, excludes unread event tables from mobile changesets, and safely ignores inbound changes for tables converted to local-only storage. It also updates storage policy presentation and documents the CRR rollout and compaction constraints.

  • Deletes eligible event-log rows in bounded batches with event-loop yields.
  • Propagates event-log pruning failures into storage maintenance reports.
  • Prevents local-only tables from wedging mixed-version sync rollouts.
  • Adds corresponding storage-ledger declarations, tests, and documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported event-log pruning error is now propagated and recorded as a failed maintenance action, while the AI usage-log pruning path was removed.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/state/dbMaintenanceApi.ts Adds the bounded, event-loop-paced deletion helper and event-log retention API contract.
apps/desktop/src/main/services/state/kvDb.ts Implements event-log retention with error propagation and skips inbound CRR changes for local-only tables.
apps/desktop/src/main/services/storage/storageInsightsService.ts Awaits asynchronous database maintenance steps and records rejected event-log pruning as a failure.
apps/ade-cli/src/services/sync/syncHostService.ts Excludes event tables without mobile read paths from outbound mobile changesets.
apps/desktop/src/main/services/state/kvDb.test.ts Covers retained and exempt tables, malformed-table failures, and invalid timestamp representations.
apps/desktop/src/main/services/state/dbMaintenanceApi.test.ts Verifies delete batching, yielding, termination, and maximum-work bounds.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Storage maintenance sweep] --> B[Prune retained event logs]
  B --> C[Delete up to 2,000 eligible rows]
  C --> D{Batch full?}
  D -->|Yes| E[Yield to event loop]
  E --> C
  D -->|No| F[Record successful result]
  C -->|SQLite error| G[Propagate error]
  G --> H[Record failed maintenance action]
Loading

Reviews (8): Last reviewed commit: "review: drop ai_usage_log retention — it..." | Re-trigger Greptile

Context used:

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Aug 10, 2026 6:47am

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds bounded pruning for AI usage and selected event logs, integrates the operations with storage maintenance and the journal, skips excluded incoming tables, and expands mobile outbound changeset exclusions.

Changes

Retention API and database maintenance

Layer / File(s) Summary
Retention pruning API and tests
apps/desktop/src/main/services/state/dbMaintenanceApi.ts, apps/desktop/src/main/services/state/dbMaintenanceApi.test.ts
Adds retention constants, bounded rowid batch deletion, asynchronous pruning methods, and coverage for batch limits, yields, and termination.
KV database retention and sync handling
apps/desktop/src/main/services/state/kvDb.ts, apps/desktop/src/main/services/state/kvDb.test.ts, apps/desktop/src/main/services/ai/aiIntegrationService.ts
Prunes stale AI usage and selected event-log rows, preserves unsupported or non-text timestamp cases, propagates maintenance failures, skips excluded incoming tables, and documents account-wide usage counting.
Maintenance orchestration and storage journal
apps/desktop/src/main/services/storage/storageInsightsService.ts, apps/desktop/src/main/services/storage/storageLedger.ts, apps/desktop/src/renderer/components/settings/storage/storageView.ts
Awaits asynchronous maintenance callbacks, adds optional pruning steps, records retention policies, and adds journal labels.
Mobile outbound event exclusions
apps/ade-cli/src/services/sync/syncHostService.ts
Excludes additional event-log tables from mobile outbound changesets.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • arul28/ADE#853: Extends database retention and maintenance behavior in the same services.
  • arul28/ADE#875: Modifies mobile sync exclusions for designated event-log tables.
  • arul28/ADE#1056: Expands mobile changeset exclusions in syncHostService.ts.

Suggested labels: desktop, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: retention, paced deletes, and the reproduced CRR rollout issue.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/t3-crsqlite-retention

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread apps/desktop/src/main/services/state/kvDb.ts Outdated
@arul28
arul28 changed the base branch from ade/t3-perf-hygiene-d09630c4 to main August 10, 2026 04:57
@arul28
arul28 force-pushed the ade/t3-crsqlite-retention branch from 0d5f32f to f81574e Compare August 10, 2026 05:01
@arul28

arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f81574e8e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4049 to +4054
} catch (error) {
logger.warn("db.maintenance_failed", {
action,
error: error instanceof Error ? error.message : String(error),
});
return unsupportedMaintenanceResult();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate retention failures to the storage doctor

When a batched prune encounters an operational SQLite error—such as a lock, disk failure, or malformed table—the new async wrapper converts it to skippedReason: "unsupported" instead of rethrowing. runStep therefore records no error, so the maintenance journal, analytics, and Settings UI report a completed/tidy run; for pruneEventLogs, earlier table batches may already have committed while their affected-row count is also discarded. Preserve genuine unsupported results for missing tables, but let operational failures reach runStep so the partial failure is reported.

AGENTS.md reference: AGENTS.md:L61-L62

Useful? React with 👍 / 👎.

@arul28

arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65f67df360

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

pruneAiUsageLog: () => runMaintenanceSafelyAsync("pruneAiUsageLog", async () => {
if (!rawHasTable(db, "ai_usage_log")) return unsupportedMaintenanceResult();
const cutoff = new Date(
Date.now() - AI_USAGE_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1_000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve usage aggregates before pruning raw rows

For projects with more than 90 days of AI activity, the storage doctor now irreversibly deletes the only database rows used by collectAdeDatabaseUsageStats to calculate tracked tokens, calls, duration, provider/model totals, features, and active dates. The Stats UI still offers both Year and All ranges—and describes the latter as “lifetime”—so after the first sweep those ranges silently report only the retained 90-day subset. Preserve durable daily aggregates before deleting these rows, or constrain and relabel the affected ranges so the displayed period remains accurate.

AGENTS.md reference: AGENTS.md:L61-L62

Useful? React with 👍 / 👎.

@arul28
arul28 force-pushed the ade/t3-crsqlite-retention branch 2 times, most recently from b92f142 to 5cbf440 Compare August 10, 2026 06:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/ade-cli/src/services/sync/syncHostService.ts (1)

316-339: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add named mobile sync regression coverage for the new outbound exclusions.

The new table names are in the mobile outbound filter and also affect the mobile replica reseed cache, but the current test only covers an already-empty filtered batch. Add cases that include mobile rows for these tables and unrelated mobile rows so the omitting behavior and still-synced table behavior are both asserted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/services/sync/syncHostService.ts` around lines 316 - 339,
Add named mobile sync regression tests covering all newly excluded tables in the
outbound filter and mobile replica reseed cache. Include batches containing rows
from each excluded table alongside rows from an allowed table, then assert
excluded rows are omitted while unrelated allowed rows still synchronize; retain
coverage for the empty filtered-batch case.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/ade-cli/src/services/sync/syncHostService.ts`:
- Around line 316-339: Add named mobile sync regression tests covering all newly
excluded tables in the outbound filter and mobile replica reseed cache. Include
batches containing rows from each excluded table alongside rows from an allowed
table, then assert excluded rows are omitted while unrelated allowed rows still
synchronize; retain coverage for the empty filtered-batch case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ae1f4fed-f89d-449f-a854-44d5f0e647ef

📥 Commits

Reviewing files that changed from the base of the PR and between 00eca20 and 5cbf440.

⛔ Files ignored due to path filters (3)
  • docs/features/storage-and-recovery/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/crdt-model.md is excluded by !docs/**
📒 Files selected for processing (9)
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.ts
  • apps/desktop/src/main/services/state/dbMaintenanceApi.test.ts
  • apps/desktop/src/main/services/state/dbMaintenanceApi.ts
  • apps/desktop/src/main/services/state/kvDb.test.ts
  • apps/desktop/src/main/services/state/kvDb.ts
  • apps/desktop/src/main/services/storage/storageInsightsService.ts
  • apps/desktop/src/main/services/storage/storageLedger.ts
  • apps/desktop/src/renderer/components/settings/storage/storageView.ts

@arul28

arul28 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Good catch — confirmed and fixed by removing the retention rather than working around it.

ActivityModule.tsx defaults to the all preset and renders "${formatTokens(stats.summary.totalTokens)} lifetime tokens", computed from ai_usage_log via usageStatsStore. A 90-day prune would have made that label quietly false after the first sweep, and the Year range with it.

ai_usage_log now gets neither the CRR exclusion nor retention here. Both are blocked on the same missing piece — a durable daily aggregate — and whoever builds it can add retention in the same change, since the aggregate is precisely what lets a lifetime total survive pruning the raw rows. Recorded in code beside the exclusion set and in docs/features/storage-and-recovery/README.md.

Event-log retention stands: none of those four tables feeds a lifetime-labelled surface.

arul28 and others added 4 commits August 10, 2026 02:44
The research framed this as "make cr-sqlite tombstone compaction run on
paired machines." Investigating it produced the opposite conclusion, and
the measurements are why.

**Tombstones are under 4% of the metadata.** On the live 28.7 MB project
DB, `__crsql_clock`/`__crsql_pks` is 5.7 MB (20%) — but ~1,950 of ~53,000
clock rows are tombstones. `ai_usage_log`, the single worst table at 0.93 MB
of metadata for 0.27 MB of data, has **exactly zero**. The bloat is
structural — one clock row per live row per non-PK column — not garbage.
A perfect compaction would recover ~220 KB of 28.7 MB.

**And it cannot be made safe.** Reproduced against the vendored crsqlite
extension: `rebuildCrrTableWithBackfill` drops and recreates the clock
table, resetting `col_version` 3→1 and destroying `cl=-1` tombstones. A
peer that has not acked then rejects the host's push as older and diverges
permanently, and a row the host deleted is resurrected by the peer pushing
it back. Silently, with no error and no resync. A watermark would not fix
it either: no durable host-owned per-peer ack version exists (the number
lives on the client), and peer seeding replays `crsql_changes` from version
0, so any clock row removed is absent from every future seed.

**The `has_peers` guard is therefore correct and is left exactly as it is.**
The measured 4% figure is now in the docs so nobody re-litigates it.

What this actually ships:

- **A reproduced rollout wedge, fixed.** `applyChanges` threw
  `could not find the schema information` for a table that is local-only
  here but still a CRR on a peer running an older build. That rolls back
  the whole `BEGIN IMMEDIATE` — and a batch is ordered by db_version, so
  one usage row took that peer's chats, lanes, and commits with it. The
  peer's outbound cursor only advances on an ok ack, so it re-exported the
  same poisoned range forever. Inbound changes for local-only tables are
  now skipped. This latent hazard fires on any future local-only move.
- **Paced deletes.** Every existing prune is one unbounded DELETE on a
  fully synchronous driver. `pruneRowsInBatches` does 2,000 rows per batch
  with a `setImmediate` yield between them, bounded at 200 batches.
- **Retention** for `ai_usage_log` (90d) and four event logs (30d), wired
  into the doctor sweep and registered in the storage ledger so Settings
  shows a real policy chip instead of "Ai usage log".
- **Mobile exclusion** for all seven event logs — an outbound filter only,
  with zero Swift read paths (they exist solely in DatabaseBootstrap.sql).

Three of the seven proposed tables are deliberately NOT pruned, each
documented: `linear_ingress_events` is the webhook replay guard (pruning it
re-dispatches automations — re-running agent work and re-posting Linear
comments), `cto_session_logs` is two-way reconciled from an append-only
jsonl so a prune is undone by the next read while writing more CRR
metadata, and `worker_agent_runs` is a lifecycle table whose prune would
delete pending runs and orphan live references.

Measured on a copy of the real DB: ai_usage_log 2,235 -> 1,313 rows, its
clock rows 20,115 -> 12,739 (-37%), 688 KB off the file after vacuum. The
clock arithmetic confirms the model exactly — 922 rows x 9 columns = 8,298
clock rows removed, 922 tombstones added, net -7,376.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scoping the "make it local-only, serve the budget from a slim synced
aggregate" plan turned up three things that make it larger than it looks,
so the raw table keeps replicating and only retention ships.

The aggregate as originally specified would not have worked:
`ai.budgets.<feature>.dailyLimit` is enforced by counting *successful
requests per feature* for today, not spend or tokens, and not per provider.

It also has to be keyed (day, feature, site) and summed at read. A shared
(day, feature) counter cannot work — cr-sqlite is last-writer-wins per
column, so one machine's upsert discards another's count, which is the same
silent under-count by a different route.

And it cannot ship in one release. The moment raw rows stop replicating, a
machine on the new build sees nothing from a peer still on the old one — the
inbound local-only skip added in the previous commit drops those changes —
so it under-counts and overruns the account cap during exactly the window a
rollout guarantees. Safe sequencing is two releases.

Recorded at both places someone would act on it: beside the exclusion set in
kvDb, and above countDailyUsage, whose row count is what makes the cap
account-wide in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`runMaintenanceSafelyAsync` swallowed any error into an `unsupported`
result, copying the synchronous wrapper's shape. But "unsupported" means
"this handle does not implement the step" — reporting a locked database or
a malformed table that way makes the maintenance journal, the analytics
event, and the Settings run summary all show a tidy completed sweep, and
for the multi-table `pruneEventLogs` it hides a partial sweep entirely.

`runStep` already catches and records `error` per action, so rethrowing is
what surfaces it, and a failing step still does not abort the rest of the
run. Rows deleted before the throw stay deleted and the next sweep
continues from there.

Genuinely-unsupported handles still report `unsupported`; the test pins
both halves.

Raised independently by Greptile and Codex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ActivityModule` defaults to the All range and renders a "lifetime tokens"
total computed from `ai_usage_log`, and the Stats surface offers a Year
range too. A 90-day prune would have silently turned both into
trailing-90-day figures after the first doctor sweep — the same class of
quiet behavior loss as the per-machine budget, and equally invisible.

So `ai_usage_log` now gets neither the CRR exclusion nor retention in this
PR. Both wait on the durable daily aggregate; whoever builds it can add
retention safely in the same change, because the aggregate is what makes a
lifetime total survive pruning the raw rows.

Event-log retention is unaffected — none of those four tables feeds a
lifetime-labelled surface.

Raised by Codex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@arul28
arul28 force-pushed the ade/t3-crsqlite-retention branch from bf8209b to ca1d97e Compare August 10, 2026 06:47
@arul28
arul28 merged commit 6df1bb6 into main Aug 10, 2026
37 checks passed
@arul28
arul28 deleted the ade/t3-crsqlite-retention branch August 10, 2026 07:01
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