Skip to content

refactor: route local inference through shared service - #425

Merged
yyiilluu merged 2 commits into
mainfrom
codex/service-only-inference
Aug 7, 2026
Merged

refactor: route local inference through shared service#425
yyiilluu merged 2 commits into
mainfrom
codex/service-only-inference

Conversation

@yyiilluu

@yyiilluu yyiilluu commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add the profile module cutover plan for the LLM/pipeline architecture migration.
  • Update enterprise profile test imports to the canonical OSS profile service/extractor paths.
  • Bump the OSS submodule pointer to the profile service/extractor cutover commit.

Changes

  • Planning
    • Added docs/superpowers/plans/2026-06-26-profile-module-cutover.md.
  • Enterprise tests
    • Updated profile extractor and usage-metrics tests to import ProfileGenerationService from profile.service and ProfileExtractor from profile.components.extractor.
  • Submodule
    • Points open_source/reflexio at the OSS profile module cutover commit.

Related PRs

Test Plan

  • uv run pytest open_source/reflexio/tests/server/services/profile/test_profile_module_contract.py open_source/reflexio/tests/server/services/profile/test_profile_extractor.py open_source/reflexio/tests/server/services/profile/test_profile_generation_service.py open_source/reflexio/tests/server/services/profile/test_profile_consolidator.py open_source/reflexio/tests/server/services/extraction/test_resume_worker.py reflexio_ext/tests/server/services/profile/test_profile_extractor.py -q -o 'addopts=' -> 122 passed
  • uv run pytest open_source/reflexio/tests/lib/test_profiles_unit.py open_source/reflexio/tests/lib/test_generation_unit.py open_source/reflexio/tests/eval/extraction/test_extraction_eval.py -q -o 'addopts=' -> 71 passed, 2 skipped
  • uv run ruff check open_source/reflexio/reflexio/server/services/profile open_source/reflexio/reflexio/server/services/generation_service.py open_source/reflexio/reflexio/server/services/extraction/resume_worker.py open_source/reflexio/reflexio/lib/_profiles.py open_source/reflexio/reflexio/lib/_generation.py open_source/reflexio/tests/server/services/profile open_source/reflexio/tests/server/services/extraction/test_resume_worker.py reflexio_ext/tests/server/services/profile/test_profile_extractor.py
  • uv run pyright open_source/reflexio/reflexio/server/services/profile open_source/reflexio/reflexio/server/services/generation_service.py open_source/reflexio/reflexio/server/services/extraction/resume_worker.py open_source/reflexio/reflexio/lib/_profiles.py open_source/reflexio/reflexio/lib/_generation.py open_source/reflexio/tests/server/services/profile open_source/reflexio/tests/server/services/extraction/test_resume_worker.py reflexio_ext/tests/server/services/profile/test_profile_extractor.py -> 0 errors
  • uv run python -c "import reflexio; print('import OK')"
  • rg -n -P "profile\.profile_generation_service(?!_utils)|profile\.profile_extractor|profile_generation_service\.py|profile_extractor\.py" open_source/reflexio/reflexio open_source/reflexio/tests reflexio_ext --glob '*.py' --glob '*.md' --glob '*.mdx' -> only the removed-file assertion in the new contract test

Summary by CodeRabbit

  • Documentation

    • Added a detailed cutover plan for the profile module, including updated import paths, validation steps, and verification checks.
  • Bug Fixes

    • Updated several imports and test hooks to match the new service and component locations for profile, playbook, and agent success evaluation workflows.
    • Adjusted end-to-end test timing patches to use the current scheduling and runner paths.
  • Tests

    • Updated unit and e2e tests to reflect the refactored module structure and keep coverage aligned with the new layout.

Rebase status

  • Rebased onto OSS origin/main b0e0755.
  • Resolved the playbook reviewer conflict by retaining upstream fatal-reason validation; the feature branch only needed the already-present modern return annotation.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change routes embeddings and reranking through a shared inference service. It adds reranker health, readiness, model discovery, and failure handling. It removes in-process embedding fallback and warmup gates. Retrieval floors now use model-specific defaults and global reranker enablement.

Changes

Shared inference routing and lifecycle

Layer / File(s) Summary
Service routing and startup
.env.example, reflexio/cli/..., reflexio/server/llm/...
Embedding providers now use shared inference-service routing. Local service startup depends on the configured service URL. In-process fallback and related dependency checks were removed.
Service-boundary validation
tests/cli/..., tests/server/llm/..., tests/server/services/storage/...
Tests validate service routing, capability caching, import boundaries, startup behavior, and deterministic embedding fixtures.

Reranker contract and execution

Layer / File(s) Summary
Reranker runtime
reflexio/server/llm/rerank/..., reflexio/server/llm/embedding_service.py
The reranker supports shared-service scoring, model discovery, readiness health, disabled operation, local runner loading, and prewarming.
Reranker validation
tests/server/llm/rerank/..., tests/server/llm/test_embedding_service_rerank.py, tests/e2e_tests/...
Tests cover model selection, readiness, disabled behavior, service failures, malformed responses, prewarming, and semantic scoring.

Retrieval floors and failure handling

Layer / File(s) Summary
Model-specific floor behavior
reflexio/models/config_schema.py, reflexio/server/services/retrieval/..., reflexio/server/services/unified_search_service.py
Retrieval floors accept nullable values and resolve calibrated defaults for the discovered reranker model. Floor processing requires enabled reranking.
Route and integration handling
reflexio/server/routes/..., tests/server/services/..., tests/server/routes/...
Unavailable rerankers return stable HTTP 503 responses or preserve unfiltered results. Expected local outages do not emit failure reports.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant APIWorkers
  participant InferenceService
  participant SearchRoute
  CLI->>InferenceService: Start or use shared inference service
  APIWorkers->>InferenceService: Request embeddings
  SearchRoute->>InferenceService: Request reranking
  InferenceService-->>SearchRoute: Return model-aware scores
Loading

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 33.33% 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 summarizes the primary change: routing local inference through a shared service.
✨ 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/service-only-inference

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

🧹 Nitpick comments (6)
reflexio/server/llm/rerank/common.py (1)

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

Sort __all__ to satisfy RUF022.

Ruff reports __all__ is not sorted. Apply isort-style ordering.

♻️ Proposed ordering
 __all__ = [
-    "RERANK_ENABLED_ENV_VAR",
     "ENGLISH_RERANK_MODEL",
-    "MULTILINGUAL_RERANK_MODEL",
     "ENGLISH_RERANK_REVISION",
-    "MULTILINGUAL_RERANK_REVISION",
-    "RERANK_MODEL_REVISIONS",
-    "RERANK_FLOOR_DEFAULTS",
-    "RERANK_MODEL",
+    "MULTILINGUAL_RERANK_MODEL",
+    "MULTILINGUAL_RERANK_REVISION",
+    "RERANK_ENABLED_ENV_VAR",
+    "RERANK_FLOOR_DEFAULTS",
+    "RERANK_MODEL",
+    "RERANK_MODEL_REVISIONS",
     "CrossEncoderUnavailableError",
     "reranker_enabled",
     "reranker_model_for_embedding",
     "reranker_revision",
     "resolve_retrieval_floor",
 ]
🤖 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/llm/rerank/common.py` around lines 83 - 97, Sort the entries
in the __all__ declaration in common.py using isort-style ordering to satisfy
RUF022, while preserving all existing exports.

Source: Linters/SAST tools

tests/server/llm/test_embedding_service_rerank.py (1)

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

Consider covering the disabled reranker health status.

_Runner.status returns only "ready" or "unavailable". The production CrossEncoderRunner.status also returns "disabled". No test requests /health/rerank while REFLEXIO_RERANK_ENABLED=false, so the disabled value of the RerankHealthResponse.status Literal is never exercised.

Add that assertion to pin the readiness contract that resolve_inference_service_capabilities consumes.

Also applies to: 99-112

🤖 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/llm/test_embedding_service_rerank.py` around lines 40 - 41,
Extend the reranker health tests around _Runner.status and the /health/rerank
request to cover REFLEXIO_RERANK_ENABLED=false, asserting that
RerankHealthResponse.status is "disabled". Keep the existing ready and
unavailable assertions unchanged, and ensure the test exercises the status
consumed by resolve_inference_service_capabilities.
tests/server/services/test_unified_search_temporal.py (1)

203-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared reranker constant.

relevance_floor binds score_pairs_with_model, so the patch target is valid. Import ENGLISH_RERANK_MODEL and return it instead of duplicating the literal model name.

🤖 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/test_unified_search_temporal.py` around lines 203 -
208, Update the test’s fake_score in the unified search temporal test to import
and return the shared ENGLISH_RERANK_MODEL constant instead of duplicating the
reranker model string; keep the existing score list and relevance_floor patch
target unchanged.
tests/server/services/retrieval/test_relevance_floor.py (1)

197-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for an unknown reranker model with a null floor.

These two tests pin the known-model default and the explicit 0.0 override. No test covers a discovered model that is absent from RERANK_FLOOR_DEFAULTS while the configured floor is None. That combination is the default configuration and currently escapes the fail-open guard in reflexio/server/services/retrieval/relevance_floor.py Line 160.

Add a test that returns an unrecognized model from score_pairs_with_model and asserts the unfiltered pool is returned.

🤖 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/retrieval/test_relevance_floor.py` around lines 197 -
220, Add a test alongside test_nullable_floor_uses_discovered_model_default that
patches score_pairs_with_model to return an unrecognized model with scores,
invokes apply_relevance_floors with a None floor, and asserts the original
unfiltered item pool is returned. Use the existing arms and assertion style,
specifically covering the unknown-model/null-floor path.
tests/server/services/search/test_rerank_integration.py (1)

43-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the fake scorer tolerant of punctuation.

document.lower().split() keeps trailing punctuation on a token. A seeded content such as "The user loves pizza." yields the token pizza., which does not match food_terms. Every document then scores -1.0. The sort is stable, so the relevant profiles still appear first by input order, and test_rerank_surfaces_relevant_profile_above_irrelevant passes without exercising the scoring logic.

Strip punctuation before matching, or match with a substring test.

♻️ Proposed refactor
     def json(self) -> dict:
         food_terms = {"pasta", "pizza", "ramen", "peanuts"}
         return {
             "data": [
                 {
                     "index": index,
                     "score": 1.0
-                    if food_terms.intersection(document.lower().split())
+                    if food_terms.intersection(re.findall(r"[a-z]+", document.lower()))
                     else -1.0,
                 }
                 for index, document in enumerate(self._documents)
             ]
         }

Add the import at the top of the file:

import re
🤖 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/search/test_rerank_integration.py` around lines 43 -
55, Update the fake scorer’s json method to normalize punctuation before
checking food_terms, ensuring inputs such as “pizza.” match the existing terms
and produce the relevant score. Use the suggested re import or an equivalent
substring-based match, while preserving the current scoring and response
structure.
reflexio/server/llm/rerank/cross_encoder_reranker.py (1)

99-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Catch all httpx.TransportError subclasses for colocated outages.

httpx.WriteError, httpx.ReadError, and httpx.PoolTimeout currently use the catch-all branch and set report_failure=True. Catch httpx.TransportError so all transport failures use the colocated-outage rule.

♻️ Proposed refactor
-    except (
-        httpx.ConnectError,
-        httpx.ConnectTimeout,
-        httpx.ReadTimeout,
-        httpx.RemoteProtocolError,
-    ) as exc:
+    except httpx.TransportError as exc:
         raise CrossEncoderUnavailableError(
             f"Rerank service request failed at {url}: {exc}",
             report_failure=not expected_local_unavailability,
         ) from exc
🤖 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/llm/rerank/cross_encoder_reranker.py` around lines 99 - 121,
Update the exception handling around the rerank service request to catch
httpx.TransportError, including WriteError, ReadError, and PoolTimeout, in the
colocated-outage branch. Apply the expected_local_unavailability rule when
setting report_failure, while preserving the existing handling for
HTTPStatusError and non-transport errors.
🤖 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/llm/model_defaults.py`:
- Line 490: Update the startup diagnostics to use
REFLEXIO_EMBEDDING_SERVICE_URL: in reflexio/server/llm/model_defaults.py lines
490-490, report remote inference when the configured URL is non-local and
colocated inference otherwise; apply the same distinction in
reflexio/cli/commands/services.py lines 83-84 and replace the stale ChromaDB
fallback matrix there.

In `@reflexio/server/llm/providers/embedding_service_provider.py`:
- Around line 95-99: Update embedding_service_url() to normalize the
REFLEXIO_EMBEDDING_SERVICE_URL value by stripping whitespace before each
presence check, so whitespace-only values are treated as unset and do not route
to inference_service_url(). Add a regression test covering a whitespace-only
environment value.
- Around line 76-84: Update inference_service_url() to parse and validate
REFLEXIO_EMBEDDING_SERVICE_URL: permit http only for an exact loopback host,
require https for every other host, and reject invalid or unsupported schemes
while preserving normalized trailing-slash handling. Document this HTTPS
requirement and the loopback-only HTTP exception in .env.example at lines
106-112.

In `@reflexio/server/services/retrieval/relevance_floor.py`:
- Around line 143-160: Move the per-arm resolve_retrieval_floor calls in
_apply_floors into the existing try block that catches
CrossEncoderUnavailableError, resolving all floors before entering the results
loop. Preserve the current fail-open return of unfiltered RelevanceFloorResult
values when either model scoring or floor resolution raises
CrossEncoderUnavailableError.

In `@tests/e2e_tests/test_service_mode_embedding_e2e.py`:
- Around line 216-226: Update the embedding daemon startup coverage in
_EmbeddingDaemon.start and the reranker health assertion to allow enough time
for both model downloads, including the cross-encoder prewarm. Increase the
startup timeout beyond 90 seconds and include the /health/rerank response
details in the readiness assertion failure so prewarm errors are diagnosable.

---

Nitpick comments:
In `@reflexio/server/llm/rerank/common.py`:
- Around line 83-97: Sort the entries in the __all__ declaration in common.py
using isort-style ordering to satisfy RUF022, while preserving all existing
exports.

In `@reflexio/server/llm/rerank/cross_encoder_reranker.py`:
- Around line 99-121: Update the exception handling around the rerank service
request to catch httpx.TransportError, including WriteError, ReadError, and
PoolTimeout, in the colocated-outage branch. Apply the
expected_local_unavailability rule when setting report_failure, while preserving
the existing handling for HTTPStatusError and non-transport errors.

In `@tests/server/llm/test_embedding_service_rerank.py`:
- Around line 40-41: Extend the reranker health tests around _Runner.status and
the /health/rerank request to cover REFLEXIO_RERANK_ENABLED=false, asserting
that RerankHealthResponse.status is "disabled". Keep the existing ready and
unavailable assertions unchanged, and ensure the test exercises the status
consumed by resolve_inference_service_capabilities.

In `@tests/server/services/retrieval/test_relevance_floor.py`:
- Around line 197-220: Add a test alongside
test_nullable_floor_uses_discovered_model_default that patches
score_pairs_with_model to return an unrecognized model with scores, invokes
apply_relevance_floors with a None floor, and asserts the original unfiltered
item pool is returned. Use the existing arms and assertion style, specifically
covering the unknown-model/null-floor path.

In `@tests/server/services/search/test_rerank_integration.py`:
- Around line 43-55: Update the fake scorer’s json method to normalize
punctuation before checking food_terms, ensuring inputs such as “pizza.” match
the existing terms and produce the relevant score. Use the suggested re import
or an equivalent substring-based match, while preserving the current scoring and
response structure.

In `@tests/server/services/test_unified_search_temporal.py`:
- Around line 203-208: Update the test’s fake_score in the unified search
temporal test to import and return the shared ENGLISH_RERANK_MODEL constant
instead of duplicating the reranker model string; keep the existing score list
and relevance_floor patch target unchanged.
🪄 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: 8208be87-bf43-4941-a9e5-a8861000a38f

📥 Commits

Reviewing files that changed from the base of the PR and between c21a00c and e22295b.

📒 Files selected for processing (51)
  • .env.example
  • README.md
  • reflexio/cli/README.md
  • reflexio/cli/commands/services.py
  • reflexio/cli/commands/setup_cmd.py
  • reflexio/cli/run_services.py
  • reflexio/lib/_profiles.py
  • reflexio/models/config_schema.py
  • reflexio/server/__main__.py
  • reflexio/server/api.py
  • reflexio/server/llm/_litellm_embedding.py
  • reflexio/server/llm/embedding_service.py
  • reflexio/server/llm/litellm_client.py
  • reflexio/server/llm/model_defaults.py
  • reflexio/server/llm/providers/embedder_warmup.py
  • reflexio/server/llm/providers/embedding_service_provider.py
  • reflexio/server/llm/rerank/__init__.py
  • reflexio/server/llm/rerank/common.py
  • reflexio/server/llm/rerank/cross_encoder_model.py
  • reflexio/server/llm/rerank/cross_encoder_reranker.py
  • reflexio/server/routes/search.py
  • reflexio/server/routes/system.py
  • reflexio/server/services/playbook/components/reviewer.py
  • reflexio/server/services/retrieval/relevance_floor.py
  • reflexio/server/services/unified_search_service.py
  • tests/cli/test_run_services_workers.py
  • tests/cli/test_service_builders.py
  • tests/cli/test_services_first_run.py
  • tests/conftest.py
  • tests/e2e_tests/test_service_mode_embedding_e2e.py
  • tests/models/test_retrieval_floor_config.py
  • tests/server/llm/rerank/test_cross_encoder_device.py
  • tests/server/llm/rerank/test_prewarm.py
  • tests/server/llm/rerank/test_remote_service.py
  • tests/server/llm/test_embedder_warmup.py
  • tests/server/llm/test_embedding_service_provider.py
  • tests/server/llm/test_embedding_service_rerank.py
  • tests/server/llm/test_litellm_client_unit.py
  • tests/server/llm/test_local_embedding_provider.py
  • tests/server/llm/test_model_defaults.py
  • tests/server/llm/test_service_only_inference_boundary.py
  • tests/server/routes/test_health_warm_gate.py
  • tests/server/routes/test_rerank_profile_route.py
  • tests/server/services/retrieval/test_relevance_floor.py
  • tests/server/services/search/test_rerank_integration.py
  • tests/server/services/storage/sqlite_storage/test_profiles_atomicity_characterization_integration.py
  • tests/server/services/storage/test_lineage_b3d_interactions_integration.py
  • tests/server/services/storage/test_lineage_b3f_profile_atomic_integration.py
  • tests/server/services/test_unified_search_floor.py
  • tests/server/services/test_unified_search_temporal.py
  • tests/server/test_recycle_smoke_integration.py
💤 Files with no reviewable changes (4)
  • reflexio/server/llm/litellm_client.py
  • tests/server/llm/test_embedder_warmup.py
  • tests/server/routes/test_health_warm_gate.py
  • reflexio/server/llm/providers/embedder_warmup.py

Comment thread reflexio/server/llm/model_defaults.py Outdated
Comment thread reflexio/server/llm/providers/embedding_service_provider.py
Comment thread reflexio/server/llm/providers/embedding_service_provider.py Outdated
Comment thread reflexio/server/services/retrieval/relevance_floor.py Outdated
Comment thread tests/e2e_tests/test_service_mode_embedding_e2e.py
Remove API-process embedding and reranker execution, discover both service models from one health response, and add nullable model-specific retrieval floors. Keep cloud embedding providers direct and make colocated inference a supervised HTTP service.
@yyiilluu
yyiilluu force-pushed the codex/service-only-inference branch from 1e00565 to 75627df Compare August 7, 2026 16:57
@yyiilluu
yyiilluu merged commit c24e1e7 into main Aug 7, 2026
1 check passed
yilu331 added a commit that referenced this pull request Aug 7, 2026
…centroid (#436)

## 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 all** — `grep '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.
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