claude-smart 0.2.46 → 0.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,270 @@
1
+ """Interaction + profile search methods for SQLite storage.
2
+
3
+ Extracted verbatim from ``_profiles.py`` (the Search bucket). Profile CRUD lives
4
+ in ``profiles._profile_store``; interaction CRUD in ``profiles._interaction_store``.
5
+ The shared ``_build_tags_sql`` helper stays module-level in ``_profiles`` (it is
6
+ used by both this bucket and ProfileStore) and is imported here.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from reflexio.models.api_schema.retriever_schema import (
13
+ SearchInteractionRequest,
14
+ SearchUserProfileRequest,
15
+ )
16
+ from reflexio.models.api_schema.service_schemas import (
17
+ Interaction,
18
+ Status,
19
+ UserProfile,
20
+ )
21
+ from reflexio.models.config_schema import SearchMode
22
+
23
+ from .._base import (
24
+ SQLiteStorageBase,
25
+ _build_status_sql,
26
+ _effective_search_mode,
27
+ _epoch_now,
28
+ _row_to_interaction,
29
+ _row_to_profile,
30
+ _sanitize_fts_query,
31
+ _true_rrf_merge,
32
+ _vector_rank_rows,
33
+ )
34
+ from .._profiles import _build_tags_sql
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class ProfileSearchMixin:
40
+ """Mixin providing profile + interaction search."""
41
+
42
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
43
+ _fetchall: Any
44
+
45
+ # ------------------------------------------------------------------
46
+ # Search — Interactions & Profiles
47
+ # ------------------------------------------------------------------
48
+
49
+ @SQLiteStorageBase.handle_exceptions
50
+ def search_interaction(
51
+ self,
52
+ search_interaction_request: SearchInteractionRequest,
53
+ query_embedding: list[float] | None = None,
54
+ ) -> list[Interaction]:
55
+ req = search_interaction_request
56
+ has_query = bool(req.query)
57
+ match_count = req.most_recent_k or 10
58
+ mode = _effective_search_mode(req.search_mode, query_embedding, req.query)
59
+
60
+ conditions: list[str] = ["i.user_id = ?"]
61
+ params: list[str | int | float] = [req.user_id]
62
+
63
+ if req.request_id:
64
+ conditions.append("i.request_id = ?")
65
+ params.append(req.request_id)
66
+ if req.start_time:
67
+ conditions.append("i.created_at >= ?")
68
+ params.append(req.start_time.timestamp())
69
+ if req.end_time:
70
+ conditions.append("i.created_at <= ?")
71
+ params.append(req.end_time.timestamp())
72
+
73
+ where_clause = " AND ".join(conditions)
74
+ overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
75
+
76
+ # Vector-only: rank by embedding similarity
77
+ if (
78
+ mode in (SearchMode.VECTOR, SearchMode.HYBRID)
79
+ and query_embedding
80
+ and not has_query
81
+ ):
82
+ vector_limit = match_count * 10
83
+ sql = f"""SELECT i.* FROM interactions i
84
+ WHERE {where_clause}
85
+ ORDER BY i.created_at DESC
86
+ LIMIT ?"""
87
+ rows = self._fetchall(sql, (*params, vector_limit))
88
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
89
+ elif has_query:
90
+ # FTS search (with optional HYBRID re-ranking)
91
+ fts_query = _sanitize_fts_query(req.query) # type: ignore[arg-type]
92
+ fts_conditions = ["interactions_fts MATCH ?", *conditions]
93
+ fts_where = " AND ".join(fts_conditions)
94
+ fts_params: list[str | int | float] = [fts_query, *params, overfetch]
95
+ sql = f"""SELECT i.* FROM interactions i
96
+ JOIN interactions_fts f ON i.interaction_id = f.rowid
97
+ WHERE {fts_where}
98
+ ORDER BY bm25(interactions_fts, 1.0, 2.0)
99
+ LIMIT ?"""
100
+ fts_rows = self._fetchall(sql, tuple(fts_params))
101
+
102
+ if mode == SearchMode.HYBRID and query_embedding:
103
+ vec_limit = match_count * 10
104
+ vec_sql = f"""SELECT i.* FROM interactions i
105
+ WHERE {where_clause}
106
+ ORDER BY i.created_at DESC
107
+ LIMIT ?"""
108
+ vec_candidates = self._fetchall(vec_sql, (*params, vec_limit))
109
+ vec_rows = _vector_rank_rows(vec_candidates, query_embedding, overfetch)
110
+ rows = _true_rrf_merge(
111
+ fts_rows,
112
+ vec_rows,
113
+ "interaction_id",
114
+ match_count,
115
+ )
116
+ else:
117
+ rows = fts_rows[:match_count]
118
+ else:
119
+ if req.most_recent_k:
120
+ # No query — just fetch most recent interactions by time
121
+ sql = f"""SELECT i.* FROM interactions i
122
+ WHERE {where_clause}
123
+ ORDER BY i.created_at DESC
124
+ LIMIT ?"""
125
+ rows = self._fetchall(sql, (*params, req.most_recent_k))
126
+ return [_row_to_interaction(r) for r in reversed(rows)]
127
+ return []
128
+
129
+ interactions = [_row_to_interaction(r) for r in rows]
130
+ if req.most_recent_k:
131
+ sorted_ints = sorted(interactions, key=lambda x: x.created_at, reverse=True)
132
+ return list(reversed(sorted_ints[: req.most_recent_k]))
133
+ return interactions
134
+
135
+ @SQLiteStorageBase.handle_exceptions
136
+ def search_user_profile( # noqa: C901
137
+ self,
138
+ search_user_profile_request: SearchUserProfileRequest,
139
+ status_filter: list[Status | None] | None = None,
140
+ query_embedding: list[float] | None = None,
141
+ ) -> list[UserProfile]:
142
+ if status_filter is None:
143
+ status_filter = [None]
144
+
145
+ req = search_user_profile_request
146
+ match_count = req.top_k or 10
147
+ current_ts = _epoch_now()
148
+ has_query = bool(req.query)
149
+ mode = _effective_search_mode(req.search_mode, query_embedding, req.query)
150
+ has_embedding = query_embedding is not None
151
+ logger.info(
152
+ "Profile search: requested_mode=%s, effective_mode=%s, has_query=%s, has_embedding=%s, user_id=%s",
153
+ req.search_mode,
154
+ mode,
155
+ has_query,
156
+ has_embedding,
157
+ req.user_id,
158
+ )
159
+
160
+ conditions: list[str] = ["p.expiration_timestamp >= ?"]
161
+ params: list[object] = [current_ts]
162
+
163
+ if req.user_id:
164
+ conditions.append("p.user_id = ?")
165
+ params.append(req.user_id)
166
+ if req.start_time:
167
+ conditions.append("p.last_modified_timestamp >= ?")
168
+ params.append(int(req.start_time.timestamp()))
169
+ if req.end_time:
170
+ conditions.append("p.last_modified_timestamp <= ?")
171
+ params.append(int(req.end_time.timestamp()))
172
+ if req.source:
173
+ conditions.append("LOWER(p.source) = LOWER(?)")
174
+ params.append(req.source)
175
+ if status_filter is not None:
176
+ frag, sparams = _build_status_sql(status_filter)
177
+ conditions.append(frag)
178
+ params.extend(sparams)
179
+ tag_frag, tag_params = _build_tags_sql("p", req.tags)
180
+ if tag_frag:
181
+ conditions.append(tag_frag)
182
+ params.extend(tag_params)
183
+
184
+ where_clause = " AND ".join(conditions)
185
+ overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
186
+
187
+ # Pure vector search: fetch all candidates, rank by cosine similarity
188
+ if mode == SearchMode.VECTOR and query_embedding:
189
+ if req.generated_from_request_id:
190
+ conditions.append("p.generated_from_request_id = ?")
191
+ params.append(req.generated_from_request_id)
192
+ where_clause = " AND ".join(conditions)
193
+ sql = f"""SELECT p.* FROM profiles p
194
+ WHERE {where_clause}
195
+ ORDER BY p.last_modified_timestamp DESC"""
196
+ rows = self._fetchall(sql, tuple(params))
197
+ logger.info(
198
+ "VECTOR search: %d candidates fetched, ranking by embedding", len(rows)
199
+ )
200
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
201
+ elif has_query:
202
+ fts_query = _sanitize_fts_query(req.query) # type: ignore[arg-type]
203
+ sql = f"""SELECT p.* FROM profiles p
204
+ JOIN profiles_fts f ON p.profile_id = f.profile_id
205
+ WHERE profiles_fts MATCH ?
206
+ AND {where_clause}
207
+ ORDER BY bm25(profiles_fts, 0.0, 1.0)
208
+ LIMIT ?"""
209
+ params_list: list[object] = [fts_query, *params, overfetch]
210
+ fts_rows = self._fetchall(sql, tuple(params_list))
211
+ logger.info("FTS search: %d results from BM25", len(fts_rows))
212
+
213
+ if mode == SearchMode.HYBRID and query_embedding:
214
+ logger.info("HYBRID merging FTS + vector results via RRF")
215
+ vec_limit = match_count * 10
216
+ vec_sql = f"""SELECT p.* FROM profiles p
217
+ WHERE {where_clause}
218
+ ORDER BY p.last_modified_timestamp DESC
219
+ LIMIT ?"""
220
+ vec_candidates = self._fetchall(vec_sql, (*params, vec_limit))
221
+ vec_rows = _vector_rank_rows(vec_candidates, query_embedding, overfetch)
222
+ rows = _true_rrf_merge(
223
+ fts_rows,
224
+ vec_rows,
225
+ "profile_id",
226
+ match_count,
227
+ )
228
+ else:
229
+ rows = fts_rows
230
+ elif query_embedding:
231
+ # HYBRID without query text: rank by embedding only
232
+ if req.generated_from_request_id:
233
+ conditions.append("p.generated_from_request_id = ?")
234
+ params.append(req.generated_from_request_id)
235
+ where_clause = " AND ".join(conditions)
236
+ sql = f"""SELECT p.* FROM profiles p
237
+ WHERE {where_clause}
238
+ ORDER BY p.last_modified_timestamp DESC"""
239
+ rows = self._fetchall(sql, tuple(params))
240
+ logger.info(
241
+ "HYBRID (no query text) search: %d candidates, ranking by embedding",
242
+ len(rows),
243
+ )
244
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
245
+ else:
246
+ if req.generated_from_request_id:
247
+ conditions.append("p.generated_from_request_id = ?")
248
+ params.append(req.generated_from_request_id)
249
+ where_clause = " AND ".join(conditions)
250
+ sql = f"""SELECT p.* FROM profiles p
251
+ WHERE {where_clause}
252
+ ORDER BY p.last_modified_timestamp DESC
253
+ LIMIT ?"""
254
+ params_list = [*params, overfetch]
255
+ rows = self._fetchall(sql, tuple(params_list))
256
+
257
+ profiles = [_row_to_profile(r) for r in rows]
258
+ logger.info("Profile search: %d profiles before post-filtering", len(profiles))
259
+
260
+ # Apply filters that can't easily go into SQL
261
+ filtered: list[UserProfile] = []
262
+ for profile in profiles:
263
+ if req.custom_feature and (
264
+ req.custom_feature.lower() not in str(profile.custom_features).lower()
265
+ ):
266
+ continue
267
+ filtered.append(profile)
268
+ if len(filtered) >= match_count:
269
+ break
270
+ return filtered
@@ -25,21 +25,45 @@ from ._base import BaseStorageCore, matches_status_filter
25
25
  from ._extras import ExtrasMixin
26
26
  from ._lineage import EntityType, LineageEventMixin
27
27
  from ._operations import OperationMixin
28
- from ._playbook import PlaybookMixin
29
- from ._profiles import ProfileMixin
30
28
  from ._requests import RequestMixin
31
29
  from ._retrieval_log import RetrievalLogMixin
32
30
  from ._shadow_verdicts import ShadowVerdictsMixin
33
31
  from ._share_links import ShareLinkMixin
34
32
  from ._stall_state import StallStateMixin
33
+ from .governance import (
34
+ AuditEventStoreMixin,
35
+ GovernanceEraseExecutionMixin,
36
+ PurgeOperationStoreMixin,
37
+ RebuildHideMixin,
38
+ SubjectBarrierMixin,
39
+ )
40
+ from .playbook import (
41
+ AgentEvaluationResultStoreMixin,
42
+ AgentPlaybookStoreMixin,
43
+ OptimizationJobStoreMixin,
44
+ PlaybookSourceLinkageMixin,
45
+ UserPlaybookStoreMixin,
46
+ )
47
+ from .profiles import InteractionStoreMixin, ProfileSearchMixin, ProfileStoreMixin
35
48
 
36
49
 
37
50
  class BaseStorage(
38
51
  AgentRunMixin,
39
- ProfileMixin,
52
+ ProfileStoreMixin,
53
+ InteractionStoreMixin,
54
+ ProfileSearchMixin,
40
55
  RequestMixin,
41
- PlaybookMixin,
56
+ AgentPlaybookStoreMixin,
57
+ UserPlaybookStoreMixin,
58
+ PlaybookSourceLinkageMixin,
59
+ OptimizationJobStoreMixin,
60
+ AgentEvaluationResultStoreMixin,
42
61
  RetrievalLogMixin,
62
+ AuditEventStoreMixin,
63
+ PurgeOperationStoreMixin,
64
+ SubjectBarrierMixin,
65
+ GovernanceEraseExecutionMixin,
66
+ RebuildHideMixin,
43
67
  LineageEventMixin,
44
68
  OperationMixin,
45
69
  ExtrasMixin,
@@ -150,8 +174,8 @@ class BaseStorage(
150
174
  interaction_count = len(self.get_user_interaction(user_id))
151
175
 
152
176
  # All statuses a user's row can have — including tombstones (SUPERSEDED,
153
- # MERGED). Erasure MUST reach every row the user owns regardless of
154
- # status; the old filter excluded tombstones, leaving them in the DB
177
+ # MERGED, EXPIRED). Erasure MUST reach every row the user owns regardless
178
+ # of status; the old filter excluded tombstones, leaving them in the DB
155
179
  # after clear_user_data (GDPR regression).
156
180
  _all_statuses: list[Status | None] = [
157
181
  None, # CURRENT
@@ -160,6 +184,7 @@ class BaseStorage(
160
184
  Status.ARCHIVE_IN_PROGRESS,
161
185
  Status.SUPERSEDED,
162
186
  Status.MERGED,
187
+ Status.EXPIRED,
163
188
  ]
164
189
 
165
190
  # Snapshot user_playbook ids for the user and partition into
@@ -178,10 +203,14 @@ class BaseStorage(
178
203
  )
179
204
 
180
205
  # Snapshot profile ids for the user and partition into
181
- # purge vs delete sets.
206
+ # purge vs delete sets. include_expired=True drops the
207
+ # expiration_timestamp >= now guard so EXPIRED tombstones (expiry in
208
+ # the past) are enumerated and reached by the erasure path.
182
209
  raw_profile_ids = [
183
210
  p.profile_id
184
- for p in self.get_user_profile(user_id, status_filter=_all_statuses)
211
+ for p in self.get_user_profile(
212
+ user_id, status_filter=_all_statuses, include_expired=True
213
+ )
185
214
  if p.profile_id is not None
186
215
  ]
187
216
  purge_profile_ids, delete_profile_ids = self._partition_purge_vs_delete(
@@ -244,7 +273,19 @@ __all__ = [
244
273
  "PendingToolCallRecord",
245
274
  "PendingToolCallStatus",
246
275
  "PendingToolCallUpsertResult",
247
- "PlaybookMixin",
276
+ "AgentEvaluationResultStoreMixin",
277
+ "AuditEventStoreMixin",
278
+ "PurgeOperationStoreMixin",
279
+ "SubjectBarrierMixin",
280
+ "GovernanceEraseExecutionMixin",
281
+ "RebuildHideMixin",
282
+ "AgentPlaybookStoreMixin",
283
+ "OptimizationJobStoreMixin",
284
+ "PlaybookSourceLinkageMixin",
285
+ "UserPlaybookStoreMixin",
286
+ "InteractionStoreMixin",
287
+ "ProfileSearchMixin",
288
+ "ProfileStoreMixin",
248
289
  "RetrievalLogMixin",
249
290
  "PriorAnswerMatch",
250
291
  "RunToolDependencyKind",