claude-smart 0.2.48 → 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 (193) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -40
  3. package/bin/claude-smart.js +393 -55
  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/opencode/dist/server.mjs +60 -2
  27. package/plugin/opencode/server.mts +64 -2
  28. package/plugin/pyproject.toml +2 -2
  29. package/plugin/scripts/_lib.sh +255 -7
  30. package/plugin/scripts/backend-python-runner.py +46 -0
  31. package/plugin/scripts/backend-service.sh +766 -123
  32. package/plugin/scripts/codex-hook.js +79 -229
  33. package/plugin/scripts/dashboard-open.sh +6 -4
  34. package/plugin/scripts/dashboard-service.sh +118 -137
  35. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  36. package/plugin/scripts/hook_entry.sh +3 -0
  37. package/plugin/scripts/opencode-claude-compat.js +8 -1
  38. package/plugin/scripts/smart-install.sh +116 -21
  39. package/plugin/src/README.md +1 -1
  40. package/plugin/src/claude_smart/cli.py +93 -29
  41. package/plugin/src/claude_smart/context_inject.py +3 -0
  42. package/plugin/src/claude_smart/env_config.py +56 -11
  43. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  44. package/plugin/src/claude_smart/events/session_end.py +2 -1
  45. package/plugin/src/claude_smart/events/stop.py +3 -0
  46. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  47. package/plugin/src/claude_smart/internal_call.py +5 -2
  48. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  49. package/plugin/src/claude_smart/publish.py +59 -7
  50. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  51. package/plugin/src/claude_smart/runtime.py +15 -6
  52. package/plugin/src/claude_smart/state.py +211 -52
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +13 -0
  55. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  56. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  58. package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
  59. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  60. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  61. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  62. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
  63. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  64. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  65. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  66. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  67. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  68. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  69. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  70. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  71. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  72. package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
  73. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  74. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  75. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  76. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  77. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
  78. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  79. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  80. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  81. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  82. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  83. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  84. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
  85. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  86. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  90. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  91. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  92. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  93. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  94. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  95. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  96. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  97. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  98. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  99. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  100. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  101. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  102. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  103. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  107. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  108. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  109. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  111. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  112. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  113. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  114. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  115. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  120. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  121. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  122. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  123. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  124. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  125. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  126. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  127. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
  129. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  131. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  133. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  135. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  136. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  137. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  138. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  139. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  140. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  141. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  142. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  143. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  144. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
  145. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  146. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  150. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
  172. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
  173. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  174. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  175. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  176. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  178. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  179. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  180. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  181. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  182. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  183. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  184. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  185. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
  186. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
  187. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  188. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  189. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  190. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  191. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  192. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  193. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -1,16 +1,30 @@
1
1
  """Agent success evaluation result store methods for SQLite storage."""
2
2
 
3
3
  import sqlite3
4
+ from datetime import UTC, datetime
4
5
  from typing import Any
5
6
 
6
7
  from reflexio.models.api_schema.service_schemas import (
7
8
  AgentSuccessEvaluationResult,
9
+ RetrievedLearningEvaluationResult,
8
10
  )
9
11
 
12
+ from ...storage_base.retrieved_learning_state import (
13
+ CANONICAL_RETRIEVED_KINDS,
14
+ DEFAULT_TRANSCRIPT_CHAR_LIMIT,
15
+ TERMINAL_RETRIEVED_STATUSES,
16
+ BoundedRetrievedLearningSnapshot,
17
+ RetrievedLearningCommitResult,
18
+ SessionFingerprintBuilder,
19
+ append_bounded_snapshot_interaction,
20
+ build_retrieved_learning_state_key,
21
+ )
10
22
  from .._base import (
11
23
  SQLiteStorageBase,
12
24
  _epoch_to_iso,
25
+ _iso_to_epoch,
13
26
  _json_dumps,
27
+ _json_loads,
14
28
  _row_to_eval_result,
15
29
  )
16
30
 
@@ -23,9 +37,12 @@ class AgentEvaluationResultStoreMixin:
23
37
  conn: sqlite3.Connection
24
38
  _execute: Any
25
39
  _fetchall: Any
40
+ _fetchone: Any
26
41
  _get_embedding: Any
27
42
  _subject_ref_for_user_id: Any
28
43
  _assert_subject_writable_locked: Any
44
+ _current_timestamp: Any
45
+ get_operation_state: Any
29
46
 
30
47
  # ------------------------------------------------------------------
31
48
  # Agent Success Evaluation methods
@@ -36,11 +53,12 @@ class AgentEvaluationResultStoreMixin:
36
53
  self, results: list[AgentSuccessEvaluationResult]
37
54
  ) -> None:
38
55
  for result in results:
39
- embedding_text = f"{result.failure_type} {result.failure_reason}"
40
- if embedding_text.strip():
41
- result.embedding = self._get_embedding(embedding_text)
42
- else:
43
- result.embedding = []
56
+ if not result.embedding:
57
+ embedding_text = f"{result.failure_type} {result.failure_reason}"
58
+ if embedding_text.strip():
59
+ result.embedding = self._get_embedding(embedding_text)
60
+ else:
61
+ result.embedding = []
44
62
 
45
63
  created_at_iso = _epoch_to_iso(result.created_at)
46
64
  subject_ref = self._subject_ref_for_user_id(result.user_id)
@@ -187,3 +205,410 @@ class AgentEvaluationResultStoreMixin:
187
205
  list(result_ids),
188
206
  )
189
207
  return cur.rowcount
208
+
209
+ # ------------------------------------------------------------------
210
+ # Retrieved-learning evaluation methods
211
+ # ------------------------------------------------------------------
212
+ # Generation fencing + session-fingerprint CAS; see
213
+ # storage_base/retrieved_learning_state.py for the concurrency contract.
214
+ # All state reads/writes inside a transaction use raw self.conn.execute
215
+ # (never self._execute or self-committing helpers) so replacement stays
216
+ # atomic on the shared connection.
217
+
218
+ def _rle_state_row(self, state_key: str) -> dict[str, Any]:
219
+ cur = self.conn.execute(
220
+ "SELECT operation_state FROM _operation_state WHERE service_name = ?",
221
+ (state_key,),
222
+ )
223
+ row = cur.fetchone()
224
+ if not row:
225
+ return {}
226
+ return _json_loads(row["operation_state"]) or {}
227
+
228
+ def _rle_upsert_state(self, state_key: str, state: dict[str, Any]) -> None:
229
+ self.conn.execute(
230
+ """INSERT INTO _operation_state (service_name, operation_state, updated_at)
231
+ VALUES (?,?,?)
232
+ ON CONFLICT(service_name) DO UPDATE SET
233
+ operation_state = excluded.operation_state,
234
+ updated_at = excluded.updated_at""",
235
+ (state_key, _json_dumps(state), self._current_timestamp()),
236
+ )
237
+
238
+ def _rle_fingerprint_now(self, user_id: str, session_id: str) -> str:
239
+ """Recompute the session fingerprint from live rows (in-transaction)."""
240
+ cur = self.conn.execute(
241
+ """SELECT i.interaction_id, i.role,
242
+ substr(i.content, 1, ?) AS content, i.retrieved_learnings
243
+ FROM interactions i JOIN requests r ON i.request_id = r.request_id
244
+ WHERE r.session_id = ? AND i.user_id = ?
245
+ ORDER BY i.created_at ASC, i.interaction_id ASC""",
246
+ (DEFAULT_TRANSCRIPT_CHAR_LIMIT, session_id, user_id),
247
+ )
248
+ builder = SessionFingerprintBuilder()
249
+ for row in cur:
250
+ builder.add(
251
+ int(row["interaction_id"]),
252
+ _parse_attachment_refs(row["retrieved_learnings"]),
253
+ row["role"] or "User",
254
+ row["content"] or "",
255
+ )
256
+ return builder.hexdigest()
257
+
258
+ def _rle_attached_refs(self, user_id: str, session_id: str) -> set[tuple[str, str]]:
259
+ """Canonical ``(kind, learning_id)`` refs attached to the live session."""
260
+ cur = self.conn.execute(
261
+ """SELECT i.retrieved_learnings
262
+ FROM interactions i JOIN requests r ON i.request_id = r.request_id
263
+ WHERE r.session_id = ? AND i.user_id = ?""",
264
+ (session_id, user_id),
265
+ )
266
+ attached: set[tuple[str, str]] = set()
267
+ for row in cur:
268
+ attached.update(_parse_attachment_refs(row["retrieved_learnings"]))
269
+ return attached
270
+
271
+ def _rle_resolvable_refs(
272
+ self, user_id: str, results: list[RetrievedLearningEvaluationResult]
273
+ ) -> set[tuple[str, str]]:
274
+ """Recheck original source rows for historical resolution (in-transaction)."""
275
+ by_kind: dict[str, list[str]] = {}
276
+ for r in results:
277
+ by_kind.setdefault(r.kind, []).append(r.learning_id)
278
+ resolvable: set[tuple[str, str]] = set()
279
+ for chunk in _chunked(by_kind.get("profile", [])):
280
+ ph = ",".join("?" for _ in chunk)
281
+ cur = self.conn.execute(
282
+ f"""SELECT profile_id FROM profiles
283
+ WHERE user_id = ? AND profile_id IN ({ph})""",
284
+ (user_id, *chunk),
285
+ )
286
+ resolvable.update(("profile", str(row[0])) for row in cur.fetchall())
287
+ user_playbook_ids = [
288
+ i for i in (_as_int(x) for x in by_kind.get("user_playbook", [])) if i
289
+ ]
290
+ for chunk in _chunked(user_playbook_ids):
291
+ ph = ",".join("?" for _ in chunk)
292
+ cur = self.conn.execute(
293
+ f"""SELECT user_playbook_id FROM user_playbooks
294
+ WHERE user_id = ? AND user_playbook_id IN ({ph})""",
295
+ (user_id, *chunk),
296
+ )
297
+ resolvable.update(("user_playbook", str(row[0])) for row in cur.fetchall())
298
+ agent_playbook_ids = [
299
+ i for i in (_as_int(x) for x in by_kind.get("agent_playbook", [])) if i
300
+ ]
301
+ for chunk in _chunked(agent_playbook_ids):
302
+ ph = ",".join("?" for _ in chunk)
303
+ cur = self.conn.execute(
304
+ f"""SELECT agent_playbook_id FROM agent_playbooks
305
+ WHERE agent_playbook_id IN ({ph})""",
306
+ chunk,
307
+ )
308
+ resolvable.update(("agent_playbook", str(row[0])) for row in cur.fetchall())
309
+ return resolvable
310
+
311
+ @SQLiteStorageBase.handle_exceptions
312
+ def begin_retrieved_learning_evaluation_run(
313
+ self, user_id: str, session_id: str
314
+ ) -> int:
315
+ state_key = build_retrieved_learning_state_key(user_id, session_id)
316
+ subject_ref = self._subject_ref_for_user_id(user_id)
317
+ with self._lock:
318
+ try:
319
+ self.conn.execute("BEGIN IMMEDIATE")
320
+ self._assert_subject_writable_locked(subject_ref)
321
+ state = self._rle_state_row(state_key)
322
+ generation = int(state.get("generation") or 0) + 1
323
+ state.update(
324
+ {
325
+ "user_id": user_id,
326
+ "session_id": session_id,
327
+ "generation": generation,
328
+ "attempted_at": int(self._current_epoch()),
329
+ }
330
+ )
331
+ state.setdefault("status", "pending")
332
+ state.setdefault("session_fingerprint", "")
333
+ self._rle_upsert_state(state_key, state)
334
+ self.conn.commit()
335
+ except Exception:
336
+ self.conn.rollback()
337
+ raise
338
+ return generation
339
+
340
+ @SQLiteStorageBase.handle_exceptions
341
+ def load_bounded_retrieved_learning_snapshot(
342
+ self,
343
+ user_id: str,
344
+ session_id: str,
345
+ raw_ref_limit: int = 5_000,
346
+ transcript_char_limit: int = DEFAULT_TRANSCRIPT_CHAR_LIMIT,
347
+ ) -> BoundedRetrievedLearningSnapshot:
348
+ snapshot = BoundedRetrievedLearningSnapshot()
349
+ row = self._fetchone(
350
+ """SELECT MIN(created_at) AS earliest FROM requests
351
+ WHERE session_id = ? AND user_id = ?""",
352
+ (session_id, user_id),
353
+ )
354
+ if row and row["earliest"]:
355
+ snapshot.earliest_request_created_at = _iso_to_epoch(row["earliest"])
356
+ row = self._fetchone(
357
+ """SELECT agent_version FROM requests
358
+ WHERE session_id = ? AND user_id = ?
359
+ ORDER BY created_at DESC LIMIT 1""",
360
+ (session_id, user_id),
361
+ )
362
+ if row:
363
+ snapshot.agent_version = row["agent_version"] or ""
364
+ cursor = self.conn.execute(
365
+ # ``fp_content`` is truncated to the fixed fingerprint limit,
366
+ # independent of the caller's ``transcript_char_limit`` snapshot
367
+ # budget, so the commit-side recompute (which has no such budget)
368
+ # produces an identical digest.
369
+ """SELECT i.interaction_id, i.role,
370
+ substr(i.content, 1, ?) AS content,
371
+ substr(i.content, 1, ?) AS fp_content,
372
+ i.created_at, i.retrieved_learnings
373
+ FROM interactions i JOIN requests r ON i.request_id = r.request_id
374
+ WHERE r.session_id = ? AND i.user_id = ?
375
+ ORDER BY i.created_at ASC, i.interaction_id ASC""",
376
+ (transcript_char_limit, DEFAULT_TRANSCRIPT_CHAR_LIMIT, session_id, user_id),
377
+ )
378
+ builder = SessionFingerprintBuilder()
379
+ transcript_chars_remaining = transcript_char_limit
380
+ for r in cursor:
381
+ refs = _parse_attachment_refs(r["retrieved_learnings"])
382
+ snapshot.raw_attachment_count += len(refs)
383
+ builder.add(
384
+ int(r["interaction_id"]),
385
+ refs,
386
+ r["role"] or "User",
387
+ r["fp_content"] or "",
388
+ )
389
+ if snapshot.raw_attachment_count > raw_ref_limit:
390
+ snapshot.attachment_limit_exceeded = True
391
+ snapshot.interactions.clear()
392
+ transcript_chars_remaining = 0
393
+ continue
394
+ transcript_chars_remaining = append_bounded_snapshot_interaction(
395
+ snapshot,
396
+ interaction_id=int(r["interaction_id"]),
397
+ role=r["role"] or "User",
398
+ content=r["content"] or "",
399
+ created_at=_iso_to_epoch(r["created_at"]),
400
+ refs=refs,
401
+ transcript_chars_remaining=transcript_chars_remaining,
402
+ )
403
+ snapshot.precomputed_fingerprint = builder.hexdigest()
404
+ return snapshot
405
+
406
+ @SQLiteStorageBase.handle_exceptions
407
+ def get_matching_retrieved_learning_terminal_state(
408
+ self, user_id: str, session_id: str, session_fingerprint: str
409
+ ) -> dict[str, Any] | None:
410
+ state_key = build_retrieved_learning_state_key(user_id, session_id)
411
+ with self._lock:
412
+ try:
413
+ self.conn.execute("BEGIN IMMEDIATE")
414
+ state = self._rle_state_row(state_key)
415
+ live_fingerprint = self._rle_fingerprint_now(user_id, session_id)
416
+ matched = (
417
+ state.get("status") in TERMINAL_RETRIEVED_STATUSES
418
+ and state.get("session_fingerprint") == session_fingerprint
419
+ and live_fingerprint == session_fingerprint
420
+ )
421
+ self.conn.commit()
422
+ except Exception:
423
+ self.conn.rollback()
424
+ raise
425
+ return state if matched else None
426
+
427
+ @SQLiteStorageBase.handle_exceptions
428
+ def replace_retrieved_learning_evaluation_results(
429
+ self,
430
+ user_id: str,
431
+ session_id: str,
432
+ generation: int,
433
+ session_fingerprint: str,
434
+ proposed_status: str,
435
+ diagnostics: dict[str, Any],
436
+ results: list[RetrievedLearningEvaluationResult],
437
+ ) -> RetrievedLearningCommitResult:
438
+ if proposed_status not in ("complete", "degraded"):
439
+ raise ValueError(f"invalid proposed_status: {proposed_status}")
440
+ state_key = build_retrieved_learning_state_key(user_id, session_id)
441
+ subject_ref = self._subject_ref_for_user_id(user_id)
442
+ with self._lock:
443
+ try:
444
+ self.conn.execute("BEGIN IMMEDIATE")
445
+ self._assert_subject_writable_locked(subject_ref)
446
+ state = self._rle_state_row(state_key)
447
+ if int(state.get("generation") or 0) != generation:
448
+ self.conn.rollback()
449
+ return RetrievedLearningCommitResult(disposition="superseded")
450
+ if self._rle_fingerprint_now(user_id, session_id) != (
451
+ session_fingerprint
452
+ ):
453
+ self.conn.rollback()
454
+ return RetrievedLearningCommitResult(disposition="stale")
455
+ # Persist only records that are still attached to the live
456
+ # session AND whose original row still exists. A duplicate identity is left
457
+ # to trip the UNIQUE index and roll back the commit — caller
458
+ # bugs fail loud rather than silently dropping data.
459
+ attached = self._rle_attached_refs(user_id, session_id)
460
+ resolvable = self._rle_resolvable_refs(user_id, results)
461
+ kept = [
462
+ r
463
+ for r in results
464
+ if (r.kind, r.learning_id) in attached
465
+ and (r.kind, r.learning_id) in resolvable
466
+ ]
467
+ final_status = proposed_status if kept else "not_applicable"
468
+ self.conn.execute(
469
+ """DELETE FROM retrieved_learning_evaluation
470
+ WHERE user_id = ? AND session_id = ?""",
471
+ (user_id, session_id),
472
+ )
473
+ for r in kept:
474
+ self.conn.execute(
475
+ """INSERT INTO retrieved_learning_evaluation
476
+ (user_id, session_id, agent_version, kind,
477
+ learning_id, is_relevant, relevance_reason, impact,
478
+ impact_reason, created_at, governance_subject_ref)
479
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
480
+ (
481
+ user_id,
482
+ session_id,
483
+ r.agent_version,
484
+ r.kind,
485
+ r.learning_id,
486
+ None if r.is_relevant is None else int(r.is_relevant),
487
+ r.relevance_reason,
488
+ r.impact,
489
+ r.impact_reason,
490
+ r.created_at,
491
+ subject_ref,
492
+ ),
493
+ )
494
+ state.update(diagnostics)
495
+ state.update(
496
+ {
497
+ "user_id": user_id,
498
+ "session_id": session_id,
499
+ "generation": generation,
500
+ "status": final_status,
501
+ "session_fingerprint": session_fingerprint,
502
+ "resolvable_count": len(resolvable),
503
+ "committed_count": len(kept),
504
+ "completed_at": int(self._current_epoch()),
505
+ }
506
+ )
507
+ self._rle_upsert_state(state_key, state)
508
+ self.conn.commit()
509
+ except Exception:
510
+ self.conn.rollback()
511
+ raise
512
+ return RetrievedLearningCommitResult(
513
+ disposition="applied", status=final_status, committed_count=len(kept)
514
+ )
515
+
516
+ @SQLiteStorageBase.handle_exceptions
517
+ def finish_retrieved_learning_evaluation_run(
518
+ self,
519
+ user_id: str,
520
+ session_id: str,
521
+ generation: int,
522
+ status: str,
523
+ diagnostics: dict[str, Any],
524
+ ) -> None:
525
+ if status not in ("failed", "pending"):
526
+ raise ValueError(f"invalid finish status: {status}")
527
+ state_key = build_retrieved_learning_state_key(user_id, session_id)
528
+ with self._lock:
529
+ try:
530
+ self.conn.execute("BEGIN IMMEDIATE")
531
+ state = self._rle_state_row(state_key)
532
+ if int(state.get("generation") or 0) != generation:
533
+ self.conn.rollback()
534
+ return
535
+ state.update(diagnostics)
536
+ state["status"] = status
537
+ self._rle_upsert_state(state_key, state)
538
+ self.conn.commit()
539
+ except Exception:
540
+ self.conn.rollback()
541
+ raise
542
+
543
+ @SQLiteStorageBase.handle_exceptions
544
+ def get_retrieved_learning_evaluation_results(
545
+ self,
546
+ user_id: str | None = None,
547
+ session_id: str | None = None,
548
+ limit: int = 100,
549
+ ) -> list[RetrievedLearningEvaluationResult]:
550
+ sql = "SELECT * FROM retrieved_learning_evaluation"
551
+ clauses: list[str] = []
552
+ params: list[Any] = []
553
+ if user_id is not None:
554
+ clauses.append("user_id = ?")
555
+ params.append(user_id)
556
+ if session_id is not None:
557
+ clauses.append("session_id = ?")
558
+ params.append(session_id)
559
+ if clauses:
560
+ sql += " WHERE " + " AND ".join(clauses)
561
+ sql += " ORDER BY created_at DESC, result_id DESC LIMIT ?"
562
+ params.append(limit)
563
+ rows = self._fetchall(sql, params)
564
+ return [_row_to_retrieved_learning_result(r) for r in rows]
565
+
566
+ def _current_epoch(self) -> int:
567
+ return int(datetime.now(UTC).timestamp())
568
+
569
+
570
+ def _parse_attachment_refs(raw: Any) -> list[tuple[str, str]]:
571
+ """Parse a stored retrieved_learnings JSON blob into (kind, learning_id) tuples."""
572
+ parsed = _json_loads(raw) if isinstance(raw, str) else raw
573
+ if not parsed or not isinstance(parsed, list):
574
+ return []
575
+ refs: list[tuple[str, str]] = []
576
+ for item in parsed:
577
+ if not isinstance(item, dict):
578
+ continue
579
+ kind = str(item.get("kind") or "")
580
+ learning_id = str(item.get("learning_id") or "")
581
+ if kind in CANONICAL_RETRIEVED_KINDS and learning_id:
582
+ refs.append((kind, learning_id))
583
+ return refs
584
+
585
+
586
+ def _row_to_retrieved_learning_result(
587
+ row: sqlite3.Row,
588
+ ) -> RetrievedLearningEvaluationResult:
589
+ d = dict(row)
590
+ return RetrievedLearningEvaluationResult(
591
+ result_id=int(d["result_id"]),
592
+ user_id=d["user_id"],
593
+ session_id=d["session_id"],
594
+ agent_version=d.get("agent_version") or "",
595
+ kind=d["kind"],
596
+ learning_id=d["learning_id"],
597
+ is_relevant=None if d.get("is_relevant") is None else bool(d["is_relevant"]),
598
+ relevance_reason=d.get("relevance_reason") or "",
599
+ impact=d.get("impact"),
600
+ impact_reason=d.get("impact_reason") or "",
601
+ created_at=int(d["created_at"]),
602
+ )
603
+
604
+
605
+ def _as_int(value: str) -> int | None:
606
+ try:
607
+ parsed = int(value)
608
+ except (TypeError, ValueError):
609
+ return None
610
+ return parsed if parsed > 0 else None
611
+
612
+
613
+ def _chunked(items: list, size: int = 500) -> list[list]:
614
+ return [items[i : i + size] for i in range(0, len(items), size)]
@@ -22,6 +22,7 @@ class PlaybookSourceLinkageMixin:
22
22
  _fetchall: Any
23
23
  _subject_ref_for_user_id: Any
24
24
  _assert_subject_writable_locked: Any
25
+ _own_transaction: Any
25
26
 
26
27
  @SQLiteStorageBase.handle_exceptions
27
28
  def set_source_user_playbook_ids_for_agent_playbook(
@@ -87,8 +88,10 @@ class PlaybookSourceLinkageMixin:
87
88
  ids.append(source_id)
88
89
  seen.add(source_id)
89
90
  with self._lock:
91
+ own_txn = self._own_transaction()
90
92
  try:
91
- self.conn.execute("BEGIN IMMEDIATE")
93
+ if own_txn:
94
+ self.conn.execute("BEGIN IMMEDIATE")
92
95
  for user_playbook_id in by_id:
93
96
  row = self.conn.execute(
94
97
  """SELECT user_id, governance_subject_ref FROM user_playbooks
@@ -120,9 +123,11 @@ class PlaybookSourceLinkageMixin:
120
123
  for upid, source_interaction_ids in by_id.items()
121
124
  ],
122
125
  )
123
- self.conn.commit()
126
+ if own_txn:
127
+ self.conn.commit()
124
128
  except Exception:
125
- self.conn.rollback()
129
+ if own_txn:
130
+ self.conn.rollback()
126
131
  raise
127
132
 
128
133
  @SQLiteStorageBase.handle_exceptions
@@ -10,6 +10,9 @@ from reflexio.models.api_schema.common import BlockingIssue
10
10
  from reflexio.models.api_schema.retriever_schema import SearchUserPlaybookRequest
11
11
  from reflexio.models.api_schema.service_schemas import Status, UserPlaybook
12
12
  from reflexio.models.config_schema import SearchMode, SearchOptions
13
+ from reflexio.server.services.storage.lifecycle_filters import (
14
+ validate_include_inactive,
15
+ )
13
16
 
14
17
  from .._base import (
15
18
  _TOMBSTONE_STATUS_VALUES,
@@ -71,6 +74,7 @@ class UserPlaybookStoreMixin:
71
74
  _delete_playbook_search_rows: Any
72
75
  _subject_ref_for_user_id: Any
73
76
  _assert_subject_writable_locked: Any
77
+ _own_transaction: Any
74
78
 
75
79
  def _subject_ref_from_user_playbook_row(self, row: sqlite3.Row) -> str:
76
80
  subject_ref = row["governance_subject_ref"]
@@ -96,12 +100,17 @@ class UserPlaybookStoreMixin:
96
100
  )
97
101
  return row
98
102
 
99
- @SQLiteStorageBase.handle_exceptions
100
- def save_user_playbooks(self, user_playbooks: list[UserPlaybook]) -> None:
101
- for up in user_playbooks:
102
- subject_ref = self._subject_ref_for_user_id(up.user_id)
103
- with self._lock:
104
- self._assert_subject_writable_locked(subject_ref)
103
+ def precompute_user_playbook_embeddings(
104
+ self, playbooks: list[UserPlaybook]
105
+ ) -> None:
106
+ """Populate ``.embedding`` / ``.expanded_terms`` in place; no DB write.
107
+
108
+ Extracted verbatim from the former ``save_user_playbooks`` prelude
109
+ (including the ``if embedding_text:`` guard) so the durable
110
+ compute/persist split can embed outside the writer transaction and then
111
+ persist with ``skip_embedding=True``.
112
+ """
113
+ for up in playbooks:
105
114
  embedding_text = up.trigger or up.content
106
115
  if embedding_text:
107
116
  if self._should_expand_documents():
@@ -117,10 +126,30 @@ class UserPlaybookStoreMixin:
117
126
  else:
118
127
  up.embedding = self._get_embedding(embedding_text)
119
128
 
129
+ @SQLiteStorageBase.handle_exceptions
130
+ def save_user_playbooks(
131
+ self,
132
+ user_playbooks: list[UserPlaybook],
133
+ *,
134
+ skip_embedding: bool = False,
135
+ ) -> None:
136
+ for up in user_playbooks:
137
+ subject_ref = self._subject_ref_for_user_id(up.user_id)
138
+ with self._lock:
139
+ self._assert_subject_writable_locked(subject_ref)
140
+ # Default (skip_embedding=False) recomputes unconditionally, exactly
141
+ # as before — model_copy callers that change content while keeping
142
+ # the old embedding depend on this. The durable persist path opts
143
+ # out (embedding already set by precompute_user_playbook_embeddings).
144
+ if not skip_embedding:
145
+ self.precompute_user_playbook_embeddings([up])
146
+
120
147
  created_at_iso = _epoch_to_iso(up.created_at)
121
148
  with self._lock:
149
+ own_txn = self._own_transaction()
122
150
  try:
123
- self.conn.execute("BEGIN IMMEDIATE")
151
+ if own_txn:
152
+ self.conn.execute("BEGIN IMMEDIATE")
124
153
  self._assert_subject_writable_locked(subject_ref)
125
154
  cur = self.conn.execute(
126
155
  """INSERT INTO user_playbooks
@@ -159,9 +188,11 @@ class UserPlaybookStoreMixin:
159
188
  )
160
189
  upid = cur.lastrowid or 0
161
190
  up.user_playbook_id = upid
162
- self.conn.commit()
191
+ if own_txn:
192
+ self.conn.commit()
163
193
  except Exception:
164
- self.conn.rollback()
194
+ if own_txn:
195
+ self.conn.rollback()
165
196
  raise
166
197
 
167
198
  fts_parts = [up.trigger or "", up.content or ""]
@@ -516,13 +547,25 @@ class UserPlaybookStoreMixin:
516
547
  user_id: str,
517
548
  user_playbook_ids: list[int],
518
549
  status_filter: list[Status | None] | None = None,
550
+ *,
551
+ include_inactive: bool = False,
519
552
  ) -> list[UserPlaybook]:
553
+ validate_include_inactive(
554
+ include_inactive=include_inactive, status_filter=status_filter
555
+ )
520
556
  if not user_playbook_ids:
521
557
  return []
558
+ ph = ",".join("?" for _ in user_playbook_ids)
559
+ if include_inactive:
560
+ rows = self._fetchall(
561
+ "SELECT * FROM user_playbooks "
562
+ f"WHERE user_id = ? AND user_playbook_id IN ({ph})",
563
+ (user_id, *user_playbook_ids),
564
+ )
565
+ return [_row_to_user_playbook(r) for r in rows]
522
566
  if status_filter is None:
523
567
  status_filter = [None]
524
568
  frag, sparams = _build_status_sql(status_filter)
525
- ph = ",".join("?" for _ in user_playbook_ids)
526
569
  sql = (
527
570
  f"SELECT * FROM user_playbooks "
528
571
  f"WHERE user_id = ? AND user_playbook_id IN ({ph}) AND {frag}"
@@ -789,7 +832,8 @@ class UserPlaybookStoreMixin:
789
832
  to_status=None,
790
833
  status_namespace=None,
791
834
  )
792
- self.conn.commit()
835
+ if self._own_transaction():
836
+ self.conn.commit()
793
837
 
794
838
  @SQLiteStorageBase.handle_exceptions
795
839
  def supersede_user_playbooks_by_ids(
@@ -840,5 +884,6 @@ class UserPlaybookStoreMixin:
840
884
  request_id=request_id,
841
885
  )
842
886
  updated += 1
843
- self.conn.commit()
887
+ if self._own_transaction():
888
+ self.conn.commit()
844
889
  return updated