Skip to content

fix(self-healing): oscillation guard prevents A→B→A→A patch loops#221

Open
Gradata wants to merge 1 commit into
mainfrom
fix/self-healing-oscillation-guard
Open

fix(self-healing): oscillation guard prevents A→B→A→A patch loops#221
Gradata wants to merge 1 commit into
mainfrom
fix/self-healing-oscillation-guard

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 21, 2026

Problem

Real-world bug observed 2026-05-21 on production brain: lesson 911130b3 showed 5 consecutive ROLLBACK rows on the Self-Healing dashboard ping-ponging between exactly two rule texts (see screenshot):

  • A: "Don't give prospects a way out when they already expressed interest"
  • B: "When prospect has expressed interest: close the loop immediately — no softeners, no opt-outs, no 'whenever you're ready'"

Each rollback claimed "-62 (100% reduction)" — mathematically meaningless because the new text has zero observations at the moment of measurement.

Root cause

_patches.py::observe_patch() had no cycle detection. Every time the compliance scorer flagged the current rule as "failing this session," the patcher rewrote it back to the previous text. The "reduction %" metric games itself: the new text trivially shows zero failures because it hasn't been observed yet.

Fix

New module enhancements/self_improvement/_oscillation_guard.py:

  • detect_cycle() checks the last N rule_patch_observed events (default 5, 30-day window) for any prior patch where new_text == proposed_old_text AND old_text == proposed_new_text — i.e. a direct A→B then B→A cycle.
  • Whitespace-normalized comparison so trivial differences don't bypass.
  • Category-isolated so a TONE cycle doesn't affect URL patches.
  • On detection: emits a rule_patch_cycle_detected event with the matched prior event ID + a recommended lock duration (10 sessions, configurable).

observe_patch() now consults the guard before recording the patch. If a cycle is detected, no rule_patch_observed event is emitted and the dashboard's Self-Healing tab gets a single cycle_detected row instead of another fake-reduction entry.

Out of scope (separate issues)

  • cloud/app/routes/rule_patches.py::_recurrence_change should return insufficient_data when sessions_observed < 3 (filed as [040a09dd])
  • Bulk-resolve the existing 5 rollbacks on lesson 911130b3 (one-shot data fix, not code)
  • Full feature-logic audit across the dashboard (research issue [3034e85e])

Tests

  • 12 new tests in tests/test_oscillation_guard.py covering empty history, direct cycle, self-identity, category isolation, whitespace normalization, lookback expiry, and the integration path through observe_patch().
  • All 166 existing patch/self-healing/test_jit_inject tests still pass.

Verification

pytest tests/test_oscillation_guard.py -v   # 12 passed
pytest tests/ -k 'patch or self_heal or oscillation' -q   # 166 passed

Closes: paperclip issue 1983a5c6 (BUG-P0: self-healing oscillates)

Real-world bug observed 2026-05-21 on production brain: lesson 911130b3
oscillated between two rule phrasings A and B for 5 consecutive rollbacks
spanning 20 days, each marked '100% reduction' in the dashboard.

Root cause: `observe_patch()` had no cycle detection — every time the
compliance scorer flagged the current text as 'failing,' the patcher
rewrote it back to the previous text without checking it had just patched
away from that text. The 'reduction' metric games itself: the new text
trivially shows zero failures because it has zero observations yet.

Fix: new module `_oscillation_guard.py` is consulted before each
`observe_patch()` call. Detects direct A→B then B→A cycles within a
30-day / 5-patch lookback window. On detection, emits a
`rule_patch_cycle_detected` event and aborts the patch instead of
recording another fake-reduction row. Conservative scope (only direct
cycles, only within a single category, whitespace-normalized comparison)
to minimize false-positive risk.

The next sibling fix (`recurrence_change → insufficient_data when <3
sessions`) is filed as a separate cloud-side issue [040a09dd].

Tests: 12 new (oscillation_guard) + 166 existing self-healing/patch
tests still green.

Refs: paperclip issue 1983a5c6
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

📝 Walkthrough
  • Fixes production incident where self-healing rule rewrites oscillated between two texts (A→B→A), producing spurious "-62 (100% reduction)" metrics
  • Root cause: observe_patch() lacked cycle detection, allowing rewrites to flip back to previous rule text without safeguards
  • New module _oscillation_guard.py detects direct A→B→A cycles by querying prior patch events within configurable 30-day/N-patch lookback window
  • Cycle detection uses whitespace-normalized text comparison and restricts checks to same category, preventing false positives
  • On detection, emits rule_patch_cycle_detected event instead of rule_patch_observed, avoiding fake reduction metrics in dashboard
  • Configurable lock duration via GRADATA_OSCILLATION_LOCK_SESSIONS env var (default 10 sessions) prevents immediate re-patching
  • New public API: detect_cycle(), emit_cycle_detected() in oscillation guard module; observe_patch(), resolve_patch_compliance(), patch_acceptance_rate() in patches module
  • Comprehensive test coverage with 12 new tests covering edge cases (no history, unrelated patches, cycle detection, category isolation, whitespace normalization, lookback boundaries)
  • Fails open on event query errors, returning None or empty {} to avoid blocking legitimate patches
  • No breaking changes; all existing patch/self-healing tests (166+) continue to pass

Walkthrough

This PR introduces oscillation prevention and patch telemetry for rule self-improvement. A new _oscillation_guard.py module detects when a rule rewrite cycles back to a prior state (A→B→A), querying recent patch history with configurable lookback windows. A new _patches.py module records patch observations, resolves compliance metrics after 3 sessions, and computes acceptance rates. The oscillation guard integrates into patch observation to prevent cycles from being recorded. Comprehensive test coverage validates cycle detection across edge cases and integration scenarios using a minimal FakeBrain test double.

Changes

Rule Oscillation Guard and Patch Telemetry

Layer / File(s) Summary
Oscillation Guard: Cycle Detection and Emission
Gradata/src/gradata/enhancements/self_improvement/_oscillation_guard.py
Cycle detection normalizes rule text and queries rule_patch_observed events within a configurable lookback window (capped by days and patch count), detecting direct A→B then B→A matches. Returns cycle metadata with lock duration. emit_cycle_detected() emits audit events with truncated text and cycle tags, failing open on errors.
Patch Telemetry: Observation, Resolution, and Acceptance Rate
Gradata/src/gradata/enhancements/self_improvement/_patches.py
observe_patch() checks for oscillation cycles before recording rule_patch_observed with pre-patch compliance. resolve_patch_compliance() finds pending observations ≥3 sessions old, computes post-patch compliance, and emits resolved events. patch_acceptance_rate() aggregates resolved observations to compute acceptance metrics and pending count.
Oscillation Guard and Integration Tests
Gradata/tests/test_oscillation_guard.py
FakeBrain test double with event store. Unit tests validate cycle detection (no history, unrelated patches, direct cycles, self-identity, category isolation, normalization, lookback boundaries). Integration tests confirm cycles block rule_patch_observed while normal patches succeed. Normalization helper validated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.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
Title check ✅ Passed The title accurately describes the main change: adding an oscillation guard to prevent A→B→A patch loops in the self-healing system.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the production bug, root cause, the implemented fix, scope boundaries, and test verification.
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/self-healing-oscillation-guard

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.21.0)

OpenGrep fatal error (exit code 2):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.20][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


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

@coderabbitai coderabbitai Bot added the bug Something isn't working label May 21, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Gradata/tests/test_oscillation_guard.py (1)

194-232: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add a regression test for one-time resolution semantics.

Please add a deterministic unit test that calls resolve_patch_compliance() twice and asserts the same original patch is not resolved/emitted twice.

🤖 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 `@Gradata/tests/test_oscillation_guard.py` around lines 194 - 232, Add a
deterministic unit test that uses _FakeBrain to simulate a patch, calls
resolve_patch_compliance(brain, <rule_id>, <old>, <new>) twice, and asserts the
first call returns/emits the expected "rule_patch_resolved" (or appropriate
resolved event) while the second call does not emit a duplicate resolution
(e.g., returns None or an event with a different type), thereby verifying
one-time resolution semantics; locate resolve_patch_compliance, _FakeBrain and
observe_patch in the test file to mirror setup used by existing tests (create
the same initial patch before calling resolve_patch_compliance twice and assert
identical inputs produce only a single resolved event).
🤖 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 `@Gradata/src/gradata/enhancements/self_improvement/_oscillation_guard.py`:
- Around line 114-120: The try/except around brain.query_events in
_oscillation_guard.py currently swallows exceptions and returns None; update the
exception handlers (the block around brain.query_events and the similar block at
lines ~189-190) to keep the fail-open behavior but log the exception context by
calling logger.warning with a descriptive message and exc_info=True (e.g.,
"cycle guard: failed to query events, failing open") so outages are diagnosable;
locate and modify the handlers around brain.query_events and the second silent
except to replace bare excepts with logging that includes exc_info=True while
still returning None.

In `@Gradata/src/gradata/enhancements/self_improvement/_patches.py`:
- Around line 37-44: The try/except around the call to brain.query_events
currently swallows exceptions and returns 0; replace the bare except with a log
call that preserves the existing fallback behavior by calling
logger.warning("Failed to query events", exc_info=True) (or similar) before
returning 0 so telemetry failures are observable. Do the same change for the
other bare except blocks in this module that return fallbacks (the ones around
the other brain.* calls) — add a logger.warning with exc_info=True in each
except and keep the original return value. Ensure you reference the same symbols
(e.g., brain.query_events and the other brain.* calls) and do not change the
fallback logic.
- Around line 60-66: The session detection currently swallows all errors with a
bare except, which hides failures; modify the try/except around
brain.query_events(...) and the subsequent max(...) call to catch Exception as e
(or narrower exceptions if known), log the error with the module logger (e.g.,
logger.warning("session detection failed", exc_info=True) or similar) and then
return the default 1; reference the brain.query_events call and the
events/max((e.get("session") ...) expression when making the change so you only
replace the bare except: pass with a typed exception handler that logs
exc_info=True before returning 1.
- Around line 154-193: The loop that resolves patches can run multiple times
because the original source event (ev) is never updated with
"observed_compliance_after_3_sessions", so add an atomic update/write to the
original event before emitting "rule_patch_observed": after computing
compliance_after (in the pending_events loop where ev, data, compliance_after
and compliance_before are computed) mutate/persist the source event's data to
set "observed_compliance_after_3_sessions" (and optionally "resolution_session"
or "compliance_improved") using the same event persistence API your codebase
uses, then call brain.emit("rule_patch_observed",
"_patches.resolve_patch_compliance", ...). This ensures the guard
data.get("observed_compliance_after_3_sessions") will be present on subsequent
runs and prevents duplicate "resolved" emissions; ensure the update is performed
before or as part of the emit so retries do not produce duplicates.

---

Outside diff comments:
In `@Gradata/tests/test_oscillation_guard.py`:
- Around line 194-232: Add a deterministic unit test that uses _FakeBrain to
simulate a patch, calls resolve_patch_compliance(brain, <rule_id>, <old>, <new>)
twice, and asserts the first call returns/emits the expected
"rule_patch_resolved" (or appropriate resolved event) while the second call does
not emit a duplicate resolution (e.g., returns None or an event with a different
type), thereby verifying one-time resolution semantics; locate
resolve_patch_compliance, _FakeBrain and observe_patch in the test file to
mirror setup used by existing tests (create the same initial patch before
calling resolve_patch_compliance twice and assert identical inputs produce only
a single resolved event).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80abbafa-c945-4493-8d1d-a53f115c5f64

📥 Commits

Reviewing files that changed from the base of the PR and between a197bff and 4b04136.

📒 Files selected for processing (3)
  • Gradata/src/gradata/enhancements/self_improvement/_oscillation_guard.py
  • Gradata/src/gradata/enhancements/self_improvement/_patches.py
  • Gradata/tests/test_oscillation_guard.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: pytest ubuntu-latest / py3.11
  • GitHub Check: pytest macos-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest (py3.11)
  • GitHub Check: pytest (py3.12)
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/enhancements/self_improvement/_patches.py
  • Gradata/src/gradata/enhancements/self_improvement/_oscillation_guard.py
Gradata/tests/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and skip them by default (they hit real LLM APIs)

Files:

  • Gradata/tests/test_oscillation_guard.py

Comment on lines +114 to +120
try:
events = brain.query_events(
event_type="rule_patch_observed",
limit=200,
)
except Exception:
return None # Fail open — never block patches on a query failure.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Log exception context when failing open.

Line 119 and Line 189 suppress errors silently. Keep fail-open behavior, but add logger.warning(..., exc_info=True) so cycle-guard outages are diagnosable in production.

Proposed fix
 from __future__ import annotations
 
 import os
+import logging
 from datetime import UTC, datetime, timedelta
 from typing import TYPE_CHECKING
 
 if TYPE_CHECKING:
     from gradata.brain import Brain
+
+logger = logging.getLogger(__name__)
@@
-    except Exception:
-        return None  # Fail open — never block patches on a query failure.
+    except Exception:
+        logger.warning("detect_cycle query failed; failing open", exc_info=True)
+        return None  # Fail open — never block patches on a query failure.
@@
-    except Exception:
-        return {}
+    except Exception:
+        logger.warning("emit_cycle_detected failed", exc_info=True)
+        return {}

As per coding guidelines, “Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product”.

Also applies to: 189-190

🤖 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 `@Gradata/src/gradata/enhancements/self_improvement/_oscillation_guard.py`
around lines 114 - 120, The try/except around brain.query_events in
_oscillation_guard.py currently swallows exceptions and returns None; update the
exception handlers (the block around brain.query_events and the similar block at
lines ~189-190) to keep the fail-open behavior but log the exception context by
calling logger.warning with a descriptive message and exc_info=True (e.g.,
"cycle guard: failed to query events, failing open") so outages are diagnosable;
locate and modify the handlers around brain.query_events and the second silent
except to replace bare excepts with logging that includes exc_info=True while
still returning None.

Comment on lines +37 to +44
try:
events = brain.query_events(
event_type="RULE_FAILURE",
last_n_sessions=lookback_sessions,
limit=500,
)
except Exception:
return 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add warning logs for swallowed telemetry exceptions.

These paths currently fail silently and return fallback values. Add warning logs with exc_info=True so production telemetry failures are observable.

As per coding guidelines, “Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product”.

Also applies to: 128-129, 147-149, 192-193

🤖 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 `@Gradata/src/gradata/enhancements/self_improvement/_patches.py` around lines
37 - 44, The try/except around the call to brain.query_events currently swallows
exceptions and returns 0; replace the bare except with a log call that preserves
the existing fallback behavior by calling logger.warning("Failed to query
events", exc_info=True) (or similar) before returning 0 so telemetry failures
are observable. Do the same change for the other bare except blocks in this
module that return fallbacks (the ones around the other brain.* calls) — add a
logger.warning with exc_info=True in each except and keep the original return
value. Ensure you reference the same symbols (e.g., brain.query_events and the
other brain.* calls) and do not change the fallback logic.

Comment on lines +60 to +66
try:
events = brain.query_events(limit=10)
if events:
return max((e.get("session") or 0) for e in events)
except Exception:
pass
return 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace bare except: pass in session detection.

Line 64-65 uses a silent bare pass, which hides failures in compliance resolution scheduling.

Proposed fix
+import logging
@@
+logger = logging.getLogger(__name__)
@@
-    except Exception:
-        pass
+    except Exception:
+        logger.warning("_get_current_session failed; defaulting to session=1", exc_info=True)
     return 1

As per coding guidelines, “Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product”.

🤖 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 `@Gradata/src/gradata/enhancements/self_improvement/_patches.py` around lines
60 - 66, The session detection currently swallows all errors with a bare except,
which hides failures; modify the try/except around brain.query_events(...) and
the subsequent max(...) call to catch Exception as e (or narrower exceptions if
known), log the error with the module logger (e.g., logger.warning("session
detection failed", exc_info=True) or similar) and then return the default 1;
reference the brain.query_events call and the events/max((e.get("session") ...)
expression when making the change so you only replace the bare except: pass with
a typed exception handler that logs exc_info=True before returning 1.

Comment on lines +154 to +193
for ev in pending_events:
data = ev.get("data", {})
if data.get("observed_compliance_after_3_sessions") is not None:
continue

patch_session = ev.get("session") or 0
if current_session - patch_session < min_session_gap:
continue

category = data.get("category", "")
new_rule_text = data.get("new_rule_text", "")

compliance_after = _count_failures_for_rule(brain, category, new_rule_text)
compliance_before = data.get("observed_compliance_before") or 0
improved = compliance_after < compliance_before

try:
updated = brain.emit(
"rule_patch_observed",
"_patches.resolve_patch_compliance",
{
**data,
"observed_compliance_after_3_sessions": compliance_after,
"compliance_improved": improved,
"resolution_session": current_session,
"original_event_id": ev.get("id"),
},
[f"category:{category}", "self_healing", _PATCH_TAG, "resolved"],
)
updates.append(
{
"category": category,
"compliance_before": compliance_before,
"compliance_after": compliance_after,
"improved": improved,
"event": updated if isinstance(updated, dict) else {},
}
)
except Exception:
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make compliance resolution idempotent to prevent duplicate “resolved” emissions.

The unresolved source event is never mutated, so repeated runs can resolve the same patch multiple times. This will skew acceptance telemetry and create duplicate audit rows.

Proposed fix
 def resolve_patch_compliance(
@@
-    current_session = _get_current_session(brain)
+    current_session = _get_current_session(brain)
+    already_resolved_ids = {
+        (e.get("data", {}) or {}).get("original_event_id")
+        for e in pending_events
+        if (e.get("data", {}) or {}).get("observed_compliance_after_3_sessions") is not None
+    }
@@
     for ev in pending_events:
         data = ev.get("data", {})
         if data.get("observed_compliance_after_3_sessions") is not None:
             continue
+        if ev.get("id") in already_resolved_ids:
+            continue
@@
         try:
             updated = brain.emit(
🤖 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 `@Gradata/src/gradata/enhancements/self_improvement/_patches.py` around lines
154 - 193, The loop that resolves patches can run multiple times because the
original source event (ev) is never updated with
"observed_compliance_after_3_sessions", so add an atomic update/write to the
original event before emitting "rule_patch_observed": after computing
compliance_after (in the pending_events loop where ev, data, compliance_after
and compliance_before are computed) mutate/persist the source event's data to
set "observed_compliance_after_3_sessions" (and optionally "resolution_session"
or "compliance_improved") using the same event persistence API your codebase
uses, then call brain.emit("rule_patch_observed",
"_patches.resolve_patch_compliance", ...). This ensures the guard
data.get("observed_compliance_after_3_sessions") will be present on subsequent
runs and prevents duplicate "resolved" emissions; ensure the update is performed
before or as part of the emit so retries do not produce duplicates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant