diff --git a/devtools/synthetic_benchmark_runtime.py b/devtools/synthetic_benchmark_runtime.py index b9011d2f96..af0ef97414 100644 --- a/devtools/synthetic_benchmark_runtime.py +++ b/devtools/synthetic_benchmark_runtime.py @@ -189,10 +189,10 @@ async def run_filter_scan_campaign(db_path: Path) -> CampaignResult: metrics["list_50_count"] = len(results) elapsed, results = await _ameasure( - backend.queries.list_sessions(SessionRecordQuery(provider="chatgpt", limit=50)) + backend.queries.list_sessions(SessionRecordQuery(origin="chatgpt", limit=50)) ) - metrics["filter_provider_wall_s"] = round(elapsed, 4) - metrics["filter_provider_count"] = len(results) + metrics["filter_origin_wall_s"] = round(elapsed, 4) + metrics["filter_origin_count"] = len(results) elapsed, results = await _ameasure( backend.queries.list_sessions(SessionRecordQuery(has_tool_use=True, limit=50)) diff --git a/polylogue/api/archive.py b/polylogue/api/archive.py index 33074e0bf8..f920911018 100644 --- a/polylogue/api/archive.py +++ b/polylogue/api/archive.py @@ -386,12 +386,6 @@ def _active_archive_root(config: Config) -> Path: return archive_file_set_root_for_paths(archive_root_path=config.archive_root, db_anchor=config.db_path) -def _archive_origin_for_provider(provider: str | None) -> str | None: - if provider is None: - return None - return origin_from_provider(Provider.from_string(provider)).value - - def _provider_for_archive_origin(origin: str) -> Provider: try: return provider_from_origin(Origin(origin)) @@ -1492,7 +1486,7 @@ async def get_insight_readiness_report(self, query: object | None = None) -> Ins async def list_session_profile_insights(self, query: object) -> list[ArchiveInsightModel]: return list( self._archive.list_session_profile_insights( - provider=str(provider) if (provider := getattr(query, "provider", None)) is not None else None, + origin=str(origin) if (origin := getattr(query, "origin", None)) is not None else None, workflow_shape=getattr(query, "workflow_shape", None), terminal_state=getattr(query, "terminal_state", None), since_ms=_archive_query_date_ms("since", getattr(query, "since", None)), @@ -1507,7 +1501,7 @@ async def list_session_work_event_insights(self, query: object) -> list[ArchiveI return list( self._archive.list_session_work_event_insights( session_id=getattr(query, "session_id", None), - provider=str(provider) if (provider := getattr(query, "provider", None)) is not None else None, + origin=str(origin) if (origin := getattr(query, "origin", None)) is not None else None, heuristic_label=getattr(query, "heuristic_label", None), since_ms=_archive_query_date_ms("since", getattr(query, "since", None)), until_ms=_archive_query_date_ms("until", getattr(query, "until", None)), @@ -1520,7 +1514,7 @@ async def list_session_phase_insights(self, query: object) -> list[ArchiveInsigh return list( self._archive.list_session_phase_insights( session_id=getattr(query, "session_id", None), - provider=str(provider) if (provider := getattr(query, "provider", None)) is not None else None, + origin=str(origin) if (origin := getattr(query, "origin", None)) is not None else None, kind=getattr(query, "kind", None), since_ms=_archive_query_date_ms("since", getattr(query, "since", None)), until_ms=_archive_query_date_ms("until", getattr(query, "until", None)), @@ -1543,7 +1537,7 @@ async def list_thread_insights(self, query: object) -> list[ArchiveInsightModel] async def list_session_tag_rollup_insights(self, query: object) -> list[ArchiveInsightModel]: return list( self._archive.list_session_tag_rollup_insights( - provider=str(provider) if (provider := getattr(query, "provider", None)) is not None else None, + origin=str(origin) if (origin := getattr(query, "origin", None)) is not None else None, query=getattr(query, "query", None), since_ms=_archive_query_date_ms("since", getattr(query, "since", None)), until_ms=_archive_query_date_ms("until", getattr(query, "until", None)), @@ -1555,8 +1549,8 @@ async def list_session_tag_rollup_insights(self, query: object) -> list[ArchiveI async def list_archive_coverage_insights(self, query: object) -> list[ArchiveInsightModel]: return list( self._archive.list_archive_coverage_insights( - group_by=str(getattr(query, "group_by", "provider")), - provider=str(provider) if (provider := getattr(query, "provider", None)) is not None else None, + group_by=str(getattr(query, "group_by", "origin")), + origin=str(origin) if (origin := getattr(query, "origin", None)) is not None else None, since_ms=_archive_query_date_ms("since", getattr(query, "since", None)), until_ms=_archive_query_date_ms("until", getattr(query, "until", None)), limit=getattr(query, "limit", None), @@ -1613,8 +1607,8 @@ async def list( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -1636,8 +1630,8 @@ async def list( for summary in await self.list_summaries( limit=limit, offset=offset, - provider=provider, - providers=providers, + origin=origin, + origins=origins, since=since, until=until, title_contains=title_contains, @@ -1664,8 +1658,8 @@ async def list_summaries( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, source: str | None = None, since: str | None = None, until: str | None = None, @@ -1685,14 +1679,14 @@ async def list_summaries( message_type: str | None = None, ) -> builtins.list[SessionSummary]: del source - origin, origins = self._provider_filters(provider=provider, providers=providers) + filter_origin, filter_origins = self._origin_filters(origin=origin, origins=origins) return [ _archive_summary_to_domain(summary) for summary in self._archive.list_summaries( limit=limit or 50, offset=offset, - origin=origin, - origins=origins, + origin=filter_origin, + origins=filter_origins, referenced_paths=tuple(referenced_path or ()), cwd_prefix=cwd_prefix, action_terms=tuple(action_terms or ()), @@ -1714,8 +1708,8 @@ async def list_summaries( async def count( self, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -1733,12 +1727,12 @@ async def count( max_words: int | None = None, message_type: str | None = None, ) -> int: - origin, origins = self._provider_filters(provider=provider, providers=providers) + filter_origin, filter_origins = self._origin_filters(origin=origin, origins=origins) return cast( int, self._archive.count_sessions( - origin=origin, - origins=origins, + origin=filter_origin, + origins=filter_origins, referenced_paths=tuple(referenced_path or ()), cwd_prefix=cwd_prefix, action_terms=tuple(action_terms or ()), @@ -1762,16 +1756,16 @@ async def search_summary_hits( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, ) -> builtins.list[Any]: from polylogue.archive.query.search_hits import session_search_hit_from_summary - _origin, origins = self._provider_filters(provider=None, providers=providers) + _origin, filter_origins = self._origin_filters(origin=None, origins=origins) hits = self._archive.search_summaries( query, limit=limit, - origins=origins, + origins=filter_origins, since_ms=_archive_query_date_ms("since", since), ) results: builtins.list[Any] = [] @@ -1797,10 +1791,10 @@ async def search( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> builtins.list[Session]: sessions: builtins.list[Session] = [] - for hit in await self.search_summary_hits(query, limit=limit, providers=providers): + for hit in await self.search_summary_hits(query, limit=limit, origins=origins): session = await self.get(hit.session_id) if session is not None: sessions.append(session) @@ -1810,9 +1804,9 @@ async def search_summaries( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> builtins.list[SessionSummary]: - return [hit.summary for hit in await self.search_summary_hits(query, limit=limit, providers=providers)] + return [hit.summary for hit in await self.search_summary_hits(query, limit=limit, origins=origins)] def iter_messages( self, @@ -1849,9 +1843,9 @@ async def search_similar( return [] def _query_kwargs(self, query: object, *, default_limit: int) -> dict[str, object]: - origin, origins = self._provider_filters( - provider=getattr(query, "provider", None), - providers=builtins.list(getattr(query, "providers", ()) or ()), + origin, origins = self._origin_filters( + origin=getattr(query, "origin", None), + origins=builtins.list(getattr(query, "origins", ()) or ()), ) return { "limit": getattr(query, "limit", None) or default_limit, @@ -1877,19 +1871,15 @@ def _query_kwargs(self, query: object, *, default_limit: int) -> dict[str, objec "until_ms": _archive_query_date_ms("until", getattr(query, "until", None)), } - def _provider_filters( + def _origin_filters( self, *, - provider: str | None, - providers: builtins.list[str] | None, + origin: str | None, + origins: builtins.list[str] | None, ) -> tuple[str | None, tuple[str, ...]]: - origin = _archive_origin_for_provider(provider) - origins: builtins.list[str] = [] - for provider_value in providers or []: - candidate = _archive_origin_for_provider(provider_value) - if candidate is not None: - origins.append(candidate) - return origin, tuple(origins) + validated_origin = Origin(origin).value if origin is not None else None + validated_origins = tuple(Origin(value).value for value in origins or []) + return validated_origin, validated_origins def _actions_for_session(session: Session) -> tuple[Action, ...]: @@ -4151,7 +4141,7 @@ async def list_session_profile_insights( request = query or SessionProfileInsightQuery() with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_session_profile_insights( - provider=request.provider, + origin=request.origin, workflow_shape=request.workflow_shape, terminal_state=request.terminal_state, since_ms=_archive_query_date_ms("since", request.since), @@ -4697,7 +4687,7 @@ async def neighbor_candidates( *, session_id: str | None = None, query: str | None = None, - provider: str | None = None, + origin: str | None = None, limit: int = 10, window_hours: int = 24, ) -> list[SessionNeighborCandidate]: @@ -4717,7 +4707,7 @@ async def neighbor_candidates( NeighborDiscoveryRequest( session_id=session_id, query=query, - provider=provider, + origin=origin, limit=limit, window_hours=window_hours, ), @@ -4728,7 +4718,7 @@ async def neighbor_candidate_payloads( *, session_id: str | None = None, query: str | None = None, - provider: str | None = None, + origin: str | None = None, limit: int = 10, window_hours: int = 24, ) -> list[JSONDocument]: @@ -4739,7 +4729,7 @@ async def neighbor_candidate_payloads( candidates = await self.neighbor_candidates( session_id=session_id, query=query, - provider=provider, + origin=origin, limit=limit, window_hours=window_hours, ) diff --git a/polylogue/api/insights.py b/polylogue/api/insights.py index d728ac0740..ed84cbc28b 100644 --- a/polylogue/api/insights.py +++ b/polylogue/api/insights.py @@ -37,7 +37,7 @@ UsageTimelineInsightQuery, ) from polylogue.insights.cost_enrichment import enrich_session_cost_insights -from polylogue.insights.tag_rollups import synthesize_provider_tag_rollups +from polylogue.insights.tag_rollups import synthesize_origin_tag_rollups from polylogue.insights.tool_usage import ToolUsageInsight, ToolUsageInsightQuery from polylogue.insights.topology import ( LogicalSession, @@ -316,29 +316,29 @@ async def list_session_tag_rollup_insights( since_ms = _archive_query_date_ms("since", request.since) until_ms = _archive_query_date_ms("until", request.until) with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: - # The archive rebuild does not write ``provider:`` rows into - # session_tags (provider identity lives on sessions.origin), so the + # The archive rebuild does not write ``origin:`` rows into + # session_tags (origin identity lives on sessions.origin), so the # archive read only returns explicit/auto tags. Synthesize the - # provider rollups to preserve the legacy ``insights tags`` + # origin rollups to preserve the legacy ``insights tags`` # contract, then merge them with the materialized tag rollups. materialized = archive.list_session_tag_rollup_insights( - provider=request.provider, + origin=request.origin, query=request.query, since_ms=since_ms, until_ms=until_ms, limit=None, offset=0, ) - provider_rollups = synthesize_provider_tag_rollups( + origin_rollups = synthesize_origin_tag_rollups( archive, - provider=request.provider, + origin=request.origin, query=request.query, since_ms=since_ms, until_ms=until_ms, materialized_at=datetime.now(UTC).isoformat(), ) rollups = sorted( - [*materialized, *provider_rollups], + [*materialized, *origin_rollups], key=lambda rollup: (-rollup.session_count, rollup.tag), ) if request.offset: @@ -370,7 +370,7 @@ async def list_session_work_event_insights( with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_session_work_event_insights( session_id=request.session_id, - provider=request.provider, + origin=request.origin, heuristic_label=request.heuristic_label, since_ms=since_ms, until_ms=until_ms, @@ -401,7 +401,7 @@ async def list_session_phase_insights( with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_session_phase_insights( session_id=request.session_id, - provider=request.provider, + origin=request.origin, since_ms=since_ms, until_ms=until_ms, limit=request.limit, @@ -434,7 +434,7 @@ async def list_archive_coverage_insights( with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_archive_coverage_insights( group_by=request.group_by, - provider=request.provider, + origin=request.origin, since_ms=_archive_query_date_ms("since", request.since), until_ms=_archive_query_date_ms("until", request.until), limit=request.limit, @@ -465,7 +465,7 @@ async def list_session_cost_insights( archive, archive.list_session_cost_insights( session_id=request.session_id, - provider=request.provider, + origin=request.origin, status=None, model=None, since_ms=_archive_query_date_ms("since", request.since), @@ -499,7 +499,7 @@ async def list_session_latency_profile_insights( with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_session_latency_profile_insights( session_id=request.session_id, - provider=request.provider, + origin=request.origin, only_stuck=request.only_stuck, since_ms=_archive_query_date_ms("since", request.since), until_ms=_archive_query_date_ms("until", request.until), @@ -514,7 +514,7 @@ async def find_stuck_session_latency_profile_insights( request = query or SessionLatencyProfileInsightQuery() with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.find_stuck_session_latency_profile_insights( - provider=request.provider, + origin=request.origin, since_ms=_archive_query_date_ms("since", request.since), until_ms=_archive_query_date_ms("until", request.until), limit=request.limit, @@ -527,7 +527,7 @@ async def list_cost_rollup_insights( request = query or CostRollupInsightQuery() with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_cost_rollup_insights( - provider=request.provider, + origin=request.origin, model=request.model, since_ms=_archive_query_date_ms("since", request.since), until_ms=_archive_query_date_ms("until", request.until), @@ -542,7 +542,7 @@ async def list_usage_timeline_insights( request = query or UsageTimelineInsightQuery() with ArchiveStore.open_existing(_active_archive_root(self.config)) as archive: return archive.list_usage_timeline_insights( - provider=request.provider, + origin=request.origin, model=request.model, group_by=request.group_by, since_ms=_archive_query_date_ms("since", request.since), @@ -612,7 +612,7 @@ async def aggregate_sessions( group_by: str = "workflow_shape", since: str | None = None, until: str | None = None, - provider: str | None = None, + origin: str | None = None, ) -> dict[str, object]: """GROUP BY session counts over workflow_shape/terminal_state/origin. @@ -621,7 +621,7 @@ async def aggregate_sessions( from polylogue.insights.archive_rollups import aggregate_session_profiles_by_dimension profiles = await self.list_session_profile_insights( - SessionProfileInsightQuery(provider=provider, since=since, until=until, limit=None) + SessionProfileInsightQuery(origin=origin, since=since, until=until, limit=None) ) buckets = aggregate_session_profiles_by_dimension(profiles, group_by) return {"group_by": group_by, "total_sessions": len(profiles), "buckets": buckets} @@ -632,7 +632,7 @@ async def workflow_shape_distribution( group_by: str = "week", since: str | None = None, until: str | None = None, - provider: str | None = None, + origin: str | None = None, ) -> dict[str, object]: """Histogram session workflow shapes by week, origin, or project. @@ -642,7 +642,7 @@ async def workflow_shape_distribution( from polylogue.insights.archive_rollups import workflow_shape_distribution_buckets profiles = await self.list_session_profile_insights( - SessionProfileInsightQuery(provider=provider, since=since, until=until, limit=None) + SessionProfileInsightQuery(origin=origin, since=since, until=until, limit=None) ) buckets = workflow_shape_distribution_buckets(profiles, group_by) return {"group_by": group_by, "total_sessions": len(profiles), "buckets": buckets} @@ -670,7 +670,7 @@ async def tool_call_latency_distribution( *, since: str | None = None, until: str | None = None, - provider: str | None = None, + origin: str | None = None, tool_category: str | None = None, limit: int = 500, ) -> dict[str, object]: @@ -678,7 +678,7 @@ async def tool_call_latency_distribution( from polylogue.insights.archive_rollups import tool_call_latency_distribution_payload insights = await self.list_session_latency_profile_insights( - SessionLatencyProfileInsightQuery(provider=provider, since=since, until=until, limit=limit) + SessionLatencyProfileInsightQuery(origin=origin, since=since, until=until, limit=limit) ) return tool_call_latency_distribution_payload(insights, tool_category=tool_category) @@ -731,7 +731,7 @@ async def find_similar_sessions_by_metadata( if ref_profile is None: return None candidates = await self.list_session_profile_insights( - SessionProfileInsightQuery(provider=ref_profile.source_name, limit=candidate_pool_limit) + SessionProfileInsightQuery(origin=ref_profile.source_name, limit=candidate_pool_limit) ) scored = compute_metadata_similarity_candidates(ref_profile, candidates, exclude_session_id=session_id) return { @@ -745,7 +745,7 @@ async def correlate_sessions( *, metric_x: str, metric_y: str, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, ) -> dict[str, object]: @@ -760,7 +760,7 @@ async def correlate_sessions( ensure_known_session_metric(metric_y, "metric_y") profiles = await self.list_session_profile_insights( - SessionProfileInsightQuery(provider=provider, since=since, until=until, limit=None) + SessionProfileInsightQuery(origin=origin, since=since, until=until, limit=None) ) return pearson_session_correlation(profiles, metric_x=metric_x, metric_y=metric_y) diff --git a/polylogue/api/sync/sessions.py b/polylogue/api/sync/sessions.py index 32b80a8442..d905183afc 100644 --- a/polylogue/api/sync/sessions.py +++ b/polylogue/api/sync/sessions.py @@ -186,7 +186,7 @@ def neighbor_candidates( *, session_id: str | None = None, query: str | None = None, - provider: str | None = None, + origin: str | None = None, limit: int = 10, window_hours: int = 24, ) -> list[SessionNeighborCandidate]: @@ -194,7 +194,7 @@ def neighbor_candidates( self._facade.neighbor_candidates( session_id=session_id, query=query, - provider=provider, + origin=origin, limit=limit, window_hours=window_hours, ) diff --git a/polylogue/archive/query/fields.py b/polylogue/archive/query/fields.py index 5a6b61c6b9..b1e5552f8f 100644 --- a/polylogue/archive/query/fields.py +++ b/polylogue/archive/query/fields.py @@ -8,7 +8,6 @@ from typing import Literal, Protocol, TypeAlias, cast from polylogue.core.enums import Origin -from polylogue.core.sources import provider_from_origin from polylogue.storage.query_models import SessionRecordQuery PresenceCheck = Callable[[object], bool] @@ -890,36 +889,34 @@ def has_message_content_type_filter(plan: object) -> bool: return False -def provider_scope_for_plan(plan: _ProviderScopedPlan) -> tuple[str | None, tuple[str, ...]]: - # The plan filters on origin tokens internally; the legacy record-query / - # source_name read path still keys on provider tokens, so project back here - # (#1743 — removed once that path moves onto the origin column in Phase 2). - values = tuple(provider_from_origin(Origin.from_string(token)).value for token in plan.origins) - provider = values[0] if len(values) == 1 else None - provider_group = values if len(values) > 1 else () - return provider, provider_group +def origin_scope_for_plan(plan: _ProviderScopedPlan) -> tuple[str | None, tuple[str, ...]]: + """Return canonical values for the sessions.origin SQL column.""" + values = tuple(Origin.from_string(token).value for token in plan.origins) + origin = values[0] if len(values) == 1 else None + origin_group = values if len(values) > 1 else () + return origin, origin_group def session_record_query_for_plan(plan: object) -> SessionRecordQuery: - provider, providers = provider_scope_for_plan(cast(_ProviderScopedPlan, plan)) + origin, origins = origin_scope_for_plan(cast(_ProviderScopedPlan, plan)) changes: dict[str, object] = {} for descriptor in QUERY_FIELD_DESCRIPTORS: if descriptor.record_attr is None or not descriptor.is_active_for_plan(plan): continue changes[descriptor.record_attr] = descriptor.storage_plan_value(plan) return _ReplaceRecordQuery( - SessionRecordQuery(provider=provider, providers=providers), + SessionRecordQuery(origin=origin, origins=origins), **changes, ) def sql_pushdown_params_for_plan(plan: object) -> SqlPushdownParams: params: SqlPushdownParams = {} - provider, providers = provider_scope_for_plan(cast(_ProviderScopedPlan, plan)) - if provider is not None: - params["provider"] = provider - elif providers: - params["providers"] = list(providers) + origin, origins = origin_scope_for_plan(cast(_ProviderScopedPlan, plan)) + if origin is not None: + params["origin"] = origin + elif origins: + params["origins"] = list(origins) for descriptor in QUERY_FIELD_DESCRIPTORS: if descriptor.sql_param is None or not descriptor.is_active_for_plan(plan): continue diff --git a/polylogue/archive/query/retrieval_candidates.py b/polylogue/archive/query/retrieval_candidates.py index a9ad768151..628f0e7897 100644 --- a/polylogue/archive/query/retrieval_candidates.py +++ b/polylogue/archive/query/retrieval_candidates.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Literal, overload -from polylogue.archive.query.support import _origins_as_provider_tokens +from polylogue.archive.query.support import _canonical_origins if TYPE_CHECKING: from polylogue.archive.models import Session, SessionSummary @@ -176,23 +176,18 @@ async def fetch_search_results( return True, results query = " ".join(plan.fts_terms) - # Legacy repository search filters the `sessions.source_name` provider - # column; the archive path (archive_execution) filters `origin`. Until the - # source_name→origin reconciliation (#1743 Phase 2) moves this SQL onto the - # origin column, project the plan's origin tokens back to provider tokens - # for this leg. - source_names = _origins_as_provider_tokens(plan.origins) + origins = _canonical_origins(plan.origins) if summaries: summary_results = await repository.search_summaries( query, limit=search_limit(plan), - providers=source_names, + origins=origins, ) return True, summary_results session_results = await repository.search( query, limit=search_limit(plan), - providers=source_names, + origins=origins, ) return True, session_results diff --git a/polylogue/archive/query/retrieval_search.py b/polylogue/archive/query/retrieval_search.py index d43808a741..9c80cc27ea 100644 --- a/polylogue/archive/query/retrieval_search.py +++ b/polylogue/archive/query/retrieval_search.py @@ -6,7 +6,7 @@ from heapq import heappush, heappushpop from typing import TYPE_CHECKING -from polylogue.archive.query.support import _origins_as_provider_tokens +from polylogue.archive.query.support import _canonical_origins from polylogue.logging import get_logger from polylogue.storage.search_providers.hybrid import reciprocal_rank_fusion @@ -120,11 +120,11 @@ async def search_action_results( from polylogue.errors import DatabaseError query = search_query_text(plan) - source_names = _origins_as_provider_tokens(plan.origins) + origins = _canonical_origins(plan.origins) if not await action_search_ready(plan, repository): raise DatabaseError("Action search index is not fresh; daemon repair must complete before search.") try: - return await repository.search_actions(query, limit=limit, providers=source_names) + return await repository.search_actions(query, limit=limit, origins=origins) except Exception as exc: logger.warning( "action search failed", @@ -147,8 +147,8 @@ async def search_hybrid_results( lane_ranks maps session_id -> {"text": rank_or_None, "action": rank_or_None, "vector": rank_or_None}. """ query = search_query_text(plan) - source_names = _origins_as_provider_tokens(plan.origins) - text_results = await repository.search(query, limit=limit * 3, providers=source_names) + origins = _canonical_origins(plan.origins) + text_results = await repository.search(query, limit=limit * 3, origins=origins) action_results = await search_action_results(plan, repository, limit=limit * 3) vector_results: list[Session] = [] if plan.vector_provider is not None: diff --git a/polylogue/archive/query/support.py b/polylogue/archive/query/support.py index faf8e2a8f7..a109518741 100644 --- a/polylogue/archive/query/support.py +++ b/polylogue/archive/query/support.py @@ -5,22 +5,16 @@ from typing import TYPE_CHECKING from polylogue.core.enums import Origin -from polylogue.core.sources import provider_from_origin if TYPE_CHECKING: from polylogue.archive.models import Session, SessionSummary -def _origins_as_provider_tokens(origins: tuple[str, ...]) -> list[str] | None: - """Project origin tokens back to provider tokens for the archive repository - search leg, which still filters the ``sessions.source_name`` provider column. - - Removed once the source_name→origin reconciliation (#1743 Phase 2) moves that - SQL onto the ``origin`` column. - """ +def _canonical_origins(origins: tuple[str, ...]) -> list[str] | None: + """Normalize an optional origin scope for archive search queries.""" if not origins: return None - return [provider_from_origin(Origin.from_string(token)).value for token in origins] + return [Origin(token).value for token in origins] def session_has_branches(session: Session) -> bool: @@ -51,5 +45,5 @@ def session_to_summary(session: Session) -> SessionSummary: __all__ = [ "session_has_branches", "session_to_summary", - "_origins_as_provider_tokens", + "_canonical_origins", ] diff --git a/polylogue/archive/session/neighbor_candidates.py b/polylogue/archive/session/neighbor_candidates.py index c8a7a4e365..df5f5ab4b7 100644 --- a/polylogue/archive/session/neighbor_candidates.py +++ b/polylogue/archive/session/neighbor_candidates.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING -from polylogue.core.enums import Provider +from polylogue.core.enums import Origin from polylogue.errors import PolylogueError from polylogue.storage.query_models import SessionRecordQuery @@ -100,7 +100,7 @@ class NeighborDiscoveryRequest: session_id: str | None = None query: str | None = None - provider: str | None = None + origin: str | None = None limit: int = 10 window_hours: int = 24 candidate_pool_limit: int = 100 @@ -120,15 +120,15 @@ def _normalized_title(value: str | None) -> str: return " ".join(value.casefold().split()) -def _canonical_provider(value: str | None) -> str | None: +def _canonical_origin(value: str | None) -> str | None: if value is None: return None - return str(Provider.from_string(value)) + return Origin.from_string(value).value -def _provider_list(value: str | None) -> list[str] | None: - provider = _canonical_provider(value) - return [provider] if provider else None +def _origin_list(value: str | None) -> list[str] | None: + origin = _canonical_origin(value) + return [origin] if origin else None def _source_timestamp(session: Session) -> datetime | None: @@ -283,7 +283,7 @@ async def _add_same_title_candidates( store: SessionQueryRuntimeStore, *, target: Session, - provider: str | None, + origin: str | None, source_id: str, pool_limit: int, ) -> None: @@ -292,7 +292,7 @@ async def _add_same_title_candidates( return summaries = await store.list_summaries_by_query( SessionRecordQuery( - provider=provider, + origin=origin, title_contains=target.display_title, limit=pool_limit, ) @@ -316,7 +316,7 @@ async def _add_nearby_candidates( store: SessionQueryRuntimeStore, *, target: Session, - provider: str | None, + origin: str | None, source_id: str, window_hours: int, pool_limit: int, @@ -327,7 +327,7 @@ async def _add_nearby_candidates( since, until = _date_window(anchor, window_hours) summaries = await store.list_summaries_by_query( SessionRecordQuery( - provider=provider, + origin=origin, since=since, until=until, limit=pool_limit, @@ -357,13 +357,13 @@ async def _add_query_candidates( store: SessionQueryRuntimeStore, *, query: str | None, - providers: list[str] | None, + origins: list[str] | None, source_id: str | None, pool_limit: int, ) -> None: if not query or not query.strip(): return - hits = await store.search_summary_hits(query.strip(), limit=pool_limit, providers=providers) + hits = await store.search_summary_hits(query.strip(), limit=pool_limit, origins=origins) for hit in hits: if source_id is not None and hit.session_id == source_id: continue @@ -375,12 +375,12 @@ async def _add_shared_attachment_candidates( store: SessionQueryRuntimeStore, *, target: Session, - providers: list[str] | None, + origins: list[str] | None, source_id: str, pool_limit: int, ) -> None: for identity in _attachment_identities(target): - hits = await store.search_summary_hits(identity, limit=pool_limit, providers=providers) + hits = await store.search_summary_hits(identity, limit=pool_limit, origins=origins) for hit in hits: if hit.session_id == source_id: continue @@ -401,14 +401,14 @@ async def _add_source_content_search_candidates( store: SessionQueryRuntimeStore, *, target: Session, - providers: list[str] | None, + origins: list[str] | None, source_id: str, pool_limit: int, ) -> None: seed = _source_search_seed(target) if seed is None: return - hits = await store.search_summary_hits(seed, limit=pool_limit, providers=providers) + hits = await store.search_summary_hits(seed, limit=pool_limit, origins=origins) for hit in hits: if hit.session_id == source_id: continue @@ -501,8 +501,8 @@ async def discover_neighbor_candidates( target = await _load_target(store, request.session_id) source_id = str(target.id) if target is not None else None - provider = _canonical_provider(request.provider) - providers = _provider_list(provider) + origin = _canonical_origin(request.origin) + origins = _origin_list(origin) pool_limit = max(request.candidate_pool_limit, request.limit) drafts: dict[str, _CandidateDraft] = {} @@ -511,7 +511,7 @@ async def discover_neighbor_candidates( drafts, store, target=target, - provider=provider, + origin=origin, source_id=source_id, pool_limit=pool_limit, ) @@ -519,7 +519,7 @@ async def discover_neighbor_candidates( drafts, store, target=target, - provider=provider, + origin=origin, source_id=source_id, window_hours=request.window_hours, pool_limit=pool_limit, @@ -528,7 +528,7 @@ async def discover_neighbor_candidates( drafts, store, target=target, - providers=providers, + origins=origins, source_id=source_id, pool_limit=pool_limit, ) @@ -536,7 +536,7 @@ async def discover_neighbor_candidates( drafts, store, target=target, - providers=providers, + origins=origins, source_id=source_id, pool_limit=pool_limit, ) @@ -545,7 +545,7 @@ async def discover_neighbor_candidates( drafts, store, query=request.query, - providers=providers, + origins=origins, source_id=source_id, pool_limit=pool_limit, ) diff --git a/polylogue/cli/commands/diagnostics.py b/polylogue/cli/commands/diagnostics.py index 1ed1673ed9..211b93b928 100644 --- a/polylogue/cli/commands/diagnostics.py +++ b/polylogue/cli/commands/diagnostics.py @@ -237,7 +237,7 @@ async def _turns(env: AppEnv, session_id: str, limit: int) -> None: type=click.Choice(["headline", "full"]), default="full", show_default=True, - help="Report detail: headline skips expensive provider-event and stale-rollup diagnostics.", + help="Report detail: headline skips expensive origin-event and stale-rollup diagnostics.", ) @click.option( "--json", @@ -262,9 +262,9 @@ def usage_command( detail: str, output_format: str, ) -> None: - """Audit provider usage accounting without turning it into a cost report. + """Audit origin usage accounting without turning it into a cost report. - Provider event rows, provider cumulative counters, transcript words, and + Provider event rows, origin cumulative counters, transcript words, and model rollups are printed as separate evidence streams so cached tokens, reasoning tokens, zero-token events, missing models, multi-model sessions, source acquisition debt, and stale rollups stay visible. @@ -298,7 +298,7 @@ def _render_usage_report(env: AppEnv, report: object) -> None: pricing_lanes = tuple(getattr(report, "pricing_lanes", ())) if pricing_lanes: env.ui.console.print( - f" stored/provider-priced cost: ${float(getattr(report, 'stored_provider_priced_usd', 0.0) or 0.0):,.2f}" + f" stored/origin-priced cost: ${float(getattr(report, 'stored_provider_priced_usd', 0.0) or 0.0):,.2f}" ) env.ui.console.print( f" catalog API-equivalent cost: ${float(getattr(report, 'catalog_api_equivalent_usd', 0.0) or 0.0):,.2f}" @@ -348,7 +348,7 @@ def _render_usage_report(env: AppEnv, report: object) -> None: env.ui.console.print(f"\n[bold]{row.origin}[/bold]") env.ui.console.print( " coverage: " - f"provider={row.provider} declared={row.declared_coverage} state={row.coverage_state} " + f"origin={row.origin} declared={row.declared_coverage} state={row.coverage_state} " f"evidence={row.evidence_stream}" ) if row.coverage_basis: @@ -365,7 +365,7 @@ def _render_usage_report(env: AppEnv, report: object) -> None: f"acquired_not_materialized={row.acquired_not_materialized_count}" ) env.ui.console.print( - " provider events: " + " origin events: " f"{row.provider_event_count} across {row.provider_event_session_count} sessions " f"token_count={row.token_count_event_count} message_usage={row.message_usage_event_count} " f"zero_token={row.zero_token_event_count} missing_model={row.missing_model_event_count}" @@ -376,8 +376,8 @@ def _render_usage_report(env: AppEnv, report: object) -> None: f"estimated={row.estimated_model_row_count} multi_model_sessions={row.multi_model_session_count} " f"stale_sessions={row.stale_rollup_session_count}" ) - env.ui.console.print(f" provider request usage: {_usage_counter_line(row.provider_request_usage)}") - env.ui.console.print(f" provider cumulative usage: {_usage_counter_line(row.provider_cumulative_usage)}") + env.ui.console.print(f" origin request usage: {_usage_counter_line(row.provider_request_usage)}") + env.ui.console.print(f" origin cumulative usage: {_usage_counter_line(row.provider_cumulative_usage)}") env.ui.console.print( f" model rollup usage ({row.model_rollup_grain}): {_usage_counter_line(row.model_rollup_usage)}" ) @@ -414,7 +414,7 @@ def _usage_counter_line(counters: object) -> str: @click.command("tools") -@click.option("--origin", help="Filter by origin or provider token") +@click.option("--origin", help="Filter by origin or origin token") @click.option("--tool", help="Only entries for this exact normalized tool name, e.g. mcp__serena__find_symbol") @click.option("--mcp-server", help="Only MCP tools with this server prefix, e.g. serena -> mcp__serena__*") @click.option("--action-kind", help="Only entries for this action kind") @@ -578,7 +578,7 @@ def payload_for( ) query = ToolUsageInsightQuery( - provider=origin, + origin=origin, tool=tool, mcp_server=mcp_server, action_kind=action_kind, @@ -593,14 +593,14 @@ def payload_for( mcp_family = family.replace("-", "_") action_patterns = tuple(dict.fromkeys((family, *detail_patterns))) mcp_query = ToolUsageInsightQuery( - provider=origin, + origin=origin, mcp_server=mcp_family, action_kind=action_kind, since_ms=since_ms, limit=limit, ) action_query = ToolUsageInsightQuery( - provider=origin, + origin=origin, action_kind=action_kind, since_ms=since_ms, limit=limit, diff --git a/polylogue/cli/commands/insights.py b/polylogue/cli/commands/insights.py index 7e63ff7321..c6003975cd 100644 --- a/polylogue/cli/commands/insights.py +++ b/polylogue/cli/commands/insights.py @@ -117,9 +117,7 @@ def _make_callback(pt: InsightType) -> Callable[..., None]: """ # Pre-resolve accepted fields so we only inject keys the query class understands. accepted_query_fields = query_model_field_names(pt) - accepted_root_keys = tuple( - key for key in _ROOT_FILTER_KEYS if ("provider" if key == "origin" else key) in accepted_query_fields - ) + accepted_root_keys = tuple(key for key in _ROOT_FILTER_KEYS if key in accepted_query_fields) @click.pass_context def callback( @@ -216,16 +214,16 @@ def _render_status_plain(report: InsightReadinessReport) -> None: def origin_label(value: str | None) -> str: if not value: return "-" - projected = project_origin_payload({"provider": value}) + projected = project_origin_payload({"origin": value}) if isinstance(projected, dict): return str(projected.get("origin") or "-") return "-" click.echo(f"Insight Readiness: {report.aggregate_verdict}") click.echo(f"Total sessions: {report.total_sessions}") - if report.provider or report.since or report.until: + if report.origin or report.since or report.until: click.echo( - f"Scope: origin={origin_label(report.provider)} since={report.since or '-'} until={report.until or '-'}" + f"Scope: origin={origin_label(report.origin)} since={report.since or '-'} until={report.until or '-'}" ) click.echo("") for insight in report.insights: @@ -297,7 +295,7 @@ def insights_status_command( ) query = InsightReadinessQuery( insights=insights, - provider=filters["provider"] if isinstance(filters["provider"], str) else None, + origin=filters["origin"] if isinstance(filters["origin"], str) else None, since=filters["since"] if isinstance(filters["since"], str) else None, until=filters["until"] if isinstance(filters["until"], str) else None, ) @@ -355,7 +353,7 @@ def insights_export_command( request = InsightExportBundleRequest( output_path=output_path, insights=insights, - provider=filters["provider"] if isinstance(filters["provider"], str) else None, + origin=filters["origin"] if isinstance(filters["origin"], str) else None, since=filters["since"] if isinstance(filters["since"], str) else None, until=filters["until"] if isinstance(filters["until"], str) else None, output_format=export_format, diff --git a/polylogue/cli/read_views/neighbors.py b/polylogue/cli/read_views/neighbors.py index 1482304ce0..ceec12a9dc 100644 --- a/polylogue/cli/read_views/neighbors.py +++ b/polylogue/cli/read_views/neighbors.py @@ -58,8 +58,6 @@ def run_read_neighbors(env: AppEnv, request: RootModeRequest, invocation: ReadVi from polylogue.archive.session.neighbor_candidates import NeighborDiscoveryError from polylogue.cli.shared.helper_support import fail from polylogue.cli.shared.machine_errors import emit_success - from polylogue.core.enums import Origin - from polylogue.core.sources import provider_from_origin from polylogue.surfaces.payloads import SessionNeighborCandidatePayload, model_json_document query_seed = " ".join(request.query_terms).strip() or None @@ -68,7 +66,6 @@ def run_read_neighbors(env: AppEnv, request: RootModeRequest, invocation: ReadVi options = cast(ReadViewNeighborOptions, invocation.options or ReadViewNeighborOptions()) origin = request.params.get("origin") - provider = provider_from_origin(Origin(str(origin))).value if origin else None projection = invocation.projection_spec.projection if invocation.projection_spec is not None else None limit = ( @@ -84,7 +81,7 @@ def run_read_neighbors(env: AppEnv, request: RootModeRequest, invocation: ReadVi env.polylogue.neighbor_candidates( session_id=invocation.session_id, query=query_seed, - provider=provider, + origin=str(origin) if origin is not None else None, limit=max(1, limit if limit is not None else 10), window_hours=max(1, window_hours), ) diff --git a/polylogue/insights/archive.py b/polylogue/insights/archive.py index 7f5f38c113..29caf45973 100644 --- a/polylogue/insights/archive.py +++ b/polylogue/insights/archive.py @@ -79,15 +79,15 @@ def wants_search(self) -> bool: return bool(self.query) -class ProviderTimeWindowInsightQuery(TimeWindowInsightQuery): - provider: str | None = None +class OriginTimeWindowInsightQuery(TimeWindowInsightQuery): + origin: str | None = None -class ProviderSearchInsightQuery(SearchableTimeWindowInsightQuery): - provider: str | None = None +class OriginSearchInsightQuery(SearchableTimeWindowInsightQuery): + origin: str | None = None -class SessionWindowInsightQuery(ProviderSearchInsightQuery): +class SessionWindowInsightQuery(OriginSearchInsightQuery): first_message_since: str | None = None first_message_until: str | None = None session_date_since: str | None = None @@ -97,7 +97,7 @@ class SessionWindowInsightQuery(ProviderSearchInsightQuery): sort: str = "source" -class SessionTimelineWindowInsightQuery(ProviderTimeWindowInsightQuery): +class SessionTimelineWindowInsightQuery(OriginTimeWindowInsightQuery): session_id: str | None = None session_date_since: str | None = None session_date_until: str | None = None @@ -117,7 +117,7 @@ class SessionProfileInsightQuery(SessionWindowInsightQuery): terminal_state: str | None = None -class SessionLatencyProfileInsightQuery(ProviderTimeWindowInsightQuery): +class SessionLatencyProfileInsightQuery(OriginTimeWindowInsightQuery): session_id: str | None = None only_stuck: bool = False @@ -134,27 +134,27 @@ class ThreadInsightQuery(SearchableTimeWindowInsightQuery): pass -class SessionTagRollupQuery(ProviderSearchInsightQuery): +class SessionTagRollupQuery(OriginSearchInsightQuery): limit: int | None = 100 -class ArchiveCoverageInsightQuery(ProviderTimeWindowInsightQuery): - group_by: str = "provider" +class ArchiveCoverageInsightQuery(OriginTimeWindowInsightQuery): + group_by: str = "origin" limit: int | None = None -class SessionCostInsightQuery(ProviderTimeWindowInsightQuery): +class SessionCostInsightQuery(OriginTimeWindowInsightQuery): session_id: str | None = None model: str | None = None status: str | None = None -class CostRollupInsightQuery(ProviderTimeWindowInsightQuery): +class CostRollupInsightQuery(OriginTimeWindowInsightQuery): model: str | None = None limit: int | None = None -class UsageTimelineInsightQuery(ProviderTimeWindowInsightQuery): +class UsageTimelineInsightQuery(OriginTimeWindowInsightQuery): model: str | None = None group_by: str = "month-origin-model" limit: int | None = None @@ -451,7 +451,7 @@ class WeekSessionSummaryInsight(ArchiveInsightModel): class ArchiveCoverageInsight(ArchiveInsightModel): contract_version: int = ARCHIVE_INSIGHT_CONTRACT_VERSION insight_kind: str = "archive_coverage" - group_by: str = "provider" + group_by: str = "origin" bucket: str = "" source_name: str | None = None session_count: int diff --git a/polylogue/insights/export_bundles.py b/polylogue/insights/export_bundles.py index 165080fe97..30a06cc429 100644 --- a/polylogue/insights/export_bundles.py +++ b/polylogue/insights/export_bundles.py @@ -66,7 +66,7 @@ class InsightExportBundleError(PolylogueError): class InsightExportBundleRequest(ArchiveInsightModel): output_path: Path insights: tuple[str, ...] = () - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None output_format: InsightExportFormat = "jsonl" @@ -155,7 +155,7 @@ def _query_kwargs( kwargs["limit"] = None if "offset" in fields: kwargs["offset"] = 0 - for key, value in (("provider", request.provider), ("since", request.since), ("until", request.until)): + for key, value in (("origin", request.origin), ("since", request.since), ("until", request.until)): if value is None: continue if key in fields: @@ -238,7 +238,7 @@ async def export_insight_bundle( readiness = await operations.get_insight_readiness_report( InsightReadinessQuery( insights=selected_insights, - provider=request.provider, + origin=request.origin, since=request.since, until=request.until, ) @@ -295,7 +295,7 @@ async def export_insight_bundle( output_format=request.output_format, query={ "insights": selected_insights, - "provider": request.provider, + "origin": request.origin, "since": request.since, "until": request.until, }, diff --git a/polylogue/insights/readiness.py b/polylogue/insights/readiness.py index 86c1ca3079..17df0488bf 100644 --- a/polylogue/insights/readiness.py +++ b/polylogue/insights/readiness.py @@ -20,10 +20,10 @@ _REPAIR_HINT = build_maintenance_target_catalog().repair_hint(("session_insights",), include_run_all=True) -def _origin_for_provider_value(provider: str | None) -> str | None: - from polylogue.storage.sqlite.archive_tiers.archive import _origin_for_provider_value as _impl +def _origin_value(origin: str | None) -> str | None: + from polylogue.storage.sqlite.archive_tiers.archive import _origin_value as _impl - return _impl(provider) + return _impl(origin) def _provider_for_origin_value(origin: str) -> str: @@ -48,7 +48,7 @@ def _iso_from_ms(value: object) -> str | None: class InsightReadinessQuery(ArchiveInsightModel): insights: tuple[str, ...] = () - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None @@ -101,7 +101,7 @@ class InsightReadinessReport(ArchiveInsightModel): checked_at: str aggregate_verdict: InsightReadinessVerdict total_sessions: int = 0 - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None insights: tuple[InsightReadinessEntry, ...] = () @@ -345,25 +345,25 @@ async def _table_columns(conn: aiosqlite.Connection, table: str) -> set[str]: return {str(row[1]) for row in rows} -def _provider_filter_origin(provider: str | None) -> str | None: - if not provider: +def _normalize_origin_filter(origin: str | None) -> str | None: + if not origin: return None - return _origin_for_provider_value(provider) + return _origin_value(origin) def _where_clause( spec: InsightReadinessSpec, query: InsightReadinessQuery, ) -> tuple[str, list[object]]: - """Build a provider/time filter expressed against the joined ``sessions``. + """Build a origin/time filter expressed against the joined ``sessions``. - Archive insight tables key everything on ``session_id``; provider identity + Archive insight tables key everything on ``session_id``; origin identity lives on ``sessions.origin`` and recency on ``sessions.sort_key_ms``, so every filter clause references the ``s.`` alias from the session join. """ clauses: list[str] = [] params: list[object] = [] - origin = _provider_filter_origin(query.provider) + origin = _normalize_origin_filter(query.origin) if origin is not None: clauses.append("s.origin = ?") params.append(origin) @@ -455,7 +455,7 @@ async def _fallback_coverage( def _schema_contract_issues(spec: InsightReadinessSpec, columns: set[str]) -> tuple[str, ...]: """Report structural schema drift for an archive insight table. - has no per-row version columns and derives provider/time + has no per-row version columns and derives origin/time via the ``sessions`` join, so the only contract a present table can break is its own primary ``session_id`` key (every archive insight table is keyed on it). A missing ``session_id`` column means the table is not the expected @@ -592,7 +592,7 @@ async def build_insight_readiness_report( checked_at=datetime.now(timezone.utc).isoformat(), aggregate_verdict=_aggregate_verdict(insights), total_sessions=status.total_sessions, - provider=request.provider, + origin=request.origin, since=request.since, until=request.until, insights=insights, diff --git a/polylogue/insights/registry.py b/polylogue/insights/registry.py index 3203a6be13..b0b56018cb 100644 --- a/polylogue/insights/registry.py +++ b/polylogue/insights/registry.py @@ -596,8 +596,8 @@ def list_insight_types() -> list[str]: ), mcp_default_limit=200, fields=( - InsightField("origins_with_data", _attr("providers_with_data", "0"), group=0), - InsightField("origins_without_data", _attr("providers_without_data", "0"), group=0), + InsightField("origins_with_data", _attr("origins_with_data", "0"), group=0), + InsightField("origins_without_data", _attr("origins_without_data", "0"), group=0), InsightField("total_calls", _attr("total_call_count", "0"), group=0), InsightField("distinct_tools", _attr("total_distinct_tools", "0"), group=0), InsightField("coverage_gaps", _attr("has_coverage_gaps"), group=0), diff --git a/polylogue/insights/rigor.py b/polylogue/insights/rigor.py index 55af680ffd..f55e5cb322 100644 --- a/polylogue/insights/rigor.py +++ b/polylogue/insights/rigor.py @@ -551,7 +551,7 @@ class RigorContract(ArchiveInsightModel): notes=( "provenance.materializer_version is a hardcoded literal 1 for day/week grouping " "(archive.py) with no dedicated store_constants entry, and absent entirely for " - "provider grouping -- not declared as a version_field to avoid implying a real " + "origin grouping -- not declared as a version_field to avoid implying a real " "materialized-artifact version exists. The six field_contracts entries above " "(9e5.29) render None -- never 0.0 -- whenever their declared denominator is zero; " "day/week grouping does not compute avg_user_words/avg_authored_user_words/" @@ -572,16 +572,16 @@ class RigorContract(ArchiveInsightModel): "read straight from the canonical actions view; there is no heuristic/estimate " "layer. ``mcp_server`` is a deterministic string parse, not an inference. Consumers " "should check ``has_coverage_gaps`` (or the per-entry " - "``provider_coverage[].data_available``) to distinguish a genuine zero tool-use " + "``origin_coverage[].data_available``) to distinguish a genuine zero tool-use " "count from an origin with no ingested action data at all." ), consumer_fields=( "entries", - "provider_coverage", + "origin_coverage", "total_call_count", "total_distinct_tools", - "providers_with_data", - "providers_without_data", + "origins_with_data", + "origins_without_data", "has_coverage_gaps", ), version_fields=(RigorVersionField(name="materializer_version", current_version=TOOL_USAGE_INSIGHT_VERSION),), diff --git a/polylogue/insights/tag_rollups.py b/polylogue/insights/tag_rollups.py index 59445f544d..403fa5e9e1 100644 --- a/polylogue/insights/tag_rollups.py +++ b/polylogue/insights/tag_rollups.py @@ -14,8 +14,7 @@ from typing import TYPE_CHECKING -from polylogue.core.enums import Provider -from polylogue.core.sources import origin_from_provider +from polylogue.core.enums import Origin from polylogue.insights.archive import SessionTagRollupInsight from polylogue.insights.archive_models import ArchiveInsightProvenance @@ -25,10 +24,10 @@ _ORIGIN_TAG_PREFIX = "origin:" -def synthesize_provider_tag_rollups( +def synthesize_origin_tag_rollups( archive: ArchiveStore, *, - provider: str | None = None, + origin: str | None = None, query: str | None = None, since_ms: int | None = None, until_ms: int | None = None, @@ -46,21 +45,7 @@ def synthesize_provider_tag_rollups( until_ms=until_ms, ) needle = query.strip().lower() if query else None - # Provider -> Origin, not the reverse: this direction is total and - # well-defined even for the non-injective Origin.AISTUDIO_DRIVE fiber -- - # both "gemini" and "drive" forward-map to the same "aistudio-drive" - # origin_filter (polylogue-9e5.8 Step 5 investigation / polylogue-4rrv). - # There is nothing here for a Source-family disambiguator to recover: - # disambiguation only matters for the *reverse* Origin -> Provider - # lookup (see core/sources.py's provider_from_origin family_hint), which - # this function never performs. What *is* coarse is the resulting - # rollup: origin_filter can only select the whole "aistudio-drive" - # bucket, so provider="gemini" and provider="drive" return identical - # counts today (both fold into one origin-tag rollup, and per-origin - # ArchiveStore.stats_by("origin", ...) has no finer axis to group on -- - # see polylogue-4rrv's follow-up bead for the durable capture-mode field - # that would be needed to split them). - origin_filter = origin_from_provider(Provider.from_string(provider)).value if provider is not None else None + origin_filter = Origin(origin).value if origin is not None else None rollups: list[SessionTagRollupInsight] = [] for origin_value, count in counts.items(): if origin_filter is not None and origin_value != origin_filter: diff --git a/polylogue/insights/tool_usage.py b/polylogue/insights/tool_usage.py index 307d46c8da..a644f44e49 100644 --- a/polylogue/insights/tool_usage.py +++ b/polylogue/insights/tool_usage.py @@ -86,7 +86,7 @@ class ToolUsageCoverageEntry(ArchiveInsightModel): has_affected_paths_signal: bool has_output_text_signal: bool data_available: bool - """True when the provider exposes any action rows in the archive.""" + """True when the origin exposes any action rows in the archive.""" class ToolUsageInsight(ArchiveInsightModel): @@ -94,7 +94,7 @@ class ToolUsageInsight(ArchiveInsightModel): The envelope itself is a single insight item. Each call returns exactly one ``ToolUsageInsight`` containing every aggregation entry - and every provider coverage entry resolved by the query. This shape + and every origin coverage entry resolved by the query. This shape keeps usage and coverage atomically consistent — readers never see one half without the other. """ @@ -103,11 +103,11 @@ class ToolUsageInsight(ArchiveInsightModel): insight_kind: str = "tool_usage" materializer_version: int = TOOL_USAGE_INSIGHT_VERSION entries: tuple[ToolUsageEntry, ...] = () - provider_coverage: tuple[ToolUsageCoverageEntry, ...] = () + origin_coverage: tuple[ToolUsageCoverageEntry, ...] = () total_call_count: int = 0 total_distinct_tools: int = 0 - providers_with_data: int = 0 - providers_without_data: int = 0 + origins_with_data: int = 0 + origins_without_data: int = 0 has_coverage_gaps: bool = False provenance: ArchiveInsightProvenance @@ -115,12 +115,12 @@ class ToolUsageInsight(ArchiveInsightModel): class ToolUsageInsightQuery(PaginatedInsightQuery): """Query parameters for ``list_tool_usage_insights``. - ``provider`` and ``tool`` narrow the returned aggregation entries but + ``origin`` and ``tool`` narrow the returned aggregation entries but never the coverage map — coverage gaps are always reported across all providers so a narrowed query never hides them. """ - provider: str | None = None + origin: str | None = None tool: str | None = None mcp_server: str | None = None action_kind: str | None = None @@ -170,14 +170,14 @@ def build_tool_usage_insight( Pure function — extracted so tests can exercise filtering, MCP-server derivation, and coverage-flag derivation without standing up a - database. Query filters apply only to ``entries``; ``provider_coverage`` + database. Query filters apply only to ``entries``; ``origin_coverage`` stays exhaustive so coverage gaps are never hidden when a reader narrows the result set. """ entries = [_tool_usage_entry(row) for row in rows] - if query.provider: - entries = [entry for entry in entries if entry.source_name == query.provider] + if query.origin: + entries = [entry for entry in entries if entry.source_name == query.origin] if query.tool: entries = [entry for entry in entries if entry.normalized_tool_name == query.tool] if query.mcp_server: @@ -191,18 +191,18 @@ def build_tool_usage_insight( entries = entries[: query.limit] coverage = tuple(_tool_usage_coverage(row) for row in coverage_rows) - providers_with_data = sum(1 for entry in coverage if entry.data_available) - providers_without_data = sum(1 for entry in coverage if not entry.data_available and entry.session_count > 0) + origins_with_data = sum(1 for entry in coverage if entry.data_available) + origins_without_data = sum(1 for entry in coverage if not entry.data_available and entry.session_count > 0) total_calls = sum(entry.call_count for entry in entries) distinct_tools = len({(entry.source_name, entry.normalized_tool_name) for entry in entries}) return ToolUsageInsight( entries=tuple(entries), - provider_coverage=coverage, + origin_coverage=coverage, total_call_count=total_calls, total_distinct_tools=distinct_tools, - providers_with_data=providers_with_data, - providers_without_data=providers_without_data, - has_coverage_gaps=providers_without_data > 0, + origins_with_data=origins_with_data, + origins_without_data=origins_without_data, + has_coverage_gaps=origins_without_data > 0, provenance=ArchiveInsightProvenance( materializer_version=TOOL_USAGE_INSIGHT_VERSION, materialized_at=materialized_at, diff --git a/polylogue/mcp/insight_tool_contracts.py b/polylogue/mcp/insight_tool_contracts.py index e5a12c638e..63586e281c 100644 --- a/polylogue/mcp/insight_tool_contracts.py +++ b/polylogue/mcp/insight_tool_contracts.py @@ -6,21 +6,10 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass -from polylogue.core.enums import Origin -from polylogue.core.sources import provider_from_origin from polylogue.insights.registry import InsightType from polylogue.mcp.query_contracts import MCPToolLimit, MCPToolOffset -def _origin_to_provider_token(value: object) -> str | None: - if value is None: - return None - origin = str(value) - if not origin: - return None - return provider_from_origin(Origin(origin)).value - - def _sanitize_offset(value: object) -> int: if isinstance(value, bool): return 0 @@ -55,7 +44,6 @@ def from_insight_type(cls, insight_type: InsightType) -> InsightListToolSpec: parameters: list[inspect.Parameter] = [] for field_name in sorted(query_model.model_fields): field_info = query_model.model_fields[field_name] - public_name = "origin" if field_name == "provider" else field_name if field_name == "limit": parameters.append( inspect.Parameter( @@ -69,7 +57,7 @@ def from_insight_type(cls, insight_type: InsightType) -> InsightListToolSpec: if field_name == "offset": parameters.append( inspect.Parameter( - public_name, + field_name, inspect.Parameter.KEYWORD_ONLY, default=0, annotation=MCPToolOffset, @@ -78,7 +66,7 @@ def from_insight_type(cls, insight_type: InsightType) -> InsightListToolSpec: continue parameters.append( inspect.Parameter( - public_name, + field_name, inspect.Parameter.KEYWORD_ONLY, default=None if field_info.is_required() else field_info.get_default(call_default_factory=True), annotation=field_info.annotation if field_info.annotation is not None else object, @@ -108,10 +96,6 @@ def normalize_kwargs( normalized["limit"] = clamp_limit(normalized["limit"]) if "offset" in normalized: normalized["offset"] = _sanitize_offset(normalized["offset"]) - if "origin" in normalized: - normalized["provider"] = _origin_to_provider_token(normalized.pop("origin")) - if normalized.get("group_by") == "origin": - normalized["group_by"] = "provider" return normalized diff --git a/polylogue/mcp/server_insight_tools.py b/polylogue/mcp/server_insight_tools.py index 6f138092a0..b0125dcfe1 100644 --- a/polylogue/mcp/server_insight_tools.py +++ b/polylogue/mcp/server_insight_tools.py @@ -21,7 +21,7 @@ insight_items_payload, project_origin_payload, ) -from polylogue.mcp.insight_tool_contracts import InsightListToolSpec, _origin_to_provider_token +from polylogue.mcp.insight_tool_contracts import InsightListToolSpec from polylogue.mcp.payloads import MCPRootPayload from polylogue.mcp.query_contracts import MCPSessionQueryRequest @@ -131,7 +131,7 @@ async def run() -> str: result = await poly.tool_call_latency_distribution( since=since, until=until, - provider=_origin_to_provider_token(origin), + origin=origin, tool_category=tool_category, limit=hooks.clamp_limit(limit), ) @@ -161,7 +161,7 @@ async def run() -> str: @mcp.tool() async def find_stuck_sessions(since: str | None = None, limit: int = 20) -> str: - """Find sessions with provider tool calls bounded as stuck.""" + """Find sessions with origin tool calls bounded as stuck.""" async def run() -> str: poly = hooks.get_polylogue() @@ -200,7 +200,7 @@ async def run() -> str: group_by=group_by, since=since, until=until, - provider=_origin_to_provider_token(origin), + origin=origin, ) except ValueError: return hooks.error_json( @@ -405,7 +405,7 @@ async def run() -> str: group_by=group_by, since=since, until=until, - provider=_origin_to_provider_token(origin), + origin=origin, ) except ValueError as exc: return hooks.error_json( @@ -580,7 +580,7 @@ async def run() -> str: result = await poly.correlate_sessions( metric_x=metric_x, metric_y=metric_y, - provider=_origin_to_provider_token(origin), + origin=origin, since=since, until=until, ) diff --git a/polylogue/mcp/server_tools.py b/polylogue/mcp/server_tools.py index 23fdae5ec6..996c420bae 100644 --- a/polylogue/mcp/server_tools.py +++ b/polylogue/mcp/server_tools.py @@ -13,8 +13,7 @@ from polylogue.annotations.join import AnnotationGroupDimension from polylogue.coordination import build_coordination_envelope from polylogue.coordination.payloads import AgentCoordinationPayload -from polylogue.core.enums import AssertionKind, AssertionStatus, Origin -from polylogue.core.sources import provider_from_origin +from polylogue.core.enums import AssertionKind, AssertionStatus from polylogue.mcp.archive_support import ( archive_messages_payload, archive_query_unit_payload, @@ -665,7 +664,7 @@ async def run() -> str: candidates = await poly.neighbor_candidates( session_id=id, query=query, - provider=provider_from_origin(Origin(origin)).value if origin is not None else None, + origin=origin, limit=clamped_limit, window_hours=max(1, window_hours), ) diff --git a/polylogue/protocols.py b/polylogue/protocols.py index fc390beb37..986c8766a1 100644 --- a/polylogue/protocols.py +++ b/polylogue/protocols.py @@ -111,8 +111,8 @@ async def list( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -134,8 +134,8 @@ async def list_summaries( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, source: str | None = None, since: str | None = None, until: str | None = None, @@ -156,8 +156,8 @@ async def list_summaries( async def count( self, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -197,21 +197,21 @@ async def search( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> list[Session]: ... async def search_summaries( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> list[SessionSummary]: ... async def search_summary_hits( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, ) -> list[SessionSearchHit]: ... @@ -249,7 +249,7 @@ async def search_actions( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> list[Session]: ... @@ -279,7 +279,7 @@ async def aggregate_message_stats( session_ids: list[str] | None = None, ) -> AggregateMessageStats: ... - async def get_stats_by(self, group_by: str = "provider") -> dict[str, int]: ... + async def get_stats_by(self, group_by: str = "origin") -> dict[str, int]: ... @runtime_checkable @@ -362,7 +362,7 @@ async def get_archive_stats( conn: aiosqlite.Connection | None = None, ) -> ArchiveStats: ... - async def get_stats_by(self, group_by: str = "provider") -> dict[str, int]: ... + async def get_stats_by(self, group_by: str = "origin") -> dict[str, int]: ... async def get_session_profiles_batch( self, diff --git a/polylogue/storage/query_models.py b/polylogue/storage/query_models.py index cea0faed0b..ca3f62b84e 100644 --- a/polylogue/storage/query_models.py +++ b/polylogue/storage/query_models.py @@ -9,8 +9,8 @@ class SessionListQueryKwargs(TypedDict): source: str | None - provider: str | None - providers: list[str] | None + origin: str | None + origins: list[str] | None parent_id: str | None since: str | None until: str | None @@ -38,8 +38,8 @@ class SessionListQueryKwargs(TypedDict): class SessionCountQueryKwargs(TypedDict): source: str | None - provider: str | None - providers: list[str] | None + origin: str | None + origins: list[str] | None since: str | None until: str | None title_contains: str | None @@ -67,8 +67,8 @@ class SessionRecordQuery: """Canonical record-level session selection for storage reads.""" source: str | None = None - provider: str | None = None - providers: tuple[str, ...] = () + origin: str | None = None + origins: tuple[str, ...] = () parent_id: str | None = None since: str | None = None until: str | None = None @@ -112,17 +112,17 @@ def without_unstable_semantic_filters(self) -> SessionRecordQuery: ) def for_search(self) -> tuple[str | None, list[str] | None]: - if self.provider: - return self.provider, None - if self.providers: - return None, list(self.providers) + if self.origin: + return self.origin, None + if self.origins: + return None, list(self.origins) return None, None def to_list_kwargs(self) -> SessionListQueryKwargs: return { "source": self.source, - "provider": self.provider, - "providers": list(self.providers) or None, + "origin": self.origin, + "origins": list(self.origins) or None, "parent_id": self.parent_id, "since": self.since, "until": self.until, @@ -151,8 +151,8 @@ def to_list_kwargs(self) -> SessionListQueryKwargs: def to_count_kwargs(self) -> SessionCountQueryKwargs: return { "source": self.source, - "provider": self.provider, - "providers": list(self.providers) or None, + "origin": self.origin, + "origins": list(self.origins) or None, "since": self.since, "until": self.until, "title_contains": self.title_contains, @@ -180,7 +180,7 @@ def to_count_kwargs(self) -> SessionCountQueryKwargs: class SessionProfileListQuery: """Canonical session-profile read selection for storage/runtime boundaries.""" - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None first_message_since: str | None = None @@ -203,7 +203,7 @@ class SessionTimelineListQuery: """Canonical session timeline selection for work-event and phase reads.""" session_id: str | None = None - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None session_date_since: str | None = None @@ -230,7 +230,7 @@ class ThreadListQuery: class SessionTagRollupListQuery: """Canonical tag-rollup selection for durable insight aggregate reads.""" - provider: str | None = None + origin: str | None = None since: str | None = None until: str | None = None query: str | None = None diff --git a/polylogue/storage/repository/archive/queries.py b/polylogue/storage/repository/archive/queries.py index 6f7c81a495..c3332e6ae7 100644 --- a/polylogue/storage/repository/archive/queries.py +++ b/polylogue/storage/repository/archive/queries.py @@ -32,8 +32,8 @@ async def list_summaries( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, source: str | None = None, since: str | None = None, until: str | None = None, @@ -55,8 +55,8 @@ async def list_summaries( return await self.list_summaries_by_query( SessionRecordQuery( source=source, - provider=provider, - providers=tuple(providers or ()), + origin=origin, + origins=tuple(origins or ()), since=since, until=until, title_contains=title_contains, @@ -82,8 +82,8 @@ async def iter_summary_pages( self, *, page_size: int = 50, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, source: str | None = None, since: str | None = None, until: str | None = None, @@ -107,8 +107,8 @@ async def iter_summary_pages( page = await self.list_summaries( limit=page_size, offset=offset, - provider=provider, - providers=providers, + origin=origin, + origins=origins, source=source, since=since, until=until, @@ -138,8 +138,8 @@ async def list( self, limit: int | None = 50, offset: int = 0, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -159,8 +159,8 @@ async def list( ) -> list[Session]: return await self.list_by_query( SessionRecordQuery( - provider=provider, - providers=tuple(providers or ()), + origin=origin, + origins=tuple(origins or ()), since=since, until=until, title_contains=title_contains, @@ -197,13 +197,13 @@ async def aggregate_message_stats( ) -> AggregateMessageStats: return await self.queries.aggregate_message_stats(session_ids) - async def get_stats_by(self, group_by: str = "provider") -> dict[str, int]: + async def get_stats_by(self, group_by: str = "origin") -> dict[str, int]: return await self.queries.get_stats_by(group_by) async def count( self, - provider: str | None = None, - providers: builtins.list[str] | None = None, + origin: str | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -223,8 +223,8 @@ async def count( ) -> int: return await self.count_by_query( SessionRecordQuery( - provider=provider, - providers=tuple(providers or ()), + origin=origin, + origins=tuple(origins or ()), since=since, until=until, title_contains=title_contains, diff --git a/polylogue/storage/repository/archive/search.py b/polylogue/storage/repository/archive/search.py index 04d4d4b1b4..439fa01af0 100644 --- a/polylogue/storage/repository/archive/search.py +++ b/polylogue/storage/repository/archive/search.py @@ -55,11 +55,11 @@ async def search_summaries( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> builtins.list[SessionSummary]: from polylogue.storage.hydrators import session_summary_from_record - hits, records = await self._search_records(query, limit=limit, providers=providers) + hits, records = await self._search_records(query, limit=limit, origins=origins) if not hits.hits: return [] # Hydrate message_count from the current sessions aggregate. @@ -77,7 +77,7 @@ async def search_summary_hits( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, since: str | None = None, ) -> builtins.list[SessionSearchHit]: from polylogue.archive.query.search_hits import session_search_hit_from_summary @@ -87,14 +87,14 @@ async def search_summary_hits( attachment_hits = await self.queries.search_attachment_identity_evidence_hits( query, limit=limit, - providers=providers, + origins=origins, since=since, ) try: message_hits = await self.queries.search_session_evidence_hits( query, limit=limit, - providers=providers, + origins=origins, since=since, ) except DatabaseError: @@ -142,18 +142,18 @@ async def search( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> builtins.list[Session]: - hits, records = await self._search_records(query, limit=limit, providers=providers) + hits, records = await self._search_records(query, limit=limit, origins=origins) return await self._hydrate_sessions(records, ordered_ids=hits.session_ids()) async def search_actions( self, query: str, limit: int = 20, - providers: builtins.list[str] | None = None, + origins: builtins.list[str] | None = None, ) -> builtins.list[Session]: - hits, records = await self._search_action_records(query, limit=limit, providers=providers) + hits, records = await self._search_action_records(query, limit=limit, origins=origins) return await self._hydrate_sessions(records, ordered_ids=hits.session_ids()) async def _search_records( @@ -161,9 +161,9 @@ async def _search_records( query: str, *, limit: int, - providers: builtins.list[str] | None, + origins: builtins.list[str] | None, ) -> tuple[SessionSearchResult, builtins.list[SessionRecord]]: - hits = await self.queries.search_session_hits(query, limit=limit, providers=providers) + hits = await self.queries.search_session_hits(query, limit=limit, origins=origins) if not hits.hits: return hits, [] records = await self.queries.get_sessions_batch(hits.session_ids()) @@ -174,9 +174,9 @@ async def _search_action_records( query: str, *, limit: int, - providers: builtins.list[str] | None, + origins: builtins.list[str] | None, ) -> tuple[SessionSearchResult, builtins.list[SessionRecord]]: - hits = await self.queries.search_action_session_hits(query, limit=limit, providers=providers) + hits = await self.queries.search_action_session_hits(query, limit=limit, origins=origins) if not hits.hits: return hits, [] records = await self.queries.get_sessions_batch(hits.session_ids()) diff --git a/polylogue/storage/repository/insight/profile_reads.py b/polylogue/storage/repository/insight/profile_reads.py index 1b8aaaba2b..8f3980563a 100644 --- a/polylogue/storage/repository/insight/profile_reads.py +++ b/polylogue/storage/repository/insight/profile_reads.py @@ -39,13 +39,13 @@ async def find_stuck_session_latency_profile_records( async def list_session_latency_profile_records( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, limit: int | None = 500, ) -> list[SessionLatencyProfileRecord]: return await self.queries.list_session_latency_profiles( - provider=provider, + origin=origin, since=since, until=until, limit=limit, @@ -77,7 +77,7 @@ async def _list_session_profile_records_query( async def list_session_profiles( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, first_message_since: str | None = None, @@ -96,7 +96,7 @@ async def list_session_profiles( ) -> list[SessionProfile]: records = await self._list_session_profile_records_query( SessionProfileListQuery( - provider=provider, + origin=origin, since=since, until=until, first_message_since=first_message_since, @@ -119,7 +119,7 @@ async def list_session_profiles( async def list_session_profile_records( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, first_message_since: str | None = None, @@ -138,7 +138,7 @@ async def list_session_profile_records( ) -> list[SessionProfileRecord]: return await self._list_session_profile_records_query( SessionProfileListQuery( - provider=provider, + origin=origin, since=since, until=until, first_message_since=first_message_since, @@ -163,7 +163,7 @@ async def get_session_enrichment_record(self, session_id: str) -> SessionProfile async def list_session_enrichment_records( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, first_message_since: str | None = None, @@ -179,7 +179,7 @@ async def list_session_enrichment_records( ) -> list[SessionProfileRecord]: return await self._list_session_profile_records_query( SessionProfileListQuery( - provider=provider, + origin=origin, since=since, until=until, first_message_since=first_message_since, diff --git a/polylogue/storage/repository/insight/summary_reads.py b/polylogue/storage/repository/insight/summary_reads.py index 50734d50e6..3e0b3db3c4 100644 --- a/polylogue/storage/repository/insight/summary_reads.py +++ b/polylogue/storage/repository/insight/summary_reads.py @@ -24,14 +24,14 @@ async def _list_session_tag_rollup_rows_query( async def list_session_tag_rollup_records( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, query: str | None = None, ) -> list[SessionTagRollupRecord]: return await self._list_session_tag_rollup_rows_query( SessionTagRollupListQuery( - provider=provider, + origin=origin, since=since, until=until, query=query, diff --git a/polylogue/storage/repository/insight/timeline_reads.py b/polylogue/storage/repository/insight/timeline_reads.py index 8e6523086b..83e42f6a29 100644 --- a/polylogue/storage/repository/insight/timeline_reads.py +++ b/polylogue/storage/repository/insight/timeline_reads.py @@ -56,7 +56,7 @@ async def list_session_work_events( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, session_date_since: str | None = None, @@ -69,7 +69,7 @@ async def list_session_work_events( records = await self._list_session_work_event_records_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, session_date_since=session_date_since, @@ -86,7 +86,7 @@ async def list_session_work_event_records( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, session_date_since: str | None = None, @@ -99,7 +99,7 @@ async def list_session_work_event_records( return await self._list_session_work_event_records_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, session_date_since=session_date_since, @@ -115,7 +115,7 @@ async def list_session_phases( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, kind: str | None = None, @@ -125,7 +125,7 @@ async def list_session_phases( records = await self._list_session_phase_records_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, kind=kind, @@ -139,7 +139,7 @@ async def list_session_phase_records( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, kind: str | None = None, @@ -149,7 +149,7 @@ async def list_session_phase_records( return await self._list_session_phase_records_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, kind=kind, diff --git a/polylogue/storage/repository/raw/repository_raw.py b/polylogue/storage/repository/raw/repository_raw.py index 884995d043..3ac531a1f8 100644 --- a/polylogue/storage/repository/raw/repository_raw.py +++ b/polylogue/storage/repository/raw/repository_raw.py @@ -135,13 +135,13 @@ async def upsert_source_file_cursor( async def reset_parse_status( self, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, ) -> int: async with self._backend.connection() as conn: return await raw_queries.reset_parse_status( conn, - provider=provider, + origin=origin, source_names=source_names, transaction_depth=self._backend.transaction_depth, ) @@ -149,13 +149,13 @@ async def reset_parse_status( async def reset_validation_status( self, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, ) -> int: async with self._backend.connection() as conn: return await raw_queries.reset_validation_status( conn, - provider=provider, + origin=origin, source_names=source_names, transaction_depth=self._backend.transaction_depth, ) @@ -183,13 +183,13 @@ async def get_raw_session_states( async def iter_raw_sessions( self, - provider: str | None = None, + origin: str | None = None, limit: int | None = None, ) -> AsyncIterator[RawSessionRecord]: async with self._backend.connection() as conn: async for record in raw_queries.iter_raw_sessions( conn, - provider=provider, + origin=origin, limit=limit, ): yield record @@ -216,9 +216,9 @@ async def iter_raw_headers( ): yield header - async def get_raw_session_count(self, provider: str | None = None) -> int: + async def get_raw_session_count(self, origin: str | None = None) -> int: async with self._backend.connection() as conn: - return await raw_queries.get_raw_session_count(conn, provider=provider) + return await raw_queries.get_raw_session_count(conn, origin=origin) async def get_raw_records_for_session( self, diff --git a/polylogue/storage/sqlite/archive_tiers/archive.py b/polylogue/storage/sqlite/archive_tiers/archive.py index ae66004f43..13ca6a5ec0 100644 --- a/polylogue/storage/sqlite/archive_tiers/archive.py +++ b/polylogue/storage/sqlite/archive_tiers/archive.py @@ -3029,7 +3029,7 @@ def list_session_work_event_insights( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, heuristic_label: str | None = None, since_ms: int | None = None, until_ms: int | None = None, @@ -3042,7 +3042,7 @@ def list_session_work_event_insights( if session_id is not None: where.append("we.session_id = ?") params.append(self.resolve_session_id(session_id)) - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -3105,7 +3105,7 @@ def list_session_phase_insights( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since_ms: int | None = None, until_ms: int | None = None, limit: int | None = 50, @@ -3117,7 +3117,7 @@ def list_session_phase_insights( if session_id is not None: where.append("sp.session_id = ?") params.append(self.resolve_session_id(session_id)) - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -3337,7 +3337,7 @@ def list_session_cost_insights( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, status: str | None = None, model: str | None = None, since_ms: int | None = None, @@ -3360,7 +3360,7 @@ def list_session_cost_insights( return [] where.append("s.session_id = ?") params.append(resolved_session_id) - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -3402,7 +3402,7 @@ def list_session_cost_insights( def list_cost_rollup_insights( self, *, - provider: str | None = None, + origin: str | None = None, model: str | None = None, since_ms: int | None = None, until_ms: int | None = None, @@ -3410,7 +3410,7 @@ def list_cost_rollup_insights( offset: int = 0, ) -> list[CostRollupInsight]: """Aggregate archive model-usage rows into public cost rollups.""" - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) where = ["s.sort_key_ms > 0"] params: list[object] = [] if origin is not None: @@ -3602,7 +3602,7 @@ def list_cost_rollup_insights( def list_usage_timeline_insights( self, *, - provider: str | None = None, + origin: str | None = None, model: str | None = None, group_by: str = "month-origin-model", since_ms: int | None = None, @@ -3612,7 +3612,7 @@ def list_usage_timeline_insights( ) -> list[UsageTimelineInsight]: """Aggregate provider usage and cost evidence by session-month buckets.""" - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) include_origin = group_by in {"month-origin", "month-origin-model"} include_model = group_by in {"month-model", "month-origin-model"} buckets: dict[tuple[str, str | None, str | None], _UsageTimelineAccumulator] = {} @@ -3971,7 +3971,7 @@ def list_session_latency_profile_insights( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, only_stuck: bool = False, since_ms: int | None = None, until_ms: int | None = None, @@ -3984,7 +3984,7 @@ def list_session_latency_profile_insights( if session_id is not None: where.append("s.session_id = ?") params.append(self.resolve_session_id(session_id)) - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -4016,7 +4016,7 @@ def list_session_latency_profile_insights( def find_stuck_session_latency_profile_insights( self, *, - provider: str | None = None, + origin: str | None = None, since_ms: int | None = None, until_ms: int | None = None, limit: int | None = 50, @@ -4028,7 +4028,7 @@ def find_stuck_session_latency_profile_insights( whose projected stuck count is non-zero. """ return self.list_session_latency_profile_insights( - provider=provider, + origin=origin, only_stuck=True, since_ms=since_ms, until_ms=until_ms, @@ -4089,7 +4089,7 @@ def get_session_profile_record(self, session_id: str) -> SessionProfileRecord | def list_session_profile_insights( self, *, - provider: str | None = None, + origin: str | None = None, workflow_shape: str | None = None, terminal_state: str | None = None, since_ms: int | None = None, @@ -4114,7 +4114,7 @@ def list_session_profile_insights( ) where: list[str] = [] params: list[object] = [] - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -4401,7 +4401,7 @@ def list_user_tags(self, *, origin: str | None = None) -> dict[str, int]: def list_session_tag_rollup_insights( self, *, - provider: str | None = None, + origin: str | None = None, query: str | None = None, since_ms: int | None = None, until_ms: int | None = None, @@ -4411,7 +4411,7 @@ def list_session_tag_rollup_insights( """Aggregate archive session tags into public tag-rollup insights.""" where: list[str] = [] params: list[object] = [] - origin = _origin_for_provider_value(provider) + origin = _origin_value(origin) if origin is not None: where.append("s.origin = ?") params.append(origin) @@ -4488,7 +4488,7 @@ def list_tool_call_count_rows(self, query: ToolUsageInsightQuery | None = None) request = query or ToolUsageInsightQuery() where = ["b.block_type = 'tool_use'"] params: list[object] = [] - origin = _origin_for_tool_usage_filter(request.provider) + origin = _origin_for_tool_usage_filter(request.origin) if origin: where.append("s.origin = ?") params.append(origin) @@ -4534,7 +4534,7 @@ def list_tool_call_count_rows(self, query: ToolUsageInsightQuery | None = None) ).fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"])).value, + "source_name": str(row["origin"] or "unknown-export"), "origin": str(row["origin"] or "unknown-export"), "normalized_tool_name": str(row["normalized_tool_name"] or "unknown"), "action_kind": str(row["action_kind"] or "tool_use"), @@ -4550,7 +4550,7 @@ def list_tool_observed_event_count_rows( request = query or ToolUsageInsightQuery() where = ["u.block_type = 'tool_use'"] params: list[object] = [] - origin = _origin_for_tool_usage_filter(request.provider) + origin = _origin_for_tool_usage_filter(request.origin) if origin: where.append("s.origin = ?") params.append(origin) @@ -4618,7 +4618,7 @@ def list_tool_observed_event_count_rows( ).fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"])).value, + "source_name": str(row["origin"] or "unknown-export"), "origin": str(row["origin"] or "unknown-export"), "normalized_tool_name": str(row["normalized_tool_name"] or "unknown"), "action_kind": str(row["action_kind"] or "unknown"), @@ -4647,7 +4647,7 @@ def list_tool_action_evidence_count_rows( request = query or ToolUsageInsightQuery() where: list[str] = ["u.block_type = 'tool_use'"] params: list[object] = [] - origin = _origin_for_tool_usage_filter(request.provider) + origin = _origin_for_tool_usage_filter(request.origin) if origin: where.append("s.origin = ?") params.append(origin) @@ -4729,7 +4729,7 @@ def fetch_rows() -> list[sqlite3.Row]: buckets: dict[tuple[str, str, str, str, str], dict[str, object]] = {} sessions: dict[tuple[str, str, str, str, str], set[str]] = {} for row in rows: - source_name = _provider_for_origin(str(row["origin"])).value + source_name = str(row["origin"] or "unknown-export") public_row = { "tool_name": str(row["tool_name"] or ""), "match_detail": str(row["match_detail"] or ""), @@ -4846,7 +4846,7 @@ def _list_tool_action_detail_evidence_count_rows( ).fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"])).value, + "source_name": str(row["origin"] or "unknown-export"), "origin": str(row["origin"] or "unknown-export"), "normalized_tool_name": f"{family}/command-detail", "action_kind": str(row["action_kind"] or "tool_use"), @@ -4864,7 +4864,7 @@ def _tool_usage_rows(self, query: ToolUsageInsightQuery | None = None) -> list[T request = query or ToolUsageInsightQuery() where: list[str] = [] params: list[object] = [] - origin = _origin_for_tool_usage_filter(request.provider) + origin = _origin_for_tool_usage_filter(request.origin) if origin: where.append("s.origin = ?") params.append(origin) @@ -4921,7 +4921,7 @@ def _tool_usage_rows(self, query: ToolUsageInsightQuery | None = None) -> list[T ).fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"])).value, + "source_name": str(row["origin"] or "unknown-export"), "normalized_tool_name": str(row["normalized_tool_name"] or "unknown"), "action_kind": str(row["action_kind"] or "tool_use"), "call_count": int(row["call_count"] or 0), @@ -4954,7 +4954,7 @@ def _tool_usage_provider_coverage_rows(self) -> list[ToolUsageProviderCoverageRo ).fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"])).value, + "source_name": str(row["origin"] or "unknown-export"), "session_count": int(row["session_count"] or 0), "action_count": int(row["action_count"] or 0), "distinct_tool_count": int(row["distinct_tool_count"] or 0), @@ -4969,17 +4969,17 @@ def _tool_usage_provider_coverage_rows(self) -> list[ToolUsageProviderCoverageRo def list_archive_coverage_insights( self, *, - group_by: str = "provider", - provider: str | None = None, + group_by: str = "origin", + origin: str | None = None, since_ms: int | None = None, until_ms: int | None = None, limit: int | None = None, offset: int = 0, ) -> list[ArchiveCoverageInsight]: """Aggregate archive coverage from index tables.""" - origin = _origin_for_provider_value(provider) - if group_by == "provider": - return self._provider_coverage_insights(origin=origin, limit=limit, offset=offset) + origin = _origin_value(origin) + if group_by == "origin": + return self._origin_coverage_insights(origin=origin, limit=limit, offset=offset) if group_by == "day": return self._time_bucket_coverage_insights( bucket_format="%Y-%m-%d", @@ -5000,9 +5000,9 @@ def list_archive_coverage_insights( limit=limit, offset=offset, ) - raise ValueError("archive coverage group_by must be one of: provider, day, week") + raise ValueError("archive coverage group_by must be one of: origin, day, week") - def _provider_coverage_insights( + def _origin_coverage_insights( self, *, origin: str | None, @@ -5041,7 +5041,7 @@ def _provider_coverage_insights( """, tuple(params), ).fetchall() - return [_provider_coverage_from_archive_row(row) for row in rows] + return [_origin_coverage_from_archive_row(row) for row in rows] def _time_bucket_coverage_insights( self, @@ -5954,7 +5954,7 @@ def insight_readiness_report(self, query: InsightReadinessQuery | None = None) - else known_insight_readiness_names() ) status = self.session_insight_status() - origin_filter = _origin_for_provider_value(request.provider) + origin_filter = _origin_value(request.origin) since_ms = _epoch_ms_from_iso(request.since) until_ms = _epoch_ms_from_iso(request.until) total_sessions = self.count_sessions(origin=origin_filter, since_ms=since_ms, until_ms=until_ms) @@ -5979,7 +5979,7 @@ def insight_readiness_report(self, query: InsightReadinessQuery | None = None) - checked_at=datetime.now(UTC).isoformat(), aggregate_verdict=_insight_readiness_aggregate_verdict(entries), total_sessions=total_sessions, - provider=request.provider, + origin=request.origin, since=request.since, until=request.until, insights=entries, @@ -8686,24 +8686,22 @@ def _learning_correction_from_archive_row(row: sqlite3.Row | tuple[object, ...]) ) -def _origin_for_provider_value(provider: str | None) -> str | None: - if provider is None: +def _origin_value(origin: str | None) -> str | None: + if origin is None: return None - return origin_from_provider(Provider.from_string(provider)).value + return Origin(origin).value -def _origin_for_tool_usage_filter(provider_or_origin: str | None) -> str | None: - if provider_or_origin is None: - return None - return origin_from_provider(provider_or_origin).value +def _origin_for_tool_usage_filter(origin: str | None) -> str | None: + return _origin_value(origin) def _tool_usage_builder_query(query: ToolUsageInsightQuery) -> ToolUsageInsightQuery: - origin = _origin_for_tool_usage_filter(query.provider) + origin = _origin_value(query.origin) updates: dict[str, object] = {"limit": None, "offset": 0} if origin is None: return query.model_copy(update=updates) - updates["provider"] = _provider_for_origin(origin).value + updates["origin"] = origin return query.model_copy(update=updates) @@ -10728,7 +10726,7 @@ def _archive_insight_readiness_evidence( return tuple(values) -def _provider_coverage_from_archive_row(row: sqlite3.Row) -> ArchiveCoverageInsight: +def _origin_coverage_from_archive_row(row: sqlite3.Row) -> ArchiveCoverageInsight: session_count = int(row["session_count"] or 0) message_count = int(row["message_count"] or 0) user_message_count = int(row["user_message_count"] or 0) @@ -10740,11 +10738,10 @@ def _provider_coverage_from_archive_row(row: sqlite3.Row) -> ArchiveCoverageInsi sessions_with_tools = int(row["sessions_with_tools"] or 0) sessions_with_thinking = int(row["sessions_with_thinking"] or 0) origin = str(row["origin"]) - source_name = _provider_for_origin(origin).value return ArchiveCoverageInsight( - group_by="provider", - bucket=source_name, - source_name=source_name, + group_by="origin", + bucket=origin, + source_name=origin, session_count=session_count, message_count=message_count, user_message_count=user_message_count, diff --git a/polylogue/storage/sqlite/async_sqlite_archive.py b/polylogue/storage/sqlite/async_sqlite_archive.py index cbb7159b11..b8422c6f49 100644 --- a/polylogue/storage/sqlite/async_sqlite_archive.py +++ b/polylogue/storage/sqlite/async_sqlite_archive.py @@ -247,18 +247,18 @@ async def get_session_insight_status(self, *, verify_freshness: bool = True) -> """Return materialized session-insight coverage counters.""" return await self.queries.get_session_insight_status(verify_freshness=verify_freshness) - async def search_sessions(self, query: str, limit: int = 100, providers: list[str] | None = None) -> list[str]: + async def search_sessions(self, query: str, limit: int = 100, origins: list[str] | None = None) -> list[str]: """Search sessions using the canonical ranked FTS session query.""" - return await self.queries.search_sessions(query, limit, providers) + return await self.queries.search_sessions(query, limit, origins) async def search_session_hits( self, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> SessionSearchResult: """Search sessions while preserving ordered session-hit metadata.""" - return await self.queries.search_session_hits(query, limit, providers) + return await self.queries.search_session_hits(query, limit, origins) async def iter_messages( self, @@ -295,8 +295,8 @@ async def get_message_counts_batch(self, session_ids: list[str]) -> dict[str, in """Get message counts for multiple sessions in a single query.""" return await self.queries.get_message_counts_batch(session_ids) - async def get_stats_by(self, group_by: str = "provider") -> dict[str, int]: - """Get session counts grouped by provider, month, or year.""" + async def get_stats_by(self, group_by: str = "origin") -> dict[str, int]: + """Get session counts grouped by origin, month, or year.""" return await self.queries.get_stats_by(group_by) async def get_provider_session_counts(self) -> list[ProviderSessionCountRow]: diff --git a/polylogue/storage/sqlite/async_sqlite_raw.py b/polylogue/storage/sqlite/async_sqlite_raw.py index 4e5298e5c0..3683abc62b 100644 --- a/polylogue/storage/sqlite/async_sqlite_raw.py +++ b/polylogue/storage/sqlite/async_sqlite_raw.py @@ -248,14 +248,14 @@ async def get_known_source_mtimes(self) -> dict[str, str]: async def reset_parse_status( self, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, ) -> int: """Clear parsed_at/parse_error to force re-parsing on next run.""" async with self._get_connection() as conn: return await raw_queries.reset_parse_status( conn, - provider=provider, + origin=origin, source_names=source_names, transaction_depth=self._transaction_depth, ) @@ -263,14 +263,14 @@ async def reset_parse_status( async def reset_validation_status( self, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, ) -> int: """Clear validation tracking to force re-validation on next run.""" async with self._get_connection() as conn: return await raw_queries.reset_validation_status( conn, - provider=provider, + origin=origin, source_names=source_names, transaction_depth=self._transaction_depth, ) @@ -301,18 +301,18 @@ async def get_raw_session_states( async def iter_raw_sessions( self, - provider: str | None = None, + origin: str | None = None, limit: int | None = None, ) -> AsyncIterator[RawSessionRecord]: """Iterate over raw session records.""" async with self._get_connection() as conn: - async for record in raw_queries.iter_raw_sessions(conn, provider, limit): + async for record in raw_queries.iter_raw_sessions(conn, origin, limit): yield record - async def get_raw_session_count(self, provider: str | None = None) -> int: + async def get_raw_session_count(self, origin: str | None = None) -> int: """Get count of raw sessions.""" async with self._get_connection() as conn: - return await raw_queries.get_raw_session_count(conn, provider) + return await raw_queries.get_raw_session_count(conn, origin) async def get_raw_records_for_session( self, diff --git a/polylogue/storage/sqlite/queries/attachment_records.py b/polylogue/storage/sqlite/queries/attachment_records.py index 1973588ae9..0141493c72 100644 --- a/polylogue/storage/sqlite/queries/attachment_records.py +++ b/polylogue/storage/sqlite/queries/attachment_records.py @@ -6,8 +6,7 @@ import aiosqlite -from polylogue.core.enums import Provider -from polylogue.core.sources import origin_from_provider +from polylogue.core.enums import Origin from polylogue.storage.runtime import AttachmentRecord from polylogue.storage.search.models import SessionSearchEvidenceRow from polylogue.types import SessionId @@ -173,7 +172,7 @@ async def search_attachment_identity_evidence_hits( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, since: str | None = None, ) -> list[SessionSearchEvidenceRow]: """Search selected attachment identity fields and return evidence-bearing hits.""" @@ -203,8 +202,8 @@ async def search_attachment_identity_evidence_hits( """ params: list[str | int | float] = [] - if providers: - scope_params = [origin_from_provider(Provider.from_string(provider)).value for provider in providers] + if origins: + scope_params = [Origin(origin).value for origin in origins] placeholders = ",".join("?" for _ in scope_params) sql += f" AND c.origin IN ({placeholders})" params.extend(scope_params) diff --git a/polylogue/storage/sqlite/queries/filter_builder.py b/polylogue/storage/sqlite/queries/filter_builder.py index ce740c848d..4afe487338 100644 --- a/polylogue/storage/sqlite/queries/filter_builder.py +++ b/polylogue/storage/sqlite/queries/filter_builder.py @@ -6,8 +6,7 @@ from polylogue.archive.query.fields import storage_filters_require_stats_join from polylogue.archive.query.path_prefix import escaped_sql_path_prefix_patterns from polylogue.archive.viewport.viewports import ToolCategory -from polylogue.core.enums import Provider -from polylogue.core.sources import origin_from_provider +from polylogue.core.enums import Origin from polylogue.storage.sqlite.queries.project_refs import expand_project_refs _SEMANTIC_ACTION_TYPES = tuple(category.value for category in ToolCategory) @@ -31,14 +30,14 @@ def _iso_to_epoch(iso_str: str) -> float: def _origin_value(value: str) -> str: - return origin_from_provider(Provider.from_string(value)).value + return Origin(value).value def _build_session_filters( *, source: str | None = None, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, parent_id: str | None = None, since: str | None = None, until: str | None = None, @@ -73,11 +72,11 @@ def _build_session_filters( if source is not None: where_clauses.append("c.origin = ?" if needs_stats_alias else "origin = ?") params.append(_origin_value(source)) - if provider is not None: + if origin is not None: where_clauses.append("c.origin = ?" if needs_stats_alias else "origin = ?") - params.append(_origin_value(provider)) - if providers: - source_scope_params = [_origin_value(provider_name) for provider_name in providers] + params.append(_origin_value(origin)) + if origins: + source_scope_params = [_origin_value(origin_name) for origin_name in origins] placeholders = ",".join("?" for _ in source_scope_params) origin_column = "c.origin" if needs_stats_alias else "origin" source_scope_sql = f"{origin_column} IN ({placeholders})" diff --git a/polylogue/storage/sqlite/queries/raw_reads.py b/polylogue/storage/sqlite/queries/raw_reads.py index fc769c2d15..ab19d1b2fd 100644 --- a/polylogue/storage/sqlite/queries/raw_reads.py +++ b/polylogue/storage/sqlite/queries/raw_reads.py @@ -245,7 +245,7 @@ def _state(row: aiosqlite.Row) -> RawSessionState: async def iter_raw_sessions( conn: aiosqlite.Connection, - provider: str | None = None, + origin: str | None = None, limit: int | None = None, ) -> AsyncIterator[RawSessionRecord]: offset = 0 @@ -254,9 +254,9 @@ async def iter_raw_sessions( while True: query = "SELECT * FROM raw_sessions" params: list[str | int] = [] - if provider is not None: + if origin is not None: query += f" WHERE {RAW_ORIGIN_FILTER_SQL} = ?" - params.append(origin_filter_value(provider)) + params.append(origin_filter_value(origin)) query += " ORDER BY acquired_at_ms DESC" chunk_size = 100 query_with_limit = query + " LIMIT ? OFFSET ?" @@ -314,12 +314,12 @@ async def get_raw_records_for_session( return records, total -async def get_raw_session_count(conn: aiosqlite.Connection, provider: str | None = None) -> int: +async def get_raw_session_count(conn: aiosqlite.Connection, origin: str | None = None) -> int: query = "SELECT COUNT(*) as cnt FROM raw_sessions" params: tuple[str, ...] = () - if provider is not None: + if origin is not None: query += f" WHERE {RAW_ORIGIN_FILTER_SQL} = ?" - params = (origin_filter_value(provider),) + params = (origin_filter_value(origin),) cursor = await conn.execute(query, params) row = await cursor.fetchone() return int(row["cnt"]) if row is not None else 0 diff --git a/polylogue/storage/sqlite/queries/raw_state.py b/polylogue/storage/sqlite/queries/raw_state.py index 44083570cb..93f707ee69 100644 --- a/polylogue/storage/sqlite/queries/raw_state.py +++ b/polylogue/storage/sqlite/queries/raw_state.py @@ -6,7 +6,7 @@ import aiosqlite -from polylogue.core.enums import Provider, ValidationMode, ValidationStatus +from polylogue.core.enums import Origin, Provider, ValidationMode, ValidationStatus from polylogue.core.sources import origin_from_provider from polylogue.storage.raw.models import RawSessionStateUpdate from polylogue.storage.sqlite.connection import _build_source_scope_filter @@ -18,8 +18,16 @@ def origin_filter_value(token: str) -> str: - """Translate a provider-wire token into its canonical ``origin`` value.""" - return origin_from_provider(Provider.from_string(token)).value + """Normalize a raw-wire token to the origin stored in ``raw_sessions``. + + Raw acquisition remains a Provider-wire boundary, so historical callers + may provide either a canonical origin or a provider token. Unknown input + safely maps to ``unknown-export`` rather than reaching SQL as a value. + """ + try: + return Origin(token).value + except ValueError: + return origin_from_provider(Provider.from_string(token)).value def _now_ms() -> int: @@ -146,15 +154,15 @@ async def mark_raw_validated( async def reset_parse_status( conn: aiosqlite.Connection, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, transaction_depth: int, ) -> int: where_clauses = ["(parsed_at_ms IS NOT NULL OR parse_error IS NOT NULL)"] params: list[str] = [] - if provider is not None: + if origin is not None: where_clauses.append(f"{RAW_ORIGIN_FILTER_SQL} = ?") - params.append(origin_filter_value(provider)) + params.append(origin_filter_value(origin)) predicate, scope_params = _build_source_scope_filter( source_names, source_column="origin", @@ -174,15 +182,15 @@ async def reset_parse_status( async def reset_validation_status( conn: aiosqlite.Connection, *, - provider: str | None = None, + origin: str | None = None, source_names: list[str] | None = None, transaction_depth: int, ) -> int: where_clauses = ["(validated_at_ms IS NOT NULL OR validation_status IS NOT NULL OR validation_error IS NOT NULL)"] params: list[str] = [] - if provider is not None: + if origin is not None: where_clauses.append(f"{RAW_ORIGIN_FILTER_SQL} = ?") - params.append(origin_filter_value(provider)) + params.append(origin_filter_value(origin)) predicate, scope_params = _build_source_scope_filter( source_names, source_column="origin", diff --git a/polylogue/storage/sqlite/queries/session_insight_profile_reads.py b/polylogue/storage/sqlite/queries/session_insight_profile_reads.py index 96bf17a41c..a7ea84bcd9 100644 --- a/polylogue/storage/sqlite/queries/session_insight_profile_reads.py +++ b/polylogue/storage/sqlite/queries/session_insight_profile_reads.py @@ -73,9 +73,9 @@ async def list_session_profiles( where = [] order_by = _session_profile_order_by(query.sort) - if query.provider: + if query.origin: where.append("sp.source_name = ?") - params.append(query.provider) + params.append(query.origin) if query.since: where.append("COALESCE(sp.last_message_at, sp.source_updated_at, sp.first_message_at) >= ?") params.append(query.since) diff --git a/polylogue/storage/sqlite/queries/session_insight_summary_queries.py b/polylogue/storage/sqlite/queries/session_insight_summary_queries.py index a16bdbf003..5952dc4c04 100644 --- a/polylogue/storage/sqlite/queries/session_insight_summary_queries.py +++ b/polylogue/storage/sqlite/queries/session_insight_summary_queries.py @@ -21,9 +21,9 @@ async def list_session_tag_rollup_rows( ) -> list[SessionTagRollupRecord]: params: list[object] = [] where: list[str] = [] - if query.provider: + if query.origin: where.append("source_name = ?") - params.append(query.provider) + params.append(query.origin) if query.since: where.append("bucket_day >= date(?)") params.append(query.since) diff --git a/polylogue/storage/sqlite/queries/session_insight_timeline_reads.py b/polylogue/storage/sqlite/queries/session_insight_timeline_reads.py index 5695e8c613..ff1e9b84f5 100644 --- a/polylogue/storage/sqlite/queries/session_insight_timeline_reads.py +++ b/polylogue/storage/sqlite/queries/session_insight_timeline_reads.py @@ -219,9 +219,9 @@ async def list_work_events( if query.session_id: where.append("swe.session_id = ?") params.append(query.session_id) - if query.provider: + if query.origin: where.append("s.origin = ?") - params.append(query.provider) + params.append(query.origin) if query.heuristic_label: where.append("swe.work_event_type = ?") params.append(query.heuristic_label) @@ -267,9 +267,9 @@ async def list_session_phases( if query.session_id: where.append("sph.session_id = ?") params.append(query.session_id) - if query.provider: + if query.origin: where.append("s.origin = ?") - params.append(query.provider) + params.append(query.origin) if query.kind: where.append("? = 'phase'") params.append(query.kind) diff --git a/polylogue/storage/sqlite/queries/session_latency_profile_reads.py b/polylogue/storage/sqlite/queries/session_latency_profile_reads.py index 35b46d74b7..e664df1c22 100644 --- a/polylogue/storage/sqlite/queries/session_latency_profile_reads.py +++ b/polylogue/storage/sqlite/queries/session_latency_profile_reads.py @@ -78,16 +78,16 @@ async def find_stuck_session_latency_profiles( async def list_session_latency_profiles( conn: aiosqlite.Connection, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, limit: int | None = 500, ) -> list[SessionLatencyProfileRecord]: where: list[str] = [] params: list[object] = [] - if provider: + if origin: where.append("source_name = ?") - params.append(provider) + params.append(origin) if since: where.append("COALESCE(last_message_at, source_updated_at, first_message_at) >= ?") params.append(since) diff --git a/polylogue/storage/sqlite/queries/sessions_reads.py b/polylogue/storage/sqlite/queries/sessions_reads.py index df73e7b20e..562a8b333f 100644 --- a/polylogue/storage/sqlite/queries/sessions_reads.py +++ b/polylogue/storage/sqlite/queries/sessions_reads.py @@ -129,8 +129,8 @@ async def list_sessions( conn: aiosqlite.Connection, *, source: str | None = None, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, parent_id: str | None = None, since: str | None = None, until: str | None = None, @@ -167,8 +167,8 @@ async def list_sessions( ) where_sql, params = _build_session_filters( source=source, - provider=provider, - providers=providers, + origin=origin, + origins=origins, parent_id=parent_id, since=since, until=until, @@ -225,8 +225,8 @@ async def list_session_summaries( conn: aiosqlite.Connection, *, source: str | None = None, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, parent_id: str | None = None, since: str | None = None, until: str | None = None, @@ -263,8 +263,8 @@ async def list_session_summaries( ) where_sql, params = _build_session_filters( source=source, - provider=provider, - providers=providers, + origin=origin, + origins=origins, parent_id=parent_id, since=since, until=until, @@ -321,8 +321,8 @@ async def count_sessions( conn: aiosqlite.Connection, *, source: str | None = None, - provider: str | None = None, - providers: list[str] | None = None, + origin: str | None = None, + origins: list[str] | None = None, since: str | None = None, until: str | None = None, title_contains: str | None = None, @@ -356,8 +356,8 @@ async def count_sessions( ) where_sql, params = _build_session_filters( source=source, - provider=provider, - providers=providers, + origin=origin, + origins=origins, since=since, until=until, title_contains=title_contains, diff --git a/polylogue/storage/sqlite/queries/sessions_search.py b/polylogue/storage/sqlite/queries/sessions_search.py index 4e1fb0283e..c18e33e9c1 100644 --- a/polylogue/storage/sqlite/queries/sessions_search.py +++ b/polylogue/storage/sqlite/queries/sessions_search.py @@ -13,7 +13,7 @@ async def search_session_hits( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> SessionSearchResult: from polylogue.storage.fts.fts_lifecycle import check_fts_readiness, message_fts_search_readiness_async @@ -28,7 +28,7 @@ async def search_session_hits( query_spec = build_ranked_session_search_query( query=query, limit=limit, - scope_names=providers, + scope_names=origins, ) if query_spec is None: return SessionSearchResult(hits=[]) @@ -43,7 +43,7 @@ async def search_session_evidence_hits( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, since: str | None = None, ) -> list[SessionSearchEvidenceRow]: from polylogue.storage.fts.fts_lifecycle import check_fts_readiness, message_fts_search_readiness_async @@ -57,7 +57,7 @@ async def search_session_evidence_hits( query_spec = build_ranked_session_search_query( query=query, limit=limit, - scope_names=providers, + scope_names=origins, since=since, include_snippet=True, ) @@ -92,16 +92,16 @@ async def search_sessions( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> list[str]: - return (await search_session_hits(conn, query, limit, providers)).session_ids() + return (await search_session_hits(conn, query, limit, origins)).session_ids() async def search_action_session_hits( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> SessionSearchResult: from polylogue.storage.fts.fts_lifecycle import check_fts_readiness, message_fts_search_readiness_async from polylogue.storage.search import build_ranked_action_search_query @@ -112,7 +112,7 @@ async def search_action_session_hits( query_spec = build_ranked_action_search_query( query=query, limit=limit, - scope_names=providers, + scope_names=origins, ) if query_spec is None: return SessionSearchResult(hits=[]) @@ -127,9 +127,9 @@ async def search_action_sessions( conn: aiosqlite.Connection, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> list[str]: - return (await search_action_session_hits(conn, query, limit, providers)).session_ids() + return (await search_action_session_hits(conn, query, limit, origins)).session_ids() __all__ = [ diff --git a/polylogue/storage/sqlite/queries/stats.py b/polylogue/storage/sqlite/queries/stats.py index 7a602218da..aac2883582 100644 --- a/polylogue/storage/sqlite/queries/stats.py +++ b/polylogue/storage/sqlite/queries/stats.py @@ -295,15 +295,15 @@ async def upsert_session_stats( await conn.commit() -async def get_stats_by(conn: aiosqlite.Connection, group_by: str = "provider") -> dict[str, int]: - """Get session counts grouped by provider, day, month, or year. +async def get_stats_by(conn: aiosqlite.Connection, group_by: str = "origin") -> dict[str, int]: + """Get session counts grouped by origin, day, month, or year. Raises ValueError on unknown ``group_by`` rather than silently returning - provider counts. Each branch is a literal SQL constant — the validated + origin counts. Each branch is a literal SQL constant — the validated input never reaches string interpolation — but the explicit reject closes the door on future branches that might. - These are the sessions-table calendar/provider dimensions. The CLI's + These are the sessions-table calendar/origin dimensions. The CLI's additional ``action``/``tool``/``repo``/``work-kind`` dimensions are not plain session counts — they are computed via insight-summary aggregation paths in ``cli/query.py:_handle_stats_by`` and are not exposed @@ -336,7 +336,7 @@ async def get_stats_by(conn: aiosqlite.Connection, group_by: str = "provider") - GROUP BY period ORDER BY period DESC """ ) - elif group_by == "provider": + elif group_by == "origin": cursor = await conn.execute( """ SELECT origin as period, COUNT(*) as count @@ -345,7 +345,7 @@ async def get_stats_by(conn: aiosqlite.Connection, group_by: str = "provider") - """ ) else: - raise ValueError(f"Unknown group_by {group_by!r}; expected one of: provider, day, month, year") + raise ValueError(f"Unknown group_by {group_by!r}; expected one of: origin, day, month, year") rows = await cursor.fetchall() return {row["period"]: row["count"] for row in rows} diff --git a/polylogue/storage/sqlite/queries/tool_usage.py b/polylogue/storage/sqlite/queries/tool_usage.py index 8ab15a2496..9da383666d 100644 --- a/polylogue/storage/sqlite/queries/tool_usage.py +++ b/polylogue/storage/sqlite/queries/tool_usage.py @@ -14,7 +14,7 @@ from typing_extensions import TypedDict from polylogue.core.enums import Origin, Provider -from polylogue.core.sources import origin_from_provider, provider_from_origin +from polylogue.core.sources import provider_from_origin from polylogue.insights.tool_usage import ToolUsageInsightQuery __all__ = [ @@ -66,7 +66,7 @@ async def get_tool_usage_rows( request = query or ToolUsageInsightQuery() where: list[str] = [] params: list[object] = [] - origin = _origin_for_tool_usage_filter(request.provider) + origin = _origin_for_tool_usage_filter(request.origin) if origin: where.append("s.origin = ?") params.append(origin) @@ -122,7 +122,7 @@ async def get_tool_usage_rows( rows = await cursor.fetchall() return [ { - "source_name": _provider_for_origin(str(row["origin"] or "unknown-export")).value, + "source_name": str(row["origin"] or "unknown-export"), "normalized_tool_name": str(row["normalized_tool_name"] or "unknown"), "action_kind": str(row["action_kind"] or "unknown"), "call_count": int(row["call_count"] or 0), @@ -178,14 +178,14 @@ async def get_tool_usage_provider_coverage_rows( ] -def _origin_for_tool_usage_filter(provider_or_origin: str | None) -> str | None: - if provider_or_origin is None: +def _origin_for_tool_usage_filter(origin: str | None) -> str | None: + if origin is None: return None - return origin_from_provider(provider_or_origin).value + return Origin.from_string(origin).value def _provider_for_origin(origin: str) -> Provider: - """Return the canonical provider-wire ``Provider`` for an origin token. + """Return the canonical origin-wire ``Provider`` for an origin token. Delegates to the single source of truth in ``core/sources.py`` instead of a hand-copied dict -- see ``archive/query/archive_execution.py``'s diff --git a/polylogue/storage/sqlite/query_store.py b/polylogue/storage/sqlite/query_store.py index 28143c9577..bc696e4a15 100644 --- a/polylogue/storage/sqlite/query_store.py +++ b/polylogue/storage/sqlite/query_store.py @@ -103,14 +103,14 @@ async def _list_session_tag_rollup_rows_query( async def list_session_tag_rollup_rows( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, query: str | None = None, ) -> list[SessionTagRollupRecord]: return await self._list_session_tag_rollup_rows_query( SessionTagRollupListQuery( - provider=provider, + origin=origin, since=since, until=until, query=query, diff --git a/polylogue/storage/sqlite/query_store_archive.py b/polylogue/storage/sqlite/query_store_archive.py index 22c8062ef3..7f54e712e5 100644 --- a/polylogue/storage/sqlite/query_store_archive.py +++ b/polylogue/storage/sqlite/query_store_archive.py @@ -91,51 +91,49 @@ async def resolve_id(self, id_prefix: str, *, strict: bool = False) -> str | Non async with self._connection_factory() as conn: return await sessions_q.resolve_id(conn, id_prefix, strict=strict) - async def search_sessions(self, query: str, limit: int = 100, providers: list[str] | None = None) -> list[str]: - return (await self.search_session_hits(query, limit=limit, providers=providers)).session_ids() + async def search_sessions(self, query: str, limit: int = 100, origins: list[str] | None = None) -> list[str]: + return (await self.search_session_hits(query, limit=limit, origins=origins)).session_ids() - async def search_action_sessions( - self, query: str, limit: int = 100, providers: list[str] | None = None - ) -> list[str]: - return (await self.search_action_session_hits(query, limit=limit, providers=providers)).session_ids() + async def search_action_sessions(self, query: str, limit: int = 100, origins: list[str] | None = None) -> list[str]: + return (await self.search_action_session_hits(query, limit=limit, origins=origins)).session_ids() async def search_session_hits( self, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> SessionSearchResult: async with self._connection_factory() as conn: - return await sessions_q.search_session_hits(conn, query, limit, providers) + return await sessions_q.search_session_hits(conn, query, limit, origins) async def search_session_evidence_hits( self, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, since: str | None = None, ) -> list[SessionSearchEvidenceRow]: async with self._connection_factory() as conn: - return await sessions_q.search_session_evidence_hits(conn, query, limit, providers, since) + return await sessions_q.search_session_evidence_hits(conn, query, limit, origins, since) async def search_attachment_identity_evidence_hits( self, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, since: str | None = None, ) -> list[SessionSearchEvidenceRow]: async with self._connection_factory() as conn: - return await attachments_q.search_attachment_identity_evidence_hits(conn, query, limit, providers, since) + return await attachments_q.search_attachment_identity_evidence_hits(conn, query, limit, origins, since) async def search_action_session_hits( self, query: str, limit: int = 100, - providers: list[str] | None = None, + origins: list[str] | None = None, ) -> SessionSearchResult: async with self._connection_factory() as conn: - return await sessions_q.search_action_session_hits(conn, query, limit, providers) + return await sessions_q.search_action_session_hits(conn, query, limit, origins) async def get_messages(self, session_id: str) -> list[MessageRecord]: async with self._connection_factory() as conn: @@ -284,7 +282,7 @@ async def aggregate_message_stats(self, session_ids: list[str] | None = None) -> async with self._connection_factory() as conn: return await stats_q.aggregate_message_stats(conn, session_ids) - async def get_stats_by(self, group_by: str = "provider") -> dict[str, int]: + async def get_stats_by(self, group_by: str = "origin") -> dict[str, int]: async with self._connection_factory() as conn: return await stats_q.get_stats_by(conn, group_by) diff --git a/polylogue/storage/sqlite/query_store_insight_profiles.py b/polylogue/storage/sqlite/query_store_insight_profiles.py index 53c5055e66..bafafeb539 100644 --- a/polylogue/storage/sqlite/query_store_insight_profiles.py +++ b/polylogue/storage/sqlite/query_store_insight_profiles.py @@ -43,7 +43,7 @@ async def find_stuck_session_latency_profiles( async def list_session_latency_profiles( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, limit: int | None = 500, @@ -51,7 +51,7 @@ async def list_session_latency_profiles( async with self._connection_factory() as conn: return await session_latency_profiles_q.list_session_latency_profiles( conn, - provider=provider, + origin=origin, since=since, until=until, limit=limit, @@ -77,7 +77,7 @@ async def _list_session_profiles_query( async def list_session_profiles( self, *, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, first_message_since: str | None = None, @@ -94,7 +94,7 @@ async def list_session_profiles( ) -> list[SessionProfileRecord]: return await self._list_session_profiles_query( SessionProfileListQuery( - provider=provider, + origin=origin, since=since, until=until, first_message_since=first_message_since, diff --git a/polylogue/storage/sqlite/query_store_insight_timelines.py b/polylogue/storage/sqlite/query_store_insight_timelines.py index b140591679..ce0772672e 100644 --- a/polylogue/storage/sqlite/query_store_insight_timelines.py +++ b/polylogue/storage/sqlite/query_store_insight_timelines.py @@ -51,7 +51,7 @@ async def list_session_work_events( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, heuristic_label: str | None = None, @@ -62,7 +62,7 @@ async def list_session_work_events( return await self._list_session_work_events_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, heuristic_label=heuristic_label, @@ -76,7 +76,7 @@ async def list_session_phases( self, *, session_id: str | None = None, - provider: str | None = None, + origin: str | None = None, since: str | None = None, until: str | None = None, kind: str | None = None, @@ -86,7 +86,7 @@ async def list_session_phases( return await self._list_session_phases_query( SessionTimelineListQuery( session_id=session_id, - provider=provider, + origin=origin, since=since, until=until, kind=kind, diff --git a/tests/benchmarks/test_reader_api.py b/tests/benchmarks/test_reader_api.py index 27e502f98b..83b03fa2a9 100644 --- a/tests/benchmarks/test_reader_api.py +++ b/tests/benchmarks/test_reader_api.py @@ -61,7 +61,7 @@ def test_bench_reader_list_with_provider( benchmark_store_call( benchmark, bench_db_5k, - lambda store: store.repository.list_summaries(provider="claude-code"), + lambda store: store.repository.list_summaries(origin="claude-code"), ) diff --git a/tests/benchmarks/test_search_filters.py b/tests/benchmarks/test_search_filters.py index 4792e20f90..a577b95cf2 100644 --- a/tests/benchmarks/test_search_filters.py +++ b/tests/benchmarks/test_search_filters.py @@ -60,7 +60,7 @@ def test_bench_filter_provider(benchmark: BenchmarkFixture, bench_db_5k: Path) - benchmark_store_call( benchmark, bench_db_5k, - lambda store: store.repository.list_summaries(provider="chatgpt"), + lambda store: store.repository.list_summaries(origin="chatgpt"), ) @@ -90,7 +90,7 @@ def test_bench_filter_count(benchmark: BenchmarkFixture, bench_db_5k: Path) -> N benchmark_store_call( benchmark, bench_db_5k, - lambda store: store.repository.count_by_query(SessionRecordQuery(provider="chatgpt")), + lambda store: store.repository.count_by_query(SessionRecordQuery(origin="chatgpt")), ) @@ -101,7 +101,7 @@ def test_bench_filter_combined(benchmark: BenchmarkFixture, bench_db_10k: Path) benchmark, bench_db_10k, lambda store: store.repository.list_summaries( - provider="claude-ai", + origin="claude-ai-export", has_tool_use=True, min_messages=2, ), diff --git a/tests/benchmarks/test_storage.py b/tests/benchmarks/test_storage.py index 4deddc1b3b..31c8d59933 100644 --- a/tests/benchmarks/test_storage.py +++ b/tests/benchmarks/test_storage.py @@ -44,7 +44,7 @@ def test_bench_list_sessions_provider_filter(benchmark: BenchmarkFixture, bench_ benchmark_store_call( benchmark, bench_db_5k, - lambda store: store.backend.queries.list_sessions(SessionRecordQuery(provider="chatgpt", limit=50)), + lambda store: store.backend.queries.list_sessions(SessionRecordQuery(origin="chatgpt", limit=50)), ) @@ -76,7 +76,7 @@ def test_bench_list_sessions_combined_filter(benchmark: BenchmarkFixture, bench_ bench_db_10k, lambda store: store.backend.queries.list_sessions( SessionRecordQuery( - provider="claude-ai", + origin="claude-ai-export", has_tool_use=True, min_messages=2, limit=50, diff --git a/tests/unit/api/test_facade_contracts.py b/tests/unit/api/test_facade_contracts.py index c15963c059..807e0411a3 100644 --- a/tests/unit/api/test_facade_contracts.py +++ b/tests/unit/api/test_facade_contracts.py @@ -3183,7 +3183,7 @@ async def test_archive_tiers_api_reads_native_sessions(tmp_path: Path) -> None: unit_envelope = await archive.query_units("messages where text:needle", limit=5) normal_neighbors = await archive.neighbor_candidates( query="needle", - provider=Provider.CODEX.value, + origin="codex-session", limit=3, ) paged_messages, total_messages = await archive.get_messages_paginated( @@ -3524,7 +3524,7 @@ async def test_archive_tiers_api_tag_rollups_read_index_and_user_tiers(tmp_path: ) rollups = await archive.list_session_tag_rollup_insights( - SessionTagRollupQuery(provider=Provider.CODEX.value, query="foc", limit=10) + SessionTagRollupQuery(origin=Provider.CODEX.value, query="foc", limit=10) ) assert len(rollups) == 1 @@ -3622,13 +3622,13 @@ async def test_archive_tiers_api_archive_coverage_reads_index_tier(tmp_path: Pat conn.commit() provider_rows = await archive.list_archive_coverage_insights( - ArchiveCoverageInsightQuery(group_by="provider", provider=Provider.CODEX.value) + ArchiveCoverageInsightQuery(group_by="origin", origin="codex-session") ) day_rows = await archive.list_archive_coverage_insights( - ArchiveCoverageInsightQuery(group_by="day", provider=Provider.CODEX.value, limit=10) + ArchiveCoverageInsightQuery(group_by="day", origin=Provider.CODEX.value, limit=10) ) week_rows = await archive.list_archive_coverage_insights( - ArchiveCoverageInsightQuery(group_by="week", provider=Provider.CODEX.value, limit=10) + ArchiveCoverageInsightQuery(group_by="week", origin=Provider.CODEX.value, limit=10) ) assert len(provider_rows) == 1 @@ -3722,17 +3722,17 @@ async def test_archive_tiers_api_tool_usage_reads_index_actions(tmp_path: Path) archive_db.write_parsed(chatgpt) [insight] = await archive.list_tool_usage_insights( - ToolUsageInsightQuery(provider=Provider.CODEX.value, tool="read") + ToolUsageInsightQuery(origin=Origin.CODEX_SESSION.value, tool="read") ) assert insight.total_call_count == 1 assert insight.total_distinct_tools == 1 - assert insight.providers_with_data == 1 - assert insight.providers_without_data == 1 + assert insight.origins_with_data == 1 + assert insight.origins_without_data == 1 assert insight.has_coverage_gaps is True assert len(insight.entries) == 1 entry = insight.entries[0] - assert entry.source_name == Provider.CODEX.value + assert entry.source_name == Origin.CODEX_SESSION.value assert entry.normalized_tool_name == "read" assert entry.action_kind == "file_read" assert entry.call_count == 1 @@ -3741,11 +3741,11 @@ async def test_archive_tiers_api_tool_usage_reads_index_actions(tmp_path: Path) assert entry.distinct_tool_ids == 1 assert entry.affected_path_calls == 1 assert entry.output_text_calls == 1 - coverage = {item.source_name: item for item in insight.provider_coverage} - assert coverage[Provider.CODEX.value].data_available is True - assert coverage[Provider.CODEX.value].action_count == 1 - assert coverage[Provider.CHATGPT.value].data_available is False - assert coverage[Provider.CHATGPT.value].session_count == 1 + coverage = {item.source_name: item for item in insight.origin_coverage} + assert coverage[Origin.CODEX_SESSION.value].data_available is True + assert coverage[Origin.CODEX_SESSION.value].action_count == 1 + assert coverage[Origin.CHATGPT_EXPORT.value].data_available is False + assert coverage[Origin.CHATGPT_EXPORT.value].session_count == 1 finally: await archive.close() @@ -3927,7 +3927,7 @@ async def test_archive_tiers_api_timeline_insights_read_index_tier(tmp_path: Pat events = await archive.get_session_work_event_insights(session_id) filtered_events = await archive.list_session_work_event_insights( SessionWorkEventInsightQuery( - provider=Provider.CODEX.value, + origin="codex-session", heuristic_label="implementation", since="2026-02-02T02:40:30Z", limit=10, @@ -3935,7 +3935,7 @@ async def test_archive_tiers_api_timeline_insights_read_index_tier(tmp_path: Pat ) phases = await archive.get_session_phase_insights(session_id) filtered_phases = await archive.list_session_phase_insights( - SessionPhaseInsightQuery(provider=Provider.CODEX.value, limit=10) + SessionPhaseInsightQuery(origin=Provider.CODEX.value, limit=10) ) assert len(events) == 1 @@ -4225,13 +4225,13 @@ async def test_archive_tiers_api_session_costs_read_index_tier(tmp_path: Path) - ) costs = await archive.list_session_cost_insights( - SessionCostInsightQuery(provider=Provider.CODEX.value, status="priced", limit=10) + SessionCostInsightQuery(origin=Provider.CODEX.value, status="priced", limit=10) ) unavailable = await archive.list_session_cost_insights( - SessionCostInsightQuery(provider=Provider.CHATGPT.value, status="unavailable", limit=10) + SessionCostInsightQuery(origin=Provider.CHATGPT.value, status="unavailable", limit=10) ) model_filtered = await archive.list_session_cost_insights(SessionCostInsightQuery(model="claude-sonnet-4-5")) - rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(provider=Provider.CODEX.value)) + rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(origin=Provider.CODEX.value)) all_rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(limit=10)) model_rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(model="claude-sonnet-4-5")) @@ -4338,13 +4338,13 @@ async def test_archive_tiers_api_latency_profiles_read_index_tier(tmp_path: Path profile = await archive.get_session_latency_profile_insight(session_id) listed = await archive.list_session_latency_profile_insights( - SessionLatencyProfileInsightQuery(provider=Provider.CODEX.value, limit=10) + SessionLatencyProfileInsightQuery(origin=Provider.CODEX.value, limit=10) ) only_stuck = await archive.list_session_latency_profile_insights( - SessionLatencyProfileInsightQuery(provider=Provider.CODEX.value, only_stuck=True, limit=10) + SessionLatencyProfileInsightQuery(origin=Provider.CODEX.value, only_stuck=True, limit=10) ) stuck = await archive.find_stuck_session_latency_profile_insights( - SessionLatencyProfileInsightQuery(provider=Provider.CODEX.value, limit=10) + SessionLatencyProfileInsightQuery(origin=Provider.CODEX.value, limit=10) ) assert profile is not None @@ -4502,7 +4502,7 @@ async def test_archive_tiers_api_session_profiles_read_index_tier(tmp_path: Path inference_only = await archive.get_session_profile_insight(session_id, tier="inference") listed = await archive.list_session_profile_insights( SessionProfileInsightQuery( - provider=Provider.CODEX.value, + origin="codex-session", workflow_shape="implementation", terminal_state="completed", tier="merged", diff --git a/tests/unit/api/test_session_analytics_facade.py b/tests/unit/api/test_session_analytics_facade.py index 0e1c3c968e..f67d751a99 100644 --- a/tests/unit/api/test_session_analytics_facade.py +++ b/tests/unit/api/test_session_analytics_facade.py @@ -76,11 +76,11 @@ async def test_aggregate_sessions_maps_provider_filter_through(tmp_path: Path) - fetch_mock = AsyncMock(return_value=[]) poly.list_session_profile_insights = fetch_mock # type: ignore[method-assign] - await poly.aggregate_sessions(group_by="workflow_shape", provider="claude-code") + await poly.aggregate_sessions(group_by="workflow_shape", origin="claude-code") assert fetch_mock.await_args is not None query = fetch_mock.await_args.args[0] - assert query.provider == "claude-code" + assert query.origin == "claude-code" @pytest.mark.asyncio diff --git a/tests/unit/archive/test_query_search_runtime.py b/tests/unit/archive/test_query_search_runtime.py index 9a534a61f6..6fde03beae 100644 --- a/tests/unit/archive/test_query_search_runtime.py +++ b/tests/unit/archive/test_query_search_runtime.py @@ -146,7 +146,7 @@ async def test_search_action_results_uses_ready_path_and_raises_on_error(monkeyp ) assert [str(session.id) for session in ready_results] == ["conv-direct"] - repository.search_actions.assert_awaited_once_with("needle", limit=3, providers=["chatgpt"]) + repository.search_actions.assert_awaited_once_with("needle", limit=3, origins=["chatgpt-export"]) repository.search_actions.reset_mock(side_effect=True, return_value=True) repository.search_actions.side_effect = RuntimeError("boom") @@ -400,3 +400,60 @@ async def test_search_hits_for_plan_degrades_semantic_and_hybrid_without_embeddi config, ) assert hybrid_hits == [] + + +@pytest.mark.asyncio +async def test_search_hits_origin_filter_excludes_other_origins(tmp_path: Path) -> None: + """An origin filter over identical FTS content selects only that origin. + + Regression for the origin/provider seam (#2820 review P1): the FTS leg + passed provider tokens (``chatgpt``) into a SQL filter over origin tokens + (``chatgpt-export``), silently matching nothing — or, with the filter + dropped, everything. Two origins sharing the same searchable text pin both + failure directions against a real seeded ``index.db``: mutating the + production filter tokens or removing the origin restriction each flips an + assertion. + """ + archive_root = tmp_path / "archive" + archive_root.mkdir(parents=True, exist_ok=True) + render_root = tmp_path / "render" + render_root.mkdir(parents=True, exist_ok=True) + db_path = archive_root / "index.db" + + ( + SessionBuilder(db_path, "conv-chatgpt") + .provider(Provider.CHATGPT.value) + .title("Needle One") + .updated_at("2026-04-22T12:00:00+00:00") + .add_message("m1", role="user", text="shared needle payload for origin filtering") + .save() + ) + ( + SessionBuilder(db_path, "conv-codex") + .provider(Provider.CODEX.value) + .title("Needle Two") + .updated_at("2026-04-22T13:00:00+00:00") + .add_message("m1", role="user", text="shared needle payload for origin filtering") + .save() + ) + + config = Config( + archive_root=archive_root, + render_root=render_root, + sources=[Source(name="test", path=tmp_path / "inbox")], + db_path=db_path, + ) + + async def _hit_ids(origins: tuple[str, ...] | None) -> list[str]: + hits = await search_hits_for_plan( + SessionQueryPlan(query_terms=("needle",), origins=origins or (), limit=10), + config, + ) + return sorted(hit.session_id for hit in hits) + + chatgpt_id = native_session_id_for("chatgpt", "conv-chatgpt") + codex_id = native_session_id_for("codex", "conv-codex") + + assert await _hit_ids(("chatgpt-export",)) == [chatgpt_id] + assert await _hit_ids(("codex-session",)) == [codex_id] + assert await _hit_ids(None) == sorted([chatgpt_id, codex_id]) diff --git a/tests/unit/cli/test_analytics.py b/tests/unit/cli/test_analytics.py index 0d56f19ebf..95a5646eb2 100644 --- a/tests/unit/cli/test_analytics.py +++ b/tests/unit/cli/test_analytics.py @@ -1,4 +1,4 @@ -"""Tests for provider analytics product computation.""" +"""Tests for origin analytics product computation.""" from __future__ import annotations @@ -19,7 +19,7 @@ def _archive(tmp_path: Path) -> Polylogue: async def _coverage(archive: Polylogue) -> list[ArchiveCoverageInsight]: - return await archive.list_archive_coverage_insights(ArchiveCoverageInsightQuery(group_by="provider")) + return await archive.list_archive_coverage_insights(ArchiveCoverageInsightQuery(group_by="origin")) class TestArchiveCoverageInsight: diff --git a/tests/unit/cli/test_diagnostics.py b/tests/unit/cli/test_diagnostics.py index 8cdcae4fa7..1a1aec19ea 100644 --- a/tests/unit/cli/test_diagnostics.py +++ b/tests/unit/cli/test_diagnostics.py @@ -645,7 +645,7 @@ async def test_tools_passes_filters_to_tool_usage_insight(monkeypatch: pytest.Mo query = store.queries[0] assert isinstance(query, ToolUsageInsightQuery) - assert query.provider == "claude-code-session" + assert query.origin == "claude-code-session" assert query.tool == "mcp__serena__find_symbol" assert query.mcp_server == "serena" assert query.action_kind == "tool_use" diff --git a/tests/unit/cli/test_insights.py b/tests/unit/cli/test_insights.py index 79a7ba3949..6c66f106e8 100644 --- a/tests/unit/cli/test_insights.py +++ b/tests/unit/cli/test_insights.py @@ -69,9 +69,9 @@ def _exception_message(result: Result) -> str: def test_insight_items_payload_can_render_cli_and_mcp_keys() -> None: product = ArchiveCoverageInsight( - group_by="provider", - bucket="claude-code", - source_name="claude-code", + group_by="origin", + bucket="claude-code-session", + source_name="claude-code-session", session_count=1, message_count=2, user_message_count=1, @@ -358,7 +358,7 @@ def test_usage_timeline_first_page_skips_later_provider_events(cli_workspace: Cl until_ms=None, limit=1, ) - rows = archive.list_usage_timeline_insights(provider="chatgpt", limit=1) + rows = archive.list_usage_timeline_insights(origin="chatgpt", limit=1) assert cutoff_ms is not None assert skip_event_scan is True diff --git a/tests/unit/core/test_cwd_prefix_filter.py b/tests/unit/core/test_cwd_prefix_filter.py index 703b27e24a..07dd5b8d93 100644 --- a/tests/unit/core/test_cwd_prefix_filter.py +++ b/tests/unit/core/test_cwd_prefix_filter.py @@ -56,7 +56,7 @@ def test_cwd_prefix_absent_emits_no_clause() -> None: def test_cwd_prefix_combines_with_other_filters() -> None: where, params = _build_session_filters( cwd_prefix="/repo", - provider="claude-code", + origin="claude-code-session", title_contains="bug", ) assert "origin = ?" in where diff --git a/tests/unit/core/test_facade_api.py b/tests/unit/core/test_facade_api.py index 44e50735a2..6d1304917f 100644 --- a/tests/unit/core/test_facade_api.py +++ b/tests/unit/core/test_facade_api.py @@ -467,13 +467,13 @@ async def test_durable_session_insights_are_publicly_queryable( profile = await archive.get_session_profile_insight(root_id) profiles = await archive.list_session_profile_insights( SessionProfileInsightQuery( - provider="claude-code", + origin="claude-code-session", first_message_since="2026-03-01T00:00:00+00:00", session_date_since="2026-03-01", limit=10, ) ) - phases = await archive.list_session_phase_insights(SessionPhaseInsightQuery(provider="claude-code", limit=10)) + phases = await archive.list_session_phase_insights(SessionPhaseInsightQuery(origin="claude-code", limit=10)) threads = await archive.list_thread_insights(ThreadInsightQuery(limit=10)) assert profile is not None @@ -506,18 +506,18 @@ async def test_durable_session_insights_are_publicly_queryable( assert threads[0].thread.member_evidence[1].role == "parent_continuation" assert threads[0].thread.member_evidence[1].parent_id == root_id - tag_rollups = await archive.list_session_tag_rollup_insights(SessionTagRollupQuery(provider="claude-code")) + tag_rollups = await archive.list_session_tag_rollup_insights(SessionTagRollupQuery(origin="claude-code")) day_coverage = await archive.list_archive_coverage_insights( - ArchiveCoverageInsightQuery(provider="claude-code", group_by="day", limit=10) + ArchiveCoverageInsightQuery(origin="claude-code", group_by="day", limit=10) ) week_coverage = await archive.list_archive_coverage_insights( - ArchiveCoverageInsightQuery(provider="claude-code", group_by="week", limit=10) + ArchiveCoverageInsightQuery(origin="claude-code", group_by="week", limit=10) ) archive_debt = await archive.list_archive_debt_insights(ArchiveDebtInsightQuery(limit=10)) session_costs = await archive.list_session_cost_insights( - SessionCostInsightQuery(provider="claude-code", limit=10) + SessionCostInsightQuery(origin="claude-code", limit=10) ) - cost_rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(provider="claude-code")) + cost_rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(origin="claude-code")) assert any(item.tag == "origin:claude-code-session" for item in tag_rollups) assert len(day_coverage) == 1 diff --git a/tests/unit/core/test_insight_export_bundles.py b/tests/unit/core/test_insight_export_bundles.py index 293e56cf3f..3d74eabb61 100644 --- a/tests/unit/core/test_insight_export_bundles.py +++ b/tests/unit/core/test_insight_export_bundles.py @@ -88,7 +88,7 @@ async def test_insight_export_bundle_writes_bounded_insights(cli_workspace: dict InsightExportBundleRequest( output_path=target, insights=("profiles", "work-events"), - provider="codex", + origin="codex-session", since="2026-03-01", until="2026-03-31", ) diff --git a/tests/unit/core/test_insight_registry_runtime.py b/tests/unit/core/test_insight_registry_runtime.py index 30eb444b0b..e6b81519e8 100644 --- a/tests/unit/core/test_insight_registry_runtime.py +++ b/tests/unit/core/test_insight_registry_runtime.py @@ -26,9 +26,9 @@ def _archive_coverage_insight() -> ArchiveCoverageInsight: return ArchiveCoverageInsight( - group_by="provider", - bucket="claude-code", - source_name="claude-code", + group_by="origin", + bucket="claude-code-session", + source_name="claude-code-session", session_count=1, message_count=2, user_message_count=1, @@ -141,10 +141,10 @@ def test_fetch_insights_sync_uses_registry_dispatch() -> None: class _Operations: def list_archive_coverage_insights(self, query: object) -> str: - return f"sync:{query.provider}" + return f"sync:{query.origin}" with patch("polylogue.api.sync.bridge.run_coroutine_sync", side_effect=lambda value: [value]): - assert fetch_insights(insight_type, _Operations(), provider="claude-code") == ["sync:claude-code"] + assert fetch_insights(insight_type, _Operations(), origin="claude-code") == ["sync:claude-code"] @pytest.mark.asyncio @@ -153,6 +153,6 @@ async def test_fetch_insights_async_uses_registry_dispatch() -> None: class _AsyncOperations: async def list_archive_coverage_insights(self, query: object) -> list[str]: - return [f"async:{query.provider}"] + return [f"async:{query.origin}"] - assert await fetch_insights_async(insight_type, _AsyncOperations(), provider="claude-code") == ["async:claude-code"] + assert await fetch_insights_async(insight_type, _AsyncOperations(), origin="claude-code") == ["async:claude-code"] diff --git a/tests/unit/core/test_neighbor_candidates.py b/tests/unit/core/test_neighbor_candidates.py index 2a29b40557..e3c9589c67 100644 --- a/tests/unit/core/test_neighbor_candidates.py +++ b/tests/unit/core/test_neighbor_candidates.py @@ -24,7 +24,7 @@ async def _discover( candidates = await archive.neighbor_candidates( session_id=request.session_id, query=request.query, - provider=request.provider, + origin=request.origin, limit=request.limit, window_hours=request.window_hours, ) @@ -39,7 +39,7 @@ async def _discover_candidates(db_path: Path, request: NeighborDiscoveryRequest) return await archive.neighbor_candidates( session_id=request.session_id, query=request.query, - provider=request.provider, + origin=request.origin, limit=request.limit, window_hours=request.window_hours, ) @@ -165,7 +165,7 @@ async def test_query_seed_returns_explainable_candidates(db_path: Path) -> None: ids = await _discover( db_path, - NeighborDiscoveryRequest(query="schema audit", provider="chatgpt"), + NeighborDiscoveryRequest(query="schema audit", origin="chatgpt"), ) assert ids == [native_session_id_for("chatgpt", "query-candidate")] diff --git a/tests/unit/core/test_provider_origin_dedup.py b/tests/unit/core/test_provider_origin_dedup.py index c733c2d11e..6d0b5cb202 100644 --- a/tests/unit/core/test_provider_origin_dedup.py +++ b/tests/unit/core/test_provider_origin_dedup.py @@ -1,40 +1,4 @@ -"""Regression tests for the provider<->origin reverse-lookup dedup (polylogue-9e5.8 Step 1). - -Ground truth found while executing polylogue-9e5.8's plan: the bead's design -field described ``archive/query/archive_execution.py``'s ``_ORIGIN_TO_PROVIDER`` -dict as uniquely, silently collapsing ``aistudio-drive`` onto -``Provider.GEMINI``. Reading the live source showed this collapse is actually -a *deliberate, documented* choice already centralized in -``core/sources.py::provider_from_origin`` (``Origin.AISTUDIO_DRIVE`` is -non-injective -- both ``Provider.GEMINI`` and ``Provider.DRIVE`` produce it -- -and ``GEMINI`` is the documented canonical choice so a Gemini session -round-trips). The real, verified bug was that three independent modules -(``archive/query/archive_execution.py``, ``storage/sqlite/archive_tiers/ -archive.py``, ``storage/sqlite/queries/tool_usage.py``) each hand-rolled an -*independent copy* of that exact table instead of delegating to the -canonical function -- and had already silently drifted: all three copies were -missing a ``grok-export`` entry, so a Grok-origin session would resolve to -``Provider.UNKNOWN`` instead of ``Provider.GROK`` through these three call -sites specifically, while the canonical ``core/sources.py`` table already -handled it correctly. - -The fix replaces all three hand-rolled dicts with delegation to -``provider_from_origin``. These tests prove: (1) all three now agree with the -canonical function for every ``Origin`` value, including the previously-drifted -``grok-export`` case, and (2) the well-known, deliberate GEMINI/DRIVE collapse -is preserved (not "fixed" -- that needs a Source-family disambiguator, Step 5 -of the plan, out of scope here). - -The two tool-usage filter paths had the same drift shape in the opposite -direction: incomplete hand-maintained sets failed to recognize the canonical -``grok-export`` token. Their production helpers are covered together below so -every current and future ``Origin`` member must pass through unchanged while -legacy provider tokens continue to normalize through the shared helper. - -Un-collapsing DRIVE from GEMINI is explicitly *not* what this fix does or -claims to do -- see polylogue-9e5.8's design field, "The non-injective -GEMINI/DRIVE blocker" section. -""" +"""Regression tests for source-wire mappings and canonical origin filters.""" from __future__ import annotations @@ -42,11 +6,9 @@ import pytest -import polylogue.mcp.server_insight_tools as server_insight_tools_module from polylogue.archive.query.archive_execution import _provider_for_origin as _archive_execution_provider_for_origin from polylogue.core.enums import Origin, Provider from polylogue.core.sources import provider_from_origin -from polylogue.mcp.insight_tool_contracts import _origin_to_provider_token as _contracts_origin_to_provider_token from polylogue.storage.sqlite.archive_tiers.archive import ( _origin_for_tool_usage_filter as _archive_tiers_origin_for_tool_usage_filter, ) @@ -109,32 +71,3 @@ def test_tool_usage_origin_filters_preserve_every_canonical_origin( ) -> None: for origin in Origin: assert fn(origin.value) == origin.value - - -@pytest.mark.parametrize("fn", _TOOL_USAGE_ORIGIN_FILTERS) -def test_tool_usage_origin_filters_accept_legacy_and_unknown_values( - fn: Callable[[str | None], str | None], -) -> None: - assert fn(Provider.GROK.value) == Origin.GROK_EXPORT.value - assert fn("not-a-real-provider-or-origin") == Origin.UNKNOWN_EXPORT.value - assert fn(None) is None - - -def test_origin_to_provider_token_is_a_single_shared_definition() -> None: - """polylogue-9e5.8's design field flagged _origin_to_provider_token as - defined twice, independently, in mcp/insight_tool_contracts.py and - mcp/server_insight_tools.py -- itself evidence of an ad hoc, uncentralized - shim. server_insight_tools now imports the contracts module's definition - instead of re-declaring it; this asserts there is exactly one function - object, not two behaviorally-identical copies.""" - assert getattr(server_insight_tools_module, "_origin_to_provider_token") is _contracts_origin_to_provider_token # noqa: B009 - - -def test_origin_to_provider_token_round_trips_known_origins() -> None: - for origin in Origin: - assert _contracts_origin_to_provider_token(origin.value) == provider_from_origin(origin).value - - -def test_origin_to_provider_token_none_and_empty_are_none() -> None: - assert _contracts_origin_to_provider_token(None) is None - assert _contracts_origin_to_provider_token("") is None diff --git a/tests/unit/core/test_query_fields.py b/tests/unit/core/test_query_fields.py index 4dde1497a9..47394f8e3c 100644 --- a/tests/unit/core/test_query_fields.py +++ b/tests/unit/core/test_query_fields.py @@ -112,7 +112,7 @@ def test_query_field_catalog_drives_plan_presence_descriptions_and_pushdown() -> ] assert plan.sql_pushdown_params() == { - "provider": "codex", + "origin": "codex-session", "referenced_path": ["polylogue/storage"], "tool_terms": ["bash"], "repo_names": ["thoughtspace"], @@ -127,7 +127,7 @@ def test_query_field_catalog_drives_plan_presence_descriptions_and_pushdown() -> ) record_query = plan.record_query - assert record_query.provider == "codex" + assert record_query.origin == "codex-session" assert record_query.referenced_path == ("polylogue/storage",) assert record_query.tool_terms == ("bash",) assert record_query.repo_names == ("thoughtspace",) diff --git a/tests/unit/core/test_repo_filter.py b/tests/unit/core/test_repo_filter.py index aa97c48b80..b9758f1077 100644 --- a/tests/unit/core/test_repo_filter.py +++ b/tests/unit/core/test_repo_filter.py @@ -48,7 +48,7 @@ def test_repo_absent_emits_no_clause() -> None: def test_repo_combines_with_other_filters() -> None: where, params = _build_session_filters( repo_names=["thoughtspace"], - provider="claude-code", + origin="claude-code-session", title_contains="bug", ) assert "origin = ?" in where diff --git a/tests/unit/insights/test_cost_rollup_summary_path.py b/tests/unit/insights/test_cost_rollup_summary_path.py index 7dfd0e6a03..a56d71f0d4 100644 --- a/tests/unit/insights/test_cost_rollup_summary_path.py +++ b/tests/unit/insights/test_cost_rollup_summary_path.py @@ -101,7 +101,7 @@ async def test_cost_rollups_aggregate_typed_cost_rows_without_message_load( target = "polylogue.storage.repository.archive.sessions.RepositoryArchiveSessionMixin.list_by_query" with patch(target, side_effect=AssertionError("messages loaded")) as spy: - rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(provider="claude-code")) + rollups = await archive.list_cost_rollup_insights(CostRollupInsightQuery(origin="claude-code-session")) assert spy.call_count == 0 total = sum(rollup.total_usd for rollup in rollups) diff --git a/tests/unit/insights/test_tag_rollups.py b/tests/unit/insights/test_tag_rollups.py index d2dfe82d81..49a5b9676b 100644 --- a/tests/unit/insights/test_tag_rollups.py +++ b/tests/unit/insights/test_tag_rollups.py @@ -1,20 +1,8 @@ -"""Regression tests for ``synthesize_provider_tag_rollups`` (polylogue-4rrv). - -polylogue-9e5.8's Step 5 investigation into the non-injective -``Origin.AISTUDIO_DRIVE`` collapse (``Provider.GEMINI`` and ``Provider.DRIVE`` -both produce it) named ``insights/tag_rollups.py:49`` as a candidate leak -site. Reading it to ground truth shows it performs the *forward* -``Provider -> Origin`` conversion (via ``origin_from_provider``), which is -total and well-defined even for this fiber -- there is no reverse lookup here -for a disambiguator to fix. These tests pin that: both ``provider="gemini"`` -and ``provider="drive"`` correctly resolve to the ``aistudio-drive`` origin -filter (not silently falling back to ``unknown-export`` or raising), proving -the site was already correct and is not blocked by the collapse. -""" +"""Regression tests for origin-filtered session-tag rollups (polylogue-4rrv).""" from __future__ import annotations -from polylogue.insights.tag_rollups import synthesize_provider_tag_rollups +from polylogue.insights.tag_rollups import synthesize_origin_tag_rollups class _FakeArchive: @@ -42,52 +30,18 @@ def _counts() -> dict[str, int]: } -def test_provider_gemini_filters_to_aistudio_drive_origin() -> None: - rollups = synthesize_provider_tag_rollups( - _FakeArchive(_counts()), # type: ignore[arg-type] - provider="gemini", - materialized_at="2026-07-12T00:00:00+00:00", - ) - assert [r.tag for r in rollups] == ["origin:aistudio-drive"] - assert rollups[0].session_count == 7 - - -def test_provider_drive_filters_to_the_same_aistudio_drive_origin() -> None: - """Provider.GEMINI and Provider.DRIVE both forward-map to - Origin.AISTUDIO_DRIVE (origin_from_provider is total, non-injective only - in the reverse direction) -- "drive" must resolve identically to - "gemini", not fall back to unknown-export or raise.""" - rollups = synthesize_provider_tag_rollups( - _FakeArchive(_counts()), # type: ignore[arg-type] - provider="drive", - materialized_at="2026-07-12T00:00:00+00:00", - ) - assert [r.tag for r in rollups] == ["origin:aistudio-drive"] - assert rollups[0].session_count == 7 - - -def test_gemini_and_drive_provider_filters_agree() -> None: - """Documents the known coarseness (not a bug this bead fixes): since no - storage tier persists which acquisition mechanism produced an - aistudio-drive session, both provider filters necessarily return the - identical rollup. Splitting them needs a durable capture-mode field - (tracked as this bead's follow-up), not a smarter reverse lookup here.""" - gemini_rollups = synthesize_provider_tag_rollups( - _FakeArchive(_counts()), # type: ignore[arg-type] - provider="gemini", - materialized_at="2026-07-12T00:00:00+00:00", - ) - drive_rollups = synthesize_provider_tag_rollups( +def test_origin_filter_selects_one_rollup() -> None: + """A canonical origin filter selects its public origin-tag payload.""" + rollups = synthesize_origin_tag_rollups( _FakeArchive(_counts()), # type: ignore[arg-type] - provider="drive", + origin="aistudio-drive", materialized_at="2026-07-12T00:00:00+00:00", ) - assert [r.tag for r in gemini_rollups] == [r.tag for r in drive_rollups] - assert [r.session_count for r in gemini_rollups] == [r.session_count for r in drive_rollups] + assert [(rollup.tag, rollup.session_count) for rollup in rollups] == [("origin:aistudio-drive", 7)] -def test_no_provider_filter_returns_every_origin() -> None: - rollups = synthesize_provider_tag_rollups( +def test_no_origin_filter_returns_every_origin() -> None: + rollups = synthesize_origin_tag_rollups( _FakeArchive(_counts()), # type: ignore[arg-type] materialized_at="2026-07-12T00:00:00+00:00", ) diff --git a/tests/unit/insights/test_tool_usage.py b/tests/unit/insights/test_tool_usage.py index c32e93bb86..2221a0b1bd 100644 --- a/tests/unit/insights/test_tool_usage.py +++ b/tests/unit/insights/test_tool_usage.py @@ -10,7 +10,7 @@ actions surface as ``data_available=False`` rather than silent zeros. 4. MCP envelope shape: ``list_tool_usage_insights`` returns a single - ``ToolUsageInsight`` whose ``provider_coverage`` is always exhaustive, + ``ToolUsageInsight`` whose ``origin_coverage`` is always exhaustive, even when ``entries`` are narrowed by a query filter. """ @@ -58,7 +58,7 @@ def test_malformed_mcp_prefix(self) -> None: def _row( *, - provider: str, + origin: str, tool: str, action_kind: str = "tool_use", calls: int = 1, @@ -69,7 +69,7 @@ def _row( outputs: int = 0, ) -> ToolUsageRow: return { - "source_name": provider, + "source_name": origin, "normalized_tool_name": tool, "action_kind": action_kind, "call_count": calls, @@ -83,7 +83,7 @@ def _row( def _coverage( *, - provider: str, + origin: str, sessions: int, events: int = 0, tools: int = 0, @@ -93,7 +93,7 @@ def _coverage( outputs: int = 0, ) -> ToolUsageProviderCoverageRow: return { - "source_name": provider, + "source_name": origin, "session_count": sessions, "action_count": events, "distinct_tool_count": tools, @@ -109,11 +109,11 @@ class TestBuildToolUsageInsight: def test_basic_aggregation(self) -> None: rows = [ - _row(provider="claude-code", tool="Read", calls=10, sessions=3, messages=8), - _row(provider="claude-code", tool="Bash", calls=4, sessions=2), + _row(origin="claude-code-session", tool="Read", calls=10, sessions=3, messages=8), + _row(origin="claude-code-session", tool="Bash", calls=4, sessions=2), ] coverage = [ - _coverage(provider="claude-code", sessions=3, events=14, tools=2, kinds=1, paths=14), + _coverage(origin="claude-code-session", sessions=3, events=14, tools=2, kinds=1, paths=14), ] insight = build_tool_usage_insight( rows=rows, @@ -126,51 +126,51 @@ def test_basic_aggregation(self) -> None: assert [entry.normalized_tool_name for entry in insight.entries] == ["Read", "Bash"] assert insight.total_call_count == 14 assert insight.total_distinct_tools == 2 - assert insight.providers_with_data == 1 - assert insight.providers_without_data == 0 + assert insight.origins_with_data == 1 + assert insight.origins_without_data == 0 assert insight.has_coverage_gaps is False - assert insight.provider_coverage[0].data_available is True - assert insight.provider_coverage[0].has_affected_paths_signal is True + assert insight.origin_coverage[0].data_available is True + assert insight.origin_coverage[0].has_affected_paths_signal is True - def test_filter_by_provider_does_not_hide_coverage_gaps(self) -> None: + def test_filter_by_origin_does_not_hide_coverage_gaps(self) -> None: rows = [ - _row(provider="claude-code", tool="Read", calls=5), - _row(provider="codex", tool="apply_patch", calls=2), + _row(origin="claude-code-session", tool="Read", calls=5), + _row(origin="codex-session", tool="apply_patch", calls=2), ] coverage = [ - _coverage(provider="claude-code", sessions=2, events=5, tools=1, kinds=1), - _coverage(provider="codex", sessions=1, events=2, tools=1, kinds=1), + _coverage(origin="claude-code-session", sessions=2, events=5, tools=1, kinds=1), + _coverage(origin="codex-session", sessions=1, events=2, tools=1, kinds=1), # ChatGPT has sessions but no tool data — this MUST stay - # visible even when the user narrows to a different provider. - _coverage(provider="chatgpt", sessions=4, events=0), + # visible even when the user narrows to a different origin. + _coverage(origin="chatgpt-export", sessions=4, events=0), ] insight = build_tool_usage_insight( rows=rows, coverage_rows=coverage, - query=ToolUsageInsightQuery(provider="claude-code"), + query=ToolUsageInsightQuery(origin="claude-code-session"), materialized_at="2026-05-17T00:00:00+00:00", ) - assert [entry.source_name for entry in insight.entries] == ["claude-code"] + assert [entry.source_name for entry in insight.entries] == ["claude-code-session"] # Coverage is exhaustive even though entries are narrowed. - assert {entry.source_name for entry in insight.provider_coverage} == { - "claude-code", - "codex", - "chatgpt", + assert {entry.source_name for entry in insight.origin_coverage} == { + "claude-code-session", + "codex-session", + "chatgpt-export", } - assert insight.providers_with_data == 2 - assert insight.providers_without_data == 1 + assert insight.origins_with_data == 2 + assert insight.origins_without_data == 1 assert insight.has_coverage_gaps is True - chatgpt = next(entry for entry in insight.provider_coverage if entry.source_name == "chatgpt") + chatgpt = next(entry for entry in insight.origin_coverage if entry.source_name == "chatgpt-export") assert chatgpt.data_available is False assert chatgpt.action_count == 0 def test_mcp_server_extracted_for_each_entry(self) -> None: rows = [ - _row(provider="claude-code", tool="mcp__github__create_pull_request", calls=3), - _row(provider="claude-code", tool="mcp__polylogue__search", calls=2), - _row(provider="claude-code", tool="Read", calls=1), + _row(origin="claude-code-session", tool="mcp__github__create_pull_request", calls=3), + _row(origin="claude-code-session", tool="mcp__polylogue__search", calls=2), + _row(origin="claude-code-session", tool="Read", calls=1), ] - coverage = [_coverage(provider="claude-code", sessions=1, events=6, tools=3, kinds=1)] + coverage = [_coverage(origin="claude-code-session", sessions=1, events=6, tools=3, kinds=1)] insight = build_tool_usage_insight( rows=rows, coverage_rows=coverage, @@ -184,10 +184,10 @@ def test_mcp_server_extracted_for_each_entry(self) -> None: def test_filter_by_mcp_server_keeps_only_matching(self) -> None: rows = [ - _row(provider="claude-code", tool="mcp__github__create_pull_request", calls=3), - _row(provider="claude-code", tool="mcp__polylogue__search", calls=2), + _row(origin="claude-code-session", tool="mcp__github__create_pull_request", calls=3), + _row(origin="claude-code-session", tool="mcp__polylogue__search", calls=2), ] - coverage = [_coverage(provider="claude-code", sessions=1, events=5)] + coverage = [_coverage(origin="claude-code-session", sessions=1, events=5)] insight = build_tool_usage_insight( rows=rows, coverage_rows=coverage, @@ -197,28 +197,28 @@ def test_filter_by_mcp_server_keeps_only_matching(self) -> None: assert [entry.normalized_tool_name for entry in insight.entries] == ["mcp__github__create_pull_request"] assert insight.entries[0].mcp_server == "github" - def test_provider_without_sessions_not_counted_as_gap(self) -> None: - # A provider that has neither sessions nor actions still + def test_origin_without_sessions_not_counted_as_gap(self) -> None: + # An origin that has neither sessions nor actions still # appears in coverage with session_count=0, but it should not - # contribute to ``providers_without_data`` because there is nothing + # contribute to ``origins_without_data`` because there is nothing # to be missing. coverage = [ - _coverage(provider="claude-code", sessions=2, events=4), - _coverage(provider="legacy-empty", sessions=0, events=0), + _coverage(origin="claude-code-session", sessions=2, events=4), + _coverage(origin="unknown-export", sessions=0, events=0), ] insight = build_tool_usage_insight( - rows=[_row(provider="claude-code", tool="Read", calls=4)], + rows=[_row(origin="claude-code-session", tool="Read", calls=4)], coverage_rows=coverage, query=ToolUsageInsightQuery(), materialized_at="2026-05-17T00:00:00+00:00", ) - assert insight.providers_with_data == 1 - assert insight.providers_without_data == 0 + assert insight.origins_with_data == 1 + assert insight.origins_without_data == 0 assert insight.has_coverage_gaps is False def test_pagination_respects_offset_and_limit(self) -> None: - rows = [_row(provider="claude-code", tool=f"tool_{i:02d}", calls=100 - i) for i in range(10)] - coverage = [_coverage(provider="claude-code", sessions=1, events=10)] + rows = [_row(origin="claude-code-session", tool=f"tool_{i:02d}", calls=100 - i) for i in range(10)] + coverage = [_coverage(origin="claude-code-session", sessions=1, events=10)] insight = build_tool_usage_insight( rows=rows, coverage_rows=coverage, @@ -242,13 +242,13 @@ class TestListToolUsageInsightsEndToEnd: The archive ``actions`` view derives tool calls from ``tool_use`` content blocks (paired with ``tool_result`` blocks by ``tool_id``), so the - per-(provider, tool) rollups are seeded by writing those blocks through the + per-(origin, tool) rollups are seeded by writing those blocks through the archive ``SessionBuilder``. The native ``_tool_usage_rows`` query lower-cases ``tool_name``, so the normalized tool names asserted below are lower-case (``read`` / ``bash`` / ``apply_patch``). """ - async def test_aggregates_per_provider_and_tool(self, tmp_path: Path) -> None: + async def test_aggregates_per_origin_and_tool(self, tmp_path: Path) -> None: archive = _archive(tmp_path) db_path = archive.archive_root / "index.db" # claude-code: two `read` tool_use blocks with distinct tool_ids and @@ -292,7 +292,7 @@ async def test_aggregates_per_provider_and_tool(self, tmp_path: Path) -> None: read = next( entry for entry in insight.entries - if entry.source_name == "claude-code" and entry.normalized_tool_name == "read" + if entry.source_name == "claude-code-session" and entry.normalized_tool_name == "read" ) assert read.call_count == 2 assert read.session_count == 1 @@ -301,19 +301,19 @@ async def test_aggregates_per_provider_and_tool(self, tmp_path: Path) -> None: bash = next( entry for entry in insight.entries - if entry.source_name == "claude-code" and entry.normalized_tool_name == "bash" + if entry.source_name == "claude-code-session" and entry.normalized_tool_name == "bash" ) assert bash.output_text_calls == 1 assert bash.affected_path_calls == 0 - # Coverage covers both providers - providers = {entry.source_name for entry in insight.provider_coverage} - assert providers == {"claude-code", "codex"} + # Coverage covers both origins. + origins = {entry.source_name for entry in insight.origin_coverage} + assert origins == {"claude-code-session", "codex-session"} - async def test_coverage_reports_provider_without_actions(self, tmp_path: Path) -> None: + async def test_coverage_reports_origin_without_actions(self, tmp_path: Path) -> None: archive = _archive(tmp_path) db_path = archive.archive_root / "index.db" # ChatGPT session exists but carries no tool_use blocks — the - # substrate genuinely has no tool data for this provider. + # substrate genuinely has no tool data for this origin. ( SessionBuilder(db_path, "gpt-1") .provider("chatgpt") @@ -336,15 +336,15 @@ async def test_coverage_reports_provider_without_actions(self, tmp_path: Path) - result = await archive.list_tool_usage_insights(ToolUsageInsightQuery()) insight = result[0] - chatgpt = next(entry for entry in insight.provider_coverage if entry.source_name == "chatgpt") + chatgpt = next(entry for entry in insight.origin_coverage if entry.source_name == "chatgpt-export") assert chatgpt.session_count == 1 assert chatgpt.action_count == 0 assert chatgpt.data_available is False - cc = next(entry for entry in insight.provider_coverage if entry.source_name == "claude-code") + cc = next(entry for entry in insight.origin_coverage if entry.source_name == "claude-code-session") assert cc.data_available is True assert insight.has_coverage_gaps is True - assert insight.providers_without_data == 1 - assert insight.providers_with_data == 1 + assert insight.origins_without_data == 1 + assert insight.origins_with_data == 1 async def test_query_filters_entries_without_narrowing_coverage(self, tmp_path: Path) -> None: archive = _archive(tmp_path) @@ -386,17 +386,17 @@ async def test_query_filters_entries_without_narrowing_coverage(self, tmp_path: ) [insight] = await archive.list_tool_usage_insights( - ToolUsageInsightQuery(provider="claude-code-session", mcp_server="serena", limit=1) + ToolUsageInsightQuery(origin="claude-code-session", mcp_server="serena", limit=1) ) assert [entry.normalized_tool_name for entry in insight.entries] == ["mcp__serena__find_symbol"] assert insight.entries[0].call_count == 2 - assert insight.entries[0].source_name == "claude-code" + assert insight.entries[0].source_name == "claude-code-session" assert insight.entries[0].mcp_server == "serena" - assert {entry.source_name for entry in insight.provider_coverage} == { - "claude-code", - "codex", - "chatgpt", + assert {entry.source_name for entry in insight.origin_coverage} == { + "claude-code-session", + "codex-session", + "chatgpt-export", } assert insight.has_coverage_gaps is True @@ -468,12 +468,12 @@ async def test_observed_event_tool_counts_use_canonical_tool_results(self, tmp_p with ArchiveStore.open_existing(archive.archive_root) as store: rows = store.list_tool_observed_event_count_rows( - ToolUsageInsightQuery(provider="claude-code-session", mcp_server="serena", limit=5) + ToolUsageInsightQuery(origin="claude-code-session", mcp_server="serena", limit=5) ) assert rows == [ { - "source_name": "claude-code", + "source_name": "claude-code-session", "origin": "claude-code-session", "normalized_tool_name": "mcp__serena__find_symbol", "action_kind": "mcp", @@ -481,7 +481,7 @@ async def test_observed_event_tool_counts_use_canonical_tool_results(self, tmp_p "event_count": 1, }, { - "source_name": "claude-code", + "source_name": "claude-code-session", "origin": "claude-code-session", "normalized_tool_name": "mcp__serena__find_symbol", "action_kind": "mcp", @@ -559,13 +559,13 @@ async def test_action_evidence_counts_normalize_detail_matches(self, tmp_path: P with ArchiveStore.open_existing(archive.archive_root) as store: rows = store.list_tool_action_evidence_count_rows( - ToolUsageInsightQuery(provider="codex-session", limit=5), + ToolUsageInsightQuery(origin="codex-session", limit=5), detail_patterns=("codebase-memory",), ) assert rows == [ { - "source_name": "codex", + "source_name": "codex-session", "origin": "codex-session", "normalized_tool_name": "codebase-memory/command-detail", "action_kind": "tool_use", @@ -623,14 +623,14 @@ async def test_action_evidence_since_filter_uses_session_sort_key(self, tmp_path with ArchiveStore.open_existing(archive.archive_root) as store: rows = store.list_tool_action_evidence_count_rows( - ToolUsageInsightQuery(provider="codex-session", limit=5), + ToolUsageInsightQuery(origin="codex-session", limit=5), detail_patterns=("codebase-memory",), since_ms=cutoff_ms, ) assert rows == [ { - "source_name": "codex", + "source_name": "codex-session", "origin": "codex-session", "normalized_tool_name": "codebase-memory/command-detail", "action_kind": "tool_use", @@ -686,7 +686,7 @@ async def test_call_and_observed_event_counts_since_filter_uses_session_sort_key ) query = ToolUsageInsightQuery( - provider="claude-code-session", + origin="claude-code-session", mcp_server="serena", since_ms=cutoff_ms, limit=5, @@ -697,7 +697,7 @@ async def test_call_and_observed_event_counts_since_filter_uses_session_sort_key assert call_rows == [ { - "source_name": "claude-code", + "source_name": "claude-code-session", "origin": "claude-code-session", "normalized_tool_name": "mcp__serena__find_symbol", "action_kind": "tool_use", @@ -706,7 +706,7 @@ async def test_call_and_observed_event_counts_since_filter_uses_session_sort_key ] assert event_rows == [ { - "source_name": "claude-code", + "source_name": "claude-code-session", "origin": "claude-code-session", "normalized_tool_name": "mcp__serena__find_symbol", "action_kind": "mcp", @@ -720,7 +720,7 @@ async def test_empty_archive_returns_envelope_with_no_gaps(self, tmp_path: Path) assert len(result) == 1 insight = result[0] assert insight.entries == () - assert insight.provider_coverage == () + assert insight.origin_coverage == () assert insight.has_coverage_gaps is False assert insight.total_call_count == 0 @@ -729,10 +729,10 @@ def test_envelope_serializes_to_jsonable_dict() -> None: """MCP envelope shape: model dumps to a stable JSON-serializable dict.""" insight = build_tool_usage_insight( - rows=[_row(provider="claude-code", tool="Read", calls=3)], + rows=[_row(origin="claude-code-session", tool="Read", calls=3)], coverage_rows=[ - _coverage(provider="claude-code", sessions=1, events=3, tools=1, kinds=1), - _coverage(provider="chatgpt", sessions=2, events=0), + _coverage(origin="claude-code-session", sessions=1, events=3, tools=1, kinds=1), + _coverage(origin="chatgpt-export", sessions=2, events=0), ], query=ToolUsageInsightQuery(), materialized_at="2026-05-17T00:00:00+00:00", @@ -741,9 +741,9 @@ def test_envelope_serializes_to_jsonable_dict() -> None: assert payload["insight_kind"] == "tool_usage" assert payload["materializer_version"] == TOOL_USAGE_INSIGHT_VERSION assert payload["has_coverage_gaps"] is True - assert {entry["source_name"] for entry in payload["provider_coverage"]} == { - "claude-code", - "chatgpt", + assert {entry["source_name"] for entry in payload["origin_coverage"]} == { + "claude-code-session", + "chatgpt-export", } entry = payload["entries"][0] assert entry["normalized_tool_name"] == "Read" diff --git a/tests/unit/mcp/test_tool_contracts.py b/tests/unit/mcp/test_tool_contracts.py index 97dd2fc1cc..1eae36076f 100644 --- a/tests/unit/mcp/test_tool_contracts.py +++ b/tests/unit/mcp/test_tool_contracts.py @@ -1600,9 +1600,9 @@ async def test_insight_list_tools_use_archive_queries(self, mcp_server: MCPServe provenance=_provenance(), ) coverage = ArchiveCoverageInsight( - group_by="provider", - bucket="claude-code", - source_name="claude-code", + group_by="origin", + bucket="claude-code-session", + source_name="claude-code-session", session_count=1, message_count=2, user_message_count=1, @@ -1746,11 +1746,11 @@ async def test_insight_list_tools_use_archive_queries(self, mcp_server: MCPServe assert cost_rollups_payload["limit"] == 5 assert tool_usage_payload["truncated"] is False assert tool_usage_payload["limit"] is None - assert mock_poly.list_session_profile_insights.await_args.args[0].provider == "claude-code" - assert mock_poly.list_session_tag_rollup_insights.await_args.args[0].provider == "claude-code" - assert mock_poly.list_archive_coverage_insights.await_args.args[0].group_by == "provider" - assert mock_poly.list_archive_coverage_insights.await_args.args[0].provider == "claude-code" - assert mock_poly.list_tool_usage_insights.await_args.args[0].provider == "claude-code" + assert mock_poly.list_session_profile_insights.await_args.args[0].origin == "claude-code-session" + assert mock_poly.list_session_tag_rollup_insights.await_args.args[0].origin == "claude-code-session" + assert mock_poly.list_archive_coverage_insights.await_args.args[0].group_by == "origin" + assert mock_poly.list_archive_coverage_insights.await_args.args[0].origin == "claude-code-session" + assert mock_poly.list_tool_usage_insights.await_args.args[0].origin == "claude-code-session" assert mock_poly.list_tool_usage_insights.await_args.args[0].limit is None @pytest.mark.asyncio @@ -1796,7 +1796,7 @@ def cost(session_id: str) -> SessionCostInsight: assert payload["truncated"] is True assert [item["session_id"] for item in payload["items"]] == ["conv-0", "conv-1"] query = mock_poly.list_session_cost_insights.await_args.args[0] - assert query.provider == "claude-code" + assert query.origin == "claude-code-session" assert query.limit is None assert query.offset == 0 diff --git a/tests/unit/storage/test_archive_search_contracts.py b/tests/unit/storage/test_archive_search_contracts.py index 9ff954ba0e..3f3de9cfa7 100644 --- a/tests/unit/storage/test_archive_search_contracts.py +++ b/tests/unit/storage/test_archive_search_contracts.py @@ -252,7 +252,7 @@ async def test_repository_search_summary_hits_keep_evidence_and_session_order() ) repo = _FakeRepo(queries) - summary_hits = await repo.search_summary_hits("storage", limit=5, providers=["chatgpt"], since="2025-01-01") + summary_hits = await repo.search_summary_hits("storage", limit=5, origins=["chatgpt"], since="2025-01-01") assert queries.last_batch_ids == ["conv-b", "conv-a"] assert [hit.session_id for hit in summary_hits] == ["conv-b", "conv-a"] @@ -285,7 +285,7 @@ async def test_repository_search_summary_hits_prioritize_attachment_identity_evi ) repo = _FakeRepo(queries) - summary_hits = await repo.search_summary_hits("drive-file-1", limit=5, providers=["gemini"]) + summary_hits = await repo.search_summary_hits("drive-file-1", limit=5, origins=["gemini"]) assert queries.last_batch_ids == ["conv-a", "conv-b"] assert [hit.session_id for hit in summary_hits] == ["conv-a", "conv-b"] @@ -340,7 +340,7 @@ async def test_gemini_drive_attachment_id_is_searchable_after_parse_and_prepare( backend=backend, ) - hits = await repo.search_summary_hits(query, limit=5, providers=["gemini"]) + hits = await repo.search_summary_hits(query, limit=5, origins=["gemini"]) finally: await repo.close() diff --git a/tests/unit/storage/test_archive_tiers_archive.py b/tests/unit/storage/test_archive_tiers_archive.py index 8708936664..d9443d4a2a 100644 --- a/tests/unit/storage/test_archive_tiers_archive.py +++ b/tests/unit/storage/test_archive_tiers_archive.py @@ -1282,21 +1282,21 @@ def test_archive_coverage_averages_render_none_not_zero_over_empty_denominator(t facade.write_parsed(all_assistant_session) with ArchiveStore.open_existing(root) as facade: - provider_rows = facade.list_archive_coverage_insights(group_by="provider") + origin_rows = facade.list_archive_coverage_insights(group_by="origin") day_rows = facade.list_archive_coverage_insights(group_by="day") - assert len(provider_rows) == 1 - provider_row = provider_rows[0] + assert len(origin_rows) == 1 + origin_row = origin_rows[0] # session_count > 0 (a real group), so this average IS computable. - assert provider_row.session_count == 1 - assert provider_row.avg_messages_per_session == 1.0 + assert origin_row.session_count == 1 + assert origin_row.avg_messages_per_session == 1.0 # user_message_count == 0 for an all-assistant session -- the average # over zero user messages must be None, never a fabricated 0.0. - assert provider_row.avg_user_words is None - assert provider_row.avg_authored_user_words is None + assert origin_row.avg_user_words is None + assert origin_row.avg_authored_user_words is None # assistant_message_count == 1 (nonzero denominator), so this field is a # real computed value -- possibly 0.0 -- never None. - assert provider_row.avg_assistant_words is not None + assert origin_row.avg_assistant_words is not None assert len(day_rows) == 1 day_row = day_rows[0] diff --git a/tests/unit/storage/test_parse_tracking.py b/tests/unit/storage/test_parse_tracking.py index 76fdd17bef..35a5251de4 100644 --- a/tests/unit/storage/test_parse_tracking.py +++ b/tests/unit/storage/test_parse_tracking.py @@ -360,7 +360,7 @@ async def test_reset_all(self, backend: SQLiteBackend) -> None: async def test_reset_by_provider(self, backend: SQLiteBackend) -> None: """Reset specific provider only clears that provider's records.""" await self._populate(backend) - count = await backend.reset_parse_status(provider="chatgpt") + count = await backend.reset_parse_status(origin="chatgpt") assert count == 1 # Only raw-0 was chatgpt and parsed # chatgpt record is reset @@ -461,7 +461,7 @@ async def test_reset_all(self, backend: SQLiteBackend) -> None: async def test_reset_by_provider(self, backend: SQLiteBackend) -> None: await self._populate(backend) - count = await backend.reset_validation_status(provider="chatgpt") + count = await backend.reset_validation_status(origin="chatgpt") assert count == 1 rec0 = await backend.get_raw_session("raw-0") diff --git a/tests/unit/storage/test_query_security.py b/tests/unit/storage/test_query_security.py index 4470af2dbb..7f8a2fe110 100644 --- a/tests/unit/storage/test_query_security.py +++ b/tests/unit/storage/test_query_security.py @@ -45,7 +45,7 @@ async def test_message_id_sql_injection(temp_repo: SessionRepository) -> None: async def test_source_name_sql_injection(temp_repo: SessionRepository) -> None: - assert await temp_repo.list(provider="doesnotexist") == [] + assert await temp_repo.list(origin="doesnotexist") == [] async def test_session_title_sql_injection(temp_repo: SessionRepository) -> None: @@ -98,7 +98,7 @@ def test_escape_fts5_security_contract(raw_query: str, expected: str, should_com async def test_empty_string_parameters_handled(temp_repo: SessionRepository) -> None: assert await temp_repo.view("") is None - assert isinstance(await temp_repo.list(provider=""), list) + assert isinstance(await temp_repo.list(origin=""), list) async def test_none_parameters_handled(temp_repo: SessionRepository) -> None: @@ -112,7 +112,7 @@ async def test_none_parameters_handled(temp_repo: SessionRepository) -> None: async def test_very_long_string_parameters(temp_repo: SessionRepository) -> None: assert await temp_repo.view("a" * 10000) is None - assert isinstance(await temp_repo.list(provider="x" * 1000), list) + assert isinstance(await temp_repo.list(origin="x" * 1000), list) async def test_unicode_in_parameters(temp_repo: SessionRepository) -> None: @@ -144,5 +144,5 @@ def test_control_chars_in_queries_handled(text_with_control: str) -> None: @settings(max_examples=50, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture]) async def test_repository_survives_injection_property(temp_repo: SessionRepository, injection_payload: str) -> None: assert await temp_repo.view(injection_payload) is None - result = await temp_repo.list(provider=injection_payload[:50]) + result = await temp_repo.list(origin=injection_payload[:50]) assert isinstance(result, list) diff --git a/tests/unit/storage/test_raw.py b/tests/unit/storage/test_raw.py index c98369ef8a..27ea15720a 100644 --- a/tests/unit/storage/test_raw.py +++ b/tests/unit/storage/test_raw.py @@ -301,10 +301,10 @@ async def test_iter_raw_sessions_by_provider(self, backend: SQLiteBackend) -> No for r in records: await backend.save_raw_session(r) - chatgpt_records = [r async for r in backend.iter_raw_sessions(provider="chatgpt")] + chatgpt_records = [r async for r in backend.iter_raw_sessions(origin="chatgpt")] assert len(chatgpt_records) == 3 - claude_records = [r async for r in backend.iter_raw_sessions(provider="claude-ai")] + claude_records = [r async for r in backend.iter_raw_sessions(origin="claude-ai")] assert len(claude_records) == 3 async def test_iter_raw_ids_by_source_name(self, backend: SQLiteBackend) -> None: @@ -397,9 +397,9 @@ async def test_raw_provider_filters_prefer_payload_provider_when_present(self, b ) ) - matched_records = [record async for record in backend.iter_raw_sessions(provider="chatgpt")] + matched_records = [record async for record in backend.iter_raw_sessions(origin="chatgpt")] matched_ids = [raw_id async for raw_id in backend.iter_raw_ids(source_name="chatgpt")] - matched_count = await backend.get_raw_session_count(provider="chatgpt") + matched_count = await backend.get_raw_session_count(origin="chatgpt") assert [record.raw_id for record in matched_records] == ["raw-generic"] assert matched_ids == ["raw-generic"] @@ -503,9 +503,9 @@ async def test_get_raw_session_count(self, backend: SQLiteBackend) -> None: assert await backend.get_raw_session_count() == 5 # Filtered count - assert await backend.get_raw_session_count(provider="chatgpt") == 3 - assert await backend.get_raw_session_count(provider="claude-ai") == 2 - assert await backend.get_raw_session_count(provider="codex") == 0 + assert await backend.get_raw_session_count(origin="chatgpt") == 3 + assert await backend.get_raw_session_count(origin="claude-ai") == 2 + assert await backend.get_raw_session_count(origin="codex") == 0 async def test_iter_raw_sessions_without_limit_returns_all(self, backend: SQLiteBackend) -> None: """Iterating without limit returns all records.""" diff --git a/tests/unit/storage/test_repository_insight_runtime.py b/tests/unit/storage/test_repository_insight_runtime.py index 4566e1bf48..11db1e7c11 100644 --- a/tests/unit/storage/test_repository_insight_runtime.py +++ b/tests/unit/storage/test_repository_insight_runtime.py @@ -45,7 +45,7 @@ def __init__(self, queries: object) -> None: assert await repo.get_session_profile("conv-1") == "profile:record" assert await repo.get_session_profiles_batch(["conv-1"]) == {"conv-1": "profile:record-a"} assert await repo.list_session_profiles( - provider="claude-code", + origin="claude-code-session", since="2026-01-01", until="2026-01-02", first_message_since="2026-01-01T00:00:00Z", @@ -67,7 +67,7 @@ def __init__(self, queries: object) -> None: assert await repo.list_session_enrichment_records(query="enrichment") == ["record-a", "record-b"] list_query = queries._list_session_profiles_query.await_args_list[0].args[0] - assert list_query.provider == "claude-code" + assert list_query.origin == "claude-code-session" assert list_query.first_message_since == "2026-01-01T00:00:00Z" assert list_query.session_date_until == "2026-01-02" assert list_query.min_wallclock_seconds == 300 @@ -129,7 +129,7 @@ def __init__(self, queries: object) -> None: assert await repo.get_session_phases("conv-1") == ["phase:phase-record"] assert await repo.list_session_work_events( session_id="conv-1", - provider="claude-code", + origin="claude-code-session", since="2026-01-01", until="2026-01-02", session_date_since="2026-01-01", @@ -142,7 +142,7 @@ def __init__(self, queries: object) -> None: assert await repo.list_session_work_event_records(query="editor") == ["event-record"] assert await repo.list_session_phases( session_id="conv-1", - provider="claude-code", + origin="claude-code-session", since="2026-01-01", until="2026-01-02", kind="planning", @@ -158,7 +158,7 @@ def __init__(self, queries: object) -> None: timeline_query = queries._list_session_work_events_query.await_args_list[0].args[0] assert timeline_query.session_id == "conv-1" - assert timeline_query.provider == "claude-code" + assert timeline_query.origin == "claude-code-session" assert timeline_query.heuristic_label == "implementation" assert timeline_query.session_date_since == "2026-01-01" assert timeline_query.session_date_until == "2026-01-02" @@ -178,14 +178,14 @@ def __init__(self, queries: object) -> None: repo = _Repo(queries) assert await repo.list_session_tag_rollup_records( - provider="claude-code", + origin="claude-code-session", since="2026-01-01", until="2026-01-02", query="tag", ) == ["tag-row"] tag_query = queries._list_session_tag_rollup_rows_query.await_args.args[0] - assert tag_query.provider == "claude-code" + assert tag_query.origin == "claude-code-session" assert tag_query.query == "tag" @@ -297,8 +297,8 @@ def __init__(self, backend: object) -> None: assert await repo.get_known_source_cursors() == { "inbox": {"st_dev": 1, "st_ino": 2, "st_size": 3, "mtime_ns": 4} } - assert await repo.reset_parse_status(provider="chatgpt", source_names=["inbox"]) == 3 - assert await repo.reset_validation_status(provider="chatgpt", source_names=["inbox"]) == 4 + assert await repo.reset_parse_status(origin="chatgpt", source_names=["inbox"]) == 3 + assert await repo.reset_validation_status(origin="chatgpt", source_names=["inbox"]) == 4 assert await repo.get_raw_sessions_batch(["raw-1"]) == ["a"] assert await repo.get_raw_blob_sizes(["raw-1"]) == [("a", 12)] assert await repo.get_raw_session_states(["raw-1"]) == {"a": "state"} @@ -361,7 +361,7 @@ def __init__(self, backend: object) -> None: return_value=_aiter([("raw-a", 1), ("raw-b", 2)]), ) as mock_iter_headers, ): - sessions = [record async for record in repo.iter_raw_sessions(provider="chatgpt", limit=2)] + sessions = [record async for record in repo.iter_raw_sessions(origin="chatgpt", limit=2)] headers = [ header async for header in repo.iter_raw_headers( diff --git a/tests/unit/storage/test_schema_safety.py b/tests/unit/storage/test_schema_safety.py index 1c3b905084..d589bac2d5 100644 --- a/tests/unit/storage/test_schema_safety.py +++ b/tests/unit/storage/test_schema_safety.py @@ -377,11 +377,11 @@ def test_build_filters_with_special_characters(self) -> None: # SQL clause must use ? placeholder, not string interpolation assert "?" in where_clause - def test_build_filters_with_empty_provider(self) -> None: - """Empty provider should still produce valid SQL.""" + def test_build_filters_with_empty_origin(self) -> None: + """Empty origin should resolve to the safe unknown-origin filter.""" from polylogue.storage.sqlite.queries.filter_builder import _build_session_filters - where_clause, params = _build_session_filters(provider="") + where_clause, params = _build_session_filters(origin="") assert isinstance(where_clause, str) assert isinstance(params, list) diff --git a/tests/unit/storage/test_store_ops.py b/tests/unit/storage/test_store_ops.py index 36f881411e..6dc5c38f04 100644 --- a/tests/unit/storage/test_store_ops.py +++ b/tests/unit/storage/test_store_ops.py @@ -60,7 +60,7 @@ class RecordQueryKwargs(TypedDict, total=False): - provider: str + origin: str referenced_path: tuple[str, ...] action_terms: tuple[str, ...] excluded_action_terms: tuple[str, ...] @@ -490,7 +490,7 @@ async def test_list_summaries_by_query_uses_current_session_columns(tmp_path: Pa backend = SQLiteBackend(db_path=db_path) repo = SessionRepository(backend=backend) try: - summaries = await repo.list_summaries_by_query(_record_query(provider="codex", limit=1)) + summaries = await repo.list_summaries_by_query(_record_query(origin="codex", limit=1)) assert len(summaries) == 1 summary = summaries[0] assert str(summary.id) == "codex-session:conv-large-meta"