Skip to content

feat(playbook): schedule bounded incremental aggregation - #405

Merged
yyiilluu merged 7 commits into
mainfrom
codex/review-deduplication-repair-plan
Aug 3, 2026
Merged

feat(playbook): schedule bounded incremental aggregation#405
yyiilluu merged 7 commits into
mainfrom
codex/review-deduplication-repair-plan

Conversation

@yyiilluu

@yyiilluu yyiilluu commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace per-generation, full-corpus playbook aggregation with a durable signal and bounded scheduled work.
  • Make newly armed work eligible immediately; apply the configured one-hour minimum only after a version drains successfully.
  • Keep automatic runs efficient beyond REFLEXIO_MAX_CLUSTERING_PLAYBOOKS by processing a bounded row budget rather than treating it as a corpus ceiling.
  • Match new playbooks only to compatible centroids for the same agent version, then cluster unmatched residuals without rereading or reclustering the full corpus.

Changes

Scheduling and execution

  • Convert the post-generation trigger into an idempotent durable scheduling write.
  • Add per-organization claims, database-time leases, fencing, retry handling, and backlog-aware continuation.
  • Continue promptly while backlog remains; after a drained success, wait at least REFLEXIO_AGGREGATION_MIN_INTERVAL_SECONDS (default: one hour).
  • Keep the administrative full-rerun path capped and make it honor the same configured minimum interval.

Incremental aggregation

  • Add bounded intake, same-version nearest-centroid attachment, residual clustering, stable cluster identity, and centroid maintenance.
  • Use agglomerative clustering for residual batches below 50 rows and HDBSCAN for larger batches.
  • Treat REFLEXIO_MAX_CLUSTERING_PLAYBOOKS (default: 20,000) as the maximum rows admitted to one scheduled unit of work, including intake and invalidation repair; it is also the fail-before-mutation cap for an administrative full rerun.
  • Distinguish generated, semantic-null, retryable-failure, and missing-embedding outcomes so healthy effects commit while only unfinished members remain pending.
  • Reuse shared prompt context per batch and avoid repeated full-corpus reads or quadratic prompt grouping.

Durable storage

  • Add backend-neutral contracts for claims, backlog discovery, clusters, item dispositions, lifecycle invalidations, and atomic effects.
  • Implement the contracts for SQLite, including vector-index dirty repair and pre-delete invalidation capture.
  • Capture archive, revise, merge, status, purge, and delete changes without allowing stale cluster membership to survive.
  • Group SQLite merge invalidations by source agent version so mixed-version merges arm every affected version.
  • Filter SQLite ANN candidates for model, dimension, version, and active state before applying the nearest-neighbor limit.
  • Preserve the pending-invalidation partial index across repeated SQLite storage initialization instead of dropping and recreating it.\n- Skip empty legacy fingerprints so they cannot become active clusters without a centroid or vector-index row.

Documentation and compatibility

  • Rewrite the playbook and server READMEs as code maps for the scheduler, storage contracts, cadence, budget semantics, version isolation, failure dispositions, and extension points.
  • Isolate the durable-learning transaction regression test from the local scheduler thread so the test measures the transaction boundary deterministically.
  • Rebase on current OSS main through receipt-finalization PR fix: make receipt finalization winner-aware #408 while preserving the invariant from feat: add open-world evidence foundation #407 that aggregation does not create a second learning charge.
  • Calculate finite profile TTLs in UTC so crossing a local daylight-saving transition does not add or subtract an hour.

Flow

flowchart LR
    A["User playbook committed"] --> B["Durable state due now"]
    B --> C["Scheduler claim"]
    C --> D["Bounded intake for one agent version"]
    D --> E{"Compatible centroid match?"}
    E -->|Yes| F["Attach and update centroid"]
    E -->|No| G["Durable residual"]
    G --> H["Bounded residual clustering"]
    H --> I["Generate agent playbook"]
    F --> J["Atomic fenced commit"]
    I --> J
    J --> K{"Backlog remains?"}
    K -->|Yes| C
    K -->|No| L["One-hour minimum before next drained run"]
Loading

Test Plan

  • uv run ruff check and uv run ruff format --check passed across the complete OSS source and test tree (879 files).
  • Pyright on all changed Python paths: 0 errors.
  • CodeRabbit regression coverage: 40 focused storage/profile tests passed, including mixed-version merge invalidation, repeated initialization, ANN limit/version isolation, empty legacy-cluster adoption, and guaranteed timezone restoration.
  • Exact OSS CI unit command: 4,401 passed, 9 skipped, 6 subtests passed.
  • OSS E2E suite: 47 passed, 51 skipped.
  • A production-like companion self-host/native-Postgres run completed all 17 launch phases against an isolated database and real MiniMax LLM calls: 16 phases passed. Aggregation scheduling, profile/playbook generation, aggregation, search, cleanup, and evaluation all passed. The resumable-extraction phase completed its suspend/resume lifecycle but its evidence reviewer rejected the generated candidate, so no durable playbook was saved; this was reproduced twice on the companion checkout's currently pinned pre-evidence revision and is not counted as a pass.
  • The live run did not exercise the four SQLite-only CodeRabbit fixes; those are covered by the focused SQLite regression suite above.

Summary by CodeRabbit

  • New Features

    • Added durable, incremental playbook aggregation with scheduling, retries, progress tracking, and safe coordination.
    • Playbook updates now trigger bounded, resumable, version-scoped aggregation with invalidation handling and full-rerun support.
    • Added configurable minimum aggregation intervals and automatic local scheduling where supported.
    • Improved clustering with bounded processing, embedding-based matching, stable results, and safer large-cluster handling.
  • Documentation

    • Updated storage and aggregation documentation for durable scheduling and incremental processing.
  • Bug Fixes

    • Improved timestamp handling for profile expiration across timezone and daylight-saving changes.
  • Tests

    • Expanded coverage for scheduling, retries, invalidations, clustering, reruns, and failure handling.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change replaces direct background aggregation with durable, lease-based scheduling. It adds SQLite aggregation state, bounded incremental processing, invalidation handling, fenced reruns, application lifecycle wiring, configuration, documentation, and integration coverage.

Changes

Playbook aggregation pipeline

Layer / File(s) Summary
Aggregation storage contracts and SQLite state
reflexio/server/services/storage/...
Adds durable aggregation contracts, SQLite tables, leases, invalidations, cluster state, retry metadata, and vector-index operations.
Scheduling, leasing, and application lifecycle
reflexio/server/services/playbook/aggregation_scheduler.py, reflexio/server/services/playbook/aggregation_trigger.py, reflexio/server/api.py, reflexio/lib/_generation.py
Replaces background dispatch with persisted scheduling, fenced claims, lease heartbeats, retries, and managed scheduler startup and shutdown.
Bounded aggregation and rerun processing
reflexio/server/services/playbook/components/aggregator.py, reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
Adds bounded incremental processing, residual handling, cluster maintenance, typed generation outcomes, ranked prompt context, and fenced rerun snapshots.
Integration coverage and documentation
tests/server/services/storage/test_playbook_aggregation_state_integration.py, tests/server/services/playbook/test_aggregation_scheduler.py, tests/server/services/playbook/test_playbook_aggregator.py, reflexio/server/services/playbook/README.md, .env.example
Adds coverage for claims, retries, invalidations, vector clusters, incremental runs, legacy adoption, rerun snapshots, and the durable aggregation configuration and flow.

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

Possibly related PRs

Suggested reviewers: yilu331

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.80% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's primary change: scheduling bounded incremental playbook aggregation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/review-deduplication-repair-plan

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

@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

Caution

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

⚠️ Outside diff range comments (1)
reflexio/server/services/playbook/components/aggregator.py (1)

957-974: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Emit a failure event for the safety-cap abort.

This branch now raises instead of returning skip stats, but it still records outcome="should_skip" on aggregation_gate_evaluated. The raise also happens before the try block at line 1084, so no aggregation_failed event is emitted. An oversized rerun therefore reports as a skip in telemetry while the caller sees an exception, and the scheduler or admin route marks the claim failed.

Record the abort as a failure so telemetry matches the control flow.

🤖 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/components/aggregator.py` around lines 957
- 974, The safety-cap branch in the aggregation gate currently records
outcome="should_skip" even though it raises and fails the claim. Update the
record_usage_event call in this branch to emit the appropriate failure
event/outcome, ensuring oversized reruns are represented as failures despite
occurring before the try block.
🧹 Nitpick comments (15)
tests/server/services/storage/test_playbook_aggregation_state_integration.py (5)

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

Extract the sqlite-vec skip guard into a fixture.

The pattern if not store._has_sqlite_vec: pytest.skip(...) is repeated at lines 205, 382, 426, 479, 592, 684, 768, and 838. It also reaches into a private attribute from test code. Replace it with one autouse-free helper fixture that builds the store and skips once.

♻️ Proposed helper
`@pytest.fixture`
def vec_store(tmp_path) -> SQLiteStorage:
    store = _store(tmp_path)
    if not store._has_sqlite_vec:
        pytest.skip("sqlite-vec is unavailable")
    return store

Then each vector test takes vec_store instead of tmp_path plus the inline guard.

🤖 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_playbook_aggregation_state_integration.py`
around lines 380 - 383, Introduce a shared `vec_store` fixture that creates the
storage via `_store(tmp_path)`, skips when sqlite-vec is unavailable, and
returns the validated store. Update all affected vector tests, including
`test_sqlite_vec_centroid_lookup_and_attachment`, to depend on `vec_store` and
remove their `tmp_path` setup and inline `_has_sqlite_vec` guards.

342-377: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use keyword arguments for PlaybookAggregationBacklog at line 188.

Line 370 constructs PlaybookAggregationBacklog with keywords, but line 188 uses the positional form PlaybookAggregationBacklog(0, 0, 0). The positional call binds to declaration order. If a field is reordered or inserted before invalidations, line 188 still compiles and silently means a different backlog. That test relies on the backlog being empty so pending is False; a wrong binding would flip its meaning without an obvious failure.

♻️ Proposed change at line 188
-        backlog=PlaybookAggregationBacklog(0, 0, 0),
+        backlog=PlaybookAggregationBacklog(
+            undisposed=0, residual=0, invalidations=0
+        ),
🤖 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_playbook_aggregation_state_integration.py`
around lines 342 - 377, Update the positional PlaybookAggregationBacklog
construction in the affected test to use explicit keyword arguments for each
zero-valued field, matching the keyword-based construction in
test_dirty_cluster_keeps_aggregation_pending. Preserve the assertion that an
empty backlog has pending set to False.

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

Document that line 903 relies on a database trigger.

Line 903 deletes the row with raw SQL and bypasses delete_user_playbook. Line 927 still expects a ("hard_delete", 1) invalidation. That row can only be produced by a SQL trigger, not by the Python method. The dependency is load-bearing but implicit.

Add a short comment at line 903 stating that the raw delete exercises the trigger path. Without it, a later change to delete_user_playbook looks safe while this assertion quietly proves something different from what a reader expects.

🤖 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_playbook_aggregation_state_integration.py`
around lines 888 - 928, Add a concise inline comment immediately before the raw
SQL DELETE in test_rerun_snapshot_finishes_only_materialized_work, explicitly
noting that the direct deletion intentionally exercises the database trigger
path that creates the hard_delete invalidation. Leave the test behavior and
assertions unchanged.

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

Move the repeated function-local imports to module scope.

from unittest.mock import MagicMock and import json are imported inside the test body here and repeated in test_incremental_run_creates_once_then_attaches_without_replacement (lines 589, 596), test_incremental_run_commits_healthy_clusters_when_one_llm_outcome_fails (lines 681, 688), and test_fenced_full_rerun_rebuilds_typed_cluster_state (lines 835, 842). Neither import is optional or cycle-breaking. Hoist both to the module header with the other imports.

The fencing assertion itself is correct: replacement bumps the fence past stale, so the run must abort before any agent_playbooks row is written.

🤖 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_playbook_aggregation_state_integration.py`
around lines 199 - 249, Move the function-local imports of MagicMock and json
used by test_stale_claim_cannot_commit_incremental_effect and the other named
tests to the module-level import section, alongside the existing imports. Remove
the duplicated in-function import statements while preserving all test behavior
and the fencing assertions.

60-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the AST scan robust to module-scope calls and to rglob ordering.

Two fragility points:

  1. Line 63 reads self.function_stack[-1]. If a matching call ever appears at module scope, in a class body, or in a lambda, the visitor raises IndexError instead of failing the assertion with a readable diff.
  2. Line 78 compares cleanup_statuses with an ordered list. Path.rglob does not guarantee a stable order across platforms. The assertion passes today only because exactly one element is collected. Sort the list or compare a Counter so a second call site does not produce order-dependent failures.
♻️ Proposed hardening
             if isinstance(node.func, ast.Attribute):
                 if node.func.attr == "delete_all_user_playbooks_by_status":
-                    call_sites.append((self.relative_path, self.function_stack[-1]))
+                    enclosing = (
+                        self.function_stack[-1] if self.function_stack else "<module>"
+                    )
+                    call_sites.append((self.relative_path, enclosing))
-    assert cleanup_statuses == ["ARCHIVED"]
+    assert sorted(cleanup_statuses) == ["ARCHIVED"]
🤖 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_playbook_aggregation_state_integration.py`
around lines 60 - 78, Harden the AST visitor around CallVisitor.visit_Call:
record a safe module/class/lambda context when function_stack is empty instead
of indexing self.function_stack[-1], so unexpected matching calls produce an
assertion diff rather than IndexError. Make the cleanup_statuses assertion
order-independent by sorting the collected values before comparison, while
preserving the existing expected statuses.
reflexio/server/services/playbook/components/aggregator.py (2)

434-441: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Decouple the dedup-context read from the remaining work budget.

limit=min(500, budget) shrinks the existing-playbook context as the budget shrinks. A near-exhausted budget (for example budget=4 after invalidations and legacy adoption) gives the LLM only 4 existing playbooks for deduplication, so the aggregator can regenerate near-duplicates. The prompt is already bounded downstream by _select_relevant_existing_playbooks at 20 entries.

Use a fixed context bound instead of the work budget.

♻️ Proposed change
         existing_playbooks = self.storage.get_agent_playbooks(  # type: ignore[attr-defined]
-            limit=min(500, budget),
+            limit=500,
             agent_version=self.agent_version,
🤖 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/components/aggregator.py` around lines 434
- 441, Update the existing-playbook read in the aggregator method to use a fixed
limit of 500 rather than min(500, budget), keeping deduplication context
independent of the remaining work budget and preserving the downstream 20-entry
selection bound.

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

Key the replacement map by index instead of id(outcome).

id(outcome) is only valid while every outcome object stays alive and unreplaced. The mapping breaks silently if a later change rebuilds or copies an outcome between the two loops. Index the outcomes list instead.

♻️ Proposed change
-        replacement_agent_ids_by_outcome = {
-            id(outcome): self.storage.get_playbook_aggregation_replacement_agent_ids(  # type: ignore[attr-defined]
+        replacement_agent_ids_by_outcome = {
+            index: self.storage.get_playbook_aggregation_replacement_agent_ids(  # type: ignore[attr-defined]
                 self.agent_version,
                 [
                     int(item.user_playbook_id)
                     for item in outcome.source_cluster
                     if item.user_playbook_id is not None
                 ],
             )
-            for outcome in outcomes
+            for index, outcome in enumerate(outcomes)
             if outcome.status == "generated"
         }

Then iterate with for index, outcome in enumerate(outcomes): and use replacement_agent_ids_by_outcome[index].

Also applies to: 580-580

🤖 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/components/aggregator.py` around lines 487
- 498, Update the replacement map comprehension in the outcome aggregation flow
to key entries by the outcome’s list index rather than id(outcome), using
enumerate(outcomes) and preserving the generated-status filter. Update the later
lookup to use the corresponding index while iterating outcomes, including the
logic around the replacement map access.
reflexio/server/services/playbook/README.md (1)

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

Fix the data-flow indentation for AgentPlaybook.

AgentPlaybook is produced by PlaybookAggregator, but line 41 is indented at the level of PlaybookAggregationScheduler. The diagram now reads as if the durable signal writes AgentPlaybook directly.

📝 Proposed diagram fix
         -> Durable aggregation signal
           -> PlaybookAggregationScheduler (hourly cadence, bounded work)
             -> PlaybookAggregator (incremental clustering)
-          -> AgentPlaybook (aggregated insights) -> Storage
+              -> AgentPlaybook (aggregated insights) -> Storage
🤖 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/README.md` around lines 38 - 41, Adjust the
README data-flow diagram indentation so AgentPlaybook (aggregated insights) ->
Storage is nested under PlaybookAggregator, reflecting that PlaybookAggregator
produces AgentPlaybook rather than the durable signal or scheduler.
tests/server/services/playbook/test_playbook_aggregator.py (1)

1840-1860: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that each outcome keeps its original source cluster.

The test checks only the statuses. The important invariant in _generate_playbook_outcomes_with_source_clusters is that source_cluster holds the original UserPlaybook objects, not the prompt-preprocessed copies. _run_incremental derives item dispositions from source_cluster[*].user_playbook_id, so a regression there writes dispositions for the wrong rows.

💚 Proposed assertion
+        cluster_a = [_raw(1)]
+        cluster_b = [_raw(2)]
         outcomes = agg._generate_playbook_outcomes_with_source_clusters(
-            {0: [_raw(1)], 1: [_raw(2)]}, []
+            {0: cluster_a, 1: cluster_b}, []
         )
 
         assert [item.status for item in outcomes] == [
             "semantic_null",
             "retryable_failure",
         ]
+        assert [item.source_cluster for item in outcomes] == [cluster_a, cluster_b]
🤖 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/playbook/test_playbook_aggregator.py` around lines 1840
- 1860, Update test_batch_preserves_one_tagged_outcome_per_cluster to also
assert that each returned outcome’s source_cluster contains the original
UserPlaybook objects for its corresponding cluster, including their original
user_playbook_id values, rather than prompt-preprocessed copies. Keep the
existing status assertions and verify both cluster entries preserve their input
objects.
reflexio/lib/_generation.py (1)

95-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the retry constants instead of duplicating them.

60 and 1 repeat _RETRY_SECONDS and _BACKLOG_RETRY_SECONDS from aggregation_scheduler, and they repeat again at both call sites in this function. The module already imports aggregation_min_interval_seconds from that module. Export and import the two retry constants so the retry policy has one definition.

🤖 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/lib/_generation.py` around lines 95 - 110, Replace the duplicated
retry values in the playbook aggregation claim handling with _RETRY_SECONDS and
_BACKLOG_RETRY_SECONDS from aggregation_scheduler. Export those constants there,
import them alongside aggregation_min_interval_seconds, and use them at both
finish_playbook_aggregation_claim call sites while preserving the existing retry
behavior.
reflexio/server/services/playbook/aggregation_trigger.py (1)

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

Reuse the scheduler module logger instead of the string literal.

The module already imports from reflexio.server.services.playbook.aggregation_scheduler at line 8. The literal logger name duplicates that path and goes stale without any failure if the module is renamed. Import the logger object from the scheduler module, or emit this event through the module-level logger on line 12.

🤖 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/aggregation_trigger.py` around lines 81 -
89, Update the playbook aggregation progress logging to reuse the existing
module-level logger from aggregation_scheduler instead of calling
logging.getLogger with a duplicated string literal. Adjust the relevant import
and emit the scheduled event through logger, preserving the current message and
arguments.
tests/server/services/playbook/test_aggregation_scheduler.py (2)

118-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the positive case for the repair interval.

The test proves the second scan is suppressed. It does not prove the scan resumes after _REPAIR_INTERVAL_SECONDS. Advance the clock past the interval and assert a second repair_playbook_aggregation_pending_state call. Without it, a change that disables repair permanently still passes.

🤖 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/playbook/test_aggregation_scheduler.py` around lines
118 - 132, The test_scheduler_throttles_idle_repair_scans test only verifies
suppression, not resumption. Advance the mocked
aggregation_scheduler.time.monotonic value beyond _REPAIR_INTERVAL_SECONDS
before a subsequent scheduler._run_context(context) call, then assert
repair_playbook_aggregation_pending_state was called twice while preserving the
existing claim call assertions.

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

Patching time.monotonic mutates the stdlib module globally.

aggregation_scheduler.time is the stdlib time module, so this replaces time.monotonic for every thread in the process until the test ends. Background threads started by other code in the same process observe the frozen value. Drive the throttle through the scheduler state instead, for example by seeding scheduler._last_repair_at, or by injecting a clock into PlaybookAggregationScheduler.

reflexio/server/services/playbook/aggregation_scheduler.py (1)

199-217: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider backoff for repeated failures.

retry_after_seconds is the constant _RETRY_SECONDS (60). An agent version that fails persistently retries every 60 seconds without limit. Each attempt constructs an LLM client and runs aggregation work. Add exponential backoff based on a consecutive-failure count, or cap the retry rate for a version that keeps failing.

🤖 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/aggregation_scheduler.py` around lines 199
- 217, The failure path in the aggregation scheduler retries persistent
agent-version failures every fixed 60 seconds. Update the claim completion flow
around `finish_playbook_aggregation_claim` to track consecutive failures and
apply exponential backoff or an equivalent capped retry rate, while preserving
the existing retry behavior for successful or non-repeated failures.
reflexio/server/api.py (1)

518-541: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Construction of RequestContext blocks the startup event loop.

RequestContext(org_id=bootstrap_org_id) builds a configurator, a prompt manager, and a storage connection synchronously inside the async lifespan. The neighbouring schedulers avoid this by passing lambda org_id: RequestContext(org_id=org_id) and letting the scheduler thread construct the context. Consider deferring construction the same way so startup does not perform storage I/O on the event loop.

🤖 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/api.py` around lines 518 - 541, Defer RequestContext
construction in the OSS scheduler startup path instead of instantiating it
synchronously in lifespan. Update ensure_local_playbook_aggregation_scheduler
usage to pass a callable such as the neighboring scheduler patterns, allowing
the scheduler thread to create RequestContext for each org while preserving
bootstrap_org_id behavior and existing exception logging.
🤖 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/lib/_generation.py`:
- Around line 71-90: The admin rerun claim created in the playbook aggregation
flow has no lease renewal and can expire during long runs. Reuse _LeaseHeartbeat
from aggregation_scheduler around PlaybookAggregator execution, passing the
claimed aggregation_claim and ensuring the heartbeat starts after claim
acquisition and stops when the rerun completes or fails, while preserving the
existing claim and fence handling.
- Around line 91-102: Protect the failure-path call to
storage.finish_playbook_aggregation_claim within the except block so any
exception from claim release cannot replace the original playbook_aggregator.run
error. Preserve the original exception by handling or suppressing cleanup
failures, then execute the existing bare raise to re-raise the aggregation
error.

In `@reflexio/server/services/playbook/aggregation_scheduler.py`:
- Around line 65-73: Update the lease-renewal loop in _run to catch any
exception from storage.renew_playbook_aggregation_claim, set _lost when renewal
fails or returns None, and return immediately so require_live() no longer treats
the lease as healthy after a renewal error.
- Around line 273-292: Update ensure_local_playbook_aggregation_scheduler and
the scheduler shutdown path so a stopped scheduler is removed from
_LOCAL_SCHEDULERS, or replaced when the incoming RequestContext differs. Ensure
a later app lifespan creates or uses a scheduler bound to the current context
rather than restarting one that retains the stale lambda-captured context.

In `@reflexio/server/services/playbook/components/aggregator.py`:
- Around line 2008-2020: The aggregation flow around the retryable-failure
outcome must enforce a configured maximum attempt count instead of retrying
indefinitely. Track and evaluate each member’s attempt count, transition members
to terminal_noop once the limit is reached, and record the terminal reason;
preserve retryable behavior and backoff for attempts below the limit. Use the
existing retryable-failure handling and member state symbols rather than
introducing a separate retry path.

In `@reflexio/server/services/storage/sqlite_storage/_base.py`:
- Around line 846-848: Ensure playbook aggregation does not access
playbook_aggregation_clusters_vec when _has_sqlite_vec is false: either make
supports_incremental_playbook_aggregation reflect extension availability or
guard every aggregation mixin vector path, including invalidation, rerun,
cleanup, legacy adoption, creation, and attachment. Preserve existing behavior
when sqlite-vec is available and prevent no-such-table errors otherwise.

In `@reflexio/server/services/storage/sqlite_storage/_lineage.py`:
- Around line 101-119: Update the candidate ID construction and invalidation
insert in the lineage flow to retain the parsed numeric value for entity_id,
rather than calling int(entity_id) again. Ensure the invalidation record uses
that parsed value when entity_id is numeric, while preserving source-ID-based
version lookup and avoiding ValueError when entity_id is non-numeric.

In `@reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py`:
- Around line 762-789: Replace the unbounded retry_rows fetch and Python min
scan in the residual cooldown calculation with a single SQL aggregate over
residual rows for the agent_version, computing each row’s capped exponential
retry deadline and returning the minimum remaining cooldown. Preserve the
current zero result when no residual rows exist or when attempt_count is
zero/last_attempt_at is null, and reuse the existing
idx_playbook_aggregation_item_residual-covered predicate.
- Around line 344-358: In the CAS failure branch of
finish_playbook_aggregation_claim, roll back self.conn before returning False
when the caller owns the transaction. Preserve the existing rowcount check and
successful-update behavior, and ensure the rollback is limited to caller-owned
transaction handling.
- Around line 989-1008: Update the vector search query in the surrounding
aggregation method to explicitly declare sqlite-vec’s cosine distance metric for
playbook_aggregation_clusters_vec, using the supported distance-metric
configuration for the MATCH parameter. Keep the existing similarity conversion
and _run_incremental cosine-threshold comparison consistent with that metric.

In `@tests/server/services/playbook/test_playbook_generation_service.py`:
- Around line 86-92: Rename the test function
test_inline_aggregation_default_path_does_not_inject_processor to reflect its
actual assertion that _trigger_playbook_aggregation schedules durable
aggregation with version "v1", such as
test_inline_aggregation_default_path_schedules_durably. Leave the test body
unchanged.

In
`@tests/server/services/storage/test_playbook_aggregation_state_integration.py`:
- Around line 122-132: Update test_org_claim_is_fenced_and_fence_is_monotonic to
make both scheduled playbook aggregation rows use the same next_attempt_at value
before claiming. Preserve the existing v2-then-v1 scheduling and assertions so
the test deterministically exercises the agent_version tie-break and fencing
behavior.

---

Outside diff comments:
In `@reflexio/server/services/playbook/components/aggregator.py`:
- Around line 957-974: The safety-cap branch in the aggregation gate currently
records outcome="should_skip" even though it raises and fails the claim. Update
the record_usage_event call in this branch to emit the appropriate failure
event/outcome, ensuring oversized reruns are represented as failures despite
occurring before the try block.

---

Nitpick comments:
In `@reflexio/lib/_generation.py`:
- Around line 95-110: Replace the duplicated retry values in the playbook
aggregation claim handling with _RETRY_SECONDS and _BACKLOG_RETRY_SECONDS from
aggregation_scheduler. Export those constants there, import them alongside
aggregation_min_interval_seconds, and use them at both
finish_playbook_aggregation_claim call sites while preserving the existing retry
behavior.

In `@reflexio/server/api.py`:
- Around line 518-541: Defer RequestContext construction in the OSS scheduler
startup path instead of instantiating it synchronously in lifespan. Update
ensure_local_playbook_aggregation_scheduler usage to pass a callable such as the
neighboring scheduler patterns, allowing the scheduler thread to create
RequestContext for each org while preserving bootstrap_org_id behavior and
existing exception logging.

In `@reflexio/server/services/playbook/aggregation_scheduler.py`:
- Around line 199-217: The failure path in the aggregation scheduler retries
persistent agent-version failures every fixed 60 seconds. Update the claim
completion flow around `finish_playbook_aggregation_claim` to track consecutive
failures and apply exponential backoff or an equivalent capped retry rate, while
preserving the existing retry behavior for successful or non-repeated failures.

In `@reflexio/server/services/playbook/aggregation_trigger.py`:
- Around line 81-89: Update the playbook aggregation progress logging to reuse
the existing module-level logger from aggregation_scheduler instead of calling
logging.getLogger with a duplicated string literal. Adjust the relevant import
and emit the scheduled event through logger, preserving the current message and
arguments.

In `@reflexio/server/services/playbook/components/aggregator.py`:
- Around line 434-441: Update the existing-playbook read in the aggregator
method to use a fixed limit of 500 rather than min(500, budget), keeping
deduplication context independent of the remaining work budget and preserving
the downstream 20-entry selection bound.
- Around line 487-498: Update the replacement map comprehension in the outcome
aggregation flow to key entries by the outcome’s list index rather than
id(outcome), using enumerate(outcomes) and preserving the generated-status
filter. Update the later lookup to use the corresponding index while iterating
outcomes, including the logic around the replacement map access.

In `@reflexio/server/services/playbook/README.md`:
- Around line 38-41: Adjust the README data-flow diagram indentation so
AgentPlaybook (aggregated insights) -> Storage is nested under
PlaybookAggregator, reflecting that PlaybookAggregator produces AgentPlaybook
rather than the durable signal or scheduler.

In `@tests/server/services/playbook/test_aggregation_scheduler.py`:
- Around line 118-132: The test_scheduler_throttles_idle_repair_scans test only
verifies suppression, not resumption. Advance the mocked
aggregation_scheduler.time.monotonic value beyond _REPAIR_INTERVAL_SECONDS
before a subsequent scheduler._run_context(context) call, then assert
repair_playbook_aggregation_pending_state was called twice while preserving the
existing claim call assertions.

In `@tests/server/services/playbook/test_playbook_aggregator.py`:
- Around line 1840-1860: Update
test_batch_preserves_one_tagged_outcome_per_cluster to also assert that each
returned outcome’s source_cluster contains the original UserPlaybook objects for
its corresponding cluster, including their original user_playbook_id values,
rather than prompt-preprocessed copies. Keep the existing status assertions and
verify both cluster entries preserve their input objects.

In
`@tests/server/services/storage/test_playbook_aggregation_state_integration.py`:
- Around line 380-383: Introduce a shared `vec_store` fixture that creates the
storage via `_store(tmp_path)`, skips when sqlite-vec is unavailable, and
returns the validated store. Update all affected vector tests, including
`test_sqlite_vec_centroid_lookup_and_attachment`, to depend on `vec_store` and
remove their `tmp_path` setup and inline `_has_sqlite_vec` guards.
- Around line 342-377: Update the positional PlaybookAggregationBacklog
construction in the affected test to use explicit keyword arguments for each
zero-valued field, matching the keyword-based construction in
test_dirty_cluster_keeps_aggregation_pending. Preserve the assertion that an
empty backlog has pending set to False.
- Around line 888-928: Add a concise inline comment immediately before the raw
SQL DELETE in test_rerun_snapshot_finishes_only_materialized_work, explicitly
noting that the direct deletion intentionally exercises the database trigger
path that creates the hard_delete invalidation. Leave the test behavior and
assertions unchanged.
- Around line 199-249: Move the function-local imports of MagicMock and json
used by test_stale_claim_cannot_commit_incremental_effect and the other named
tests to the module-level import section, alongside the existing imports. Remove
the duplicated in-function import statements while preserving all test behavior
and the fencing assertions.
- Around line 60-78: Harden the AST visitor around CallVisitor.visit_Call:
record a safe module/class/lambda context when function_stack is empty instead
of indexing self.function_stack[-1], so unexpected matching calls produce an
assertion diff rather than IndexError. Make the cleanup_statuses assertion
order-independent by sorting the collected values before comparison, while
preserving the existing expected statuses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39adcd79-aae5-4ca7-b4f3-a212b037f2a7

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6be8a and 8864ae4.

📒 Files selected for processing (29)
  • .env.example
  • reflexio/lib/_generation.py
  • reflexio/server/README.md
  • reflexio/server/api.py
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/playbook/components/aggregator.py
  • reflexio/server/services/playbook/components/aggregator_clustering.py
  • reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
  • reflexio/server/services/storage/sqlite_storage/__init__.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_lineage.py
  • reflexio/server/services/storage/sqlite_storage/playbook/__init__.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • reflexio/server/services/storage/storage_base/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/_aggregation.py
  • reflexio/server/services/storage/storage_base/playbook/_user.py
  • tests/lib/test_generation_unit.py
  • tests/server/services/playbook/test_aggregation_lineage_integration.py
  • tests/server/services/playbook/test_aggregation_scheduler.py
  • tests/server/services/playbook/test_aggregation_soft_delete_integration.py
  • tests/server/services/playbook/test_playbook_aggregator.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/playbook_optimizer/test_gepa_user_playbook_publication.py
  • tests/server/services/playbook_optimizer/test_judge_frozen_plan.py
  • tests/server/services/storage/test_playbook_aggregation_state_integration.py

Comment thread reflexio/lib/_generation.py
Comment thread reflexio/lib/_generation.py
Comment thread reflexio/server/services/playbook/aggregation_scheduler.py
Comment thread reflexio/server/services/playbook/aggregation_scheduler.py
Comment thread reflexio/server/services/playbook/components/aggregator.py
Comment thread reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py Outdated
Comment thread tests/server/services/playbook/test_playbook_generation_service.py
@yyiilluu

yyiilluu commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit follow-up is in e60bc7b.

Addressed: renewable admin leases; cleanup-error preservation; heartbeat failure fencing; stale local scheduler eviction; sqlite-vec degraded capability; numeric lineage handling; SQLite CAS rollback; SQL-only residual cooldown; cosine vec0 distance; oversized-rerun failure telemetry; deterministic/renamed/hardened tests; logger/README/index-key maintenance.

Declined after verification:

  • Terminalizing repeated retryable_failure outcomes: terminal_noop is a semantic-null disposition; using it for infrastructure/LLM failures would silently lose valid work. Residual retries already have capped exponential cooldown.
  • Scheduler-level exponential failure backoff: the approved design intentionally uses a fixed scheduler retry while item-level residual retries back off; adding persistent failure-count state is outside this change.
  • Reading a fixed 500 existing playbooks regardless of remaining budget: this violates the shared bounded per-run row budget.
  • Refactoring startup to defer RequestContext: this is a one-time OSS bootstrap construction and changing the scheduler provider contract is not justified by evidence in this fix.

Also verified the proposed SQLite hard-delete lineage change is unnecessary: the existing BEFORE DELETE trigger captures OLD.agent_version and schedules invalidation atomically before the row disappears. Existing and follow-up tests cover the raw-trigger path.

Validation after the follow-up:

  • Ruff: clean
  • Pyright: 0 errors
  • Targeted reviewer regression suite: 174 passed
  • Full OSS unit/integration: 5,492 passed, 73 skipped, 6 subtests passed
  • OSS E2E: 47 passed, 51 skipped
  • Enterprise aggregation-focused compatibility suite: 29 passed

A broad enterprise integration run was attempted but was not counted as green: it accumulated unrelated integration failures and then stalled without progress at 44%, so it was terminated. The aggregation-focused enterprise suite above completed cleanly.

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

🧹 Nitpick comments (1)
reflexio/server/services/playbook/components/aggregator.py (1)

483-498: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Batch the replacement-agent-id lookup instead of querying per outcome.

replacement_agent_ids_by_outcome calls get_playbook_aggregation_replacement_agent_ids once per "generated" outcome. The only consumer, replaced_agent_ids.update(replacement_agent_ids_by_outcome[outcome_index]) at line 580, flattens all results into one set. get_playbook_aggregation_replacement_agent_ids runs a DISTINCT ... WHERE user_playbook_id IN (...) query, so passing the union of member IDs from every generated outcome in a single call returns the same final set. Replace the per-outcome dict with one batched call before the loop.

♻️ Proposed batching
-        replacement_agent_ids_by_outcome = {
-            index: self.storage.get_playbook_aggregation_replacement_agent_ids(  # type: ignore[attr-defined]
-                self.agent_version,
-                [
-                    int(item.user_playbook_id)
-                    for item in outcome.source_cluster
-                    if item.user_playbook_id is not None
-                ],
-            )
-            for index, outcome in enumerate(outcomes)
-            if outcome.status == "generated"
-        }
+        generated_member_ids = sorted(
+            {
+                int(item.user_playbook_id)
+                for outcome in outcomes
+                if outcome.status == "generated"
+                for item in outcome.source_cluster
+                if item.user_playbook_id is not None
+            }
+        )
+        replacement_agent_ids = set(
+            self.storage.get_playbook_aggregation_replacement_agent_ids(  # type: ignore[attr-defined]
+                self.agent_version, generated_member_ids
+            )
+        )
-                replaced_agent_ids.update(
-                    replacement_agent_ids_by_outcome[outcome_index]
-                )
+                replaced_agent_ids.update(replacement_agent_ids)

Also applies to: 580-582

🤖 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/components/aggregator.py` around lines 483
- 498, Replace the per-outcome replacement_agent_ids_by_outcome dictionary in
the aggregation flow with one call to
get_playbook_aggregation_replacement_agent_ids using the union of
user_playbook_id values from all generated outcomes. Update the consumer that
currently calls
replaced_agent_ids.update(replacement_agent_ids_by_outcome[outcome_index]) to
use the single batched result while preserving the final set contents.
🤖 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.

Nitpick comments:
In `@reflexio/server/services/playbook/components/aggregator.py`:
- Around line 483-498: Replace the per-outcome replacement_agent_ids_by_outcome
dictionary in the aggregation flow with one call to
get_playbook_aggregation_replacement_agent_ids using the union of
user_playbook_id values from all generated outcomes. Update the consumer that
currently calls
replaced_agent_ids.update(replacement_agent_ids_by_outcome[outcome_index]) to
use the single batched result while preserving the final set contents.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7455b93-3cf9-4ae0-98fd-3cded4f6d36b

📥 Commits

Reviewing files that changed from the base of the PR and between 8864ae4 and e60bc7b.

📒 Files selected for processing (14)
  • reflexio/lib/_generation.py
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/playbook/components/aggregator.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_lineage.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • tests/lib/test_generation_unit.py
  • tests/server/services/playbook/test_aggregation_scheduler.py
  • tests/server/services/playbook/test_playbook_aggregator.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/storage/test_playbook_aggregation_state_integration.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • reflexio/server/services/storage/sqlite_storage/_lineage.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py
  • tests/server/services/playbook/test_playbook_aggregator.py

@yyiilluu
yyiilluu force-pushed the codex/review-deduplication-repair-plan branch from e60bc7b to 6a780fb Compare August 3, 2026 07:22

@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: 3

🧹 Nitpick comments (1)
tests/server/services/playbook/test_aggregation_scheduler.py (1)

154-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clear the global scheduler registry after the test too.

The test clears aggregation_scheduler._LOCAL_SCHEDULERS at line 156 but leaves two schedulers registered when it finishes. Later tests in the same session then observe module-level state created here, which makes failures order-dependent.

Register a cleanup that clears the registry again.

♻️ Proposed change
 def test_stopped_local_scheduler_drops_captured_context(monkeypatch) -> None:
     monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
     aggregation_scheduler._LOCAL_SCHEDULERS.clear()
+    monkeypatch.setattr(
+        aggregation_scheduler,
+        "_LOCAL_SCHEDULERS",
+        type(aggregation_scheduler._LOCAL_SCHEDULERS)(),
+    )

An addfinalizer that calls aggregation_scheduler._LOCAL_SCHEDULERS.clear() works as well.

🤖 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/playbook/test_aggregation_scheduler.py` around lines
154 - 181, Update test_stopped_local_scheduler_drops_captured_context to
register teardown cleanup that clears aggregation_scheduler._LOCAL_SCHEDULERS
after the test, while preserving the existing pre-test clear and assertions.
🤖 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/sqlite_storage/_lineage.py`:
- Around line 112-132: Update the merge invalidation logic around the
agent_version lookup to handle all candidate versions consistently: either
validate and reject mixed agent_version values before writing, or iterate over
each distinct non-empty version and create corresponding invalidation and
aggregation-state records. Do not retain the current LIMIT 1 behavior that
processes only one version.

In `@reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py`:
- Around line 93-96: Update the AGGREGATION_DDL handled by
init_playbook_aggregation_tables so the pending-invalidation index is created
with IF NOT EXISTS and is not dropped on every connection. Move the existing
DROP INDEX into the one-time schema-migration path only if required to replace
the prior definition, preserving the partial-index predicate and columns.
- Around line 991-1011: Update the candidate lookup around the ANN subquery in
the aggregation matching flow so compatibility filters for agent_version,
embedding_model, embedding_dimension, and active state are applied before
exhausting candidate_limit. Over-fetch ANN results or retry with an expanded
limit until compatible clusters can be considered, while preserving the existing
nearest-match ordering and result mapping in PlaybookAggregationClusterMatch.

---

Nitpick comments:
In `@tests/server/services/playbook/test_aggregation_scheduler.py`:
- Around line 154-181: Update
test_stopped_local_scheduler_drops_captured_context to register teardown cleanup
that clears aggregation_scheduler._LOCAL_SCHEDULERS after the test, while
preserving the existing pre-test clear and assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 22a064e2-72f5-46f3-b1bb-8b88800e47ab

📥 Commits

Reviewing files that changed from the base of the PR and between e60bc7b and 6a780fb.

📒 Files selected for processing (31)
  • .env.example
  • reflexio/lib/_generation.py
  • reflexio/server/README.md
  • reflexio/server/api.py
  • reflexio/server/services/README.md
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/playbook/components/aggregator.py
  • reflexio/server/services/playbook/components/aggregator_clustering.py
  • reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
  • reflexio/server/services/storage/sqlite_storage/__init__.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_lineage.py
  • reflexio/server/services/storage/sqlite_storage/playbook/__init__.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • reflexio/server/services/storage/storage_base/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/_aggregation.py
  • reflexio/server/services/storage/storage_base/playbook/_user.py
  • tests/lib/test_generation_unit.py
  • tests/server/services/durable_learning/test_compute_persist_split.py
  • tests/server/services/playbook/test_aggregation_lineage_integration.py
  • tests/server/services/playbook/test_aggregation_scheduler.py
  • tests/server/services/playbook/test_aggregation_soft_delete_integration.py
  • tests/server/services/playbook/test_playbook_aggregator.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/playbook_optimizer/test_gepa_user_playbook_publication.py
  • tests/server/services/playbook_optimizer/test_judge_frozen_plan.py
  • tests/server/services/storage/test_playbook_aggregation_state_integration.py
🚧 Files skipped from review as they are similar to previous changes (22)
  • reflexio/server/services/storage/storage_base/playbook/_user.py
  • reflexio/server/services/storage/sqlite_storage/playbook/init.py
  • reflexio/server/services/storage/sqlite_storage/init.py
  • reflexio/server/services/storage/storage_base/init.py
  • reflexio/server/services/playbook/components/aggregator_clustering.py
  • tests/server/services/playbook/test_aggregation_lineage_integration.py
  • tests/server/services/playbook_optimizer/test_gepa_user_playbook_publication.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/lib/_generation.py
  • tests/server/services/playbook/test_aggregation_soft_delete_integration.py
  • tests/lib/test_generation_unit.py
  • reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
  • reflexio/server/services/storage/storage_base/playbook/init.py
  • tests/server/services/playbook_optimizer/test_judge_frozen_plan.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • reflexio/server/api.py
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/storage/storage_base/playbook/_aggregation.py
  • tests/server/services/playbook/test_playbook_aggregator.py
  • reflexio/server/services/playbook/components/aggregator.py

Comment thread reflexio/server/services/storage/sqlite_storage/_lineage.py Outdated
Comment thread reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py Outdated
Persist aggregation work, attach new playbooks to same-version centroids, and cluster residuals in bounded hourly runs.
Partition lifecycle invalidations by agent version, keep SQLite aggregation index initialization idempotent, and filter vec0 candidates before the ANN limit.
Avoid local daylight-saving transitions changing fixed profile expiration intervals.
@yyiilluu
yyiilluu force-pushed the codex/review-deduplication-repair-plan branch from 6a780fb to 9061ee0 Compare August 3, 2026 17:19

@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: 2

🧹 Nitpick comments (4)
tests/server/services/playbook/test_aggregation_scheduler.py (2)

141-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the heartbeat loop so a regression fails instead of hanging.

Line 146 makes heartbeat._stop.wait always return False. _run therefore exits only because renew_playbook_aggregation_claim raises and the loop breaks. If a future change makes _run catch the renewal error and continue, wait never returns True and this test blocks forever rather than failing.

Return True after the first iteration so the loop always terminates.

♻️ Proposed fix
-    heartbeat._stop.wait = MagicMock(return_value=False)
+    heartbeat._stop.wait = MagicMock(side_effect=[False, True])
🤖 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/playbook/test_aggregation_scheduler.py` around lines
141 - 151, Update test_lease_heartbeat_marks_renewal_exception_as_lost so the
mocked heartbeat._stop.wait returns False for the first call and True
thereafter, ensuring AggregationLeaseHeartbeat._run terminates even if renewal
errors are caught and retried.

154-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore _LOCAL_SCHEDULERS after the test.

Line 156 clears the module-level _LOCAL_SCHEDULERS registry, and the test then inserts two entries through ensure_local_playbook_aggregation_scheduler. The test never removes them. monkeypatch reverts the patched start and is_running methods, but it does not revert the dictionary. A later test that calls ensure_local_playbook_aggregation_scheduler can therefore receive this test's leftover scheduler instead of creating its own.

Clear the registry in a fixture so cleanup runs even when the test fails.

♻️ Proposed fix
+@pytest.fixture
+def clean_local_schedulers():
+    aggregation_scheduler._LOCAL_SCHEDULERS.clear()
+    yield
+    aggregation_scheduler._LOCAL_SCHEDULERS.clear()
+
+
-def test_stopped_local_scheduler_drops_captured_context(monkeypatch) -> None:
+def test_stopped_local_scheduler_drops_captured_context(
+    monkeypatch, clean_local_schedulers
+) -> None:
     monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
-    aggregation_scheduler._LOCAL_SCHEDULERS.clear()
     monkeypatch.setattr(
🤖 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/playbook/test_aggregation_scheduler.py` around lines
154 - 181, Ensure test_local_playbook_aggregation_scheduler cleanup restores the
module-level _LOCAL_SCHEDULERS registry after the test, using a fixture with
teardown so cleanup runs even on failure. Keep the existing setup and assertions
unchanged while preventing ensure_local_playbook_aggregation_scheduler state
from leaking into later tests.
tests/server/services/storage/test_playbook_aggregation_state_integration.py (1)

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

Avoid asserting exact SQL text.

Line 462 matches the literal fragment "COALESCE(MAX(0, MIN(CASE". Any whitespace or expression reordering inside get_playbook_aggregation_backlog breaks this test without changing behavior. The intent is to prove the cooldown stays a single SQL aggregate.

Assert the observable property instead: no per-item row read occurs. For example, assert that no traced statement selects attempt_count or last_attempt_at as a bare column list from playbook_aggregation_item, or assert the traced statement count stays constant as the residual set grows.

🤖 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_playbook_aggregation_state_integration.py`
around lines 455 - 462, Update the test around get_playbook_aggregation_backlog
to remove the exact SQL fragment assertion and verify the observable
single-aggregate behavior instead. Use the captured statements to assert no
per-item row read selects bare attempt_count or last_attempt_at columns from
playbook_aggregation_item, or otherwise confirm statement count does not grow
with the residual set; retain the backlog range assertion.
reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py (1)

989-1013: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider reducing per-candidate KNN round trips.

Lines 990-1008 issue one KNN statement for each candidate. The caller's budget can reach REFLEXIO_MAX_CLUSTERING_PLAYBOOKS (default 20,000) rows per scheduled unit, so one unit can execute up to 20,000 separate vector queries while holding _lock. sqlite-vec cannot accept several query vectors in one MATCH, so batching is not possible directly. Two options reduce the cost:

  • Prepare the statement once and reuse it, instead of re-parsing the same SQL for each candidate.
  • Cap the number of centroid-match probes per unit below the full row budget, and leave the remainder residual for a later unit.

Neither change is required for correctness.

🤖 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/playbook/_aggregation.py`
around lines 989 - 1013, Reduce per-candidate KNN overhead in the
candidate-matching loop by reusing a prepared SQLite statement for the repeated
query instead of calling self.conn.execute with the SQL text on every iteration.
Preserve the existing parameters, ordering, limit, lock scope, and matches
behavior.
🤖 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/sqlite_storage/playbook/_aggregation.py`:
- Around line 519-545: Update the completion and activation logic in the
aggregation update flow so clusters with no member IDs cannot be persisted as
active. When the cluster is empty, force complete to false (or skip activation),
keeping its state rebuilding and avoiding active records with a NULL centroid
and missing vector-index entry.

In `@tests/server/services/profile/test_profile_generation_service_utils.py`:
- Around line 268-286: Update
test_calculate_expiration_timestamp_ignores_local_dst so the final tzset()
restoration runs in a finally block surrounding the temporary TZ context and
calculate_expiration_timestamp call. Preserve the existing environment cleanup
and assertion while ensuring the process timezone is restored even if
calculation raises.

---

Nitpick comments:
In `@reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py`:
- Around line 989-1013: Reduce per-candidate KNN overhead in the
candidate-matching loop by reusing a prepared SQLite statement for the repeated
query instead of calling self.conn.execute with the SQL text on every iteration.
Preserve the existing parameters, ordering, limit, lock scope, and matches
behavior.

In `@tests/server/services/playbook/test_aggregation_scheduler.py`:
- Around line 141-151: Update
test_lease_heartbeat_marks_renewal_exception_as_lost so the mocked
heartbeat._stop.wait returns False for the first call and True thereafter,
ensuring AggregationLeaseHeartbeat._run terminates even if renewal errors are
caught and retried.
- Around line 154-181: Ensure test_local_playbook_aggregation_scheduler cleanup
restores the module-level _LOCAL_SCHEDULERS registry after the test, using a
fixture with teardown so cleanup runs even on failure. Keep the existing setup
and assertions unchanged while preventing
ensure_local_playbook_aggregation_scheduler state from leaking into later tests.

In
`@tests/server/services/storage/test_playbook_aggregation_state_integration.py`:
- Around line 455-462: Update the test around get_playbook_aggregation_backlog
to remove the exact SQL fragment assertion and verify the observable
single-aggregate behavior instead. Use the captured statements to assert no
per-item row read selects bare attempt_count or last_attempt_at columns from
playbook_aggregation_item, or otherwise confirm statement count does not grow
with the residual set; retain the backlog range assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 385e147e-8a16-4e3b-b0f1-8bfa77d0b589

📥 Commits

Reviewing files that changed from the base of the PR and between 6a780fb and 9061ee0.

📒 Files selected for processing (33)
  • .env.example
  • reflexio/lib/_generation.py
  • reflexio/server/README.md
  • reflexio/server/api.py
  • reflexio/server/services/README.md
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/playbook/components/aggregator.py
  • reflexio/server/services/playbook/components/aggregator_clustering.py
  • reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
  • reflexio/server/services/profile/profile_generation_service_utils.py
  • reflexio/server/services/storage/sqlite_storage/__init__.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_lineage.py
  • reflexio/server/services/storage/sqlite_storage/playbook/__init__.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_aggregation.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • reflexio/server/services/storage/storage_base/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/__init__.py
  • reflexio/server/services/storage/storage_base/playbook/_aggregation.py
  • reflexio/server/services/storage/storage_base/playbook/_user.py
  • tests/lib/test_generation_unit.py
  • tests/server/services/durable_learning/test_compute_persist_split.py
  • tests/server/services/playbook/test_aggregation_lineage_integration.py
  • tests/server/services/playbook/test_aggregation_scheduler.py
  • tests/server/services/playbook/test_aggregation_soft_delete_integration.py
  • tests/server/services/playbook/test_playbook_aggregator.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/playbook_optimizer/test_gepa_user_playbook_publication.py
  • tests/server/services/playbook_optimizer/test_judge_frozen_plan.py
  • tests/server/services/profile/test_profile_generation_service_utils.py
  • tests/server/services/storage/test_playbook_aggregation_state_integration.py
🚧 Files skipped from review as they are similar to previous changes (25)
  • reflexio/server/services/storage/storage_base/playbook/_user.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/playbook/test_aggregation_lineage_integration.py
  • reflexio/server/services/storage/sqlite_storage/playbook/init.py
  • tests/server/services/playbook/test_aggregation_soft_delete_integration.py
  • reflexio/server/services/storage/storage_base/init.py
  • reflexio/server/services/storage/sqlite_storage/init.py
  • tests/server/services/playbook_optimizer/test_judge_frozen_plan.py
  • tests/server/services/durable_learning/test_compute_persist_split.py
  • reflexio/server/services/playbook/components/aggregator_clustering.py
  • reflexio/server/services/storage/storage_base/playbook/init.py
  • tests/server/services/playbook_optimizer/test_gepa_user_playbook_publication.py
  • reflexio/server/api.py
  • reflexio/server/services/playbook/aggregation_trigger.py
  • reflexio/server/services/playbook/components/aggregator_prompt_formatting.py
  • reflexio/server/services/storage/sqlite_storage/playbook/_user.py
  • reflexio/server/services/README.md
  • tests/server/services/playbook/test_playbook_aggregator.py
  • reflexio/server/services/storage/storage_base/playbook/_aggregation.py
  • reflexio/lib/_generation.py
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/README.md
  • tests/lib/test_generation_unit.py
  • reflexio/server/services/playbook/aggregation_scheduler.py
  • reflexio/server/services/playbook/components/aggregator.py

@yyiilluu
yyiilluu merged commit 4b074cf into main Aug 3, 2026
1 check passed
yyiilluu added a commit that referenced this pull request Aug 4, 2026
## Summary

- Revert #407 and restore session outcomes, governance erasure, billing,
and search behavior to the pre-open-world-evidence contracts.
- Also revert #408 and #409 because their finalization-receipt and
exposure-retention changes depend entirely on APIs introduced by #407.
- Preserve the independent incremental aggregation work from #405 and
#410.
- Address every valid CodeRabbit finding, including SQLite downgrade
compatibility and retry-safe metering.
- Fix callback drop-rate anomaly emission on hosts with less than one
hour of monotonic uptime, discovered by the full validation run.

## Changes

### Evidence foundation rollback

- Remove search-exposure recording and session-outcome identity helpers.
- Restore the prior session outcome schemas, client surface, and
SQLite/storage contracts.
- Restore the prior governance erase/claim flow and retention behavior.
- Restore the prior resumable extraction and learning-billing behavior.

### Dependent follow-ups

- Remove receipt-winner finalization behavior from #408.
- Remove the exposure ownership and protected-retention behavior from
#409.

### Review follow-ups

- Rebuild #407-era SQLite `session_outcomes` tables into the restored
schema, preserving `success`/`failure` rows, backfilling governance
subject references, and explicitly dropping unrepresentable `unknown`
outcomes with a warning.
- Restore the SQLite 3.35 minimum required by existing `RETURNING` and
`DROP COLUMN` usage.
- Make outcome erasure resilient to governance-secret rotation and
return a stable `session_outcomes` deletion count.
- Acquire SQLite governance write locks before state checks, serialize
idempotent purge begin/prepare flows across connections, and roll back
failed target writes so SQLite cannot retain a stale writer
transaction.\n- Reject legacy session-outcome schemas with empty
governance-subject defaults and rebuild them with derived subject
references.\n- Make synchronous playbook/profile persistence atomic
while keeping scheduler dispatch strictly post-commit.
- Meter resumable extraction from persisted survivors only, use
retry-stable fallback keys, and emit learning billing from incremental
aggregation.
- Treat post-persist optimization and aggregation scheduling failures as
best-effort side effects.

### Validation follow-up

- Represent the callback executor's last anomaly time with an explicit
unset sentinel so the first threshold crossing is never suppressed by
low system uptime.

## Test Plan

- `uv run ruff check reflexio tests`
- `uv run ruff format --check reflexio tests`
- Pyright on all 23 staged Python files: 0 errors, 0 warnings
- Latest affected review files: 301 passed
- OSS non-E2E suite: 5,535 passed, 73 skipped, 6 subtests passed
- OSS E2E suite: 47 passed, 51 skipped
- `npm --prefix docs run lint`: 0 errors (3 existing warnings)
- `cd docs && npx tsc --noEmit`
- `python -c "import reflexio"`

Reverts `85a4b2255a96ef2a5b50f4cbe7c10758439e76b3`, plus dependent
follow-ups `785a9e053ff771f40704bb7b0b5bbbe36048806a` and
`eb88f44fd3b53457b76e8500ac1a30ba7d4ab16e`.


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

- **Changes**
- Session outcomes now support only success or failure, with simpler
responses and retry behavior.
- Governance data erasure workflows have streamlined retry and
completion handling, including session-outcome removal.
- Search exposure event recording has been removed; search results and
metering remain available.
- Learning-generation billing supports durable per-record tracking,
retry-stable keys, and count-based fallback.
- Scheduler failures during playbook processing are logged without
preventing other scheduled actions.

- **Documentation**
- Quick Start prerequisites now list Node.js without the previous SQLite
verification step.
  - Billing and extraction guidance has been updated.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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