claude-smart 0.2.47 → 0.2.49

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 (139) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  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/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  8. package/plugin/dashboard/package-lock.json +61 -390
  9. package/plugin/dashboard/package.json +2 -2
  10. package/plugin/opencode/dist/server.mjs +60 -2
  11. package/plugin/opencode/server.mts +64 -2
  12. package/plugin/pyproject.toml +2 -2
  13. package/plugin/scripts/_lib.sh +58 -6
  14. package/plugin/scripts/backend-service.sh +15 -10
  15. package/plugin/scripts/codex-hook.js +16 -4
  16. package/plugin/scripts/dashboard-service.sh +1 -1
  17. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  18. package/plugin/scripts/opencode-claude-compat.js +8 -1
  19. package/plugin/scripts/smart-install.sh +101 -20
  20. package/plugin/src/README.md +1 -1
  21. package/plugin/src/claude_smart/cli.py +79 -29
  22. package/plugin/src/claude_smart/env_config.py +53 -11
  23. package/plugin/uv.lock +5 -5
  24. package/plugin/vendor/reflexio/.env.example +7 -0
  25. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  26. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  27. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  28. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  29. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  30. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  31. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  33. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  34. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  35. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  36. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  37. package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
  38. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  39. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  40. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  41. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  42. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  43. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  44. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  45. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  46. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  47. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  48. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
  49. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  50. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  51. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  52. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  53. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  54. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  55. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  56. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  57. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  58. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  59. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  60. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
  61. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  62. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  63. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  64. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  66. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  68. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  73. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  74. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  76. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  77. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
  81. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
  82. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  83. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  84. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  88. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  89. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -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
@@ -1,5 +1,6 @@
1
1
  from reflexio.models.api_schema.domain.enums import Status
2
2
 
3
+ from ._learning_jobs import LearningJob, LearningJobStatus, LearningJobStoreABC
3
4
  from ._agent_run import (
4
5
  NOT_APPLICABLE_ANSWER,
5
6
  AgentBinding,
@@ -22,16 +23,22 @@ from ._agent_run import (
22
23
  not_applicable_tool_result,
23
24
  )
24
25
  from ._base import BaseStorageCore, matches_status_filter
26
+ from ._commit_scope import CommitScopeMixin
25
27
  from ._extras import ExtrasMixin
26
- from ._governance import GovernanceMixin
27
28
  from ._lineage import EntityType, LineageEventMixin
28
29
  from ._operations import OperationMixin
29
- from ._profiles import ProfileMixin
30
30
  from ._requests import RequestMixin
31
31
  from ._retrieval_log import RetrievalLogMixin
32
32
  from ._shadow_verdicts import ShadowVerdictsMixin
33
33
  from ._share_links import ShareLinkMixin
34
34
  from ._stall_state import StallStateMixin
35
+ from .governance import (
36
+ AuditEventStoreMixin,
37
+ GovernanceEraseExecutionMixin,
38
+ PurgeOperationStoreMixin,
39
+ RebuildHideMixin,
40
+ SubjectBarrierMixin,
41
+ )
35
42
  from .playbook import (
36
43
  AgentEvaluationResultStoreMixin,
37
44
  AgentPlaybookStoreMixin,
@@ -39,11 +46,16 @@ from .playbook import (
39
46
  PlaybookSourceLinkageMixin,
40
47
  UserPlaybookStoreMixin,
41
48
  )
49
+ from .profiles import InteractionStoreMixin, ProfileSearchMixin, ProfileStoreMixin
42
50
 
43
51
 
44
52
  class BaseStorage(
53
+ LearningJobStoreABC,
54
+ CommitScopeMixin,
45
55
  AgentRunMixin,
46
- ProfileMixin,
56
+ ProfileStoreMixin,
57
+ InteractionStoreMixin,
58
+ ProfileSearchMixin,
47
59
  RequestMixin,
48
60
  AgentPlaybookStoreMixin,
49
61
  UserPlaybookStoreMixin,
@@ -51,7 +63,11 @@ class BaseStorage(
51
63
  OptimizationJobStoreMixin,
52
64
  AgentEvaluationResultStoreMixin,
53
65
  RetrievalLogMixin,
54
- GovernanceMixin,
66
+ AuditEventStoreMixin,
67
+ PurgeOperationStoreMixin,
68
+ SubjectBarrierMixin,
69
+ GovernanceEraseExecutionMixin,
70
+ RebuildHideMixin,
55
71
  LineageEventMixin,
56
72
  OperationMixin,
57
73
  ExtrasMixin,
@@ -162,8 +178,8 @@ class BaseStorage(
162
178
  interaction_count = len(self.get_user_interaction(user_id))
163
179
 
164
180
  # All statuses a user's row can have — including tombstones (SUPERSEDED,
165
- # MERGED). Erasure MUST reach every row the user owns regardless of
166
- # status; the old filter excluded tombstones, leaving them in the DB
181
+ # MERGED, EXPIRED). Erasure MUST reach every row the user owns regardless
182
+ # of status; the old filter excluded tombstones, leaving them in the DB
167
183
  # after clear_user_data (GDPR regression).
168
184
  _all_statuses: list[Status | None] = [
169
185
  None, # CURRENT
@@ -172,6 +188,7 @@ class BaseStorage(
172
188
  Status.ARCHIVE_IN_PROGRESS,
173
189
  Status.SUPERSEDED,
174
190
  Status.MERGED,
191
+ Status.EXPIRED,
175
192
  ]
176
193
 
177
194
  # Snapshot user_playbook ids for the user and partition into
@@ -190,10 +207,14 @@ class BaseStorage(
190
207
  )
191
208
 
192
209
  # Snapshot profile ids for the user and partition into
193
- # purge vs delete sets.
210
+ # purge vs delete sets. include_expired=True drops the
211
+ # expiration_timestamp >= now guard so EXPIRED tombstones (expiry in
212
+ # the past) are enumerated and reached by the erasure path.
194
213
  raw_profile_ids = [
195
214
  p.profile_id
196
- for p in self.get_user_profile(user_id, status_filter=_all_statuses)
215
+ for p in self.get_user_profile(
216
+ user_id, status_filter=_all_statuses, include_expired=True
217
+ )
197
218
  if p.profile_id is not None
198
219
  ]
199
220
  purge_profile_ids, delete_profile_ids = self._partition_purge_vs_delete(
@@ -243,8 +264,21 @@ class BaseStorage(
243
264
  "purged_user_playbooks": len(purge_upb_ids),
244
265
  }
245
266
 
267
+ def learning_jobs_columns(self) -> list[str]:
268
+ """Return the column names of the learning_jobs table.
269
+
270
+ Each backend overrides this to query its own schema introspection
271
+ mechanism (SQLite: PRAGMA table_info; Postgres/Supabase:
272
+ information_schema.columns).
273
+ """
274
+ raise NotImplementedError
275
+
246
276
 
247
277
  __all__ = [
278
+ "LearningJob",
279
+ "LearningJobStatus",
280
+ "LearningJobStoreABC",
281
+ "CommitScopeMixin",
248
282
  "AgentBinding",
249
283
  "AgentRunMixin",
250
284
  "EntityType",
@@ -257,10 +291,18 @@ __all__ = [
257
291
  "PendingToolCallStatus",
258
292
  "PendingToolCallUpsertResult",
259
293
  "AgentEvaluationResultStoreMixin",
294
+ "AuditEventStoreMixin",
295
+ "PurgeOperationStoreMixin",
296
+ "SubjectBarrierMixin",
297
+ "GovernanceEraseExecutionMixin",
298
+ "RebuildHideMixin",
260
299
  "AgentPlaybookStoreMixin",
261
300
  "OptimizationJobStoreMixin",
262
301
  "PlaybookSourceLinkageMixin",
263
302
  "UserPlaybookStoreMixin",
303
+ "InteractionStoreMixin",
304
+ "ProfileSearchMixin",
305
+ "ProfileStoreMixin",
264
306
  "RetrievalLogMixin",
265
307
  "PriorAnswerMatch",
266
308
  "RunToolDependencyKind",