Skip to content

fix: make durable playbook recovery deterministic - #443

Merged
yyiilluu merged 2 commits into
mainfrom
codex/fix-prod-sentry-cp-qx
Aug 10, 2026
Merged

fix: make durable playbook recovery deterministic#443
yyiilluu merged 2 commits into
mainfrom
codex/fix-prod-sentry-cp-qx

Conversation

@yyiilluu

@yyiilluu yyiilluu commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Make automatic playbook review reconstruct the exact interaction window persisted by extraction, eliminating the sliding-window race behind production finalization failures.
  • Require new durable playbook runs to have one non-empty owner while recovering legacy nullable rows only from complete, unanimous persisted evidence.
  • Query authoritative resume discovery before the bootstrap context on every tick so a stale organization cannot block work on healthy refs.
  • Keep LiteLLM fallback classification tied to the current exception so stale provider metadata does not create noisy or misleading Sentry events.

Changes

Playbook review and ownership

  • Added a shared persisted-window reconstruction and validation boundary for automatic, manual, and resumed review.
  • Pass the extraction run through generation and finalization instead of querying a new last-K interaction window.
  • Enforce non-empty user_id values for new runs and fail closed on missing, cross-user, or ambiguous evidence.
  • Infer legacy nullable run ownership in memory only when all persisted interactions and requests agree.

Resume scheduling

  • Consult the injected org provider once at the beginning of every scheduler tick and treat its result as authoritative.
  • Replace stale bootstrap organizations from the current provider result while preserving one-org fallback when the provider itself raises.
  • Expire pending tool calls on every discovered storage ref before draining work.

Error classification

  • Preserve structured-output fallback metadata on the exception that produced it.
  • Prevent unrelated exception context from leaking into later LiteLLM error classification.

Documentation and tests

  • Document exact review provenance, owner compatibility, and cross-ref resume discovery.
  • Add regressions for sliding-window publication races, normal and resumed finalization, missing/cross-user evidence, provider failures, stale bootstraps, and multi-ref expiration.

Test Plan

  • uv run pytest tests/ --ignore=tests/e2e_tests/ -o 'addopts=' -q — 5,665 passed, 13 skipped
  • uv run pytest tests/e2e_tests/ -o 'addopts=' -q — 47 passed, 87 skipped
  • Focused extraction/playbook/storage regressions — 68 passed, 3 skipped
  • uv run ruff check reflexio tests
  • uv run ruff format --check reflexio tests
  • uv run pyright --threads 4 — 0 errors
  • TestSprite is configured only against production, so branch-specific black-box verification is deployment-gated.

Summary by CodeRabbit

  • Bug Fixes

    • Improved recovery from transient LLM failures, including wrapped timeouts and upstream errors.
    • Strengthened resumable extraction and finalization for legacy records with missing ownership details.
    • Scheduler recovery now handles stale or unavailable organization discovery more reliably and expires pending work across all discovered organizations.
    • Playbook reviews now reconstruct the exact persisted interaction history and fail safely when evidence is incomplete or inconsistent.
  • Documentation

    • Clarified scheduler discovery, ownership requirements, recovery behavior, and review-window rules.

Reconstruct review evidence from the extraction run's persisted window, require an unambiguous user owner, and discover resumable work from the authoritative provider before relying on a bootstrap org. Preserve exception metadata only when it belongs to the current LiteLLM failure.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 99fb741c-6723-48a7-921c-9a8f9dadb1ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc6657 and 7a5c438.

📒 Files selected for processing (1)
  • tests/server/services/extraction/test_resume_worker.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/server/services/extraction/test_resume_worker.py

📝 Walkthrough

Walkthrough

The PR strengthens LLM transient-error classification, makes organization discovery authoritative during scheduler ticks, enforces durable run ownership, and reconstructs playbook review windows from persisted interaction provenance.

Changes

Recovery and review workflows

Layer / File(s) Summary
LLM transient-error propagation
reflexio/server/llm/..., tests/server/llm/test_litellm_client_unit.py
Wrapped transient errors and upstream provider types now propagate through LiteLLMClientError.
Organization resume scheduling
reflexio/server/api.py, reflexio/server/services/extraction/resume_scheduler.py, tests/server/services/extraction/test_resume_scheduler.py
Scheduler ticks use provider organization lists, handle provider failures, replace stale organizations, and expire pending calls per organization.
Durable run ownership and resume finalization
reflexio/server/services/extraction/..., tests/server/services/extraction/test_resume_worker.py, tests/server/services/storage/test_agent_run_helpers.py
Durable runs require non-empty user IDs. Legacy playbook runs infer ownership only from complete, consistent evidence.
Persisted playbook review reconstruction
reflexio/server/services/playbook/..., tests/server/services/playbook/..., tests/eval/extraction/providers.py
Review uses validated persisted source interaction IDs and ordered request groups. Generation and service configuration require non-empty user IDs.
Workflow documentation
reflexio/server/services/extraction/README.md, reflexio/server/services/playbook/README.md
Documentation describes ownership validation, scheduler discovery, and persisted review-window behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ResumeWorker
  participant PlaybookGenerationService
  participant review_window
  participant BaseStorage
  ResumeWorker->>PlaybookGenerationService: provide extraction run and user_id
  PlaybookGenerationService->>review_window: reconstruct source_interaction_ids
  review_window->>BaseStorage: load persisted interactions and requests
  BaseStorage-->>review_window: return validated evidence
  review_window-->>PlaybookGenerationService: return ordered review window
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: deterministic durable playbook recovery.
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 💡 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/fix-prod-sentry-cp-qx

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

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

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

Extract the shared load-and-validate block.

Lines 31-50 and 106-125 are identical. Both functions dedupe the IDs, reject an empty list, load the interactions, and reject missing IDs. A shared helper keeps the two fail-closed paths from drifting apart later.

♻️ Proposed helper extraction
+def _load_source_interactions(
+    *,
+    storage: BaseStorage,
+    source_interaction_ids: Sequence[int],
+    subject: str,
+) -> dict[int, Interaction]:
+    """Load every cited interaction or fail closed."""
+    source_ids = list(dict.fromkeys(source_interaction_ids))
+    if not source_ids:
+        raise PlaybookReviewWindowError(
+            f"{subject} has no complete generation-window provenance"
+        )
+    interactions_by_id = {
+        interaction.interaction_id: interaction
+        for interaction in storage.get_interactions_by_ids(source_ids)
+    }
+    missing_interaction_ids = [
+        interaction_id
+        for interaction_id in source_ids
+        if interaction_id not in interactions_by_id
+    ]
+    if missing_interaction_ids:
+        raise PlaybookReviewWindowError(
+            f"{subject} is missing persisted generation-window interactions: "
+            f"{missing_interaction_ids}"
+        )
+    return interactions_by_id

Then both functions call it:

-    source_ids = list(dict.fromkeys(source_interaction_ids))
-    if not source_ids:
-        raise PlaybookReviewWindowError(
-            f"{subject} has no complete generation-window provenance"
-        )
-
-    interactions_by_id = {
-        interaction.interaction_id: interaction
-        for interaction in storage.get_interactions_by_ids(source_ids)
-    }
-    missing_interaction_ids = [
-        interaction_id
-        for interaction_id in source_ids
-        if interaction_id not in interactions_by_id
-    ]
-    if missing_interaction_ids:
-        raise PlaybookReviewWindowError(
-            f"{subject} is missing persisted generation-window interactions: "
-            f"{missing_interaction_ids}"
-        )
+    interactions_by_id = _load_source_interactions(
+        storage=storage,
+        source_interaction_ids=source_interaction_ids,
+        subject=subject,
+    )

Also applies to: 106-125

🤖 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/review_window.py` around lines 31 - 50,
Extract the duplicated source-ID deduplication and interaction
loading/validation logic into a shared helper in review_window.py. Have both
affected functions call the helper, preserving the existing empty-source and
missing-interaction PlaybookReviewWindowError messages and fail-closed 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 `@tests/server/services/extraction/test_resume_worker.py`:
- Line 206: Update the pytest.raises call’s match pattern in the resume-worker
test to use a raw string literal, preserving the existing regular expression and
expected ValueError assertion.

---

Nitpick comments:
In `@reflexio/server/services/playbook/review_window.py`:
- Around line 31-50: Extract the duplicated source-ID deduplication and
interaction loading/validation logic into a shared helper in review_window.py.
Have both affected functions call the helper, preserving the existing
empty-source and missing-interaction PlaybookReviewWindowError messages and
fail-closed behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d54be0fc-8bb8-40eb-8ece-68589a407885

📥 Commits

Reviewing files that changed from the base of the PR and between a204ef4 and 1fc6657.

📒 Files selected for processing (22)
  • reflexio/server/api.py
  • reflexio/server/llm/_litellm_text_generation.py
  • reflexio/server/llm/_litellm_types.py
  • reflexio/server/services/extraction/README.md
  • reflexio/server/services/extraction/agent_run_records.py
  • reflexio/server/services/extraction/resumable_agent.py
  • reflexio/server/services/extraction/resume_scheduler.py
  • reflexio/server/services/extraction/resume_worker.py
  • reflexio/server/services/playbook/README.md
  • reflexio/server/services/playbook/playbook_service_utils.py
  • reflexio/server/services/playbook/review_service.py
  • reflexio/server/services/playbook/review_window.py
  • reflexio/server/services/playbook/service.py
  • tests/eval/extraction/providers.py
  • tests/server/llm/test_litellm_client_unit.py
  • tests/server/services/extraction/test_resume_scheduler.py
  • tests/server/services/extraction/test_resume_worker.py
  • tests/server/services/playbook/test_extractor_polarity_integration.py
  • tests/server/services/playbook/test_playbook_extractor.py
  • tests/server/services/playbook/test_playbook_generation_service.py
  • tests/server/services/playbook/test_playbook_generation_service_integration.py
  • tests/server/services/storage/test_agent_run_helpers.py

Comment thread tests/server/services/extraction/test_resume_worker.py Outdated
@yyiilluu
yyiilluu merged commit 6b8b5ab into main Aug 10, 2026
1 check passed
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