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
@@ -0,0 +1,269 @@
1
+ """ABC, dataclass, and enum for the durable learning-job queue (Task 3)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+ from typing import Literal
7
+
8
+ # Upper bound of the done-row retention window.
9
+ # Shared by all backends so the value cannot drift between implementations.
10
+ # Err toward "not done" until we are sure a done row would have been GC'd.
11
+ _ABSENCE_DONE_AFTER_SECONDS = 72 * 3600
12
+
13
+ # Coverage-based status reported for a single request (§3.6). This is a
14
+ # collapsed view of the raw job statuses (e.g. claimed -> processing,
15
+ # reclaimable failed -> pending) and matches LearningStatusResponse.status.
16
+ LearningStatus = Literal["pending", "processing", "done", "failed"]
17
+
18
+
19
+ class LearningJobStatus(StrEnum):
20
+ PENDING = "pending"
21
+ CLAIMED = "claimed"
22
+ DONE = "done"
23
+ FAILED = "failed"
24
+ DEAD = "dead"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class LearningJob:
29
+ job_id: str
30
+ org_id: str
31
+ user_id: str
32
+ job_type: str
33
+ latest_request_id: str | None
34
+ status: str
35
+ attempts: int
36
+ claim_token: str | None
37
+ covers_through: (
38
+ float | None
39
+ ) # epoch seconds (converted from stored ISO/timestamptz)
40
+ force_extraction: bool = False
41
+ skip_aggregation: bool = False
42
+ max_attempts: int = 3
43
+
44
+
45
+ class LearningJobStoreABC(ABC):
46
+ """Abstract interface for durable learning-job queue operations.
47
+
48
+ All methods are scoped to the storage instance's own org (``self.org_id``).
49
+ ``enqueue_learning_job`` and ``complete_learning_job`` are safe to call
50
+ inside a ``commit_scope`` — they issue plain SQL on the scope's connection
51
+ without owning a separate BEGIN/COMMIT.
52
+ """
53
+
54
+ @abstractmethod
55
+ def enqueue_learning_job(
56
+ self,
57
+ *,
58
+ org_id: str,
59
+ user_id: str,
60
+ request_id: str,
61
+ covers_through: float,
62
+ job_type: str = "learning",
63
+ force_extraction: bool = False,
64
+ skip_aggregation: bool = False,
65
+ ) -> str:
66
+ """Coalescing upsert into the learning_jobs queue.
67
+
68
+ If a pending job for ``(org_id, user_id, job_type)`` already exists,
69
+ update its ``latest_request_id`` and keep the max ``covers_through``;
70
+ otherwise insert a new ``pending`` row. Returns the ``job_id`` of the
71
+ pending row (existing or newly inserted).
72
+
73
+ Safe to call inside a ``commit_scope`` — no own BEGIN/COMMIT issued.
74
+ """
75
+ raise NotImplementedError
76
+
77
+ @abstractmethod
78
+ def claim_learning_jobs(
79
+ self,
80
+ *,
81
+ claimed_by: str,
82
+ limit: int,
83
+ lease_seconds: int,
84
+ ) -> list[LearningJob]:
85
+ """Atomically claim up to ``limit`` of this org's claimable jobs.
86
+
87
+ A job is claimable when it is ``pending``, ``failed`` (reclaimable —
88
+ attempts < max_attempts), OR (``claimed`` AND
89
+ ``claim_expires_at < now``). Each claimed job receives a fresh
90
+ ``claim_token``, ``claimed_by``, and ``claim_expires_at``.
91
+
92
+ Postgres/Supabase: uses ``FOR UPDATE SKIP LOCKED`` via the
93
+ ``claim_learning_jobs`` SQL function. SQLite: ``BEGIN IMMEDIATE``.
94
+ The claim predicate uses the DB's ``now()`` to avoid app/DB clock skew.
95
+ """
96
+ raise NotImplementedError
97
+
98
+ @abstractmethod
99
+ def heartbeat_learning_job(
100
+ self,
101
+ *,
102
+ job_id: str,
103
+ claim_token: str,
104
+ lease_seconds: int,
105
+ ) -> bool:
106
+ """Extend the lease on a claimed job.
107
+
108
+ Updates ``claim_expires_at = now + lease_seconds`` only when
109
+ ``claim_token`` matches and status is still ``claimed``.
110
+
111
+ Returns:
112
+ True if the lease was extended (rowcount == 1), False if the token
113
+ was superseded or the job is no longer claimed.
114
+ """
115
+ raise NotImplementedError
116
+
117
+ @abstractmethod
118
+ def complete_learning_job(
119
+ self,
120
+ *,
121
+ job_id: str,
122
+ claim_token: str,
123
+ ) -> int:
124
+ """Fenced transition to ``done``.
125
+
126
+ Executes::
127
+
128
+ UPDATE learning_jobs
129
+ SET status='done', updated_at=now()
130
+ WHERE job_id=:job_id AND claim_token=:claim_token AND status='claimed'
131
+
132
+ Returns:
133
+ rowcount — 0 if the token was superseded or the job is already
134
+ done/dead/failed; 1 on success. Callers MUST raise on 0 to roll
135
+ back the enclosing ``commit_scope``.
136
+
137
+ Safe to call inside a ``commit_scope`` — no own BEGIN/COMMIT issued.
138
+ """
139
+ raise NotImplementedError
140
+
141
+ @abstractmethod
142
+ def fail_learning_job(
143
+ self,
144
+ *,
145
+ job_id: str,
146
+ claim_token: str,
147
+ dead: bool,
148
+ refund_attempt: bool = False,
149
+ ) -> None:
150
+ """Fenced fail/dead transition — sets status, does NOT increment attempts.
151
+
152
+ Fenced on ``claim_token`` and ``status='claimed'``. Sets
153
+ ``status='dead'`` when ``dead=True``, else ``status='failed'`` (and
154
+ clears ``claim_token``/``claim_expires_at`` so the job is reclaimable).
155
+
156
+ ``attempts`` is intentionally NOT incremented here. It is incremented
157
+ once per delivery attempt exclusively by ``claim_learning_jobs``; the
158
+ worker's dead-gate (``job.attempts >= max_attempts``) depends on that
159
+ single-increment-per-claim invariant. Incrementing here would
160
+ double-count and misfire the dead transition.
161
+
162
+ ``refund_attempt`` (default ``False``): when ``True``, decrement
163
+ ``attempts`` by one (floored at 0) as part of this transition. This is
164
+ the same-user-contention requeue path (F4): the worker claimed the job
165
+ (``claim_learning_jobs`` did ``attempts += 1``) but could not run because
166
+ another same-user job holds the per-user lock, so it releases the job
167
+ WITHOUT having done any real work. Refunding the claim's increment makes
168
+ a contention cycle (claim +1, contention-release -1) net to zero, so a
169
+ job that loses the same-user race repeatedly (every ~2s poll while the
170
+ holder runs its ~60s compute) does NOT inflate ``attempts`` past
171
+ ``max_attempts`` and dead-letter with zero real retries. It is a no-op
172
+ unless ``refund_attempt=True``, so the ``dead`` retry path is unchanged.
173
+
174
+ # TASK 10: the enterprise Supabase (``reflexio_ext/server/services/
175
+ # storage/supabase_storage/_learning_jobs.py``) and native-Postgres
176
+ # ``fail_learning_job`` impls (and their ``claim_learning_jobs`` SQL
177
+ # functions) need this same ``refund_attempt`` decrement. The param is
178
+ # optional with a default so this base signature stays back-compatible
179
+ # until they are updated.
180
+ """
181
+ raise NotImplementedError
182
+
183
+ @abstractmethod
184
+ def list_org_ids_with_pending_learning_jobs(self) -> list[str]:
185
+ """Return distinct org_ids that have actionable learning jobs.
186
+
187
+ Cross-org discovery query for the :class:`DurableLearningScheduler`:
188
+ surfaces every org with at least one job that is ``pending``, ``failed``
189
+ (reclaimable), or ``claimed`` with an expired lease (a stolen/abandoned
190
+ claim). Terminal ``done``/``dead`` rows and live claims are excluded.
191
+
192
+ Mirrors ``list_resumable_work_org_ids``: NOT scoped to ``self.org_id`` —
193
+ on the shared global table (Postgres ``public.learning_jobs``, or a
194
+ SQLite DB shared across orgs) it returns every org with work in one
195
+ query so the scheduler need not enumerate all orgs.
196
+
197
+ Returns:
198
+ list[str]: Distinct org_ids with actionable work, ordered ascending.
199
+ """
200
+ raise NotImplementedError
201
+
202
+ @abstractmethod
203
+ def get_oldest_pending_learning_job_age_seconds(self) -> float | None:
204
+ """Age in seconds of the oldest ``status='pending'`` job on this storage ref.
205
+
206
+ Returns:
207
+ The age in seconds (``now - created_at``) of the oldest pending job,
208
+ or ``None`` if there are no pending jobs.
209
+
210
+ Resolves the table via the same placement resolver used by
211
+ ``list_org_ids_with_pending_learning_jobs`` — never hardcodes
212
+ ``public.`` or ``reflexio_ops.``.
213
+ """
214
+ raise NotImplementedError
215
+
216
+ @abstractmethod
217
+ def count_learning_jobs_by_status(self, status: str) -> int:
218
+ """Count of learning jobs with the given ``status`` on this storage ref.
219
+
220
+ Args:
221
+ status: One of ``'pending'``, ``'claimed'``, ``'done'``,
222
+ ``'failed'``, or ``'dead'``.
223
+
224
+ Returns:
225
+ The integer count of matching rows (0 if none).
226
+
227
+ Resolves the table via the same placement resolver used by the other
228
+ query methods — never hardcodes schema prefix.
229
+ """
230
+ raise NotImplementedError
231
+
232
+ @abstractmethod
233
+ def get_learning_status_for_request(
234
+ self,
235
+ *,
236
+ user_id: str,
237
+ request_created_at: float,
238
+ ) -> LearningStatus:
239
+ """Coverage-based status for a single request (§3.6 rule).
240
+
241
+ Returns one of ``"done" | "processing" | "pending" | "failed"``.
242
+
243
+ Coverage rule (evaluated in priority order):
244
+
245
+ 1. Any ``done`` job with ``covers_through >= request_created_at``
246
+ → ``"done"`` (correct even for zero-yield windows).
247
+ 2. Any ``claimed`` job (any ``covers_through``)
248
+ → ``"processing"``.
249
+ 3. Any ``pending`` job for this user
250
+ → ``"pending"``.
251
+ 4. Any ``failed`` (reclaimable) job
252
+ → ``"pending"``.
253
+ 5. Any ``dead`` job with ``covers_through >= request_created_at``
254
+ → ``"failed"``.
255
+ 6. No rows AND ``now - request_created_at >= 72 h`` (retention window)
256
+ → ``"done"`` (done row would have been GC'd by now).
257
+ 7. No rows AND request is recent
258
+ → ``"pending"`` (err toward not-done until retention window passes).
259
+ """
260
+ raise NotImplementedError
261
+
262
+
263
+ __all__ = [
264
+ "_ABSENCE_DONE_AFTER_SECONDS",
265
+ "LearningJob",
266
+ "LearningJobStatus",
267
+ "LearningStatus",
268
+ "LearningJobStoreABC",
269
+ ]
@@ -146,6 +146,10 @@ class OperationMixin:
146
146
  the current holder) are dropped to keep the queue idempotent under
147
147
  publish retries.
148
148
 
149
+ ``request_id`` is the intentional storage-boundary name here. Higher
150
+ layers may call the same value ``lock_request_id``, but persisted lock
151
+ rows and queued entries still use the legacy ``request_id`` key.
152
+
149
153
  The queue is a FIFO drained one entry at a time when the holder
150
154
  releases the lock. It replaces the older single-slot
151
155
  ``pending_request_id`` field, which silently dropped earlier blocked
@@ -178,6 +182,9 @@ class OperationMixin:
178
182
  ) -> bool:
179
183
  """Atomically clear an in-progress lock only if ``request_id`` still owns it.
180
184
 
185
+ The storage boundary keeps the legacy ``request_id`` naming for the
186
+ persisted lock holder even when callers use clearer local names.
187
+
181
188
  Args:
182
189
  state_key: Operation-state row key for the lock.
183
190
  request_id: Request attempting to release the lock.
@@ -7,7 +7,9 @@ class RetrievalLogMixin:
7
7
  These methods are optional retrieval-capture storage hooks. They are
8
8
  intentionally concrete (not ``@abstractmethod``) so concrete storage classes
9
9
  remain instantiable; each raises ``NotImplementedError`` until a backend
10
- provides a real implementation.
10
+ provides a real implementation. Stored item rows may use either the legacy
11
+ ``agent_playbook_id`` field or the explicit ``target_kind`` / ``target_id``
12
+ pair; readers should preserve backward compatibility for both.
11
13
  """
12
14
 
13
15
  def save_playbook_retrieval_log(self, log: PlaybookRetrievalLog) -> int:
@@ -34,6 +34,10 @@ class ShadowVerdictsMixin:
34
34
  clear error message naming the backend rather than ``AttributeError``.
35
35
  """
36
36
 
37
+ def supports_shadow_comparison_verdicts(self) -> bool:
38
+ """Return whether this backend can persist shadow comparison verdicts."""
39
+ return False
40
+
37
41
  def save_shadow_comparison_verdict(
38
42
  self, verdict: ShadowComparisonVerdict
39
43
  ) -> ShadowComparisonVerdict:
@@ -44,7 +44,12 @@ class RunToolDependencyKind(StrEnum):
44
44
 
45
45
  @dataclass(frozen=True)
46
46
  class AgentBinding:
47
- """Logical run binding flattened into `_agent_runs` storage columns."""
47
+ """Logical run binding flattened into `_agent_runs` storage columns.
48
+
49
+ ``request_id`` remains the persisted provenance field name at this storage
50
+ boundary even when higher-level helpers refer to the same value as a
51
+ generation request id.
52
+ """
48
53
 
49
54
  org_id: str
50
55
  extractor_kind: str
@@ -60,6 +65,12 @@ class AgentBinding:
60
65
 
61
66
  @dataclass(frozen=True)
62
67
  class AgentRunRecord:
68
+ """Durable agent run row plus snapshots used for resume/finalization.
69
+
70
+ ``generation_request_snapshot`` intentionally keeps a legacy ``request_id``
71
+ key for stored provenance.
72
+ """
73
+
63
74
  id: str
64
75
  binding: AgentBinding
65
76
  status: AgentRunStatus
@@ -0,0 +1,78 @@
1
+ """Single source of truth for evaluation ``_operation_state`` key formats.
2
+
3
+ Three evaluation namespaces store per-session state in ``_operation_state``:
4
+
5
+ 1. ``agent_success_group_eval`` — the runner's "evaluated" marker.
6
+ 2. ``grade_on_demand`` — the 24h on-demand grading cache.
7
+ 3. ``retrieved_learning_eval`` — retrieved-learning generation/completion
8
+ state (see ``retrieved_learning_state.build_retrieved_learning_state_key``).
9
+
10
+ Governance erasure must scrub all three for an erased user's sessions, so the
11
+ key builders live here where both the producers (runner / evaluation route)
12
+ and the erasers (sqlite + supabase governance) can import them without
13
+ layering cycles.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ AGENT_SUCCESS_MARKER_PREFIX = "agent_success_group_eval"
19
+ GRADE_ON_DEMAND_CACHE_PREFIX = "grade_on_demand"
20
+
21
+
22
+ def build_agent_success_marker_key(org_id: str, user_id: str, session_id: str) -> str:
23
+ """Build the agent-success "evaluated" marker key for a session.
24
+
25
+ Historical plain ``::`` join — kept byte-identical so existing markers
26
+ stay addressable.
27
+
28
+ Args:
29
+ org_id (str): Organization ID.
30
+ user_id (str): Session owner.
31
+ session_id (str): Evaluated session.
32
+
33
+ Returns:
34
+ str: The marker key.
35
+ """
36
+ return f"{AGENT_SUCCESS_MARKER_PREFIX}::{org_id}::{user_id}::{session_id}"
37
+
38
+
39
+ def build_grade_on_demand_cache_key(
40
+ org_id: str, session_id: str, agent_version: str, evaluation_name: str
41
+ ) -> str:
42
+ """Build the grade-on-demand cache key (length-prefixed components).
43
+
44
+ Length-prefixing each free-form component keeps the join injective even
45
+ when a component contains the ``::`` delimiter.
46
+
47
+ Args:
48
+ org_id (str): Organization ID.
49
+ session_id (str): Target session.
50
+ agent_version (str): Agent version filter.
51
+ evaluation_name (str): Evaluator/result namespace.
52
+
53
+ Returns:
54
+ str: The cache key.
55
+ """
56
+ parts = "::".join(
57
+ f"{len(s)}:{s}" for s in (org_id, session_id, agent_version, evaluation_name)
58
+ )
59
+ return f"{GRADE_ON_DEMAND_CACHE_PREFIX}::{parts}"
60
+
61
+
62
+ def build_grade_on_demand_session_prefix(org_id: str, session_id: str) -> str:
63
+ """Prefix matching every grade-on-demand cache key for one session.
64
+
65
+ Used by governance erasure to find a session's cache rows regardless of
66
+ agent_version / evaluation_name.
67
+
68
+ Args:
69
+ org_id (str): Organization ID.
70
+ session_id (str): Target session.
71
+
72
+ Returns:
73
+ str: The per-session key prefix (ends with ``::``).
74
+ """
75
+ return (
76
+ f"{GRADE_ON_DEMAND_CACHE_PREFIX}"
77
+ f"::{len(org_id)}:{org_id}::{len(session_id)}:{session_id}::"
78
+ )
@@ -161,6 +161,44 @@ class AgentPlaybookStoreMixin:
161
161
  """
162
162
  raise NotImplementedError
163
163
 
164
+ @abstractmethod
165
+ def get_agent_playbooks_by_ids(
166
+ self,
167
+ agent_playbook_ids: list[int],
168
+ *,
169
+ status_filter: list[Status | None] | None = None,
170
+ playbook_status_filter: list[PlaybookStatus] | None = None,
171
+ include_inactive: bool = False,
172
+ ) -> list[AgentPlaybook]:
173
+ """Fetch agent playbooks in bulk with lifecycle filters.
174
+
175
+ Args:
176
+ agent_playbook_ids (list[int]): Playbook ids to fetch. Empty list
177
+ returns ``[]`` without hitting storage.
178
+ status_filter (list[Status | None] | None): Lifecycle statuses to
179
+ include. ``None`` (default) means CURRENT only.
180
+ playbook_status_filter (list[PlaybookStatus] | None): Approval
181
+ statuses to include. ``None`` (default) means no approval filter.
182
+ include_inactive (bool): Return every matching row regardless of
183
+ lifecycle status *or* approval status. This is the *historical
184
+ resolution* mode (see ``RetrievedLearningEvaluator``): it answers
185
+ "what did this id point at", not "what is retrievable now", so an
186
+ archived or never-approved playbook that was actually served is
187
+ still returned. It is a strict superset of ``include_tombstones``
188
+ on ``get_agent_playbook_by_id``, which only unhides
189
+ MERGED/SUPERSEDED for lineage walks. The default preserves
190
+ retrieval behavior.
191
+
192
+ Returns:
193
+ list[AgentPlaybook]: Matching playbooks. Order is unspecified.
194
+
195
+ Raises:
196
+ StorageError: If ``include_inactive`` is combined with an explicit
197
+ ``status_filter`` or ``playbook_status_filter`` — the two are
198
+ contradictory.
199
+ """
200
+ raise NotImplementedError
201
+
164
202
  @abstractmethod
165
203
  def delete_all_agent_playbooks(self) -> None:
166
204
  """Delete all agent playbooks from storage."""
@@ -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