Skip to content

Lineage Phase B3h (OSS) — retention + playbook-delete atomicity (final delete-path sweep)#198

Merged
yilu331 merged 4 commits into
mainfrom
feat/lineage-phase-b3h
Jun 21, 2026
Merged

Lineage Phase B3h (OSS) — retention + playbook-delete atomicity (final delete-path sweep)#198
yilu331 merged 4 commits into
mainfrom
feat/lineage-phase-b3h

Conversation

@yilu331

@yilu331 yilu331 commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Lineage Phase B3h (OSS) — retention-cascade + playbook-delete atomicity (final delete-path sweep)

The last of the delete-path search-index-cleanup sweep started in B3d/B3e/B3f/B3g. OSS/SQLite-only (enterprise/Postgres deletes are transactional). Closes the remaining self-committing-helper-in-atomic-block and search-before-row-delete cases.

Core fix — _delete_playbook_search_rows is the last self-committing dependency helper

It used a per-id self-committing _vec_delete loop, which broke the retention cascade's "one critical section" atomicity (_retention_perform_delete). Rewrote it as (_kind, ids, *, commit=True):

  • non-committing _delete_in_chunks for both fts and vec (vec guarded on _has_sqlite_vec),
  • wrapped in with self._lock:,
  • if commit: conn.commit().
    The 2 retention call sites pass commit=False (atomic with the block's single commit); the 8 playbook callers keep the default commit=True (also fixes a latent gap where, without sqlite-vec, the fts deletes were previously left uncommitted by this helper).

Caller ordering bugs fixed (caught in review)

  • delete_all_user_playbooks_by_status — deleted+committed the search rows BEFORE deleting the playbook rows (separate statement), and snapshotted ids via _fetchall outside the lock. Now: snapshot + row DELETE + _delete_playbook_search_rows(commit=False) in ONE with self._lock: + single commit; returns the real rowcount.
  • delete_user_playbook / delete_agent_playbook (single-record) — same search-before-row ordering via self-committing _fts_delete/_vec_delete. Now atomic: row DELETE → rowcount-guarded hard_delete lineage emit → _delete_playbook_search_rows(commit=False) → one commit. Audit-only-on-commit preserved.

The other retention dependency helpers (_delete_source_windows_*, _delete_optimizer_*) were verified already non-committing — untouched.

Tests

New retention tests (user/agent playbook fts+vec cleaned within the atomic block) + a guard test for delete_all_user_playbooks_by_status (matched rows+fts+vec gone; non-matching survivor intact). 45 storage/retention tests pass. Production file pyright-clean.

Completes the delete-path atomicity class across profiles (B3c/B3f), interactions (B3d), retention interaction/profile helpers (B3e), and now playbooks + the retention playbook helper (B3h). Independent OSS PR; enterprise OSS-pin lags this SQLite-only fix harmlessly.

Summary by CodeRabbit

  • Bug Fixes

    • Improved retention and playbook deletion so associated search-index rows are consistently cleaned up when playbooks are removed (including vector index entries when available).
    • Ensured retention deletions are fully atomic: failures during dependent cleanup or target-row deletion now roll back to prevent partial data removal.
  • Tests

    • Added/expanded integration coverage verifying search cleanup for individual deletions and bulk status-based deletions, and validating rollback behavior on retention delete failures.

yilu331 added 3 commits June 21, 2026 19:23
…on-atomic via commit=False; non-committing vec deletes; lock-wrapped)
…t single-record and bulk-by-status deletions

- delete_user_playbook: move _fts_delete/_vec_delete after row delete (call
  _delete_playbook_search_rows with commit=False inside the lock, before final
  conn.commit) — prevents crash-window where FTS row is durably gone before
  the row delete commits
- delete_agent_playbook: same fix, mirroring the user pattern
- delete_all_user_playbooks_by_status: bring snapshot (_fetchall), row delete,
  and search cleanup into one lock+single commit; snapshot was previously
  outside the lock (race) and FTS cleanup committed before the row delete
- add contract test asserting row+FTS (+vec when available) are gone and a
  non-matching playbook survives after delete_all_user_playbooks_by_status
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a31a03b9-722b-4caa-9bd3-05e03b7180cc

📥 Commits

Reviewing files that changed from the base of the PR and between 8896b83 and 4377ab8.

📒 Files selected for processing (3)
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_playbook.py
  • tests/server/services/storage/test_retention_rollback_integration.py

📝 Walkthrough

Walkthrough

Adds a commit keyword parameter to _delete_playbook_search_rows and rewrites it to perform chunked batch deletes for FTS and vec tables under a re-entrant lock, with conditional commit. Wraps _retention_perform_delete with try/except/rollback. All deletion call sites (delete_user_playbook, delete_agent_playbook, delete_all_user_playbooks_by_status, _retention_delete_dependencies) pass commit=False to defer commits to the enclosing transaction. Three contract tests verify successful FTS/vec cleanup during retention and bulk deletion; two integration tests verify transactional rollback behavior.

Changes

Playbook search-index cleanup and transaction control

Layer / File(s) Summary
Commit control refactor: _delete_playbook_search_rows and transaction wrapping
reflexio/server/services/storage/sqlite_storage/_base.py, reflexio/server/services/storage/sqlite_storage/_playbook.py
_retention_perform_delete now wraps the deletion steps in try/except with rollback on any exception before re-raising. _delete_playbook_search_rows gains a keyword-only commit parameter, re-acquires self._lock, performs chunked batch deletes on FTS and vec tables, and commits only when commit=True. _retention_delete_dependencies calls it with commit=False for both playbook types. delete_user_playbook, delete_agent_playbook, and delete_all_user_playbooks_by_status are rewired to invoke the helper with commit=False within their own lock/transaction scope, deferring the final commit to the outer caller.
FTS and vec cleanup contract tests
tests/server/services/storage/test_storage_contract_retention.py
Adds UserPlaybook/AgentPlaybook imports and module-level helper factories. Adds test_retention_user_playbook_delete_cleans_fts and test_retention_agent_playbook_delete_cleans_fts verifying that retention cascade deletes the corresponding FTS rows for removed entries and preserves FTS rows for remaining entries. Adds test_delete_all_user_playbooks_by_status_cleans_search_rows verifying bulk status-based deletion removes matching playbook rows and their FTS/vec entries while leaving non-matching playbooks and their search rows intact.
Transactional rollback integration tests
tests/server/services/storage/test_retention_rollback_integration.py
New integration test module with helpers to create a SQLiteStorage instance and UserPlaybook fixtures. Adds test_retention_perform_delete_rolls_back_on_target_row_failure which injects a failure into target-row deletion, verifies FTS rows exist before, then confirms both playbook rows and FTS rows remain after the raised StorageError. Adds test_retention_perform_delete_rolls_back_on_dependency_failure which injects failure into dependency-cascade deletion and verifies target playbook rows survive rollback.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as delete_*_playbook<br/>_retention_delete_dependencies
  participant Lock as self._lock
  participant DB as SQLite<br/>(playbooks/FTS/vec)
  participant Helper as _delete_playbook<br/>_search_rows<br/>(commit=False)

  Caller->>Lock: acquire
  Caller->>DB: DELETE FROM playbooks
  Caller->>Helper: call(ids, commit=False)
  Helper->>Lock: re-acquire (re-entrant)
  Helper->>DB: chunked DELETE FROM *_fts
  Helper->>DB: chunked DELETE FROM *_vec (if available)
  Helper-->>Caller: return (no commit)
  Caller->>DB: conn.commit()
  Caller->>Lock: release
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ReflexioAI/reflexio#66: Adds the RetentionMixin orchestration that defines playbook retention targets — the base retention cascade code path this PR modifies lives there.
  • ReflexioAI/reflexio#194: Directly modifies delete_all_user_playbooks_by_status in _playbook.py, the same method this PR rewires for unified commit control.
  • ReflexioAI/reflexio#197: Related change applying similar transaction atomicity and search-row cleanup patterns to interactions and profiles retention cascades.

Poem

🐇 No more stray FTS rows when a playbook departs,
One lock, one commit—transactional hearts!
Chunked deletes hoppity-hop through the vec,
And when things go wrong, rollback's got your back.
"Atomicity wins!" cried the rabbit with cheer,
"All search rows vanish—crystal clear!" 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% 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 accurately describes the main change: completing delete-path atomicity improvements for playbook deletion, specifically the retention and playbook-delete atomicity work as part of Lineage Phase B3h.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lineage-phase-b3h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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

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/storage/sqlite_storage/_playbook.py (1)

358-373: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add rollback guards around the new deferred-commit delete blocks.

These paths now rely on one final commit, but an exception before that commit can leave partial row/lineage/search-index changes pending on the shared connection. Roll back inside the same lock before re-raising.

Suggested pattern
 with self._lock:
-    cur = self.conn.execute(...)
-    ...
-    self._delete_playbook_search_rows("user", [user_playbook_id], commit=False)
-    self.conn.commit()
+    try:
+        cur = self.conn.execute(...)
+        ...
+        self._delete_playbook_search_rows("user", [user_playbook_id], commit=False)
+        self.conn.commit()
+    except Exception:
+        self.conn.rollback()
+        raise

Also applies to: 524-535, 890-907

🤖 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.py` around lines
358 - 373, The delete_user_playbook method (and the other similar methods at
lines 524-535 and 890-907) perform multiple operations that defer the final
commit, but lack exception handling for failures that occur between the initial
delete and the final commit call. Wrap the operations that occur after acquiring
the lock in a try-except block, ensuring that if any exception is raised during
the delete operations or search index cleanup, self.conn.rollback() is called
within the same lock before re-raising the exception to prevent partial changes
from being committed to the database.
🧹 Nitpick comments (1)
tests/server/services/storage/test_storage_contract_retention.py (1)

225-322: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Cover the retention vec cleanup branch too.

These retention tests exercise the new helper’s FTS path, but not the _has_sqlite_vec path for user_playbooks_vec / agent_playbooks_vec. Add conditional vec assertions like the bulk-delete test so regressions in the vec table-name or chunk-delete path are caught when sqlite-vec is available.

🤖 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_retention.py` around
lines 225 - 322, The retention tests
test_retention_user_playbook_delete_cleans_fts and
test_retention_agent_playbook_delete_cleans_fts currently only verify FTS table
cleanup but do not verify vec table cleanup for user_playbooks_vec and
agent_playbooks_vec tables. Add conditional assertions (similar to those in the
bulk-delete tests) after the delete_oldest_retention_target_rows call in both
test functions to verify that vec rows are properly cleaned up when sqlite-vec
is available. Check that the two oldest vec entries are removed and the
surviving entry's vec row remains by using the same pattern as the FTS
assertions but querying the appropriate vec tables instead.
🤖 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/_base.py`:
- Around line 757-764: The retention transaction block containing the
self._delete_playbook_search_rows and
self._delete_source_windows_for_agent_playbook_ids method calls uses deferred
commits (commit=False), but lacks proper error handling. If any of these delete
operations or subsequent operations fail, the pending deletes will remain
uncommitted and could be inadvertently committed by a later operation. Wrap the
entire retention transaction block in a try/except statement that catches
exceptions, calls self.conn.rollback() to roll back any pending deferred
commits, and then re-raises the exception to preserve the original error
behavior.

In `@reflexio/server/services/storage/sqlite_storage/_playbook.py`:
- Around line 524-535: The code captures user_playbook_ids via SELECT before
executing the DELETE statement, but another SQLiteStorage instance can modify
the database between these operations, causing a mismatch between the selected
IDs and the rows actually deleted. Instead of passing the pre-snapshot ids list
to the _delete_playbook_search_rows call on line 534, use the rowcount property
from the DELETE cursor (cur.rowcount) to determine which rows were actually
deleted, or restructure the code to use an atomic pattern like DELETE ...
RETURNING (if supported by SQLite) to ensure the IDs passed to
_delete_playbook_search_rows match the exact rows that were removed.

---

Outside diff comments:
In `@reflexio/server/services/storage/sqlite_storage/_playbook.py`:
- Around line 358-373: The delete_user_playbook method (and the other similar
methods at lines 524-535 and 890-907) perform multiple operations that defer the
final commit, but lack exception handling for failures that occur between the
initial delete and the final commit call. Wrap the operations that occur after
acquiring the lock in a try-except block, ensuring that if any exception is
raised during the delete operations or search index cleanup,
self.conn.rollback() is called within the same lock before re-raising the
exception to prevent partial changes from being committed to the database.

---

Nitpick comments:
In `@tests/server/services/storage/test_storage_contract_retention.py`:
- Around line 225-322: The retention tests
test_retention_user_playbook_delete_cleans_fts and
test_retention_agent_playbook_delete_cleans_fts currently only verify FTS table
cleanup but do not verify vec table cleanup for user_playbooks_vec and
agent_playbooks_vec tables. Add conditional assertions (similar to those in the
bulk-delete tests) after the delete_oldest_retention_target_rows call in both
test functions to verify that vec rows are properly cleaned up when sqlite-vec
is available. Check that the two oldest vec entries are removed and the
surviving entry's vec row remains by using the same pattern as the FTS
assertions but querying the appropriate vec tables instead.
🪄 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 Plus

Run ID: 83d85ca3-cfaf-41d0-b329-8fa54026ef56

📥 Commits

Reviewing files that changed from the base of the PR and between d424a43 and 8896b83.

📒 Files selected for processing (3)
  • reflexio/server/services/storage/sqlite_storage/_base.py
  • reflexio/server/services/storage/sqlite_storage/_playbook.py
  • tests/server/services/storage/test_storage_contract_retention.py

Comment thread reflexio/server/services/storage/sqlite_storage/_base.py
Comment thread reflexio/server/services/storage/sqlite_storage/_playbook.py
…y-ids consistency in delete_all_user_playbooks_by_status

Fix 1 (_base.py _retention_perform_delete): wrap the dependency+target
delete block in try/except and call self.conn.rollback() on any exception
so uncommitted writes on the shared connection cannot be flushed by a
subsequent unrelated commit.

Fix 2 (_playbook.py delete_all_user_playbooks_by_status): change the
DELETE to target the snapshot id set captured by the preceding SELECT
(DELETE ... WHERE user_playbook_id IN (...)) instead of re-evaluating
the WHERE predicate.  This makes the DELETE and the subsequent
_delete_playbook_search_rows cleanup consistent when two connections
modify the same table between the SELECT and DELETE.

Add tests/server/services/storage/test_retention_rollback_integration.py:
two integration tests that inject failures into _retention_delete_target_rows
and _retention_delete_dependencies and assert the connection is fully
rolled back (rows and FTS index intact).
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