Skip to content

fix(tracking): honor tracking.history_days instead of the hardcoded default - #3440

Open
iliaal wants to merge 2 commits into
rtk-ai:developfrom
iliaal:fix/tracking-history-days
Open

fix(tracking): honor tracking.history_days instead of the hardcoded default#3440
iliaal wants to merge 2 commits into
rtk-ai:developfrom
iliaal:fix/tracking-history-days

Conversation

@iliaal

@iliaal iliaal commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

tracking.history_days has no effect. Tracker::cleanup_old computes its cutoff from the DEFAULT_HISTORY_DAYS constant, so the retention window stays at 90 days no matter what the config says.

The setting looks live from every other angle. It's a field on TrackingConfig, it gets a default, Config::save writes it into the generated config.toml, and src/core/README.md documents it. Nothing reads it.

I found this with a 1.9 GB history.db (3.4M commands rows, 3.2M parse_failures) and no way to shrink the window.

Fix

Tracker resolves the window once at construction and stores it on the struct. cleanup_old reads that field.

Config::load() stays out of cleanup_old deliberately: it runs on every record() and record_parse_failure(), so loading the config there would put a file read in the per-command path.

A non-positive value falls back to the default rather than being honored. A typo'd history_days = 0 would otherwise delete the whole history on the next write. That fallback also covers an unparseable config, including a [tracking] section that omits history_days, since the field has no serde default and a partial section fails the whole parse.

Tests

test_resolve_history_days covers the clamping. Two cleanup_old tests assert both directions against a backdated row: pruned at a 3 day window, retained at 30.

Both directions are needed. A hardcoded 90 still passes "retained within window", so only "pruned beyond window" catches the regression. I checked by reverting the cutoff line on its own, and that test fails (left: 1, right: 0) while its sibling stays green.

That asymmetry is probably why the bug lasted. Every existing config test uses the default value, and a dead knob is invisible to any test that never sets it to something else.

Verification

cargo fmt --check, cargo clippy --all-targets --all-features -D warnings, and cargo test --bin rtk core::tracking are clean on this branch.

cargo test --all fails two tests, unattestable_passthrough::test_plain_command_still_rewrites and test_fd_dup_redirect_still_rewrites. They fail the same way on unmodified develop (checked in a detached worktree at 3044911), so they're unrelated to this change. They read the host's real settings.json, which is why the result depends on the machine.

End to end on a copy of the real database, with history_days = 30: 3,087,798 rows older than the window pruned on the next write, 3,369,148 commands down to 280,984.

Second commit: return the pruned space

Fixing the setting on its own doesn't shrink anything. There's no auto_vacuum on these databases, so a delete only marks pages reusable. Dropping 90 to 30 pruned 3.09M rows and left the file at 1.9 GB with 445,466 free pages, which makes the setting look broken even once it works.

So cleanup_old now counts what it deleted and, when anything went, vacuums if enough of the file is free pages.

VACUUM can't run unconditionally here. cleanup_old fires once per tracked command, and VACUUM rewrites the whole database under an exclusive lock, which would blow the startup budget and stall concurrent rtk processes sharing the DB. The free-page ratio gate keeps it dormant in steady state, where inserts reuse freed pages, and lets it fire on the bulk deletes that strand space. Thresholds are 4096 free pages (~16 MB at the default page size) and 25% of the file, both named constants.

Measured on a 196 MB database:

pages freelist size
20 consecutive tracked commands 50,067 unchanged 1 to 4 196 MB
after injecting 300k out-of-window rows 81,797 0 320 MB
next tracked command 50,053 0 196 MB

The gate stays shut across normal use and opens on the bulk prune. quick_check ok afterward.

VACUUM failure is ignored, same as the existing WAL pragma. It needs scratch space near the size of the live data and can't run inside a transaction, and neither is a reason to fail someone's command.

If you'd rather keep this PR to the config fix alone, say so and I'll split the second commit out.

iliaal added 2 commits August 4, 2026 20:39
…efault

`history_days` was declared on TrackingConfig, given a default, written into
the generated config.toml and documented in src/core/README.md — but never
read. `cleanup_old` used the DEFAULT_HISTORY_DAYS constant directly, so the
retention window was fixed at 90 days and the setting was inert.

Resolve the window once at construction and store it on Tracker rather than
loading the config inside cleanup_old, which runs on every record() and
record_parse_failure() and must not touch the filesystem per call.

A non-positive value falls back to the default instead of being honored: a
typo'd 0 would otherwise delete the entire history on the next write. The
same fallback covers an unparseable config, including a [tracking] section
that omits history_days — the field has no serde default, so a partial
section fails the whole parse.

The pruning test is asserted in both directions on purpose. A hardcoded 90
still passes the "retained within window" half, so only the "pruned beyond
window" half catches the regression, and only the pair shows the field is
what drives the cutoff.
These databases have no auto_vacuum, so retention only marks pages reusable
and the file never shrinks. Shortening history_days deletes millions of rows
and leaves the file at its old size indefinitely: on one box, dropping 90 to
30 pruned 3.09M rows and left 1.9GB on disk with 445,466 free pages.

cleanup_old now counts what it deleted and, when anything went, vacuums if
enough of the file is free pages.

VACUUM cannot run unconditionally here. cleanup_old runs once per tracked
command, and VACUUM rewrites the whole database under an exclusive lock,
which would both blow the startup budget and stall concurrent rtk processes
sharing the DB. Gating on the free-page ratio keeps it dormant in steady
state, where inserts reuse freed pages, and lets it fire on the bulk deletes
that actually strand space.

Measured on a 196MB database: 20 consecutive tracked commands leave the
freelist at 1-4 pages of 50,067, so the gate stays closed and page_count is
unchanged. Injecting 300k out-of-window rows takes it to 320MB, and the next
tracked command prunes and reclaims back to 196MB with quick_check ok.

Failure is ignored. VACUUM needs scratch space near the size of the live
data and cannot run inside a transaction; neither is a reason to fail a
user's command.
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