Skip to content

feat: restore open-world evidence foundation - #416

Draft
guangyu-reflexio wants to merge 28 commits into
mainfrom
codex/offline-tuner-open-world-phase1-restored
Draft

feat: restore open-world evidence foundation#416
guangyu-reflexio wants to merge 28 commits into
mainfrom
codex/offline-tuner-open-world-phase1-restored

Conversation

@guangyu-reflexio

@guangyu-reflexio guangyu-reflexio commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Why this replacement PR

The earlier foundation PRs were reverted after local end-to-end and evaluation failures. This branch was rebuilt from current main rather than reopening the reverted history. It keeps the independent mainline fixes and adds regression coverage for the failures found during the rebuild.

This PR must remain unmerged until the replacement enterprise PR restores PostgreSQL, Supabase, and PostgREST adapter parity and the complete stacked end-to-end matrix is green.

Verification

  • Changed shared tests: 699 passed.
  • Complete OSS E2E tier: 47 passed, 51 expected skips.
  • Focused final migration review: 60 passed.
  • Ruff lint and format: clean across all changed Python files.
  • Pyright: 0 errors, 0 warnings across all changed Python files.
  • Shared docs ESLint: 0 errors; TypeScript: clean.
  • Import and diff checks: clean.
  • Full review-loop: no open shared-local P1-P4 findings.

Prior PRs

Summary by CodeRabbit

  • New Features
    • Added durable session-outcome identities, expanded outcome states, and conflict detection for incompatible retries.
    • Added search-exposure tracking, result-limit validation, and identifier constraints.
    • Added resumable learning finalization, receipt-backed billing, and stricter usage-event delivery.
    • Added lease-based safeguards for reliable user-data erasure and recovery.
    • Added globally scheduled cleanup sweeps.
  • Bug Fixes
    • Corrected billing to count only retained learnings.
    • Improved cleanup and retention handling for session outcomes and exposure records.
  • Documentation
    • Updated search and session-outcome guidance and Quick Start SQLite prerequisites.

- Add the shared evidence foundation for a future offline tuner without
enabling a production tuner or scheduler.
- Make session outcomes immutable, conflict-aware, reconstructable
records with server-owned contract and trajectory identities.
- Record synchronous search-exposure evidence and retain it under
governance-safe lifecycle rules.
- Keep billing limited to durable learnings created by online/resumable
extraction; derived aggregation and tuning remain unmetered.

- Add durable session-outcome identity fields, canonical digest helpers,
exact-retry behavior, and conflicting-finalization rejection.
- Add synchronous search-exposure recording with deterministic
identities and incomplete-evidence classification.
- Extend retention contracts for session outcomes and exposure evidence.

- Bind erasure to authoritative tenant-scoped users.
- Add purge execution claims, lease renewal, fencing, stale-claim
recovery, and deterministic retry behavior.
- Preserve missing-schema tolerance and stable deletion receipts.

- Emit learning usage only for durable extraction-created rows.
- Remove derived aggregation billing and make resumable retry keys
deterministic.

- Document immutable outcomes, extraction-only billing, and the
intentionally unavailable tuner capability.
- Add focused SQLite, API, governance, concurrency, migration, and
compatibility coverage.

```mermaid
flowchart LR
    A[Online extraction] --> B[Durable profiles and playbooks]
    A --> C[Session outcomes]
    D[Search] --> E[Exposure evidence]
    C --> F[Governance and retention]
    E --> F
    B --> G[Future offline analysis]
    C --> G
    E --> G
    G -. disabled in Phase 1 .-> H[Candidate publication]
```

- Phase matrix: `146 passed, 9 skipped` across SQLite,
Supabase/PostgREST, native PostgreSQL, governance, exposure, and
migration cases.
- Migration/schema/placement gate: `64 passed`.
- Shared governance and storage focused suite: `58 passed`.
- Tuner availability/config gate: `7 passed`.
- Changed Python scope: Ruff clean, format clean, Pyright `0 errors, 0
warnings`.
- Changed SQL migration lint: `7 files, 0 findings`.
- Import and range diff checks passed.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **New Features**
* Session outcomes support `unknown` results with stable identity and
integrity details.
* Exact retries remain idempotent; conflicting finalizations are
rejected.
  * Search results can be recorded as user-playbook exposures.
* User-data erasure supports safer retries and recovery during
concurrent processing.
* Finalized learning records support reliable retry handling and
accurate billing.
* **Bug Fixes**
  * Session outcomes are removed using the authoritative user identity.
* Billing records avoid duplicate or unattributable finalized-learning
entries.
* **Migration**
* Existing session-outcome data is preserved and upgraded automatically.
* **Documentation**
  * Local SQLite storage now requires version 3.35.0 or newer.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- make durable finalization receipt insertion atomically identify the
sole winning transaction
- emit resumable extraction billing only for the receipt winner
- keep optimizer, aggregation, and tagging dispatch best-effort and
at-most-once
- preserve ordered winner IDs, empty receipts, and existing
list-returning service wrappers

- add an internal `FinalizationResult` carrying learning IDs and receipt
ownership
- roll back stale loser writes and reuse the committed winner receipt
- independently isolate optimizer and aggregation scheduling failures
- gate tagging and billing on receipt ownership
- add deterministic retry, empty-receipt, identical-ID race, and
scheduler coverage

- 60 focused shared receipt/resume tests passed
- repeated profile/playbook concurrency regressions passed
- Ruff format/check and focused Pyright passed
- enterprise Supabase/native-Postgres adapter contract is covered in the
related enterprise branch

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Bug Fixes**
* Improved resumable extraction finalization to prevent duplicate
learning records, tags, and billing events.
* Added reliable handling for concurrent finalization attempts,
preserving the original result and avoiding repeated processing.
* Scheduler failures are isolated so one failed follow-up task does not
disrupt completed finalization.
* Retry flows now maintain durable learning IDs and consistent outcomes.

* **Reliability**
* Finalization receipts are now immutable and safely reused across
retries and competing workers.
  * Added safeguards for empty or incomplete finalization receipts.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary

- preserve every user-playbook exposure ingested within the fixed 14-day
evidence window, even when the generic row cap is exceeded
- separate the optional retrieval subject from the served playbook owner
so unscoped searches remain attributable without becoming join-eligible
- normalize blank retrieval subjects to the unscoped representation
while retaining fail-closed owner checks for scoped searches

## Why

The Phase 1 offline-tuner evidence foundation requires complete,
reviewable exposure evidence. Generic row-cap retention could delete
current-window events, and the original exposure envelope overloaded
playbook ownership as the retrieval subject for unscoped searches. Both
behaviors could distort later evidence eligibility and exact-user
governance.

## Behavior

- exposure rows younger than 14 days are protected from row-cap deletion
- an exposure may carry a retrieval subject and a playbook owner
independently
- blank or whitespace retrieval subjects become unscoped
- scoped retrieval still rejects a user playbook owned by another user
before persistence

## Verification

- 46 shared exposure and retention contract tests passed after rebasing
onto current `main`
- final affected enterprise matrix: 164 passed, 9 adapter-applicability
skips
- Ruff formatting/lint, Pyright, import, diff, and gitlink checks passed
- correctness, security-resilience, architecture, and
verification-testing review lenses are clean

## Related PRs

- Enterprise Phase 1 stack:
ReflexioAI/reflexio-enterprise#936


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Exposure records now preserve the playbook owner separately from the
person retrieving it.
  * Blank retrieval identifiers are normalized consistently.
* Playbook exposure data now includes governance subject references
where available.

* **Bug Fixes**
* Prevented exposure records from being created when a playbook belongs
to a different scoped user.
  * Added safeguards for unscoped exposure scenarios.

* **Data Retention**
* Open-world evidence, including playbook exposure records, is retained
for at least 14 days before cleanup.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Make resumable finalization replay stable per-learning usage keys until the configured recorder durably accepts or deduplicates them, while preserving fail-open metering for ordinary product calls.
Preserve current-main storage and billing contracts while restoring the Phase 1 replay semantics.
Prefetch request and interaction rows in SQLite-safe chunks while preserving the atomic legacy outcome rebuild and canonical trajectory identities.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c50ae208-2e7f-49cc-9bbe-7e528d5e5811

📥 Commits

Reviewing files that changed from the base of the PR and between ace5b99 and d964e23.

📒 Files selected for processing (2)
  • reflexio/server/README.md
  • reflexio/server/services/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • reflexio/server/services/README.md
  • reflexio/server/README.md

📝 Walkthrough

Walkthrough

The change adds durable session-outcome identities, receipt-backed finalization, strict usage delivery, fenced governance erasure, deterministic search-exposure records, retention age limits, always-global sweeps, and SQLite compatibility checks.

Changes

Durable outcomes and finalization

Layer / File(s) Summary
Session outcome identity and migration
reflexio/models/..., reflexio/server/services/storage/..., tests/...session_outcome...
Session outcomes now store canonical IDs, revisions, contract digests, trajectory digests, and conflict reasons. SQLite migrations preserve legacy data.
Receipt finalization and billing
reflexio/server/services/{profile,playbook,extraction}/..., reflexio/server/usage_metrics.py, reflexio/server/billing_meter.py
Finalization uses immutable receipts and durable learning IDs. Strict billing delivery distinguishes retryable and terminal failures.

Governance and exposure

Layer / File(s) Summary
Governance execution claims
reflexio/server/services/governance/..., reflexio/server/services/storage/...governance...
User erasure uses authoritative identity binding, leased execution claims, fencing, heartbeat renewal, and claim-aware lifecycle operations.
Search exposure and retention
reflexio/server/services/search_exposure.py, reflexio/server/routes/search.py, reflexio/server/services/storage/retention*
Unified search records deterministic playbook exposure events. Retention applies a 14-day minimum age to exposure events.

Scheduler and compatibility

Layer / File(s) Summary
Scheduler, startup, and validation updates
reflexio/server/services/lineage/gc_scheduler.py, reflexio/server/api.py, README.md, reflexio/models/api_schema/..., tests/...
Always-global sweeps run independently of expiry reclamation. OSS startup installs an exempt recorder. SQLite 3.35+ and search limits are documented and validated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.84% 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 clearly identifies the main change: restoring the open-world evidence foundation.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/offline-tuner-open-world-phase1-restored

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

@guangyu-reflexio

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 12

🧹 Nitpick comments (25)
reflexio/server/services/governance/service.py (1)

418-449: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the tautological purge_id check.

_matching_user_erasure_purge_for_retry fetches the purge by purge_id through get_purge_operation(purge_id), then asserts that purge.purge_id == purge_id. That comparison cannot fail. The remaining checks on operation_type, scope_type, subject_ref, and request_ref carry the actual value.

♻️ Proposed simplification
         purge = self.storage.get_purge_operation(purge_id)
         expected_identity = {
-            "purge_id": purge_id,
             "operation_type": operation_type,
             "scope_type": scope_type,
             "subject_ref": subject_ref,
             "request_ref": request_ref,
         }
🤖 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 `@reflexio/server/services/governance/service.py` around lines 418 - 449,
Remove the tautological purge_id entry from expected_identity in
_matching_user_erasure_purge_for_retry, while preserving validation of
operation_type, scope_type, subject_ref, and request_ref and the
authoritative-user check.
tests/server/services/governance/test_subject_write_barrier_sqlite.py (2)

622-629: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate barrier fetch.

barrier and stored_barrier read the same row through get_subject_write_barrier(subject_ref) and then assert the same status == "erased". Keep one.

♻️ Proposed change
     barrier = storage.get_subject_write_barrier(subject_ref)
     assert barrier is not None
-    stored_barrier = storage.get_subject_write_barrier(subject_ref)
     stored_purge = storage.get_purge_operation(purge.purge_id)
 
     assert barrier.status == "erased"
-    assert stored_barrier is not None
-    assert stored_barrier.status == "erased"
     assert stored_purge is not None
     assert stored_purge.status == "complete"
🤖 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/server/services/governance/test_subject_write_barrier_sqlite.py` around
lines 622 - 629, Remove the redundant get_subject_write_barrier(subject_ref)
call in this test, keeping a single barrier fetch and its associated non-null
and status assertions; preserve the stored_purge retrieval and its independent
assertions.

42-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the deliberate lease steal in _claim_purge.

When a claim is already held, this helper forcibly expires execution_claim_expires_at and re-claims. Each call also increments the fence, so any claim obtained by an earlier call becomes stale. The current call sites use each claim before the next _claim_purge call, so the tests are correct. Add a comment stating that the helper always takes over the lease, and that callers must not hold a claim across two _claim_purge calls.

🤖 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/server/services/governance/test_subject_write_barrier_sqlite.py` around
lines 42 - 62, Add a concise comment in `_claim_purge` documenting that it
deliberately expires and steals any existing execution lease, increments the
fence, and makes claims from earlier calls stale; state that callers must use
each returned claim before invoking `_claim_purge` again.
tests/server/services/governance/test_governance_local_e2e.py (3)

963-966: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use time.sleep instead of a throwaway Event.

threading.Event().wait(0.001) creates an event that is never set, only to block for 1 ms. time.sleep(0.001) states the intent directly.

♻️ Proposed change
     def release_winner_on_duplicate_poll(_seconds: float) -> None:
         lifecycle.release_first_call.set()
-        threading.Event().wait(0.001)
+        time.sleep(0.001)

Add import time at the top of the file.

🤖 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/server/services/governance/test_governance_local_e2e.py` around lines
963 - 966, Update release_winner_on_duplicate_poll to use time.sleep(0.001)
instead of creating and waiting on a throwaway threading.Event, and add the
required time import at the file’s imports.

1052-1110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the fake-clock arithmetic.

This test depends on a precise relationship that is not stated: the claim starts at _epoch_now() == 100 with a 300-second lease, so it expires at 400. The lifecycle advances the clock to 350, waits for three renewals that push the expiry to 650, then advances to 401. The duplicate caller at 401 must therefore still see a live lease. A reader who changes _PURGE_EXECUTION_LEASE_SECONDS or one of these literals will get a confusing flake. Add a comment stating the intended invariant.

🤖 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/server/services/governance/test_governance_local_e2e.py` around lines
1052 - 1110, Document the fake-clock invariant in
test_healthy_slow_lifecycle_renews_lease_and_duplicate_converges: state that the
claim begins at 100 with a 300-second lease expiring at 400, three renewals
extend it to 650, and advancing to 401 keeps the duplicate caller within the
live lease. Place the comment next to the relevant fake_now updates and renewal
wait.

1044-1049: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the backoff expectations from the module constants.

The assertions hardcode 0.05, 1.0, and 5.0. Those values duplicate _DUPLICATE_ERASE_POLL_SECONDS, _DUPLICATE_ERASE_MAX_POLL_SECONDS, and _DUPLICATE_ERASE_WAIT_SECONDS. If a constant changes, this test fails on the literal rather than on the backoff shape. Read the constants from governance_service_module so the test keeps checking the doubling behavior and the total bound.

♻️ Proposed change
-    assert service.sleep_delays[:5] == pytest.approx([0.05, 0.1, 0.2, 0.4, 0.8])
-    assert max(service.sleep_delays) == 1.0
-    assert sum(service.sleep_delays) == pytest.approx(5.0)
+    base = governance_service_module._DUPLICATE_ERASE_POLL_SECONDS
+    assert service.sleep_delays[:5] == pytest.approx(
+        [base * 2**step for step in range(5)]
+    )
+    assert max(service.sleep_delays) == (
+        governance_service_module._DUPLICATE_ERASE_MAX_POLL_SECONDS
+    )
+    assert sum(service.sleep_delays) == pytest.approx(
+        governance_service_module._DUPLICATE_ERASE_WAIT_SECONDS
+    )
🤖 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/server/services/governance/test_governance_local_e2e.py` around lines
1044 - 1049, Update the backoff assertions in the governance erase retry test to
derive expected initial, maximum, and total delays from
governance_service_module’s _DUPLICATE_ERASE_POLL_SECONDS,
_DUPLICATE_ERASE_MAX_POLL_SECONDS, and _DUPLICATE_ERASE_WAIT_SECONDS constants.
Preserve the existing checks for doubling behavior, maximum delay, and total
wait bound without hardcoded duplicate values.
reflexio/server/services/storage/sqlite_storage/governance/_purge.py (1)

449-497: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the duplicated claim assertion.

assert_purge_operation_execution_claim and _assert_purge_operation_execution_claim_locked contain the same validation and the same five-condition row check. Only the row-fetch mechanism differs. Extract the row check so a future change to the fencing rule cannot be applied to one copy only.

♻️ Proposed refactor
+    def _assert_claim_row(self, row: Any, claim: PurgeExecutionClaim) -> None:
+        if row is None:
+            raise ValueError(f"Purge operation {claim.purge_id!r} not found")
+        if (
+            row["status"] != "running"
+            or row["execution_claim_owner"] != claim.owner
+            or int(row["execution_claim_fence"]) != claim.fence
+            or row["execution_claim_expires_at"] is None
+            or int(row["execution_claim_expires_at"]) <= _epoch_now()
+        ):
+            raise ValueError("purge execution claim is no longer active")
+
     def assert_purge_operation_execution_claim(
         self, purge_id: str, execution_claim: PurgeExecutionClaim
     ) -> None:
         purge_id = _validate_governance_purge_id("purge_id", purge_id)
         claim = validate_purge_execution_claim(purge_id, execution_claim)
-        now = _epoch_now()
-        row = self._deps()._fetchone(
+        self._assert_claim_row(
+            self._deps()._fetchone(
             """SELECT status, execution_claim_owner, execution_claim_fence,
                       execution_claim_expires_at
                FROM purge_operations
                WHERE purge_id = ? AND org_id = ?""",
-            (purge_id, self.org_id),
-        )
-        if row is None:
-            raise ValueError(f"Purge operation {purge_id!r} not found")
-        if (
-            row["status"] != "running"
-            or row["execution_claim_owner"] != claim.owner
-            or int(row["execution_claim_fence"]) != claim.fence
-            or row["execution_claim_expires_at"] is None
-            or int(row["execution_claim_expires_at"]) <= now
-        ):
-            raise ValueError("purge execution claim is no longer active")
+                (purge_id, self.org_id),
+            ),
+            claim,
+        )

Apply the same delegation in _assert_purge_operation_execution_claim_locked.

🤖 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 `@reflexio/server/services/storage/sqlite_storage/governance/_purge.py` around
lines 449 - 497, Consolidate the duplicated validation in
assert_purge_operation_execution_claim and
_assert_purge_operation_execution_claim_locked by having the locked method
delegate to the shared assertion logic while preserving its locked row-fetch
behavior. Extract or reuse a common row-check helper so both paths apply the
same five claim conditions, with only the database fetch mechanism differing.
tests/server/services/storage/sqlite_storage/test_governance_storage.py (2)

1677-1704: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the caller scan fail when the production root resolves incorrectly.

production_root is derived from parents[5], which couples the test to its exact directory depth. If this file moves one level, parents[5] points at a different directory, rglob("*.py") yields nothing, and assert violations == [] passes without scanning anything. The test then reports success while enforcing nothing.

Add an assertion that the root exists and that the scan visited a non-zero number of files.

🛡️ Proposed guard
     production_root = Path(__file__).resolve().parents[5] / "reflexio"
+    assert production_root.is_dir(), production_root
     violations: list[str] = []
+    scanned = 0
     for path in production_root.rglob("*.py"):
+        scanned += 1
         tree = ast.parse(path.read_text(), filename=str(path))
+    assert scanned > 0
     assert violations == []
🤖 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/server/services/storage/sqlite_storage/test_governance_storage.py`
around lines 1677 - 1704, Strengthen the production scan in the test around
production_root by asserting that the derived root exists and is a directory
before scanning. Track the files yielded by production_root.rglob("*.py") and
assert that the scan visited at least one file, while preserving the existing
violation checks.

218-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document that _claim_purge steals a live lease.

When a live unexpired lease already exists, claim_purge_operation_execution returns None. _claim_purge then force-expires the lease and re-claims, which increments the fence and invalidates every previously issued claim for that purge_id.

Several tests call _claim_purge more than once for the same purge_id, for example Lines 701, 708, and 722. Each call currently supersedes the prior one. That works today only because every call site uses its claim immediately. A future test that captures a claim into a variable and then calls _claim_purge again before using it will fail with purge execution claim is no longer active, and the cause will not be obvious.

Add a docstring stating the takeover behavior, or split the helper into _claim_purge (first claim only) and _take_over_purge (force-expire and re-claim).

🤖 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/server/services/storage/sqlite_storage/test_governance_storage.py`
around lines 218 - 238, Document the lease-takeover behavior in the test helper
_claim_purge: add a docstring stating that when a live lease exists it
force-expires that lease, reclaims the purge, and invalidates previously issued
claims for the same purge_id. Keep the existing claiming logic unchanged.
tests/server/services/lineage/test_gc_scheduler_global_sweep.py (2)

30-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the always-sweep helpers directly instead of guarding with getattr.

register_always_global_sweep and clear_always_global_sweeps exist in gc_scheduler. Two other tests in this file already call gc_scheduler.register_always_global_sweep directly at Line 135 and Line 153.

The fixture guard is the larger risk. If clear_always_global_sweeps is renamed or removed, the fixture silently stops clearing registrations, and always-sweep hooks leak into every later test in the session. A direct import fails loudly instead.

♻️ Proposed change
 def _isolate_hooks():
     clear_global_sweeps()
-    clear_always = getattr(gc_scheduler, "clear_always_global_sweeps", None)
-    if clear_always is not None:
-        clear_always()
+    gc_scheduler.clear_always_global_sweeps()
     yield
     clear_global_sweeps()
-    if clear_always is not None:
-        clear_always()
+    gc_scheduler.clear_always_global_sweeps()
 def test_run_once_invokes_always_global_sweep_when_expiry_disabled(monkeypatch):
     calls: list[int] = []
-    register = getattr(gc_scheduler, "register_always_global_sweep", None)
-    assert callable(register)
-    register(lambda now: calls.append(now) or 1)
+    gc_scheduler.register_always_global_sweep(lambda now: calls.append(now) or 1)

Also applies to: 110-112

🤖 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/server/services/lineage/test_gc_scheduler_global_sweep.py` around lines
30 - 36, Update the fixture cleanup to import and call
gc_scheduler.clear_always_global_sweeps directly, removing the getattr guard and
clear_always variable; likewise import register_always_global_sweep directly
where the fixture or tests use it, so missing helpers fail loudly while
preserving cleanup before and after each test.

143-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert against the scheduler constants instead of literals.

5 and 86400 duplicate _BOOTSTRAP_RETRY_INTERVAL_SECONDS and _DEFAULT_POLL_INTERVAL_SECONDS in gc_scheduler.py. If either constant changes, these tests fail with an opaque numeric mismatch that does not name the constant.

Import the constants and compare against them so the failure message points at the source of truth.

Also applies to: 170-170

🤖 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/server/services/lineage/test_gc_scheduler_global_sweep.py` at line 143,
Update the assertions in the global sweep scheduler tests, including the
assertion around scheduler._run_once() and the one at the referenced second
location, to compare against _BOOTSTRAP_RETRY_INTERVAL_SECONDS and
_DEFAULT_POLL_INTERVAL_SECONDS from gc_scheduler.py instead of numeric literals.
Import both constants from the scheduler module and preserve the existing
expected-value behavior.
reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py (1)

188-215: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated scope_type check.

Line 190 already rejects any purge whose scope_type is not "user". Line 207 repeats the same comparison inside the composite condition, so it can never be true at that point.

♻️ Proposed simplification
         if (
-            purge_row["scope_type"] != "user"
-            or purge_row["subject_ref"] != subject_ref
+            purge_row["subject_ref"] != subject_ref
             or self._subject_ref_for_user_id(authoritative_user_id) != subject_ref
🤖 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
`@reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py`
around lines 188 - 215, Remove the redundant purge_row["scope_type"] != "user"
comparison from the composite validation condition after the initial guard. Keep
the existing scope validation at the start of the completion flow and preserve
all other authoritative identity checks unchanged.
reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py (1)

92-94: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tighten the _assert_purge_operation_execution_claim_locked MRO hints to a non-optional claim.

All three mixins declare the MRO-provided callable as Callable[[str, PurgeExecutionClaim | None], None]. The implementation in reflexio/server/services/storage/sqlite_storage/governance/_purge.py declares execution_claim: PurgeExecutionClaim, and test_claimed_erasure_mutation_signatures_and_callers_require_claim asserts the public annotation is non-optional. The | None widens the type only for callers inside these three mixins, so Pyright would accept a None claim there without an error, and the failure would surface at runtime inside validate_purge_execution_claim.

  • reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py#L92-L94: change the hint to Callable[[str, PurgeExecutionClaim], None].
  • reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py#L85-L87: change the hint to Callable[[str, PurgeExecutionClaim], None].
  • reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py#L80-L82: change the hint to Callable[[str, PurgeExecutionClaim], None].
♻️ Proposed change (identical at each site)
-    _assert_purge_operation_execution_claim_locked: Callable[
-        [str, PurgeExecutionClaim | None], None
-    ]
+    _assert_purge_operation_execution_claim_locked: Callable[
+        [str, PurgeExecutionClaim], 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
`@reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py`
around lines 92 - 94, Update the MRO-provided
_assert_purge_operation_execution_claim_locked callable annotation to require
PurgeExecutionClaim rather than allowing None in
reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py:92-94,
_rebuild_hide.py:85-87, and _subject_barrier.py:80-82. Keep the callable
signature as Callable[[str, PurgeExecutionClaim], None] at all three sites,
matching the implementation and public contract.
tests/models/test_session_outcome_identity.py (1)

133-135: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Annotate the golden digest, and consider versioning the trajectory shape.

This literal pins the output of trajectory_digest over CanonicalSessionTrajectory. Any field added to CanonicalRequest or CanonicalInteraction, and any change to _canonical_trajectory_json, fails this assertion with an opaque hex mismatch. Add a comment that states what the value covers and what to do when it changes intentionally.

The deeper point: canonical_session_trajectory embeds no schema version, unlike outcome_contract_digest, which hashes an explicit schema_version, and unlike user_playbook_full_version_fingerprint in reflexio/server/services/search_exposure.py lines 103-118, which pins "user-playbook-full-version-v1" and documents the bump requirement. Persisted finalized_trajectory_digest values are compared against freshly computed ones on retry. After a field is added to the canonical shape, every stored digest silently stops matching, and callers receive conflicting_finalization for identical payloads. A schema_version key inside the trajectory payload makes that transition explicit and diagnosable.

♻️ Proposed comment for the golden value
     assert sqlite_projection == postgres_projection
     assert sqlite_projection["requests"][0]["request"]["evaluation_only"] is False
+    # Golden digest over the canonical trajectory shape. Update this value only
+    # together with an intentional change to CanonicalRequest,
+    # CanonicalInteraction, or _canonical_trajectory_json. Stored
+    # finalized_trajectory_digest values do not survive such a change.
     assert trajectory_digest(sqlite_projection) == (
         "73d0f738bb5a3c7668787c678230c4758b68923e12a16e3851b401db780e4272"
     )
🤖 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/models/test_session_outcome_identity.py` around lines 133 - 135,
Annotate the golden digest assertion for trajectory_digest with a comment
describing that it covers CanonicalSessionTrajectory and instructing maintainers
to review and intentionally update the value when the canonical shape or
_canonical_trajectory_json changes. Also add an explicit schema_version to the
canonical_session_trajectory payload, following the existing versioning pattern,
and ensure intentional version bumps make persisted digest changes diagnosable
rather than causing opaque retry conflicts.
tests/server/api_endpoints/test_session_outcomes_integration.py (1)

232-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive session_id from the parameter id, not from the string length.

session_id uses len(stored_metadata). The three current values have lengths 0, 10, and 20000, so they do not collide today. A future parameter whose length equals an existing one reuses the same session_id. The second run would then post against a session that the first run already finalized, and the failure would point at the outcome logic rather than at the duplicate id.

Pass an explicit id alongside each metadata value.

♻️ Proposed fix
 `@pytest.mark.parametrize`(
-    "stored_metadata",
+    ("case_id", "stored_metadata"),
     [
-        pytest.param("", id="empty"),
-        pytest.param("{malformed", id="malformed"),
-        pytest.param("[" * 10_000 + "]" * 10_000, id="pathological-nesting"),
+        pytest.param("empty", "", id="empty"),
+        pytest.param("malformed", "{malformed", id="malformed"),
+        pytest.param(
+            "pathological-nesting",
+            "[" * 10_000 + "]" * 10_000,
+            id="pathological-nesting",
+        ),
     ],
 )
 def test_retry_rejects_invalid_stored_metadata(
-    client_with_org: tuple[TestClient, str], stored_metadata: str
+    client_with_org: tuple[TestClient, str], case_id: str, stored_metadata: str
 ) -> None:
     client, org_id = client_with_org
     storage = get_reflexio(org_id=org_id).get_storage()
-    session_id = f"invalid-stored-metadata-{len(stored_metadata)}"
+    session_id = f"invalid-stored-metadata-{case_id}"
🤖 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/server/api_endpoints/test_session_outcomes_integration.py` around lines
232 - 245, Update test_retry_rejects_invalid_stored_metadata to parameterize an
explicit identifier alongside each stored_metadata value, then derive session_id
from that identifier instead of len(stored_metadata). Preserve the existing
metadata cases and use unique parameter ids such as empty, malformed, and
pathological-nesting to prevent collisions.
reflexio/models/api_schema/domain/entities.py (1)

829-867: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the outcome-identity validation into one shared definition.

The digest validator and the all-or-none identity rule now exist three times:

  • reflexio/models/api_schema/domain/entities.py lines 843-866 (SessionOutcomeRecord)
  • reflexio/models/api_schema/domain/entities.py lines 918-941 (SetSessionOutcomeResponse)
  • reflexio/server/services/storage/storage_base/_session_outcomes.py lines 26-38 (SessionOutcomeWriteResult.__post_init__)

If a fifth identity field is added later, all three copies must change together. A missed copy produces a model that accepts a partial identity. Define the identity tuple and its check once, then reuse it. The two Pydantic models can share a mixin base class; the dataclass can call the same helper function.

♻️ Proposed shared mixin for the two Pydantic models
+class OutcomeIdentityMixin(BaseModel):
+    outcome_id: NonEmptyStr | None = None
+    outcome_revision: int | None = Field(default=None, ge=1)
+    outcome_contract_digest: Sha256Digest | None = None
+    finalized_trajectory_digest: Sha256Digest | None = None
+
+    `@field_validator`("outcome_contract_digest", "finalized_trajectory_digest")
+    `@classmethod`
+    def validate_sha256_digest(cls, value: str | None) -> str | None:
+        if value is not None and (
+            len(value) != 64 or any(char not in "0123456789abcdef" for char in value)
+        ):
+            raise ValueError("outcome identity digests must be lowercase SHA-256 hex")
+        return value
+
+    `@model_validator`(mode="after")
+    def validate_identity_shape(self) -> Self:
+        identity = (
+            self.outcome_id,
+            self.outcome_revision,
+            self.outcome_contract_digest,
+            self.finalized_trajectory_digest,
+        )
+        if any(value is None for value in identity) and not all(
+            value is None for value in identity
+        ):
+            raise ValueError(
+                "outcome identity fields must be all populated or all null"
+            )
+        return self

SessionOutcomeRecord and SetSessionOutcomeResponse then inherit from OutcomeIdentityMixin and drop their local copies. Field order in serialized output changes, so confirm the expected dict in tests/models/test_session_outcome_identity.py lines 160-176 still matches.

🤖 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 `@reflexio/models/api_schema/domain/entities.py` around lines 829 - 867,
Extract the shared outcome identity fields, lowercase SHA-256 digest validation,
and all-or-none identity check from SessionOutcomeRecord and
SetSessionOutcomeResponse into one reusable OutcomeIdentityMixin/helper. Update
both Pydantic models to reuse it and change
SessionOutcomeWriteResult.__post_init__ to call the same shared identity
validator, preserving partial-identity rejection and digest validation. Verify
serialized field ordering and expected dictionaries in the session outcome
identity tests.
tests/server/services/storage/test_storage_contract_clear_user_data.py (1)

176-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The monkeypatch.setenv call has no effect on this test.

The storage fixture is constructed before the test body runs, so setting REFLEXIO_GOVERNANCE_REF_SECRET here does not change any secret the storage instance already resolved. The test also passes "test-governance-secret" explicitly to governance_subject_ref, and clear_user_data deletes by user_id without deriving a subject ref. Remove the monkeypatch parameter and the setenv call, or move the environment setup into the fixture if the storage-derived ref is meant to match.

🤖 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/server/services/storage/test_storage_contract_clear_user_data.py`
around lines 176 - 186, Remove the unused monkeypatch parameter from the
affected test and delete the REFLEXIO_GOVERNANCE_REF_SECRET setenv call; keep
the explicit "test-governance-secret" arguments to governance_subject_ref and
the existing SQLiteStorage setup unchanged.
tests/server/services/storage/sqlite_storage/test_session_outcome_migration.py (1)

199-254: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Derive the expected query count from the chunk size.

row_count = 501 and assert len(trajectory_input_queries) == 4 both encode the chunked() default chunk size implicitly. If that default changes, the assertion fails with no indication of the cause. Import the chunk-size constant (or compute ceil(row_count / chunk_size) * 2) so the bound the test protects is explicit.

🤖 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/server/services/storage/sqlite_storage/test_session_outcome_migration.py`
around lines 199 - 254, Update the migration test around
_migrate_session_outcomes_schema to derive the expected trajectory_input_queries
count from the shared chunk-size constant, accounting for two query types per
chunk (for example, ceil(row_count / chunk_size) * 2). Replace the hard-coded
501/4 relationship while preserving the existing row-count assertion.
tests/server/services/storage/test_storage_contract_session_outcomes.py (1)

100-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the SQLite-only tests with isinstance instead of cast.

This module is a cross-backend storage contract suite typed against BaseStorage. cast(SQLiteStorage, storage) is a static-only assertion. If the storage fixture is later parametrized with Supabase or PostgreSQL, these tests fail with an opaque AttributeError on .conn rather than a clear skip or assertion. tests/server/services/storage/test_storage_contract_clear_user_data.py already uses assert isinstance(sqlite_storage, SQLiteStorage). Apply the same pattern here for the four SQLite-specific tests.

♻️ Proposed change (repeat at each cast site)
-    sqlite_storage = cast(SQLiteStorage, storage)
+    sqlite_storage = storage
+    assert isinstance(sqlite_storage, SQLiteStorage)

Also applies to: 124-131

🤖 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/server/services/storage/test_storage_contract_session_outcomes.py`
around lines 100 - 102, Update the four SQLite-specific tests in this contract
module, including
test_legacy_all_null_identity_exact_retry_uses_available_context, to assert
isinstance(storage, SQLiteStorage) before accessing SQLite-only attributes such
as conn, then use the narrowed storage directly instead of cast. Follow the
guard pattern from test_storage_contract_clear_user_data.py so non-SQLite
backends fail clearly or skip rather than raising an opaque AttributeError.
reflexio/server/services/base_generation_service.py (1)

727-729: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename the billing parameter to match its new meaning.

emit_generation_side_effects now passes plan.billable_count into a parameter still named generated_count. The mixin docstring already describes it as "retained write-plan learnings". The name now contradicts the value at every call site and invites a future caller to pass raw generated counts again. Rename the keyword in _record_billing_learning_events to billable_count and update the two call sites.

♻️ Proposed rename at this call site
         self._record_billing_learning_events(
-            prepared=plan.prepared, generated_count=plan.billable_count
+            prepared=plan.prepared, billable_count=plan.billable_count
         )

Apply the matching signature change in reflexio/server/services/base_generation/_usage_billing.py:

def _record_billing_learning_events(
    self, *, prepared: "PreparedGenerationRun[Any]", billable_count: int
) -> None:
    ...
    record_learnings_generated(
        org_id=ctx["org_id"],
        count=billable_count,
        ...
    )

Also update tests/server/services/test_generation_billing_emission.py line 493.

🤖 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 `@reflexio/server/services/base_generation_service.py` around lines 727 - 729,
Rename the _record_billing_learning_events keyword parameter from
generated_count to billable_count, use billable_count when calling
record_learnings_generated, and update both call sites, including
emit_generation_side_effects and the affected billing emission test.
tests/server/test_usage_metrics.py (1)

31-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add fail-open coverage for record_usage_event on the same two inputs.

This test proves that a legacy recorder returning None yields UNKNOWN from record_usage_event_strict. That behavior is correct for the strict API. The untested consequence is on the non-strict API: record_usage_event now funnels the same UNKNOWN into its except handler and emits a warning for every event. See the issue raised on reflexio/server/usage_metrics.py Lines 118-157.

Add two cases that pin the intended fail-open contract:

  • record_usage_event with no recorder configured must not log a warning.
  • record_usage_event with a None-returning recorder must deliver the event and must not log a warning.

Use caplog at WARNING level to assert the absence of records.

💚 Proposed tests
def test_ordinary_recording_is_silent_without_a_recorder(caplog):
    usage_metrics.configure_usage_event_recorder(None)

    with caplog.at_level("WARNING", logger=usage_metrics.logger.name):
        usage_metrics.record_usage_event(
            org_id="7",
            event_name="search_request",
            event_category="application",
        )

    assert caplog.records == []


def test_ordinary_recording_is_silent_for_legacy_none_recorder(caplog):
    captured = []
    usage_metrics.configure_usage_event_recorder(captured.append)
    try:
        with caplog.at_level("WARNING", logger=usage_metrics.logger.name):
            usage_metrics.record_usage_event(
                org_id="7",
                event_name="search_request",
                event_category="application",
                event_key="search:1",
            )
    finally:
        usage_metrics.configure_usage_event_recorder(None)

    assert [event.event_key for event in captured] == ["search:1"]
    assert caplog.records == []
🤖 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/server/test_usage_metrics.py` around lines 31 - 46, Add fail-open
coverage in tests/server/test_usage_metrics.py for record_usage_event: add one
test with no recorder configured and another with a legacy recorder returning
None, using caplog at WARNING level to assert no warnings are emitted; verify
the latter still delivers the event and always reset the recorder configuration.
reflexio/server/services/playbook/service.py (1)

661-735: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the shared receipt-finalization flow.

This method duplicates ProfileGenerationService._finalize_extracted_items_with_outcome in reflexio/server/services/profile/service.py almost line for line: the pre-transaction receipt read, the in-transaction re-read, the persist call, the save_agent_run_finalization_receipt insert, the _FinalizationReceiptAlreadyExistsError rollback signal, and the "receipt disappeared" guard. Only two things differ: the entity_type constant, the point where learning ids become available, and the playbook-only scheduler dispatch.

The receipt protocol is the correctness-critical part of this PR. Two copies mean a future fix to one copy silently misses the other. Extract the protocol into one helper on the base service or a mixin, parameterized by entity_type, an id-extraction callback, and an optional post-commit hook.

🤖 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 `@reflexio/server/services/playbook/service.py` around lines 661 - 735, Extract
the duplicated receipt protocol from _finalize_extracted_items_with_outcome and
ProfileGenerationService._finalize_extracted_items_with_outcome into one shared
base-service or mixin helper. Parameterize it with entity_type, a callback that
derives learning IDs after _persist_write_plan, and an optional post-commit hook
for playbook scheduler dispatch; preserve the pre-transaction read,
transactional re-read/persist/receipt insert, conflict rollback signal, and
disappeared-receipt guard while making both services use the shared flow.
tests/server/test_billing_meter.py (1)

123-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the UNKNOWN mapping.

The parametrization covers FAILED and REJECTED. It does not cover the except Exception branch at Line 489 of billing_meter.py, which maps any non-UsageEventDeliveryError failure to ReceiptBillingDeliveryError(UsageEventDeliveryStatus.UNKNOWN).

That status decides the retry class in _finalization_failure_status: REJECTED terminates the run, everything else retries. Add a case that raises a plain RuntimeError from record_usage_event_strict and asserts the resulting status is UNKNOWN.

🤖 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/server/test_billing_meter.py` around lines 123 - 146, Extend
test_strict_receipt_billing_preserves_delivery_status to cover a plain
RuntimeError from record_usage_event_strict, alongside the existing
UsageEventDeliveryError cases. Assert that
emit_learnings_generated_records_strict raises ReceiptBillingDeliveryError with
status UsageEventDeliveryStatus.UNKNOWN.
reflexio/server/billing_meter.py (1)

268-288: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type record_event instead of using Any.

record_event: Any disables signature checking on the only call in this helper. record_usage_event and record_usage_event_strict must keep identical keyword parameters for both call paths to work. A Protocol or Callable alias makes drift a type error.

♻️ Proposed typing
+class _UsageEventRecorder(Protocol):
+    def __call__(
+        self,
+        *,
+        org_id: str,
+        event_name: str,
+        event_category: str,
+        pipeline: str | None,
+        user_id: str | None,
+        request_id: str | None,
+        session_id: str | None,
+        source: str | None,
+        agent_version: str | None,
+        playbook_name: str | None,
+        entity_type: str | None,
+        entity_id: str | None,
+        event_key: str | None,
+        count_value: int,
+        platform_llm: bool | None,
+        platform_storage: bool | None,
+        caller_type: str | None,
+        metadata: Mapping[str, Any] | None,
+        created_at: float | None,
+    ) -> Any: ...
+
+
 def _record_learnings_generated_records(
     *,
-    record_event: Any,
+    record_event: _UsageEventRecorder,
🤖 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 `@reflexio/server/billing_meter.py` around lines 268 - 288, The
_record_learnings_generated_records helper currently accepts record_event as
Any, bypassing signature validation. Define a Protocol or Callable type
describing the exact keyword arguments used by its record_event call, and apply
it to record_event so both record_usage_event and record_usage_event_strict
remain compatible and parameter drift is caught by type checking.
reflexio/server/services/profile/service.py (1)

360-422: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the shared receipt-fencing flow.

This method duplicates PlaybookGenerationService._finalize_extracted_items_with_outcome almost line for line: the pre-scope receipt read, the in-scope receipt re-read, the persist call, save_agent_run_finalization_receipt, the _FinalizationReceiptAlreadyExistsError conflict rollback, and the post-conflict re-read that raises RuntimeError when the receipt is missing.

Only three parts differ: the entity_type constant, when learning_ids are derived (before persist for profiles, after persist for playbooks), and the playbook scheduler dispatch. A shared helper that takes entity_type, a persist callback, and a learning-id callback would remove the copy. This is a money and durability path, so divergence between the two copies is expensive.

🤖 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 `@reflexio/server/services/profile/service.py` around lines 360 - 422, Extract
the duplicated receipt-fencing logic from profile
`_finalize_extracted_items_with_outcome` and
`PlaybookGenerationService._finalize_extracted_items_with_outcome` into a shared
helper. Parameterize the helper with `entity_type`, a persist callback, and a
learning-ID callback so profiles can derive IDs before persistence while
playbooks derive them afterward; keep playbook scheduler dispatch outside the
helper. Preserve the pre-scope and in-scope receipt checks, conflict rollback,
post-conflict receipt reread, and missing-receipt `RuntimeError` behavior
exactly.
🤖 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 `@reflexio/server/api.py`:
- Around line 475-481: Move the cleanup scope in the startup flow to begin
immediately after configure_usage_event_recorder(exempt_usage_event_recorder),
before run_startup_config_guards, data-plane initialization, or scheduler
startup can raise. Ensure every failure after configuring the recorder clears or
restores the global recorder, including failures occurring before the existing
finally block.

In `@reflexio/server/billing_meter.py`:
- Around line 398-411: Update the docstrings for emit_learnings_generated,
record_learnings_generated, and the ordinary emitter described near the
learning-ID parameters so they no longer claim resumable finalization uses the
non-strict helpers. Document ExtractionResumeWorker._record_finalized_learnings
as using emit_learnings_generated_records_strict, while preserving the existing
ownership and fail-open semantics for the ordinary paths.

In `@reflexio/server/services/extraction/resume_worker.py`:
- Around line 943-956: Align _record_finalized_learnings with the
FinalizationResult contract by either removing billing from the winner-only
side-effects wording in FinalizationResult’s docstring, or gating its invocation
on result.won_receipt; preserve the intended idempotent billing replay behavior
indicated by the existing test. Move emit_learnings_generated_records_strict
from the method-local import into the module-level billing_meter import
alongside ReceiptBillingDeliveryError.
- Around line 93-106: Update _finalization_failure_status so
ReceiptBillingDeliveryError cases other than REJECTED also check
next_attempt_count against max_finalization_attempts before returning
FINALIZATION_FAILED. Preserve immediate FAILED status for REJECTED, and return
FAILED when the attempt ceiling is reached for UNKNOWN and other non-rejected
receipt failures.

In `@reflexio/server/services/search_exposure.py`:
- Around line 172-183: Sort the exported names in __all__ alphabetically using
the project formatter, updating only the ordering in this list while preserving
all existing exports.

In `@reflexio/server/services/storage/session_outcome_identity.py`:
- Around line 11-17: Sort the exported names in __all__ alphabetically to
satisfy Ruff RUF022, preserving all existing entries and their spellings.
- Around line 165-188: The _canonical_trajectory_json function needs a
maximum-depth guard while recursively encoding mappings, tuples, and lists.
Track recursion depth and raise the established validation error when persisted
interaction JSON exceeds the permitted depth, ensuring trajectory_digest and
session-outcome finalization cannot hit RecursionError.

In `@reflexio/server/services/storage/sqlite_storage/_base.py`:
- Around line 1183-1187: Update the outcome_revision value in the INSERT
construction around the existing column-presence check to fall back to revision
1 when the column is missing or row["outcome_revision"] is NULL. Preserve
non-NULL stored revisions while ensuring legacy rows always satisfy the database
constraints.

In `@reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py`:
- Around line 27-34: Update the docstring for get_agent_run_finalization_receipt
to explicitly define the return contract: None means no receipt exists and
finalization should proceed, while an empty list means a receipt was committed
despite producing no durable learnings and must be treated as finalized.
Preserve this distinction across all backend implementations.

In `@reflexio/server/usage_metrics.py`:
- Around line 118-157: The fail-open wrapper currently logs warnings for
expected UNKNOWN delivery outcomes. In reflexio/server/usage_metrics.py lines
118-157, update record_usage_event to catch UsageEventDeliveryError separately,
suppress warnings when exc.status is UNKNOWN, log other statuses, add
exc_info=True to the generic recorder-exception handler, and document that
missing or legacy recorders are non-errors; in
tests/server/test_usage_metrics.py lines 31-46, add caplog tests covering both
no configured recorder and a legacy recorder returning None, verifying no
warning is emitted and the event reaches the legacy recorder.

In `@tests/server/api_endpoints/test_session_outcomes_integration.py`:
- Line 188: Update the retry response assertion to validate that
retry.json()["reason"] is None rather than asserting the key is absent. Keep the
existing conflict assertion that accesses the reason key so both tests verify
the response value consistently.

In `@tests/server/services/test_non_extraction_learning_metering.py`:
- Around line 45-51: Reset the module-level usage-event recorder after every
test in this file. Add an autouse pytest fixture near _capture_events that
yields for the test and calls configure_usage_event_recorder(None) in a finally
block, ensuring cleanup also occurs when assertions fail; keep _capture_events
responsible only for installing its recorder.

---

Nitpick comments:
In `@reflexio/models/api_schema/domain/entities.py`:
- Around line 829-867: Extract the shared outcome identity fields, lowercase
SHA-256 digest validation, and all-or-none identity check from
SessionOutcomeRecord and SetSessionOutcomeResponse into one reusable
OutcomeIdentityMixin/helper. Update both Pydantic models to reuse it and change
SessionOutcomeWriteResult.__post_init__ to call the same shared identity
validator, preserving partial-identity rejection and digest validation. Verify
serialized field ordering and expected dictionaries in the session outcome
identity tests.

In `@reflexio/server/billing_meter.py`:
- Around line 268-288: The _record_learnings_generated_records helper currently
accepts record_event as Any, bypassing signature validation. Define a Protocol
or Callable type describing the exact keyword arguments used by its record_event
call, and apply it to record_event so both record_usage_event and
record_usage_event_strict remain compatible and parameter drift is caught by
type checking.

In `@reflexio/server/services/base_generation_service.py`:
- Around line 727-729: Rename the _record_billing_learning_events keyword
parameter from generated_count to billable_count, use billable_count when
calling record_learnings_generated, and update both call sites, including
emit_generation_side_effects and the affected billing emission test.

In `@reflexio/server/services/governance/service.py`:
- Around line 418-449: Remove the tautological purge_id entry from
expected_identity in _matching_user_erasure_purge_for_retry, while preserving
validation of operation_type, scope_type, subject_ref, and request_ref and the
authoritative-user check.

In `@reflexio/server/services/playbook/service.py`:
- Around line 661-735: Extract the duplicated receipt protocol from
_finalize_extracted_items_with_outcome and
ProfileGenerationService._finalize_extracted_items_with_outcome into one shared
base-service or mixin helper. Parameterize it with entity_type, a callback that
derives learning IDs after _persist_write_plan, and an optional post-commit hook
for playbook scheduler dispatch; preserve the pre-transaction read,
transactional re-read/persist/receipt insert, conflict rollback signal, and
disappeared-receipt guard while making both services use the shared flow.

In `@reflexio/server/services/profile/service.py`:
- Around line 360-422: Extract the duplicated receipt-fencing logic from profile
`_finalize_extracted_items_with_outcome` and
`PlaybookGenerationService._finalize_extracted_items_with_outcome` into a shared
helper. Parameterize the helper with `entity_type`, a persist callback, and a
learning-ID callback so profiles can derive IDs before persistence while
playbooks derive them afterward; keep playbook scheduler dispatch outside the
helper. Preserve the pre-scope and in-scope receipt checks, conflict rollback,
post-conflict receipt reread, and missing-receipt `RuntimeError` behavior
exactly.

In
`@reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py`:
- Around line 92-94: Update the MRO-provided
_assert_purge_operation_execution_claim_locked callable annotation to require
PurgeExecutionClaim rather than allowing None in
reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py:92-94,
_rebuild_hide.py:85-87, and _subject_barrier.py:80-82. Keep the callable
signature as Callable[[str, PurgeExecutionClaim], None] at all three sites,
matching the implementation and public contract.

In `@reflexio/server/services/storage/sqlite_storage/governance/_purge.py`:
- Around line 449-497: Consolidate the duplicated validation in
assert_purge_operation_execution_claim and
_assert_purge_operation_execution_claim_locked by having the locked method
delegate to the shared assertion logic while preserving its locked row-fetch
behavior. Extract or reuse a common row-check helper so both paths apply the
same five claim conditions, with only the database fetch mechanism differing.

In
`@reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py`:
- Around line 188-215: Remove the redundant purge_row["scope_type"] != "user"
comparison from the composite validation condition after the initial guard. Keep
the existing scope validation at the start of the completion flow and preserve
all other authoritative identity checks unchanged.

In `@tests/models/test_session_outcome_identity.py`:
- Around line 133-135: Annotate the golden digest assertion for
trajectory_digest with a comment describing that it covers
CanonicalSessionTrajectory and instructing maintainers to review and
intentionally update the value when the canonical shape or
_canonical_trajectory_json changes. Also add an explicit schema_version to the
canonical_session_trajectory payload, following the existing versioning pattern,
and ensure intentional version bumps make persisted digest changes diagnosable
rather than causing opaque retry conflicts.

In `@tests/server/api_endpoints/test_session_outcomes_integration.py`:
- Around line 232-245: Update test_retry_rejects_invalid_stored_metadata to
parameterize an explicit identifier alongside each stored_metadata value, then
derive session_id from that identifier instead of len(stored_metadata). Preserve
the existing metadata cases and use unique parameter ids such as empty,
malformed, and pathological-nesting to prevent collisions.

In `@tests/server/services/governance/test_governance_local_e2e.py`:
- Around line 963-966: Update release_winner_on_duplicate_poll to use
time.sleep(0.001) instead of creating and waiting on a throwaway
threading.Event, and add the required time import at the file’s imports.
- Around line 1052-1110: Document the fake-clock invariant in
test_healthy_slow_lifecycle_renews_lease_and_duplicate_converges: state that the
claim begins at 100 with a 300-second lease expiring at 400, three renewals
extend it to 650, and advancing to 401 keeps the duplicate caller within the
live lease. Place the comment next to the relevant fake_now updates and renewal
wait.
- Around line 1044-1049: Update the backoff assertions in the governance erase
retry test to derive expected initial, maximum, and total delays from
governance_service_module’s _DUPLICATE_ERASE_POLL_SECONDS,
_DUPLICATE_ERASE_MAX_POLL_SECONDS, and _DUPLICATE_ERASE_WAIT_SECONDS constants.
Preserve the existing checks for doubling behavior, maximum delay, and total
wait bound without hardcoded duplicate values.

In `@tests/server/services/governance/test_subject_write_barrier_sqlite.py`:
- Around line 622-629: Remove the redundant
get_subject_write_barrier(subject_ref) call in this test, keeping a single
barrier fetch and its associated non-null and status assertions; preserve the
stored_purge retrieval and its independent assertions.
- Around line 42-62: Add a concise comment in `_claim_purge` documenting that it
deliberately expires and steals any existing execution lease, increments the
fence, and makes claims from earlier calls stale; state that callers must use
each returned claim before invoking `_claim_purge` again.

In `@tests/server/services/lineage/test_gc_scheduler_global_sweep.py`:
- Around line 30-36: Update the fixture cleanup to import and call
gc_scheduler.clear_always_global_sweeps directly, removing the getattr guard and
clear_always variable; likewise import register_always_global_sweep directly
where the fixture or tests use it, so missing helpers fail loudly while
preserving cleanup before and after each test.
- Line 143: Update the assertions in the global sweep scheduler tests, including
the assertion around scheduler._run_once() and the one at the referenced second
location, to compare against _BOOTSTRAP_RETRY_INTERVAL_SECONDS and
_DEFAULT_POLL_INTERVAL_SECONDS from gc_scheduler.py instead of numeric literals.
Import both constants from the scheduler module and preserve the existing
expected-value behavior.

In `@tests/server/services/storage/sqlite_storage/test_governance_storage.py`:
- Around line 1677-1704: Strengthen the production scan in the test around
production_root by asserting that the derived root exists and is a directory
before scanning. Track the files yielded by production_root.rglob("*.py") and
assert that the scan visited at least one file, while preserving the existing
violation checks.
- Around line 218-238: Document the lease-takeover behavior in the test helper
_claim_purge: add a docstring stating that when a live lease exists it
force-expires that lease, reclaims the purge, and invalidates previously issued
claims for the same purge_id. Keep the existing claiming logic unchanged.

In
`@tests/server/services/storage/sqlite_storage/test_session_outcome_migration.py`:
- Around line 199-254: Update the migration test around
_migrate_session_outcomes_schema to derive the expected trajectory_input_queries
count from the shared chunk-size constant, accounting for two query types per
chunk (for example, ceil(row_count / chunk_size) * 2). Replace the hard-coded
501/4 relationship while preserving the existing row-count assertion.

In `@tests/server/services/storage/test_storage_contract_clear_user_data.py`:
- Around line 176-186: Remove the unused monkeypatch parameter from the affected
test and delete the REFLEXIO_GOVERNANCE_REF_SECRET setenv call; keep the
explicit "test-governance-secret" arguments to governance_subject_ref and the
existing SQLiteStorage setup unchanged.

In `@tests/server/services/storage/test_storage_contract_session_outcomes.py`:
- Around line 100-102: Update the four SQLite-specific tests in this contract
module, including
test_legacy_all_null_identity_exact_retry_uses_available_context, to assert
isinstance(storage, SQLiteStorage) before accessing SQLite-only attributes such
as conn, then use the narrowed storage directly instead of cast. Follow the
guard pattern from test_storage_contract_clear_user_data.py so non-SQLite
backends fail clearly or skip rather than raising an opaque AttributeError.

In `@tests/server/test_billing_meter.py`:
- Around line 123-146: Extend
test_strict_receipt_billing_preserves_delivery_status to cover a plain
RuntimeError from record_usage_event_strict, alongside the existing
UsageEventDeliveryError cases. Assert that
emit_learnings_generated_records_strict raises ReceiptBillingDeliveryError with
status UsageEventDeliveryStatus.UNKNOWN.

In `@tests/server/test_usage_metrics.py`:
- Around line 31-46: Add fail-open coverage in
tests/server/test_usage_metrics.py for record_usage_event: add one test with no
recorder configured and another with a legacy recorder returning None, using
caplog at WARNING level to assert no warnings are emitted; verify the latter
still delivers the event and always reset the recorder configuration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

Comment thread reflexio/server/api.py
Comment thread reflexio/server/billing_meter.py Outdated
Comment thread reflexio/server/services/extraction/resume_worker.py
Comment thread reflexio/server/services/extraction/resume_worker.py
Comment thread reflexio/server/services/search_exposure.py
Comment thread reflexio/server/services/storage/sqlite_storage/_base.py
Comment thread reflexio/server/usage_metrics.py
Comment thread tests/server/api_endpoints/test_session_outcomes_integration.py Outdated
Comment thread tests/server/services/test_non_extraction_learning_metering.py
@guangyu-reflexio

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 1

🤖 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 `@reflexio/server/services/storage/session_outcome_identity.py`:
- Around line 167-183: Update _canonical_trajectory_json so list, tuple, and
mapping containers are rejected when depth >=
MAX_CANONICAL_TRAJECTORY_JSON_DEPTH, before traversing their contents; retain
scalar handling at the existing boundary. Add a regression test covering nested
empty containers and verify the same limit applies consistently to sequences and
mappings.
🪄 Autofix

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: CHILL

Plan: Pro

Run ID: 05dad9a2-62a0-4c36-be08-59c34adc074e

📥 Commits

Reviewing files that changed from the base of the PR and between 474d7ce and 90ffba3.

📒 Files selected for processing (15)
  • reflexio/server/api.py
  • reflexio/server/billing_meter.py
  • reflexio/server/services/deferred_learning_plan.py
  • reflexio/server/services/extraction/resume_worker.py
  • reflexio/server/services/search_exposure.py
  • reflexio/server/services/storage/session_outcome_identity.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py
  • reflexio/server/usage_metrics.py
  • tests/models/test_session_outcome_identity.py
  • tests/server/api_endpoints/test_session_outcomes_integration.py
  • tests/server/services/extraction/test_resume_worker.py
  • tests/server/services/storage/sqlite_storage/test_session_outcome_migration.py
  • tests/server/test_create_app_capabilities.py
  • tests/server/test_usage_metrics.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • reflexio/server/api.py
  • reflexio/server/usage_metrics.py
  • reflexio/server/services/deferred_learning_plan.py
  • tests/server/test_usage_metrics.py
  • tests/models/test_session_outcome_identity.py
  • reflexio/server/services/search_exposure.py
  • reflexio/server/billing_meter.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • tests/server/api_endpoints/test_session_outcomes_integration.py
  • tests/server/services/extraction/test_resume_worker.py

Comment thread reflexio/server/services/storage/session_outcome_identity.py
@guangyu-reflexio

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Persist non-empty direct user-playbook search results through the canonical synchronous exposure boundary before metering or response release.
Align direct user-playbook search admission with the 100-event exposure contract and publish the bound through shared client metadata.
@guangyu-reflexio

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Actionable comments posted: 1

🤖 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 `@reflexio/server/README.md`:
- Around line 103-104: Update create_app to register a default
SEARCH_EXPOSURE_RECORDER for every authenticated deployment, ensuring
record_search_exposures persists exposure evidence before successful responses.
If universal registration is not intended, revise the README guarantee to
explicitly apply only to deployments that configure the recorder.
🪄 Autofix

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: CHILL

Plan: Pro

Run ID: 2cf8534c-559c-4d1e-89fd-35f1a162c365

📥 Commits

Reviewing files that changed from the base of the PR and between febac98 and ace5b99.

📒 Files selected for processing (9)
  • docs/lib/methods/user-playbooks.ts
  • reflexio/client/client.py
  • reflexio/models/api_schema/retriever_schema.py
  • reflexio/server/README.md
  • reflexio/server/routes/search.py
  • reflexio/server/services/README.md
  • reflexio/server/services/search_exposure.py
  • tests/client/test_search.py
  • tests/server/routes/test_search_exposure_boundary.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • reflexio/client/client.py
  • tests/client/test_search.py
  • reflexio/models/api_schema/retriever_schema.py
  • reflexio/server/services/search_exposure.py

Comment thread reflexio/server/README.md Outdated
Clarify that authenticated enterprise capability wiring supplies the fail-closed recorder while shared create_app intentionally has no default implementation.
@guangyu-reflexio

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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