Skip to content

fix(memory): fall back to default retention on negative keep_days - #8246

Merged
iamwhatever merged 1 commit into
kirodotdev:mainfrom
soroush5:fix/prune-negative-keep-days
Sep 4, 2026
Merged

fix(memory): fall back to default retention on negative keep_days#8246
iamwhatever merged 1 commit into
kirodotdev:mainfrom
soroush5:fix/prune-negative-keep-days

Conversation

@soroush5

@soroush5 soroush5 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

A negative history_max_days in config.json (hand edit, bad import, corruption) makes the periodic heartbeat wipe the ENTIRE daily memory history, silently. Verified: seeding 3 daily files then calling prune_history(-1) deletes all 3. cutoff = today - timedelta(days=-1) lands tomorrow, so every file counts as "older" and gets unlinked with only an info log.

Why it matters

Daily history is unrecoverable user memory. The dashboard path already clamps retention to min 7, but the file-config path passes the raw value through (config/loader.py .get passthrough, no clamp in config/sections.py) straight into a periodic, unattended deleter. One bad number = total silent memory loss.

What changed (motivation → approach → change)

The destructive sink is the right place for the guard: prune_history now treats a negative (or None) keep_days as invalid, logs a warning, and falls back to the 365-day default instead of deleting everything. Zero keeps its current "drop everything before today" meaning — only the can-never-be-intentional negatives are caught.

Tests

  • New test_negative_keep_days_falls_back_to_default: seeds an old + recent file, asserts -1 deletes only the old one (red without the fix — it wiped both, green with it).
  • Full test_memory_cov80.py green (21 passed).

Manual verification

N/A — unit coverage sufficient: the wipe and the fallback are both locked by automated tests against a tmp workspace.

Fixes #8245

Pattern harvest

Rule candidate: numeric config values flowing into destructive retention/deletion paths need range validation at the sink, since file-config paths bypass dashboard clamps.

@soroush5
soroush5 requested a review from a team as a code owner September 3, 2026 18:57
@soroush5
soroush5 requested a review from CrysisDeu September 3, 2026 18:57
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🟡 CONCERNS

Design-level review of 0fc7496a7dccd39fd8e4d88dce78858aa6e21de5 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All checks done — the shipped code is sound, but the description describes a different implementation than the diff contains.

Design-Verdict: CONCERNS

Diff and description diverge: the PR describes a sink guard in prune_history, but ships a load-time sanitize in config/loader.py instead.

Watch

  • Phantom description: "What changed" claims "prune_history now treats a negative (or None) keep_days as invalid, logs a warning, and falls back" and "Tests" claims a new test_negative_keep_days_falls_back_to_default in test_memory_cov80.py — neither exists in the diff. What shipped is _safe_nonnegative_int(...) at config load plus a test_config_loader.py parametrize. The shipped shape does solve the stated harm (heartbeat is the sole prune_history caller and reads KiroCrewConfig.load().memory.history_max_days, heartbeat.py:200) and matches the loader's existing convention for sibling knobs — but the description must be rewritten to match, since it currently argues for the opposite placement ("the destructive sink is the right place") and a reviewer approving the described change is approving code that isn't there.

Suggestions

  • If the sink-guard rationale in the description is genuinely held, add the cheap floor inside prune_history too (memory.py:484) so a future direct caller can't re-open the wipe; otherwise drop that rationale from the description.

[DESIGN-REVIEWED] 0fc7496

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 0fc7496a7dccd39fd8e4d88dce78858aa6e21de5 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 0fc7496

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of 0fc7496a7dccd39fd8e4d88dce78858aa6e21de5 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All checks complete. Here is the review.

First-Principles-Verdict: CONCERNS

The diff fixes the wipe at config load — correctly — but the description narrates a different fix (a sink guard in prune_history with a warning log) that ships nowhere.

What this change ships

Intent: stop a hand-edited negative history_max_days from silently deleting all daily memory history (#8245) — a FIX.

  1. Negative history_max_days in config.json now loads as the 365 default — justified, cause-level.
  2. Non-integer values (strings, bools, non-integral floats) now also fall back to 365 — undeclared; description says "only the can-never-be-intentional negatives are caught".
  3. The described warning log on a bad value — claimed, not shipped: the fallback is silent.

Watch

  • Description/diff mismatch: "prune_history now treats a negative (or None) keep_days as invalid, logs a warning, and falls back" — but prune_history (src/kiro_crew/memory.py:484) is untouched; the only hunk is in config/loader.py. Likewise the claimed test test_negative_keep_days_falls_back_to_default in test_memory_cov80.py ("red without the fix") does not exist in the diff; the shipped test is test_history_max_days_sanitized_at_load in test_config_loader.py. The shipped fix is equivalent for the one caller (grep prune_history(: 1 call site, heartbeat.py:204, which reads the loaded config) and matches the repo's established convention — 6 existing _safe_nonnegative_int load-site uses plus _archive_retention_days and telemetry's retention_days clamp all sanitize retention at load. Update the description to match what ships, including that the fallback is silent.

No subtractions: the change reuses the existing _safe_nonnegative_int (config/sections.py:247) rather than adding a mechanism, and its documented negative→default semantics are exactly the behavior needed.

[FIRST-PRINCIPLES-REVIEWED] 0fc7496

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 0fc7496a7dccd39fd8e4d88dce78858aa6e21de5 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 0fc7496

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Sep 4, 2026
@soroush5

soroush5 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on _safe_nonnegative_int — you're right, that's the proper layer for this. I've moved the fix to the loader (history_max_days now goes through the existing helper), and kept the small guard in prune_history as last-line defense for direct callers. Thanks for the review!

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 4, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #8248 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #8248: CONTINUE_DEVELOPMENT. Sibling hardening PR, not a duplicate: disjoint symbols and hunks, verified clean merge of the two heads, so both can land independently in either order. Files: src/kiro_crew/memory.py. The two independent directions used different labels; the matrix conservatively retains OVERLAPPING for coordination.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

A hand-edited negative history_max_days reached prune_history raw via the heartbeat and wiped all daily memory. Coerce it at the loader with _safe_nonnegative_int, the existing mechanism for exactly this class. Includes a loader-level regression test.

Fixes kirodotdev#8245
@soroush5
soroush5 force-pushed the fix/prune-negative-keep-days branch from 7c79844 to 0fc7496 Compare September 4, 2026 16:55
@soroush5

soroush5 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressing the First Principles BLOCK — verified each claim and reworked accordingly:

  • Confirmed _safe_nonnegative_int (sections.py:247) has exactly the needed semantics, the loader line was the raw outlier, and heartbeat is the sole non-test caller of prune_history. The sink guard was the wrong layer.
  • Dropped the memory.py hunk entirely (including the None branch and the per-heartbeat warning). The loader line is now the single guard; squashed to 1 commit for the 2-commit gate.
  • Regression test moved to the loader level (test_history_max_days_sanitized_at_load: -5→365, 0→0, 30→30), verified end-to-end through KiroCrewConfig.load().
  • One honest disclosure: -1 stays persisted in the file and the dashboard GET will still display it; behavior is 365. Normalizing the file on load felt more invasive than this fix warrants.
  • Left the 5 sibling passthroughs alone to keep this PR minimal; happy to follow up separately if wanted.

@soroush5

soroush5 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

On the Design CONCERNS (reviewed the old commit; the PR is now loader-only, 1 commit):

  • On -1 meaning unlimited: I checked — this repo has no such convention for this knob. Defaults are 365 in all three places, nothing treats -1 as a sentinel, and _safe_nonnegative_int's docstring explicitly rejects mapping negatives to 0 because 0 is meaningful (disables a sweep). Note prune_history(0) would not skip either — it would delete everything older than today — so 0-as-recovery needs new skip semantics, i.e. a feature, not this fix. -1 → 365 follows the repo's own documented rule.
  • Load-level clamp: done, that is now the entire fix.
  • On the four 365s: down to the signature default, the MemoryConfig field default (the owner), and one loader expression where both literals provably mean the same thing (missing ≡ invalid → default). Happy to hoist to a named constant if you prefer, just say so.

@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Sep 4, 2026
@soroush5

soroush5 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Noted on the relationship audit — no overlapping change on my side, nothing to coordinate.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 4, 2026

@iamwhatever iamwhatever left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: routes memory.history_max_days through the existing _safe_nonnegative_int sanitizer at load so a hand-edited negative value falls back to the 365-day default instead of reaching prune_history raw (#8245); one production line plus a parametrized regression test. CodeQL is not applicable on this fork PR (default-setup emits no check-run); SAST coverage is Semgrep only, latest run success with 0 annotations.

@iamwhatever
iamwhatever merged commit 82795b9 into kirodotdev:main Sep 4, 2026
67 checks passed
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: negative history_max_days makes the heartbeat wipe all daily memory history

3 participants