claude-smart 0.2.49 → 0.2.50

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 (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -1,9 +1,17 @@
1
1
  """Abstract agent success evaluation result store declarations."""
2
2
 
3
3
  from abc import abstractmethod
4
+ from typing import Any
4
5
 
5
6
  from reflexio.models.api_schema.domain import (
6
7
  AgentSuccessEvaluationResult,
8
+ RetrievedLearningEvaluationResult,
9
+ )
10
+
11
+ from ..retrieved_learning_state import (
12
+ DEFAULT_TRANSCRIPT_CHAR_LIMIT,
13
+ BoundedRetrievedLearningSnapshot,
14
+ RetrievedLearningCommitResult,
7
15
  )
8
16
 
9
17
 
@@ -122,3 +130,166 @@ class AgentEvaluationResultStoreMixin:
122
130
  int: Number of rows deleted.
123
131
  """
124
132
  raise NotImplementedError
133
+
134
+ # ==============================
135
+ # Retrieved-learning evaluation methods
136
+ # ==============================
137
+ #
138
+ # Concurrency contract (see storage_base/retrieved_learning_state.py):
139
+ # generation fencing + session-fingerprint CAS recomputed under the
140
+ # replacement transaction's lock. No interaction write/delete path is
141
+ # instrumented.
142
+
143
+ @abstractmethod
144
+ def begin_retrieved_learning_evaluation_run(
145
+ self, user_id: str, session_id: str
146
+ ) -> int:
147
+ """Allocate the next evaluation generation for one session.
148
+
149
+ Atomically increments (creating on first use) the ``generation``
150
+ counter in the session's ``_operation_state`` row after asserting the
151
+ governance subject is writable. Concurrent callers receive distinct
152
+ generations.
153
+
154
+ Args:
155
+ user_id (str): Session owner.
156
+ session_id (str): Evaluated session.
157
+
158
+ Returns:
159
+ int: The allocated generation (monotonically increasing per
160
+ session).
161
+ """
162
+ raise NotImplementedError
163
+
164
+ @abstractmethod
165
+ def load_bounded_retrieved_learning_snapshot(
166
+ self,
167
+ user_id: str,
168
+ session_id: str,
169
+ raw_ref_limit: int = 5_000,
170
+ transcript_char_limit: int = DEFAULT_TRANSCRIPT_CHAR_LIMIT,
171
+ ) -> BoundedRetrievedLearningSnapshot:
172
+ """Load the bounded session projection for evaluation.
173
+
174
+ Streams interaction rows (no ``Interaction`` objects, no embeddings or
175
+ image encodings) and counts raw attachment occurrences before
176
+ appending; on occurrence ``raw_ref_limit + 1`` the scan aborts with
177
+ ``attachment_limit_exceeded=True``.
178
+
179
+ Args:
180
+ user_id (str): Session owner.
181
+ session_id (str): Evaluated session.
182
+ raw_ref_limit (int): Maximum raw attachment occurrences to parse.
183
+ transcript_char_limit (int): Maximum transcript characters retained.
184
+
185
+ Returns:
186
+ BoundedRetrievedLearningSnapshot: The bounded projection.
187
+ """
188
+ raise NotImplementedError
189
+
190
+ @abstractmethod
191
+ def get_matching_retrieved_learning_terminal_state(
192
+ self, user_id: str, session_id: str, session_fingerprint: str
193
+ ) -> dict[str, Any] | None:
194
+ """Return the session's terminal evaluation state if still fresh.
195
+
196
+ Implementations must use a writer transaction and recompute the live
197
+ session fingerprint in that transaction. A match requires terminal
198
+ status and equality among the supplied, persisted, and live
199
+ fingerprints.
200
+
201
+ Args:
202
+ user_id (str): Session owner.
203
+ session_id (str): Evaluated session.
204
+ session_fingerprint (str): Fingerprint of the current session.
205
+
206
+ Returns:
207
+ dict | None: The persisted state JSON on a match, else None.
208
+ """
209
+ raise NotImplementedError
210
+
211
+ @abstractmethod
212
+ def replace_retrieved_learning_evaluation_results(
213
+ self,
214
+ user_id: str,
215
+ session_id: str,
216
+ generation: int,
217
+ session_fingerprint: str,
218
+ proposed_status: str,
219
+ diagnostics: dict[str, Any],
220
+ results: list[RetrievedLearningEvaluationResult],
221
+ ) -> RetrievedLearningCommitResult:
222
+ """Atomically replace one session's evaluation result set.
223
+
224
+ In one transaction: locks the session's state row, requires the
225
+ current generation to equal ``generation`` (else ``superseded``),
226
+ recomputes the session fingerprint from live rows and requires it to
227
+ equal ``session_fingerprint`` (else ``stale``), rechecks every
228
+ result's source row for retrieval eligibility (ineligible rows are
229
+ dropped), then deletes the prior session set, inserts the filtered
230
+ set, and persists completion state — all or nothing.
231
+
232
+ When commit-time eligibility removes every candidate, prior rows are
233
+ cleared and the final status is ``not_applicable`` regardless of
234
+ ``proposed_status``.
235
+
236
+ Args:
237
+ user_id (str): Session owner.
238
+ session_id (str): Evaluated session.
239
+ generation (int): Generation allocated by
240
+ :meth:`begin_retrieved_learning_evaluation_run`.
241
+ session_fingerprint (str): Fingerprint of the judged snapshot.
242
+ proposed_status (str): ``"complete"`` or ``"degraded"``.
243
+ diagnostics (dict): Sanitized counters/timestamps persisted in
244
+ operation state (never prompt content or raw exception text).
245
+ results (list[RetrievedLearningEvaluationResult]): Proposed rows.
246
+
247
+ Returns:
248
+ RetrievedLearningCommitResult: Disposition plus authoritative
249
+ final status/count when applied.
250
+ """
251
+ raise NotImplementedError
252
+
253
+ @abstractmethod
254
+ def finish_retrieved_learning_evaluation_run(
255
+ self,
256
+ user_id: str,
257
+ session_id: str,
258
+ generation: int,
259
+ status: str,
260
+ diagnostics: dict[str, Any],
261
+ ) -> None:
262
+ """Record a non-applied run outcome (``failed`` or ``pending``).
263
+
264
+ Generation-guarded CAS: updates state only while ``generation`` is
265
+ still current, so an older failure cannot overwrite a newer run.
266
+ Result rows are never touched.
267
+
268
+ Args:
269
+ user_id (str): Session owner.
270
+ session_id (str): Evaluated session.
271
+ generation (int): Generation of the finishing run.
272
+ status (str): ``"failed"`` or ``"pending"``.
273
+ diagnostics (dict): Sanitized counters and error type.
274
+ """
275
+ raise NotImplementedError
276
+
277
+ @abstractmethod
278
+ def get_retrieved_learning_evaluation_results(
279
+ self,
280
+ user_id: str | None = None,
281
+ session_id: str | None = None,
282
+ limit: int = 100,
283
+ ) -> list[RetrievedLearningEvaluationResult]:
284
+ """Read persisted per-learning verdicts.
285
+
286
+ Args:
287
+ user_id (str, optional): Filter by session owner.
288
+ session_id (str, optional): Filter by session.
289
+ limit (int): Maximum rows, ordered by
290
+ ``created_at DESC, result_id DESC``.
291
+
292
+ Returns:
293
+ list[RetrievedLearningEvaluationResult]: Matching rows.
294
+ """
295
+ raise NotImplementedError
@@ -12,7 +12,44 @@ class UserPlaybookStoreMixin:
12
12
  """Abstract user playbook CRUD + search methods."""
13
13
 
14
14
  @abstractmethod
15
- def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None:
15
+ def save_user_playbooks(
16
+ self,
17
+ user_playbooks: list[UserPlaybook],
18
+ *,
19
+ skip_embedding: bool = False,
20
+ ) -> None:
21
+ """Insert user playbooks, assigning survivor ids.
22
+
23
+ Args:
24
+ user_playbooks: Playbooks to insert.
25
+ skip_embedding: When ``False`` (default — what every current caller
26
+ gets), the embedding (and, when document expansion is enabled,
27
+ ``expanded_terms``) is recomputed unconditionally at write time,
28
+ exactly as before. Callers that ``model_copy`` a DB-loaded row
29
+ with changed content but the old embedding preserved rely on
30
+ this recompute, so it must stay the default. When ``True``, the
31
+ embedding step is skipped because ``.embedding`` was already
32
+ populated up front by
33
+ :meth:`precompute_user_playbook_embeddings` (the durable
34
+ compute/persist split — the only caller that opts in). A bare
35
+ ``if not up.embedding`` guard is deliberately NOT used: it would
36
+ persist a stale vector for the ``model_copy`` callers' changed
37
+ content (silent search corruption).
38
+ """
39
+ raise NotImplementedError
40
+
41
+ @abstractmethod
42
+ def precompute_user_playbook_embeddings(
43
+ self, playbooks: list[UserPlaybook]
44
+ ) -> None:
45
+ """Populate ``.embedding`` (and ``.expanded_terms`` when document
46
+ expansion is enabled) on each playbook in place, issuing NO DB write.
47
+
48
+ Lets the durable learning worker do the (slow, LLM/embedding) compute
49
+ outside the writer transaction, then pass ``skip_embedding=True`` to
50
+ :meth:`save_user_playbooks` so the fenced persist writes the
51
+ pre-computed vector without re-embedding.
52
+ """
16
53
  raise NotImplementedError
17
54
 
18
55
  @abstractmethod
@@ -183,6 +220,8 @@ class UserPlaybookStoreMixin:
183
220
  user_id: str,
184
221
  user_playbook_ids: list[int],
185
222
  status_filter: list[Status | None] | None = None,
223
+ *,
224
+ include_inactive: bool = False,
186
225
  ) -> list[UserPlaybook]:
187
226
  """Fetch the subset of a user's playbooks whose ids are in the list.
188
227
 
@@ -197,11 +236,23 @@ class UserPlaybookStoreMixin:
197
236
  status_filter (list[Status | None] | None): Statuses to
198
237
  include. ``None`` (default) means CURRENT only — same
199
238
  default as ``get_user_playbooks`` for consistency.
239
+ include_inactive (bool): Return matching owned rows regardless of
240
+ lifecycle status. This is the *historical resolution* mode (see
241
+ ``RetrievedLearningEvaluator``): it answers "what did this id
242
+ point at", not "what is retrievable now". It is a strict superset
243
+ of ``include_tombstones`` on ``get_user_playbook_by_id``, which
244
+ only unhides MERGED/SUPERSEDED for lineage walks —
245
+ ``include_inactive`` also returns ARCHIVED rows. ``user_id``
246
+ scoping still applies. The default preserves retrieval behavior.
200
247
 
201
248
  Returns:
202
249
  list[UserPlaybook]: Matching playbooks. Order is unspecified.
203
250
  Ids that do not exist (or do not match the user / status
204
251
  filter) are silently omitted.
252
+
253
+ Raises:
254
+ StorageError: If ``include_inactive`` is combined with an explicit
255
+ ``status_filter`` — the two are contradictory.
205
256
  """
206
257
  raise NotImplementedError
207
258
 
@@ -60,6 +60,65 @@ class InteractionStoreMixin:
60
60
  """
61
61
  return
62
62
 
63
+ def iter_interactions_missing_vectors(
64
+ self,
65
+ limit: int, # noqa: ARG002
66
+ ) -> list[tuple[int, str]]:
67
+ """Enumerate interactions whose embedding vector was never persisted.
68
+
69
+ When an embedding call fails or degrades, the ingest path stores an empty
70
+ embedding (and writes no vector row), returns success, and nothing ever
71
+ re-embeds the row — so that interaction is invisible to vector/hybrid
72
+ search forever. This method surfaces those rows so a background sweep can
73
+ re-embed them (see ``backfill_missing_interaction_vectors``).
74
+
75
+ Returns ``(interaction_id, embed_text)`` pairs where ``embed_text`` is
76
+ derived exactly as the ingest path derives it, so a backfilled vector is
77
+ byte-for-byte comparable to a freshly-ingested one.
78
+
79
+ Safe default: returns ``[]`` — "this backend does not support backfill
80
+ detection". Backends that store embeddings (SQLite here; Supabase and
81
+ Postgres in the enterprise repo) MUST override this so their rows are
82
+ actually recoverable. Bounded by ``limit`` per call.
83
+
84
+ Args:
85
+ limit: Maximum number of interactions to return.
86
+
87
+ Returns:
88
+ list[tuple[int, str]]: ``(interaction_id, embed_text)`` pairs, at
89
+ most ``limit`` long. Empty when nothing needs backfilling or the
90
+ backend does not support detection.
91
+ """
92
+ return []
93
+
94
+ def backfill_missing_interaction_vectors(
95
+ self,
96
+ limit: int, # noqa: ARG002
97
+ ) -> int:
98
+ """Re-embed and persist vectors for interactions missing them.
99
+
100
+ Idempotent and bounded: enumerates up to ``limit`` interactions via
101
+ ``iter_interactions_missing_vectors``, re-embeds their text using the
102
+ same embedder + derivation the ingest path uses, and writes the vector
103
+ back through the same code path a fresh insert uses. Once a vector
104
+ exists the row is skipped on the next call.
105
+
106
+ Fail-safe: if the embedder is unavailable the implementation must skip
107
+ gracefully and leave the work for a later call (never raise, never
108
+ hot-loop a down embedder).
109
+
110
+ Safe default: returns ``0`` — "this backend does not support backfill".
111
+ SQLite overrides this; the enterprise Supabase/Postgres backends must
112
+ override it (and ``iter_interactions_missing_vectors``) for production.
113
+
114
+ Args:
115
+ limit: Maximum number of interactions to re-embed this call.
116
+
117
+ Returns:
118
+ int: Number of interactions whose vector was backfilled.
119
+ """
120
+ return 0
121
+
63
122
  @abstractmethod
64
123
  def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
65
124
  raise NotImplementedError
@@ -66,8 +66,43 @@ class ProfileStoreMixin:
66
66
  raise NotImplementedError
67
67
 
68
68
  @abstractmethod
69
- def add_user_profile(self, user_id: str, user_profiles: list[UserProfile]) -> None:
70
- """Add the user profile for a given user id."""
69
+ def add_user_profile(
70
+ self,
71
+ user_id: str,
72
+ user_profiles: list[UserProfile],
73
+ *,
74
+ skip_embedding: bool = False,
75
+ ) -> None:
76
+ """Add the user profile for a given user id.
77
+
78
+ Args:
79
+ user_id: The owning user id (positional, unused by some backends).
80
+ user_profiles: Profiles to insert.
81
+ skip_embedding: When ``False`` (default — what every current caller
82
+ gets), the embedding (and, when document expansion is enabled,
83
+ ``expanded_terms``) is recomputed unconditionally at write time,
84
+ exactly as before. Callers that ``model_copy`` a DB-loaded row
85
+ with changed content but the old embedding preserved rely on
86
+ this recompute, so it must stay the default. When ``True``, the
87
+ embedding step is skipped because ``.embedding`` was already
88
+ populated up front by :meth:`precompute_profile_embeddings`
89
+ (the durable compute/persist split — the only caller that opts
90
+ in). A bare ``if not profile.embedding`` guard is deliberately
91
+ NOT used: it would persist a stale vector for the ``model_copy``
92
+ callers' changed content (silent search corruption).
93
+ """
94
+ raise NotImplementedError
95
+
96
+ @abstractmethod
97
+ def precompute_profile_embeddings(self, profiles: list[UserProfile]) -> None:
98
+ """Populate ``.embedding`` (and ``.expanded_terms`` when document
99
+ expansion is enabled) on each profile in place, issuing NO DB write.
100
+
101
+ Lets the durable learning worker do the (slow, LLM/embedding) compute
102
+ outside the writer transaction, then pass ``skip_embedding=True`` to
103
+ :meth:`add_user_profile` so the fenced persist writes the pre-computed
104
+ vector without re-embedding.
105
+ """
71
106
  raise NotImplementedError
72
107
 
73
108
  @abstractmethod
@@ -175,6 +210,8 @@ class ProfileStoreMixin:
175
210
  user_id: str,
176
211
  profile_ids: list[str],
177
212
  status_filter: list[Status | None] | None = None,
213
+ *,
214
+ include_inactive: bool = False,
178
215
  ) -> list[UserProfile]:
179
216
  """Fetch the subset of a user's profiles whose ids are in the list.
180
217
 
@@ -189,11 +226,24 @@ class ProfileStoreMixin:
189
226
  status_filter (list[Status | None] | None): Statuses to
190
227
  include. ``None`` (default) means CURRENT only — same
191
228
  default as ``get_user_profile`` for consistency.
229
+ include_inactive (bool): Return matching owned rows regardless of
230
+ lifecycle status or expiry. This is the *historical resolution*
231
+ mode (see ``RetrievedLearningEvaluator``): it answers "what did
232
+ this id point at", not "what is retrievable now". It is a strict
233
+ superset of ``include_tombstones`` on ``get_profile_by_id``,
234
+ which only unhides MERGED/SUPERSEDED for lineage walks —
235
+ ``include_inactive`` also returns ARCHIVED and EXPIRED rows.
236
+ ``user_id`` scoping still applies. The default preserves
237
+ retrieval behavior.
192
238
 
193
239
  Returns:
194
240
  list[UserProfile]: Matching profiles. Order is unspecified.
195
241
  Ids that do not exist (or do not match the user / status
196
242
  filter) are silently omitted.
243
+
244
+ Raises:
245
+ StorageError: If ``include_inactive`` is combined with an explicit
246
+ ``status_filter`` — the two are contradictory.
197
247
  """
198
248
  raise NotImplementedError
199
249
 
@@ -0,0 +1,226 @@
1
+ """Shared pure helpers for retrieved-learning evaluation state.
2
+
3
+ Used by every storage backend (SQLite, Supabase, native Postgres) and by the
4
+ group-evaluation runner, so this module must stay free of storage imports.
5
+
6
+ Concurrency model (no mutation-site instrumentation):
7
+
8
+ - The **session fingerprint** is a SHA-256 digest over every interaction ID in
9
+ the session plus each interaction's canonical attachment refs. Any
10
+ interaction publish or delete changes it (autoincrement interaction IDs make
11
+ ABA collisions unrealizable), so it covers transcript-only changes as well
12
+ as attachment changes.
13
+ - Each evaluation run receives a monotonically increasing **generation**
14
+ allocated in ``_operation_state``. Replacement CASes generation and a
15
+ fingerprint recomputed under the replacement transaction's lock, so an
16
+ older or stale snapshot cannot overwrite newer session state.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from typing import Literal
25
+
26
+ RETRIEVED_LEARNING_STATE_PREFIX = "retrieved_learning_eval"
27
+
28
+ # Statuses persisted in _operation_state. "complete" and "not_applicable" are
29
+ # terminal (the fast path may short-circuit on them); "degraded" and "failed"
30
+ # are retried by the next scheduled or forced run.
31
+ type RetrievedLearningPersistedStatus = Literal[
32
+ "pending", "in_progress", "complete", "degraded", "failed", "not_applicable"
33
+ ]
34
+ TERMINAL_RETRIEVED_STATUSES: frozenset[str] = frozenset({"complete", "not_applicable"})
35
+
36
+ # Outcomes of one runner invocation. "stale"/"superseded"/"skipped" are
37
+ # invocation outcomes only and are never stored as persisted status.
38
+ type RetrievedLearningInvocationStatus = Literal[
39
+ "pending",
40
+ "complete",
41
+ "degraded",
42
+ "failed",
43
+ "not_applicable",
44
+ "stale",
45
+ "superseded",
46
+ "skipped",
47
+ ]
48
+
49
+ # Canonical kinds accepted and persisted by retrieved-learning evaluation.
50
+ # ``RetrievedLearning.kind`` validates these at the API boundary; storage
51
+ # parsers still filter defensively against this set.
52
+ CANONICAL_RETRIEVED_KINDS: frozenset[str] = frozenset(
53
+ {"profile", "user_playbook", "agent_playbook"}
54
+ )
55
+ DEFAULT_TRANSCRIPT_CHAR_LIMIT = 64_000
56
+
57
+
58
+ class SessionFingerprintBuilder:
59
+ """Incrementally build the canonical session fingerprint."""
60
+
61
+ def __init__(self) -> None:
62
+ self._digest = hashlib.sha256()
63
+ self._digest.update(b"[")
64
+ self._has_entries = False
65
+
66
+ def add(
67
+ self,
68
+ interaction_id: int,
69
+ refs: list[tuple[str, str]],
70
+ role: str = "",
71
+ content: str = "",
72
+ ) -> None:
73
+ """Fold one interaction into the running digest.
74
+
75
+ ``role``/``content`` are the transcript the judges actually saw, so a
76
+ content-only edit (e.g. ``INSERT OR REPLACE`` on an existing
77
+ interaction that keeps its id and attachments) still invalidates the
78
+ fingerprint. Callers on the precompute and commit-recompute sides MUST
79
+ pass content truncated identically (``DEFAULT_TRANSCRIPT_CHAR_LIMIT``);
80
+ otherwise a session would compare unequal and never commit.
81
+ """
82
+ if self._has_entries:
83
+ self._digest.update(b",")
84
+ self._digest.update(
85
+ json.dumps(
86
+ [interaction_id, sorted(refs), role, content],
87
+ separators=(",", ":"),
88
+ ).encode("utf-8")
89
+ )
90
+ self._has_entries = True
91
+
92
+ def hexdigest(self) -> str:
93
+ digest = self._digest.copy()
94
+ digest.update(b"]")
95
+ return digest.hexdigest()
96
+
97
+
98
+ def build_retrieved_learning_state_key(user_id: str, session_id: str) -> str:
99
+ """Build the ``_operation_state`` key for one session's evaluation state.
100
+
101
+ Length-prefixed (following the grade_on_demand cache-key precedent) so a
102
+ crafted user/session pair cannot collide with another pair. Org scoping is
103
+ unnecessary: ``_operation_state`` is already org-scoped on every backend.
104
+
105
+ Args:
106
+ user_id (str): Session owner.
107
+ session_id (str): Evaluated session.
108
+
109
+ Returns:
110
+ str: The state key.
111
+ """
112
+ return (
113
+ f"{RETRIEVED_LEARNING_STATE_PREFIX}"
114
+ f"::{len(user_id)}:{user_id}::{len(session_id)}:{session_id}"
115
+ )
116
+
117
+
118
+ @dataclass
119
+ class SnapshotInteraction:
120
+ """One interaction row in the bounded session projection.
121
+
122
+ ``refs`` holds ``(kind, learning_id)`` attachment tuples; interaction
123
+ content is carried only for transcript construction and is never
124
+ persisted in operation state.
125
+ """
126
+
127
+ interaction_id: int
128
+ role: str
129
+ content: str
130
+ created_at: int
131
+ refs: list[tuple[str, str]] = field(default_factory=list)
132
+
133
+
134
+ @dataclass
135
+ class BoundedRetrievedLearningSnapshot:
136
+ """Bounded projection of one session for retrieved-learning evaluation.
137
+
138
+ Loaded without constructing full ``Interaction`` objects (no embeddings,
139
+ no image encodings). ``attachment_limit_exceeded`` is set when the raw
140
+ attachment-occurrence cap was hit; the evaluator must then make zero LLM
141
+ calls.
142
+ """
143
+
144
+ interactions: list[SnapshotInteraction] = field(default_factory=list)
145
+ earliest_request_created_at: int | None = None
146
+ agent_version: str = ""
147
+ raw_attachment_count: int = 0
148
+ attachment_limit_exceeded: bool = False
149
+ precomputed_fingerprint: str | None = None
150
+
151
+
152
+ def append_bounded_snapshot_interaction(
153
+ snapshot: BoundedRetrievedLearningSnapshot,
154
+ *,
155
+ interaction_id: int,
156
+ role: str,
157
+ content: str,
158
+ created_at: int,
159
+ refs: list[tuple[str, str]],
160
+ transcript_chars_remaining: int,
161
+ ) -> int:
162
+ """Retain refs and only the transcript prefix that fits the char budget."""
163
+ retained_role = ""
164
+ retained_content = ""
165
+ if transcript_chars_remaining > 0 and content:
166
+ prefix_size = len(role) + 3
167
+ content_budget = max(0, transcript_chars_remaining - prefix_size)
168
+ if content_budget:
169
+ retained_role = role
170
+ retained_content = content[:content_budget]
171
+ transcript_chars_remaining -= prefix_size + len(retained_content)
172
+ if refs or retained_content:
173
+ snapshot.interactions.append(
174
+ SnapshotInteraction(
175
+ interaction_id=interaction_id,
176
+ role=retained_role,
177
+ content=retained_content,
178
+ created_at=created_at,
179
+ refs=refs,
180
+ )
181
+ )
182
+ return max(0, transcript_chars_remaining)
183
+
184
+
185
+ def session_fingerprint(snapshot: BoundedRetrievedLearningSnapshot) -> str:
186
+ """Compute the canonical session fingerprint for a bounded snapshot.
187
+
188
+ Covers every interaction ID, each interaction's ``(kind, learning_id)``
189
+ refs, and its transcript role/content, so any publish, delete, or in-place
190
+ content edit in the session invalidates it. Only this digest is ever
191
+ persisted.
192
+
193
+ Args:
194
+ snapshot (BoundedRetrievedLearningSnapshot): The session projection.
195
+
196
+ Returns:
197
+ str: Hex SHA-256 digest.
198
+ """
199
+ if snapshot.precomputed_fingerprint is not None:
200
+ return snapshot.precomputed_fingerprint
201
+ builder = SessionFingerprintBuilder()
202
+ for interaction in sorted(
203
+ snapshot.interactions,
204
+ key=lambda item: (item.created_at, item.interaction_id),
205
+ ):
206
+ builder.add(
207
+ interaction.interaction_id,
208
+ interaction.refs,
209
+ interaction.role,
210
+ interaction.content,
211
+ )
212
+ return builder.hexdigest()
213
+
214
+
215
+ @dataclass
216
+ class RetrievedLearningCommitResult:
217
+ """Outcome of one atomic result-set replacement.
218
+
219
+ ``status``/``committed_count`` are authoritative only when ``disposition``
220
+ is ``"applied"`` (commit-time eligibility can turn a proposed status into
221
+ ``"not_applicable"``).
222
+ """
223
+
224
+ disposition: Literal["applied", "stale", "superseded"]
225
+ status: RetrievedLearningPersistedStatus | None = None
226
+ committed_count: int = 0
@@ -20,8 +20,10 @@ import os
20
20
  import threading
21
21
  import time
22
22
  from collections.abc import Callable
23
+ from functools import partial
23
24
 
24
25
  from reflexio.server.api_endpoints.request_context import RequestContext
26
+ from reflexio.server.callback_executor import submit_callback
25
27
  from reflexio.server.llm.litellm_client import LiteLLMClient
26
28
  from reflexio.server.services.tagging.service import TaggingService
27
29
 
@@ -101,13 +103,10 @@ class TaggingScheduler:
101
103
  continue
102
104
 
103
105
  del self._scheduled[key]
104
- t = threading.Thread(
105
- target=self._run_callback,
106
- args=(key, callback),
107
- daemon=True,
108
- name=f"tagging-{key[1][:20]}",
106
+ submit_callback(
107
+ f"tagging-{key[1][:20]}",
108
+ partial(self._run_callback, key, callback),
109
109
  )
110
- t.start()
111
110
  except Exception:
112
111
  logger.exception("Error in tagging scheduler loop")
113
112
  time.sleep(1)