Skip to content

fix(autonomy): disable evidence staleness rollback by default - #315

Merged
OBenner merged 1 commit into
developfrom
fix/disable-evidence-staleness-rollback
Jun 13, 2026
Merged

fix(autonomy): disable evidence staleness rollback by default#315
OBenner merged 1 commit into
developfrom
fix/disable-evidence-staleness-rollback

Conversation

@OBenner

@OBenner OBenner commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Problem

A promoted direct-API provider lost its ready status once its recorded evidence aged past max_history_age_days (7) — even with a perfect, stable pass history — forcing a re-probe just to stay promoted. The green readiness badges we just lit would revert after a week of quiet. The ask: a proven provider should stay ready until evidence actually regresses, not roll back for going idle.

Fix

Make the freshness window opt-in (disabled by default):

  • DEFAULT_MAX_HISTORY_AGE_DAYS = 00 (or any non-positive value) disables expiry. AutonomyPolicy.max_history_age now returns timedelta | None; None means "freshness is not a promotion requirement."
  • All three consumers treat None as always-fresh — no provider_history_stale warning, no fresh_provider_history blocker, max_history_age_seconds rendered as null / max_history_age=disabled (no expiry):
    • CLI readiness (provider_smoke_commands._provider_readiness_history_freshness_complete)
    • runtime-modes readiness (runtime_commands._runtime_provider_readiness_history_freshness_complete)
    • runtime promotion gate (direct_api_autonomy._fresh_history_complete) — this one would otherwise TypeError on None, so it's the most important.
  • Still fully configurable. The strict preset keeps a 3-day window; AUTO_CODE_AUTONOMY_<PROVIDER>_MAX_HISTORY_AGE_DAYS=<N> re-enables an N-day window per provider. lax drops its 30-day seed (inherits the no-expiry default).

Tests

Each stale-history test is split into:

  1. a default case proving a year-old / 30-day-old history stays ready (no rollback), and
  2. a window-enabled case (env override or monkeypatched module constant) proving the freshness mechanism still blocks when explicitly turned on.

tests/test_autonomy_policy.py, tests/test_provider_smoke_command.py, tests/test_runtime_modes_command.py, tests/test_agent_runtime.py356 passed locally.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Evidence freshness windows can now be disabled, allowing provider history to persist indefinitely without automatic expiry.
  • Changes

    • Provider autonomy readiness assessments now disable freshness expiry by default; stale history no longer impacts readiness unless a freshness window is explicitly configured.
    • Updated readiness evaluation logic to properly handle disabled freshness windows.

A promoted direct-API provider previously lost its "ready" status once
its recorded evidence aged past max_history_age_days (7) — even with a
perfect pass history — forcing a re-probe to stay promoted. Operators
want a proven provider to STAY ready until evidence actually regresses,
not to silently roll back for going quiet.

Make the freshness window opt-in:

- DEFAULT_MAX_HISTORY_AGE_DAYS = 0, where 0 (or any non-positive value)
  DISABLES expiry. AutonomyPolicy.max_history_age now returns
  timedelta | None; None means "freshness is not a promotion requirement".
- Both readiness paths (CLI --autonomy-readiness via provider_smoke_commands
  and runtime_commands) and the runtime promotion gate
  (direct_api_autonomy._fresh_history_complete) treat None as always-fresh:
  no provider_history_stale warning, no fresh_provider_history blocker,
  max_history_age_seconds rendered as null / "disabled (no expiry)".
- The freshness check stays fully configurable: the `strict` preset keeps a
  3-day window, and AUTO_CODE_AUTONOMY_<PROVIDER>_MAX_HISTORY_AGE_DAYS=<N>
  re-enables an N-day window per provider. `lax` no longer needs its 30-day
  seed (it inherits the no-expiry default).

Tests: split each stale-history test into a default (no rollback) case and
a window-enabled case (env override / monkeypatched module constant) so the
freshness mechanism stays covered when explicitly turned on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleg Miagkov <mrobenner@gmail.com>
@github-actions github-actions Bot added area/backend bug Something isn't working size/M labels Jun 8, 2026
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR disables provider autonomy readiness history freshness expiry by default. Evidence no longer expires after 7 days; instead, staleness checks are bypassed entirely unless explicitly configured. All consumers of the autonomy policy (agent runtime gate, provider readiness evaluation, runtime CLI) now handle None max-history-age values, returning success immediately when freshness is disabled. Test expectations updated throughout to reflect the new default.

Changes

Disable evidence freshness expiry by default

Layer / File(s) Summary
Core policy: disable freshness expiry by default
apps/backend/core/autonomy_policy.py
DEFAULT_MAX_HISTORY_AGE_DAYS changes from 7 to 0, and AutonomyPolicy.max_history_age now returns None when freshness is disabled; lax preset no longer overrides the disabled window; documentation reflects new default.
Agent runtime autonomy gate: handle disabled freshness
apps/backend/agents/runtime/direct_api_autonomy.py
_fresh_history_complete assigns policy.max_history_age to local max_age and returns True immediately when None, preventing staleness rollbacks; cutoff comparison uses the local variable.
Provider readiness: propagate disabled freshness through smoke command
apps/backend/cli/provider_smoke_commands.py
PROVIDER_AUTONOMOUS_READINESS_MAX_HISTORY_AGE_SECONDS becomes None when freshness is disabled; max_history_age_seconds computation handles None gracefully; _provider_readiness_history_freshness_complete returns True when max age is None.
Runtime CLI: display and check disabled freshness
apps/backend/cli/runtime_commands.py
_runtime_provider_readiness_history_freshness_complete early-returns True when PROVIDER_AUTONOMOUS_READINESS_MAX_HISTORY_AGE_SECONDS is None; format_autonomy_readiness_text displays "disabled (no expiry)" for non-positive max_history_age_days.
Test: autonomy policy defaults and disabled freshness
tests/test_autonomy_policy.py
Default-case test now asserts max_history_age is None and max_history_age_days == 0; new test covers explicit 0 override with strict preset; lax preset tests no longer expect freshness window override; policy-file override test verifies lax does not seed max_history_age_days.
Test: agent runtime autonomy gate with disabled freshness
tests/test_agent_runtime.py
Comments document that freshness expiry is disabled by default; test setup explicitly sets AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS to 7 so stale 8-day-old runs are rejected before loosening the constraint.
Test: provider smoke readiness with disabled/enabled freshness
tests/test_provider_smoke_command.py
max_history_age_seconds expectations changed from 604800 to None across warming-up, autonomous-candidate, stability, and live-fault scenarios; new helper functions _stable_readiness_result and _stable_readiness_history; stale-history coverage split into two tests verifying ready state when expiry is disabled and warning/rollback when freshness window is re-enabled.
Test: runtime modes with disabled/enabled freshness windows
tests/test_runtime_modes_command.py
max_history_age_seconds expectations changed from 604800 to None in readiness requirements; text assertions exclude "max age" for default disabled settings; test_runtime_modes_policy_gate_warns_on_stale_provider_history replaced by two tests—one verifying full readiness with disabled expiry, another re-enabling freshness window and verifying stale-history warnings; stability/live-fault and google-provider expectations updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • OBenner/Auto-Coding#274: Modifies direct-API autonomy gate logic in direct_api_autonomy.py for autonomy enabling/bypassing, while this PR changes how freshness completeness is evaluated in the same gate.
  • OBenner/Auto-Coding#267: Both PRs modify history-freshness logic around policy.max_history_age and propagate policy-derived max-age handling through the autonomy gating path.

Suggested labels

area/backend, size/M, bug

Poem

🐰 Evidence now dwells forever long,
No expiry window to prove it wrong,
Seven days of staleness fade away,
Fresh by default, come what may,
Tests rejoice in the new way! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(autonomy): disable evidence staleness rollback by default' directly and clearly describes the main change: disabling evidence staleness checks by default in the autonomy policy system.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/disable-evidence-staleness-rollback

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 and usage tips.

@sonarqubecloud

sonarqubecloud Bot commented Jun 8, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@apps/backend/core/autonomy_policy.py`:
- Around line 84-93: The parser currently clamps negative values for the
max_history_age_days knob which conflicts with max_history_age()'s contract that
non-positive values (<= 0) disable freshness; locate the code that
parses/coerces max_history_age_days (search for the parsing block around the
knob name, e.g., the function or logic that handles "max_history_age_days" in
the config parsing code) and stop dropping or clamping negative integers—accept
integers as provided, only enforce type/sanity (raise on non-numeric types) and
preserve negative values (e.g., -1) so max_history_age() can return None; update
any docstring/comments near the parser to reflect that non-positive values
disable freshness.

In `@tests/test_runtime_modes_command.py`:
- Around line 1205-1243: The test
test_runtime_modes_policy_gate_keeps_ready_when_expiry_disabled depends on
ambient environment variables (e.g.
AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS) and must explicitly isolate
them; before calling handle_runtime_modes_command in that test, use the pytest
monkeypatch fixture to remove or unset the relevant env keys
(monkeypatch.delenv("AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS",
raising=False) and any other default max-age envs your code reads) so the test
exercises the code path where max_history_age is None and is deterministic
across CI/shell environments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbbdb20b-dd87-492d-a771-c402cff8a27d

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5cd91 and 2703f0a.

📒 Files selected for processing (8)
  • apps/backend/agents/runtime/direct_api_autonomy.py
  • apps/backend/cli/provider_smoke_commands.py
  • apps/backend/cli/runtime_commands.py
  • apps/backend/core/autonomy_policy.py
  • tests/test_agent_runtime.py
  • tests/test_autonomy_policy.py
  • tests/test_provider_smoke_command.py
  • tests/test_runtime_modes_command.py

Comment on lines +84 to 93
def max_history_age(self) -> timedelta | None:
"""Return the freshness window, or ``None`` when it is disabled.

A non-positive ``max_history_age_days`` means recorded evidence never
expires; callers treat ``None`` as "freshness is not a promotion
requirement" and never roll a provider back from ``ready``.
"""
if self.max_history_age_days <= 0:
return None
return timedelta(days=self.max_history_age_days)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Honor the “non-positive disables freshness” contract in knob parsing.

max_history_age_days now documents/implements <= 0 as disabled, but parser coercion still drops negative values (Lines 308-319). That makes -1 overrides silently ignored, which can unexpectedly keep stricter preset windows active.

💡 Suggested patch
 def _parse_knob(knob: str, value: Any) -> Any:
     """Parse a single knob value according to its declared type."""
     parser = _KNOB_PARSERS[knob]
     if parser == "_int":
-        return _coerce_int(value)
+        if knob == "max_history_age_days":
+            return _coerce_signed_int(value)
+        return _coerce_int(value)
@@
 def _coerce_int(value: Any) -> int | None:
@@
     return None
+
+
+def _coerce_signed_int(value: Any) -> int | None:
+    if isinstance(value, bool):
+        return None
+    if isinstance(value, int):
+        return value
+    if isinstance(value, str):
+        try:
+            return int(value.strip())
+        except ValueError:
+            return None
+    return None
🤖 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/backend/core/autonomy_policy.py` around lines 84 - 93, The parser
currently clamps negative values for the max_history_age_days knob which
conflicts with max_history_age()'s contract that non-positive values (<= 0)
disable freshness; locate the code that parses/coerces max_history_age_days
(search for the parsing block around the knob name, e.g., the function or logic
that handles "max_history_age_days" in the config parsing code) and stop
dropping or clamping negative integers—accept integers as provided, only enforce
type/sanity (raise on non-numeric types) and preserve negative values (e.g., -1)
so max_history_age() can return None; update any docstring/comments near the
parser to reflect that non-positive values disable freshness.

Comment on lines +1205 to +1243
def test_runtime_modes_policy_gate_keeps_ready_when_expiry_disabled(
tmp_path: Path,
monkeypatch,
capsys,
):
"""Default freshness expiry disabled → old evidence never rolls back."""
from cli.runtime_commands import handle_runtime_modes_command

ancient_run_at = _provider_smoke_run_at(days_ago=365)
_write_provider_history(tmp_path, _stable_provider_history(ancient_run_at))
monkeypatch.chdir(tmp_path)

handle_runtime_modes_command(output_json=True)
payload = json.loads(capsys.readouterr().out)
capability_rows = {
row["provider"]: row for row in payload["runtime_capability_matrix"]
}
policy_rows = {
(row["phase"], row["provider"]): row for row in payload["runtime_policy_matrix"]
}

openai_requirements = capability_rows["openai"][
"autonomous_readiness_requirements"
]
assert capability_rows["openai"]["readiness"] == "full_autonomous_candidate"
assert capability_rows["openai"]["autonomous_policy_gate"] == "passed"
assert (
"provider_history_stale"
not in capability_rows["openai"]["autonomous_readiness_warnings"]
)
assert (
"fresh_provider_history"
not in capability_rows["openai"]["autonomous_readiness_missing_requirements"]
)
assert openai_requirements["last_run_at"] == ancient_run_at
assert openai_requirements["max_history_age_seconds"] is None
assert openai_requirements["history_freshness_complete"] is True
assert policy_rows[("coder", "openai")]["autonomous_promotion_ready"] is True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Isolate default-freshness test from ambient env overrides.

Line 1205’s default-behavior test currently relies on external env state. If AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS (or default max-age env) is set in CI/shell, this test can fail nondeterministically.

Suggested fix
 def test_runtime_modes_policy_gate_keeps_ready_when_expiry_disabled(
     tmp_path: Path,
     monkeypatch,
     capsys,
 ):
     """Default freshness expiry disabled → old evidence never rolls back."""
     from cli.runtime_commands import handle_runtime_modes_command

+    monkeypatch.delenv("AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS", raising=False)
+    monkeypatch.delenv("AUTO_CODE_AUTONOMY_DEFAULT_MAX_HISTORY_AGE_DAYS", raising=False)
+
     ancient_run_at = _provider_smoke_run_at(days_ago=365)
     _write_provider_history(tmp_path, _stable_provider_history(ancient_run_at))
     monkeypatch.chdir(tmp_path)

As per coding guidelines, "Ensure tests are comprehensive and follow pytest conventions. Check for proper mocking and test isolation."

🤖 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 `@tests/test_runtime_modes_command.py` around lines 1205 - 1243, The test
test_runtime_modes_policy_gate_keeps_ready_when_expiry_disabled depends on
ambient environment variables (e.g.
AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS) and must explicitly isolate
them; before calling handle_runtime_modes_command in that test, use the pytest
monkeypatch fixture to remove or unset the relevant env keys
(monkeypatch.delenv("AUTO_CODE_AUTONOMY_OPENAI_MAX_HISTORY_AGE_DAYS",
raising=False) and any other default max-age envs your code reads) so the test
exercises the code path where max_history_age is None and is deterministic
across CI/shell environments.

Source: Coding guidelines

@OBenner
OBenner merged commit 82946f0 into develop Jun 13, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/backend bug Something isn't working size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant