Skip to content

fix(playbook): don't abort a rerun aggregation when mock mode has no centroid - #436

Merged
yilu331 merged 1 commit into
mainfrom
fix/aggregation-centroid-without-embedder
Aug 7, 2026
Merged

fix(playbook): don't abort a rerun aggregation when mock mode has no centroid#436
yilu331 merged 1 commit into
mainfrom
fix/aggregation-centroid-without-embedder

Conversation

@yilu331

@yilu331 yilu331 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

4 e2e tests fail on main:

RuntimeError: rerun agent playbook has no centroid embedding
aggregator.py:1773
  • test_playbook_workflows.py — 3 tests
  • test_openclaw_integration.py — 1 test

They pass at b0e0755 and fail from c24e1e7 onward, so this is a regression, not an environment quirk.

Root cause

Two changes that were each fine alone.

159d8ab (#410) added the invariant: on the rerun path, a saved agent playbook must carry an embedding to persist as a cluster centroid. Safe at the time — local embeddings were computed in-process, so saved_fb.embedding was always populated and the raise was unreachable outside a genuine bug.

c24e1e7 (#425) removed in-process local inference ("service-only inference boundary"). embedding_provider_mode now returns local_service for any local model with no env vars set, so embedding goes over HTTP to 127.0.0.1:8072.

With the embedder unreachable, SQLiteStorage deliberately degrades:

Embedding unavailable for document text; continuing without vector

…and saves the playbook with embedding=None. The invariant then fires and rolls back the entire aggregation. An assertion written to catch a programming error now trips on an infrastructure state.

This is not narrow: reflexio/lib/_generation.py:73 sets rerun=True for every run_playbook_aggregation() call, so the branch is the normal path, not an edge case.

Why it wasn't caught

  • This repo runs no CI workflows.
  • Enterprise ci-fast.yml:104 runs --ignore=tests/e2e_tests/.
  • The e2e tier only runs in release.yml.
  • The invariant had no test coverage at allgrep 'no centroid embedding' tests/ returns nothing.

Fix

Mock mode is the case that has no centroid by construction: it clusters by trigger rather than by vector (the MOCK_LLM_RESPONSE branch in get_clusters), so a centroid was never meaningful there. Skip the cluster bookkeeping instead of aborting.

Every other caller still raises — a centroid-less cluster row would silently break the incremental re-aggregation that table exists to feed. Production behaviour outside mock mode is unchanged.

Adds the three cases the invariant never had:

  • the raise still fires outside mock mode
  • mock mode reaches the save, then skips the centroid write
  • the happy path still records the cluster

Verified the new tests fail against the pre-fix aggregator with the exact RuntimeError.

Verification

  • OSS e2e tier: 47 passed, 87 skipped (was 4 failed / 43 passed)
  • OSS unit tier: 4368 passed, 9 skipped
  • ruff + pyright clean

Worth a second opinion

The mock-mode carve-out fixes the tests, but the underlying mismatch is broader: storage treats a missing embedding as degrade and continue, while the aggregator treats it as fatal. In any deployment where the embedding service is unreachable, run_playbook_aggregation() now hard-fails and rolls back rather than degrading. That may well be intended — failing loudly beats silently writing a useless centroid — but it changed behaviour without discussion when #425 landed, so the owners of #410/#425 should confirm which semantics they want.

`run_playbook_aggregation` always sets rerun=True (lib/_generation.py:73)
and holds a claim, so every call reaches the branch that persists a
cluster centroid. That branch requires `saved_fb.embedding`.

The requirement was safe when it was written: local embeddings were
computed in-process, so the field was always populated and the raise was
unreachable outside a genuine bug. Routing local inference through a
separate service turned "no embedding" into a normal runtime state --
with the embedder unreachable, storage logs "continuing without vector"
and saves the playbook without one, and an invariant written to catch a
programming error now fires on infrastructure and rolls the whole
aggregation back.

Mock mode is the case that has no centroid by construction: it clusters
by trigger rather than by vector, so the centroid was never meaningful
there. Skip the cluster bookkeeping instead of aborting. Every other
caller still raises -- a centroid-less cluster row would silently break
the incremental re-aggregation the table exists to feed.

Found via 4 e2e failures (test_playbook_workflows, test_openclaw_-
integration) that pass at the parent commit and fail from the routing
change onward. They went unnoticed because the OSS repo runs no
workflows and enterprise ci-fast passes --ignore=tests/e2e_tests/; the
e2e tier only runs in release.

Adds the coverage the invariant never had: the raise still fires outside
mock mode, mock mode reaches the save and skips the centroid write, and
the happy path still records the cluster.

OSS e2e 47 passed (was 4 failed / 43 passed); unit tier 4368 passed.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@yilu331, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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 Plus

Run ID: 04c339ee-13c6-4023-b9d6-978b86440417

📥 Commits

Reviewing files that changed from the base of the PR and between 936ffee and 2762310.

📒 Files selected for processing (2)
  • reflexio/server/services/playbook/components/aggregator.py
  • tests/server/services/playbook/test_cluster_change_detection.py

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.

@yilu331

yilu331 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

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

@yilu331
yilu331 merged commit 203f59f into main Aug 7, 2026
1 check passed
@yilu331
yilu331 deleted the fix/aggregation-centroid-without-embedder branch August 7, 2026 22:01
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