Skip to content

fix(playbook): restore apply_playbook_edit, deleted while still imported - #394

Merged
guangyu-reflexio merged 3 commits into
mainfrom
fix/restore-playbook-edit-apply
Jul 29, 2026
Merged

fix(playbook): restore apply_playbook_edit, deleted while still imported#394
guangyu-reflexio merged 3 commits into
mainfrom
fix/restore-playbook-edit-apply

Conversation

@guangyu-reflexio

@guangyu-reflexio guangyu-reflexio commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

main cannot boot the API.

PR #385 (4268ad1) deleted reflexio/server/services/playbook/playbook_edit_apply.py
under the changelog line "Remove the obsolete split-edit application path." That module
is not the split-edit path — the split/differentiate path lives in
playbook_generation_service.py. playbook_edit_apply.py is the shared atomic supersede
primitive, and its consumer survived the deletion:

review_service.py:33   from ...playbook.playbook_edit_apply import apply_playbook_edit
review_service.py:297  successor_id = apply_playbook_edit(...)

apply_playbook_edit exists nowhere else on main, so no replacement was shipped.
review_service.py is imported by server/routes/playbooks.py, which loads at app
startup — a guaranteed ModuleNotFoundError on boot.

How it surfaced

A production deploy built from a clean checkout of main. The task started,
migrations ran clean, then it failed container health checks on the API port and the ECS
deployment circuit breaker rolled the service back. Logs showed 200×:

ModuleNotFoundError: No module named 'reflexio.server.services.playbook.playbook_edit_apply'

Every earlier build came from a working tree that still had the file on disk, so the
image contained a file origin/main no longer has. A fresh checkout is the only build
that exposes it.

Fix

Restore the file verbatim from 4268ad1^. Every dependency it uses still exists on
main: supersede_record, commit_scope, save_user_playbooks,
precompute_user_playbook_embeddings, and LineageContext.

This is deliberately the minimal fix. If the intent was genuinely to retire this
primitive, the follow-up is to port review_service.py onto the publication API — but
that is a behavioural change and does not belong in a build-unbreaking commit.

Verification

Import, reproducing the exact production failure and its resolution:

Tree import reflexio.server.routes.playbooks
origin/main ModuleNotFoundError: ...playbook_edit_apply
this branch OK

Tests (tests/server/services/playbook):

Tree Result
origin/main collection error — the entire directory fails to collect
this branch 531 passed, 8 failed

The 8 failures are all in test_playbook_extractor.py and are pre-existing — they
fail identically on pristine main when run standalone (6 in that file alone), and are
untouched by this change, which only adds a file.

ruff check and ruff format --check pass on the restored file.

Note

CI did not catch a change that makes the app unbootable and breaks a whole test
directory's collection. That gap is worth a separate look.

Summary by CodeRabbit

  • New Features
    • Added atomic apply-playbook-edit behavior that replaces the current playbook safely while archiving the incumbent.
    • Preserve provenance and lineage for new revisions, including using the edit’s request correlation.
    • Added optional embedding generation skipping when applying an edit.
  • Bug Fixes
    • Improved handling of concurrent updates to prevent conflicting or orphaned successors; non-applicable incumbents now fail cleanly.
    • Reject empty request identifiers before any write operations.
  • Tests
    • Added comprehensive storage-backed coverage for success, concurrency, rollback/orphan prevention, and request-id validation.

#385 removed `playbook_edit_apply.py` as part of "remove the obsolete
split-edit application path", but that module was not the split path — it is
the shared atomic supersede primitive, and `review_service.py` still imports
it at module scope and calls it at line 297. `apply_playbook_edit` exists
nowhere else on main, so no replacement was shipped.

`review_service.py` is imported by `server/routes/playbooks.py`, which loads
at app startup, making this a guaranteed ModuleNotFoundError on boot. It was
caught when a production deploy built from a clean checkout of main: the task
started, failed its container health checks on the API port, and the
deployment circuit breaker rolled the service back. Builds from working
trees that still had the file on disk masked it.

The whole `tests/server/services/playbook` directory currently fails at
collection on main for the same reason.

Restored verbatim from 4268ad1^; every dependency it uses
(`supersede_record`, `commit_scope`, `save_user_playbooks`,
`precompute_user_playbook_embeddings`, `LineageContext`) still exists.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e1fb1bc-12ba-435a-bfe3-873e6d74c751

📥 Commits

Reviewing files that changed from the base of the PR and between 15bcd29 and dbb01dc.

📒 Files selected for processing (1)
  • tests/server/services/playbook/test_playbook_edit_apply.py
📝 Walkthrough

Walkthrough

Adds apply_playbook_edit for atomic playbook successor insertion and incumbent supersession, with request validation, lineage tracking, optional embeddings, CAS race handling, and rollback cleanup. New SQLite tests cover successful updates, races, validation, and lineage identifiers.

Changes

Playbook edit application

Layer / File(s) Summary
Atomic edit flow
reflexio/server/services/playbook/playbook_edit_apply.py
Adds request validation, provenance assignment, optional embedding preparation, lineage context handling, atomic successor insertion, conditional supersession, and provisional-id cleanup after a lost race.
Atomic edit validation
tests/server/services/playbook/test_playbook_edit_apply.py
Tests successful replacement, archived-incumbent races, rollback without orphan successors, invalid request IDs, and operation request-ID lineage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant apply_playbook_edit
  participant new_playbook
  participant storage.commit_scope
  participant storage.supersede_record
  apply_playbook_edit->>new_playbook: assign source and prepare embeddings
  apply_playbook_edit->>storage.commit_scope: begin atomic update
  storage.commit_scope->>new_playbook: save successor as CURRENT
  apply_playbook_edit->>storage.supersede_record: conditionally supersede incumbent
  storage.supersede_record-->>apply_playbook_edit: return CAS result
  apply_playbook_edit-->>new_playbook: reset provisional id on race
Loading

Possibly related PRs

Suggested reviewers: yilu331

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 restored apply_playbook_edit module and the build break it fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/restore-playbook-edit-apply

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.

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

73-87: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider logging when a supersede race is lost.

The insert-then-supersede sequencing and rollback-on-race logic (via commit_scope + _LostSupersedeRaceError) is correct. As an optional improvement, the lost-race path (lines 84-86) silently returns -1 with no log entry — a debug/info log here would help surface contention on frequently-edited playbooks in production.

💡 Optional logging addition
     except _LostSupersedeRaceError:
+        logger.debug(
+            "apply_playbook_edit: lost supersede race for incumbent_id=%s", incumbent_id
+        )
         new_playbook.user_playbook_id = 0
         return -1
🤖 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/playbook_edit_apply.py` around lines 73 -
87, Add a debug or info log in the _LostSupersedeRaceError handler before
resetting new_playbook.user_playbook_id and returning -1, recording that the
supersede race was lost and including relevant playbook identifiers or context
available in the surrounding flow.
🤖 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/playbook_edit_apply.py`:
- Around line 73-87: Add a debug or info log in the _LostSupersedeRaceError
handler before resetting new_playbook.user_playbook_id and returning -1,
recording that the supersede race was lost and including relevant playbook
identifiers or context available in the surrounding flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9f98fe9-f824-46a2-b5a4-7211f81c67e4

📥 Commits

Reviewing files that changed from the base of the PR and between 72093f9 and 61f8416.

📒 Files selected for processing (1)
  • reflexio/server/services/playbook/playbook_edit_apply.py

#385 deleted `tests/server/services/playbook/test_playbook_edit_apply.py`
alongside the module it covers. Restoring only the source left
`apply_playbook_edit` with no direct unit coverage — its behaviour was
exercised only indirectly, through `review_service`.

Restored verbatim from 4268ad1^; all 8 tests pass unmodified against current
main, which independently confirms the supersede CAS semantics the restored
module relies on are unchanged.

@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 (2)
tests/server/services/playbook/test_playbook_edit_apply.py (2)

89-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate test with stale docstring referencing a non-existent parameter.

test_apply_expect_current_false_archives is byte-for-byte identical to test_apply_inserts_new_and_archives_incumbent (Lines 31-58), yet its name and docstring reference an expect_current=False parameter. Per the restored apply_playbook_edit signature (storage, *, incumbent_id, new_playbook, source, request_id, skip_embedding=False, revise_context=None), no such parameter exists. This is dead-weight coverage and could mislead future readers into thinking such a parameter is part of the API.

♻️ Suggested cleanup
-def test_apply_expect_current_false_archives():
-    """With expect_current=False, new playbook is inserted and incumbent is archived."""
-    from reflexio.server.services.playbook.playbook_edit_apply import (
-        apply_playbook_edit,
-    )
-
-    with tempfile.TemporaryDirectory() as tmp:
-        s = _storage(tmp)
-        with patch.object(SQLiteStorage, "_get_embedding", return_value=[0.0] * 512):
-            old = _playbook(content="old")
-            s.save_user_playbooks([old])
-            old_id = old.user_playbook_id
-            assert old_id > 0
-
-            new = _playbook(content="new")
-            new_id = apply_playbook_edit(
-                s,
-                incumbent_id=old_id,
-                new_playbook=new,
-                source="offline_optimizer",
-                request_id="run-abc",
-            )
-        assert new_id > 0
-
-        # Only new_id should be CURRENT (status=None); old_id should be archived
-        all_pbs = s.get_user_playbooks(user_id="u1")
-        current_ids = {p.user_playbook_id for p in all_pbs if p.status is None}
-        assert new_id in current_ids
-        assert old_id not in current_ids
-
-
🤖 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_edit_apply.py` around lines 89 -
117, Remove the duplicate test_apply_expect_current_false_archives test,
including its stale docstring and assertions, and retain the existing
test_apply_inserts_new_and_archives_incumbent coverage. Do not introduce or
reference a nonexistent expect_current parameter.

15-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated embedding-patch pattern into a fixture.

patch.object(SQLiteStorage, "_get_embedding", return_value=[0.0] * 512) is repeated identically 8 times across this file (once inside _storage() and once in every test body). A pytest.fixture yielding a patched SQLiteStorage instance would remove this duplication and reduce future maintenance surface.

♻️ Example fixture
`@pytest.fixture`
def storage(tmp_path):
    with patch.object(SQLiteStorage, "_get_embedding", return_value=[0.0] * 512):
        yield SQLiteStorage(org_id="org_apply_1", db_path=f"{tmp_path}/t.db")

Also applies to: 38-38, 68-68, 97-97, 132-132, 176-176, 211-211, 238-238

🤖 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_edit_apply.py` around lines 15 -
18, Extract the repeated SQLiteStorage embedding patch into a pytest fixture,
replacing the local patch blocks in _storage and all affected test bodies. Have
the fixture yield a patched SQLiteStorage instance using the existing
temporary-path and org configuration, then update tests to reuse that fixture.
🤖 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 `@tests/server/services/playbook/test_playbook_edit_apply.py`:
- Around line 89-117: Remove the duplicate
test_apply_expect_current_false_archives test, including its stale docstring and
assertions, and retain the existing
test_apply_inserts_new_and_archives_incumbent coverage. Do not introduce or
reference a nonexistent expect_current parameter.
- Around line 15-18: Extract the repeated SQLiteStorage embedding patch into a
pytest fixture, replacing the local patch blocks in _storage and all affected
test bodies. Have the fixture yield a patched SQLiteStorage instance using the
existing temporary-path and org configuration, then update tests to reuse that
fixture.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 260a73bf-29c2-445d-b921-332ebb6c24ae

📥 Commits

Reviewing files that changed from the base of the PR and between 61f8416 and 15bcd29.

📒 Files selected for processing (1)
  • tests/server/services/playbook/test_playbook_edit_apply.py

The restored test file carried two references to an `expect_current`
parameter that `apply_playbook_edit` has never had:

- `test_apply_expect_current_false_archives` was byte-identical to
  `test_apply_inserts_new_and_archives_incumbent` (both bodies 1009 chars),
  so it added no coverage while implying an API that does not exist. Removed.
- `test_apply_expect_current_false_returns_minus1_and_no_orphan` is a genuine
  lost-race test — it asserts the -1 return, the rollback, and the absence of
  an orphan lineage event — and only its name was stale. Renamed to
  `test_apply_returns_minus1_and_leaves_no_orphan_on_lost_race`; body and
  docstring are unchanged.

`grep -rn expect_current reflexio/` returns nothing, confirming the parameter
is not part of any current signature.
@guangyu-reflexio
guangyu-reflexio merged commit bb81e38 into main Jul 29, 2026
1 check passed
@guangyu-reflexio
guangyu-reflexio deleted the fix/restore-playbook-edit-apply branch July 29, 2026 05:51
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