refactor(context): server-side design patterns — Service + Repository + DTO (plan 005) - #7
Merged
Merged
Conversation
REVIEW §25 Phase 5 mechanical extraction. Behavior-preserving — the new ``SessionRecordsRepository`` mirrors the legacy filter shape, sort discipline, and limit ceiling exactly. U2 will tighten scope and replace the limit ceiling with cursor-based pagination. What moved (all from src/opencortex/context/manager.py to the new src/opencortex/context/session_records.py): - ``_load_session_merged_records`` → ``repo.load_merged`` - ``_load_session_directory_records`` → ``repo.load_directories`` - ``_session_layer_counts`` → ``repo.layer_counts`` - session_summary URI lookup (was inline in benchmark idempotent-hit branch) → ``repo.load_summary`` Two pure record-reading helpers also moved out of ``ContextManager`` into the same module so the repo could reuse them without circular imports: - ``ContextManager._record_msg_range`` (staticmethod) → module-level ``record_msg_range`` - ``ContextManager._record_text`` (staticmethod) → module-level ``record_text`` ContextManager constructs ``self._session_records`` once in ``__init__`` with a callable that resolves the active collection name (avoids capturing the whole orchestrator). All 12 legacy call sites across the lifecycle code (production conversation_end / context_commit plus benchmark ingest plus integrity logs) now go through the repo. Verification per plan §25.1: - 7 new unit tests in tests/test_session_records_repository.py cover happy path + edge cases + the two pure helpers. - Full lifecycle regression: 168/169 tests pass. Sole failure is pre-existing flaky `test_update_regenerates_fact_points_after_content_change` documented in project history.
…ard (U2, plan 005) REVIEW §25 Phase 5 second half. SessionRecordsRepository now: - Accepts ``tenant_id`` / ``user_id`` kwargs on ``load_merged``, ``load_directories``, ``layer_counts``. Defaults to ``None`` so U1 behavior is preserved for legacy callers; when provided, the kwargs push ``source_tenant_id`` / ``source_user_id`` conds into the storage filter (closes the cross-tenant ``session_id`` collision footgun PE-6 for callers that have identity available). - Replaces the legacy ``limit=10_000`` silent truncation with cursor-based pagination via ``storage.scroll`` (page_size=1_000, max_pages=50 by default → 50 000 row safety stop). Raises ``SessionRecordOverflowError`` with ``session_id`` + ``count_at_stop`` + ``next_cursor`` + ``method`` when the safety stop fires. - Falls back to a single ``storage.filter`` call with limit = page_size × max_pages when the storage adapter does not implement ``scroll`` (in-memory test fixtures stay supported); the same overflow guard fires when the result set fills the cap. Wired through: - ``benchmark_ingest_conversation`` idempotent-hit and response-build paths now pass ``tenant_id`` + ``user_id`` to ``load_merged``, closing PE-6 on the benchmark hot path. - Admin route maps ``SessionRecordOverflowError`` → HTTP 507 Insufficient Storage with structured detail (reason, session_id, method, count_at_stop, next_cursor, hint). - Production lifecycle callers (``_run_full_session_recomposition``, ``_finalize_session_end``) keep their U1 signatures — threading identity through is a separate cleanup pass. Verification: - 4 new unit tests cover scope kwargs, no-kwargs legacy parity, overflow guard, and scroll pagination loop. All pass. - Full lifecycle regression: 168/169 (sole failure is pre-existing flaky ``test_update_regenerates_fact_points_after_content_change``).
REVIEW §25 Phase 6. Defines two new Pydantic models in src/opencortex/http/models.py: - ``BenchmarkConversationIngestRecord`` — per-record shape mirroring exactly what ``ContextManager._export_memory_record`` has been returning since PR #3 / U10. Field set is intentionally a superset of what every adapter consumes today; extra fields are optional so the leaner ``direct_evidence`` records validate alongside merged- recompose leaves. The ``content`` field's docstring documents the hydration contract (in-memory map → CortexFS fallback) so future maintainers don't rediscover the R3-RC-06 race. - ``BenchmarkConversationIngestResponse`` — top-level envelope identical to today's dict shape: ``status`` / ``session_id`` / ``source_uri`` / ``summary_uri`` / ``records`` / optional ``ingest_shape``. Replaces the bare ``Dict[str, Any]`` return that R2-28 / R4-P2-10 flagged. The DTOs are added but NOT wired through yet — admin route and service still return dict. U5 wires them. This separation makes U3 trivially safe: zero behavior change, just new types. Verification: - 5 unit tests round-trip representative response shapes from the existing ``test_04d`` (merged_recompose) and ``test_04e`` (direct_evidence) fixtures + edge cases (empty records, minimum required fields, dump round-trip). - No other test suites touched; their dict-based assertions still pass through PR #5 paths unchanged.
…an 005) §25 Phase 3 — pull the ~390-line benchmark_ingest_conversation body from ContextManager into a dedicated service so each of the six responsibilities lives on its own private method. - New src/opencortex/context/benchmark_ingest_service.py with BenchmarkConversationIngestService(manager, repo). - Public `ingest()` keeps the same dict shape and call signature the admin route already serializes (DTO wiring lands in U5). - Six private methods: _normalize_segments, _idempotent_hit_response, _ingest_merged_recompose, _write_merged_leaves (with sibling-cancel on first exception), _recompose_and_summarize (drains partial directory URIs into cleanup tracker on RecompositionError), _build_response, _ingest_direct_evidence. - ContextManager.benchmark_ingest_conversation becomes a thin shim delegating to self._benchmark_ingest_service.ingest(...) — preserves the existing call path for admin route + tests unchanged. - Service module imports RecompositionError + _BenchmarkRunCleanup lazily inside methods to avoid circular import. - _benchmark_ingest_direct_evidence deleted from manager (service owns it); _benchmark_evidence_uri staticmethod kept (service borrows via manager._benchmark_evidence_uri). Closes REVIEW closure tracker entries: - R2-11 (SRP for benchmark_ingest_conversation) - R2-12 (cleanup ownership boundary clarified — tracker still in manager, but service owns the orchestration) - F14 (ContextManager bloat — manager.py shrinks ~480 lines) Verification: - tests.test_session_records_repository (11 tests) ✅ - tests.test_benchmark_ingest_response_model (5 tests) ✅ - tests.test_e2e_phase1 + tests.test_context_manager — only the pre-existing test_update_regenerates_fact_points_after_content_change failure remains (unrelated to this refactor). - tests.test_http_server + tests.test_locomo_bench + tests.test_benchmark_ingest_lifecycle (51 tests) ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ute (U5, plan 005)
§25 Phase 6 wrap-up — declare ``response_model=BenchmarkConversationIngestResponse``
on ``/api/v1/admin/benchmark/conversation_ingest`` and validate the
service's dict return through the model before serializing.
Locks the endpoint contract: any drift in the dict shape returned by
``BenchmarkConversationIngestService`` (e.g. a future refactor that
forgets to populate a record field) surfaces as a 500 here rather
than as a silent JSON shape change for downstream adapters.
Wire-format note: the merged_recompose path now serializes
``"ingest_shape": null`` instead of omitting the key entirely. All
adapters consume ``payload.get("ingest_shape")`` so the change is
behavior-neutral. Updated the model docstring to document this.
Closes REVIEW closure tracker entries:
- R2-28 / R4-P2-10 (typed response model on admin route — replaces
the bare ``Dict[str, Any]`` return).
Verification:
- tests.test_session_records_repository (11) ✅
- tests.test_benchmark_ingest_response_model (5) ✅
- tests.test_http_server (incl. merged + direct_evidence) ✅
- tests.test_locomo_bench (40) ✅
- tests.test_benchmark_ingest_lifecycle ✅
- tests.test_e2e_phase1 + tests.test_context_manager — only the
pre-existing test_update_regenerates_fact_points_after_content_change
failure remains.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All five implementation units shipped: - U1 SessionRecordsRepository extraction (dded8c9) - U2 scope/paging/overflow guard (ab6711d) - U3 BenchmarkConversationIngestResponse DTO (4f6f001) - U4 BenchmarkConversationIngestService extraction (39ffd72) - U5 DTO wired through admin route (85a0ce3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CE review autofix pass — 8 deterministic safe_auto findings applied. Residual gated_auto / manual / advisory findings are surfaced in the PR body for downstream resolution. Mechanical fixes: - KP-01 (P1): BenchmarkConversationIngestRecord.event_date typed Optional[Any] → Optional[str]. Restores DTO validation for the field; production data is always ISO-8601 string or None. - KP-02 (P1): models.py adds `from __future__ import annotations` for consistency with the two new context/ modules. - KP-03 (P1): _write_merged_leaves / _recompose_and_summarize / _build_response cleanup parameter typed `Any` → `_BenchmarkRunCleanup` via TYPE_CHECKING import. Surface the contract to type-checkers. - KP-04 (P2): SessionRecordsRepository.__init__ storage parameter typed `Any` → `StorageInterface` via TYPE_CHECKING import. - KP-06 (P2): unused page_index loop variable → `_`. - KP-09 (P3): _ingest_direct_evidence dedupes the identical CancelledError + Exception handlers into a single `except BaseException`. Also catches a second cancellation that the prior `except Exception` would have leaked. - correctness-002 / ADV-U-002 (P2): _scroll_all fallback overflow guard requested `limit = page_size * max_pages` and raised on `len >= limit`, false-positive at exact-cap boundaries. Now requests `cap + 1` and raises only on `len > cap`. Test updated to assert the new count_at_stop = cap + 1 semantics. - PS-001 (P2): CLAUDE.md context/ directory map adds the three new modules (session_records, benchmark_ingest_service, recomposition_types). Also fixes the pre-existing stale reference storage/vikingdb_interface.py → storage/storage_interface.py. Verification: - tests.test_session_records_repository (11) ✅ (1 test updated) - tests.test_benchmark_ingest_response_model (5) ✅ - tests.test_http_server / test_locomo_bench / test_benchmark_ingest_lifecycle ✅ - tests.test_e2e_phase1 + tests.test_context_manager — only the pre-existing test_update_regenerates_fact_points_after_content_change failure remains. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures gated_auto / manual / advisory residuals from CE autofix run 20260425-130006-07250c15 against plan 005. 8 safe_auto fixes were already applied in commit 3e24971. The residuals here need human judgment before landing — most are P1/P2 spread across reliability, testing, and performance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the four highest-ROI P1 findings from the CE autofix run on plan 005 (commit 3e24971 covered the safe_auto pass; this is the gated_auto + manual layer that needs human judgment). - AC-02 (api-contract): benchmarks/oc_client.py now excludes 507 from the retryable status set. SessionRecordOverflowError is deterministic — the session record count cannot decrease on retry — so the prior status>=500 rule wasted ~4 minutes of backoff per affected session. One-line fix in _is_retryable_http_error. - REL-01 (reliability): _purge_torn_benchmark_run no longer swallows SessionRecordOverflowError via its generic `except Exception`. An overflow now raises out of the purge with a structured error log pointing operators at session_id rotation / manual paging, instead of silently treating the directory list as empty and leaving the failed prior run's directory records as permanent storage orphans. - ADV-U-001 (adversarial): _purge_torn_benchmark_run now passes tenant_id / user_id to load_directories. Every other repo call site threads scope; the missing kwargs here meant a torn-replay could, in principle, hand cross-tenant directory URIs to _delete_immediate_families (a hard-delete path with no layer check). Pin scope so the purge can never touch another tenant. - correctness-001 (correctness): _idempotent_hit_response wraps load_summary() in try/except. Restores the legacy degradation behavior — a transient storage error on the summary point lookup now logs and falls back to summary_uri=None instead of failing the entire idempotent replay. Parity with the pre-refactor _get_record_by_uri call path. - T-01 / T-02 / T-03 (testing): new tests/test_benchmark_ingest_service.py exercises the service in isolation. Covers eight cases the lifecycle / HTTP tests cannot reach: * _normalize_segments empty / whitespace / meta copy semantics * unsupported ingest_shape -> ValueError * empty-segments early-exit response shape * idempotent-hit summary lookup transient failure -> degrade * RecompositionError drain into cleanup tracker (existing test raises RuntimeError directly, bypassing the wrapper) * sibling-cancel-on-first-derive-failure (existing test only injects failure after all derives complete) Verification: - tests.test_benchmark_ingest_service (8) ✅ — new file - tests.test_session_records_repository (11) ✅ - tests.test_benchmark_ingest_response_model (5) ✅ - tests.test_http_server / test_locomo_bench / test_benchmark_ingest_lifecycle ✅ - tests.test_e2e_phase1 + tests.test_context_manager — only the pre-existing test_update_regenerates_fact_points_after_content_change failure remains (unrelated). Total: 193 tests, 1 pre-existing failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Server-side refactor of the benchmark conversation ingest pipeline per plan 005 (REVIEW §25 Phases 3 + 5 + 6). Extracts the ~390-line
ContextManager.benchmark_ingest_conversationbody into three patterns: Service Layer, Repository / Gateway, DTO. Per §25.2 abstraction guardrails, only these three patterns were warranted; no Strategy / Abstract Factory.Branched off master @
155b218(post-PR #6); rebased ontodfe0457(post-PR #5).What changed
src/opencortex/context/session_records.py(new)SessionRecordsRepositorywraps_load_session_merged_records,_load_session_directory_records,_session_layer_counts, session_summary lookup. Adds(tenant_id, user_id)scope push-down, cursor pagination viastorage.scroll, fallback tostorage.filter,SessionRecordOverflowError(HTTP 507). Serves 7+3+2 call sites.src/opencortex/http/models.pyBenchmarkConversationIngestResponse+BenchmarkConversationIngestRecordPydantic models. Replaces the bareDict[str, Any]return at the admin route.src/opencortex/context/benchmark_ingest_service.py(new)BenchmarkConversationIngestServicewith six private methods:_normalize_segments,_idempotent_hit_response,_ingest_merged_recompose,_write_merged_leaves(sibling-cancel on first exception),_recompose_and_summarize(drains partial directory URIs into cleanup tracker onRecompositionError),_build_response,_ingest_direct_evidence.ContextManager.benchmark_ingest_conversationbecomes a thin shim._benchmark_ingest_direct_evidencedeleted from manager (service owns it).src/opencortex/http/admin_routes.pyresponse_model=BenchmarkConversationIngestResponse, validates the orchestrator's dict viamodel_validate(...).manager.pyshrinks ~480 lines net (376-line method body → 28-line shim, 115-line method deleted, ~30 lines of new wiring).Closure tracker entries closed
source_uri=Nonedegrades to session-wide scan), PE-6 (cross-tenantsession_idcollision footgun), R2-22 (observability hook surface)20260425-130006: KP-01..09, correctness-002, PS-001 (8 safe_auto)Commits
dded8c9U1 — ExtractSessionRecordsRepositoryab6711dU2 — Scope discipline + cursor pagination + 507 overflow guard4f6f001U3 —BenchmarkConversationIngestResponsePydantic DTO39ffd72U4 — ExtractBenchmarkConversationIngestService85a0ce3U5 — Wire DTO through admin route3b0ca91Mark plan checkboxes complete3e24971CE autofix — 8 safe_auto fixes (typing, future annotations, off-by-one, docs)d83cc53Record residual review findings29c3f5aClose 4 P1 review residuals + add direct service unit testsVerification
tests.test_session_records_repositorytests.test_benchmark_ingest_response_modeltests.test_benchmark_ingest_service(new)tests.test_http_servertests.test_locomo_benchtests.test_benchmark_ingest_lifecycletests.test_e2e_phase1+test_context_managertest_update_regenerates_fact_points_after_content_change— unrelated to this PR)Total: 193 tests run, 1 pre-existing failure.
Residual work (not blocking, see
docs/residual-review-findings/refactor-server-side-design-patterns.md)29c3f5a)msg_rangelength constraint), KP-05/07 (constants + helper extraction), REL-02/03/04 (refinements to error handling depth), ADV-U-003/005/006 (cancellation timing windows). Best handled in follow-up PRs.Test plan
uv run python3 -m unittest tests.test_session_records_repository tests.test_benchmark_ingest_response_model tests.test_benchmark_ingest_service tests.test_http_server tests.test_locomo_bench tests.test_benchmark_ingest_lifecycle— should be all green.uv run python3 -m unittest tests.test_e2e_phase1 tests.test_context_manager— only the pre-existingtest_update_regenerates_fact_points_after_content_changeshould fail./api/v1/admin/benchmark/conversation_ingestwith bothmerged_recomposeanddirect_evidenceshapes; verify response shape matchesBenchmarkConversationIngestResponse.oc_client.pydoes NOT retry it.git log -- src/opencortex/context/manager.pyto see the ~480-line net reduction.🤖 Generated with Claude Code