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
@@ -2,14 +2,20 @@
2
2
 
3
3
  Fetches all requests and interactions for a session,
4
4
  checks completion status, runs evaluation, and marks the group as evaluated.
5
+
6
+ Also runs the retrieved-learning relevance/impact evaluation for the session
7
+ after agent-success work completes. The two completions are independent: the
8
+ existing ``agent_success_group_eval`` marker stays agent-success-only, and the
9
+ retrieved evaluation keeps its own generation/fingerprint-fenced state (see
10
+ ``storage_base/retrieved_learning_state.py``).
5
11
  """
6
12
 
7
13
  import logging
8
- import random
9
14
  from collections import defaultdict
15
+ from dataclasses import dataclass
10
16
  from datetime import UTC, datetime
17
+ from typing import Literal
11
18
 
12
- from reflexio.models.api_schema.domain.entities import Interaction
13
19
  from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
14
20
  from reflexio.server.api_endpoints.request_context import RequestContext
15
21
  from reflexio.server.llm.litellm_client import LiteLLMClient
@@ -18,6 +24,9 @@ from reflexio.server.services.agent_success_evaluation._eval_health import SkipR
18
24
  from reflexio.server.services.agent_success_evaluation.agent_success_evaluation_utils import (
19
25
  AgentSuccessEvaluationRequest,
20
26
  )
27
+ from reflexio.server.services.agent_success_evaluation.components.retrieved_learning_evaluator import (
28
+ RetrievedLearningEvaluator,
29
+ )
21
30
  from reflexio.server.services.agent_success_evaluation.scheduler import (
22
31
  _EFFECTIVE_DELAY_SECONDS,
23
32
  )
@@ -25,17 +34,53 @@ from reflexio.server.services.agent_success_evaluation.service import (
25
34
  AgentSuccessEvaluationService,
26
35
  )
27
36
  from reflexio.server.services.extractor_config_utils import get_extractor_name
28
- from reflexio.server.services.shadow_comparison.judge import ShadowComparisonJudge
37
+ from reflexio.server.services.storage.storage_base.evaluation_state_keys import (
38
+ build_agent_success_marker_key,
39
+ )
40
+ from reflexio.server.services.storage.storage_base.retrieved_learning_state import (
41
+ session_fingerprint,
42
+ )
29
43
 
30
44
  logger = logging.getLogger(__name__)
31
45
 
32
46
  # Key prefix for operation state tracking
33
47
  OPERATION_STATE_KEY_PREFIX = "agent_success_group_eval"
34
48
 
49
+ type AgentSuccessInvocationStatus = Literal[
50
+ "complete", "failed", "not_applicable", "skipped"
51
+ ]
52
+ type RetrievedLearningInvocationStatus = Literal[
53
+ "pending",
54
+ "complete",
55
+ "degraded",
56
+ "failed",
57
+ "not_applicable",
58
+ "stale",
59
+ "superseded",
60
+ "skipped",
61
+ ]
62
+
63
+
64
+ @dataclass
65
+ class GroupEvaluationOutcome:
66
+ """What one runner invocation did, per evaluation family.
67
+
68
+ ``retrieved_learning_fingerprint`` carries the session fingerprint at
69
+ which a terminal/applied retrieved outcome linearized; ``None`` for
70
+ nonterminal or skipped-without-state outcomes.
71
+ """
72
+
73
+ agent_success_status: AgentSuccessInvocationStatus
74
+ retrieved_learning_status: RetrievedLearningInvocationStatus
75
+ retrieved_learning_fingerprint: str | None = None
76
+
35
77
 
36
78
  def _build_state_key(org_id: str, user_id: str, session_id: str) -> str:
37
79
  """Build the operation state key for a session.
38
80
 
81
+ Delegates to the shared key builder so governance erasure scrubs the
82
+ exact same keys this runner writes.
83
+
39
84
  Args:
40
85
  org_id: Organization ID
41
86
  user_id: User ID
@@ -44,7 +89,7 @@ def _build_state_key(org_id: str, user_id: str, session_id: str) -> str:
44
89
  Returns:
45
90
  str: The operation state key
46
91
  """
47
- return f"{OPERATION_STATE_KEY_PREFIX}::{org_id}::{user_id}::{session_id}"
92
+ return build_agent_success_marker_key(org_id, user_id, session_id)
48
93
 
49
94
 
50
95
  def run_group_evaluation(
@@ -57,7 +102,7 @@ def run_group_evaluation(
57
102
  llm_client: LiteLLMClient,
58
103
  *,
59
104
  force_regenerate: bool = False,
60
- ) -> None:
105
+ ) -> GroupEvaluationOutcome:
61
106
  """Run agent success evaluation for an entire session.
62
107
 
63
108
  Steps:
@@ -84,30 +129,23 @@ def run_group_evaluation(
84
129
  force_regenerate: When True, bypass the already-evaluated short-circuit
85
130
  and the completeness delay gate so the regenerate worker can
86
131
  re-evaluate sessions of any age regardless of prior state.
132
+
133
+ Returns:
134
+ GroupEvaluationOutcome: Per-family statuses for this invocation.
87
135
  """
88
136
  storage = request_context.storage
89
137
  state_key = _build_state_key(org_id, user_id, session_id)
90
138
 
91
- # 1. Check if already evaluated skipped in force_regenerate mode so the
92
- # regenerate worker can re-evaluate a session that's already been marked.
93
- if not force_regenerate:
94
- existing_state = storage.get_operation_state(state_key) # type: ignore[reportOptionalMemberAccess]
95
- if existing_state and isinstance(existing_state.get("operation_state"), dict):
96
- op_state = existing_state["operation_state"]
97
- if op_state.get("evaluated"):
98
- _eval_health.record_skip(SkipReason.ALREADY_EVALUATED)
99
- logger.info("Session %s already evaluated, skipping", session_id)
100
- return
101
-
102
- # 2. Fetch all requests for the session
139
+ # 1. Fetch all requests for the session
103
140
  requests = storage.get_requests_by_session(user_id, session_id) # type: ignore[reportOptionalMemberAccess]
104
141
  if not requests:
105
142
  _eval_health.record_skip(SkipReason.NO_REQUESTS)
106
143
  logger.info("No requests found for session %s, skipping", session_id)
107
- return
144
+ return GroupEvaluationOutcome("not_applicable", "skipped")
108
145
 
109
- # 3. Verify completion: latest request must be >= delay ago — skipped in
110
- # force_regenerate mode so the operator can re-evaluate any session.
146
+ # 2. Verify completion: latest request must be >= delay ago — skipped in
147
+ # force_regenerate mode so the operator can re-evaluate any session. The
148
+ # liveness gate applies to both evaluation families.
111
149
  if not force_regenerate:
112
150
  latest_created_at = max(r.created_at for r in requests)
113
151
  now = int(datetime.now(UTC).timestamp())
@@ -120,7 +158,36 @@ def run_group_evaluation(
120
158
  elapsed,
121
159
  _EFFECTIVE_DELAY_SECONDS,
122
160
  )
123
- return
161
+ return GroupEvaluationOutcome("skipped", "skipped")
162
+
163
+ # 3. Check if agent success is already evaluated — skipped in
164
+ # force_regenerate mode so the regenerate worker can re-evaluate a session
165
+ # that's already been marked. Retrieved-learning evaluation still runs
166
+ # below: its completion is independent of the agent-success marker.
167
+ agent_success_already_evaluated = False
168
+ if not force_regenerate:
169
+ existing_state = storage.get_operation_state(state_key) # type: ignore[reportOptionalMemberAccess]
170
+ if existing_state and isinstance(existing_state.get("operation_state"), dict):
171
+ op_state = existing_state["operation_state"]
172
+ if op_state.get("evaluated"):
173
+ _eval_health.record_skip(SkipReason.ALREADY_EVALUATED)
174
+ logger.info(
175
+ "Session %s already evaluated (agent success), skipping to"
176
+ " retrieved-learning evaluation",
177
+ session_id,
178
+ )
179
+ agent_success_already_evaluated = True
180
+
181
+ if agent_success_already_evaluated:
182
+ return _finish_with_retrieved_evaluation(
183
+ "skipped",
184
+ user_id=user_id,
185
+ session_id=session_id,
186
+ agent_version=agent_version,
187
+ request_context=request_context,
188
+ llm_client=llm_client,
189
+ force_regenerate=force_regenerate,
190
+ )
124
191
 
125
192
  # 4. Fetch interactions for all requests
126
193
  request_ids = [r.request_id for r in requests]
@@ -128,7 +195,7 @@ def run_group_evaluation(
128
195
  if not all_interactions:
129
196
  _eval_health.record_skip(SkipReason.NO_INTERACTIONS)
130
197
  logger.info("No interactions found for session %s, skipping", session_id)
131
- return
198
+ return GroupEvaluationOutcome("not_applicable", "skipped")
132
199
 
133
200
  # Group interactions by request_id
134
201
  interactions_by_request: dict[str, list] = defaultdict(list)
@@ -157,7 +224,7 @@ def run_group_evaluation(
157
224
  "No request interaction data models built for session %s, skipping",
158
225
  session_id,
159
226
  )
160
- return
227
+ return GroupEvaluationOutcome("not_applicable", "skipped")
161
228
 
162
229
  # 5. When regenerating, capture the prior result_ids
163
230
  # so we can delete ONLY them AFTER the new rows have been saved. Doing
@@ -206,7 +273,7 @@ def run_group_evaluation(
206
273
  evaluation_service.last_run_save_failed,
207
274
  len(old_result_ids),
208
275
  )
209
- return
276
+ return GroupEvaluationOutcome("failed", "skipped")
210
277
 
211
278
  if evaluation_service.last_run_saved_result_count == 0:
212
279
  logger.warning(
@@ -215,21 +282,7 @@ def run_group_evaluation(
215
282
  session_id,
216
283
  len(old_result_ids),
217
284
  )
218
- return
219
-
220
- # F1: per-turn shadow comparison. Dispatched only AFTER the regular
221
- # success eval succeeds — a session whose success grade is unreliable
222
- # would yield noisy verdicts that mislead the headline metric. The
223
- # dispatch loop swallows per-interaction failures so one judge call
224
- # cannot abort an entire batch.
225
- _dispatch_shadow_comparison_judge(
226
- storage=storage,
227
- interactions=all_interactions,
228
- session_id=session_id,
229
- agent_version=agent_version,
230
- request_context=request_context,
231
- llm_client=llm_client,
232
- )
285
+ return GroupEvaluationOutcome("failed", "skipped")
233
286
 
234
287
  # 6. New rows saved successfully — now safe to remove the captured prior
235
288
  # rows. New rows have fresh auto-increment result_ids that do not overlap
@@ -254,85 +307,184 @@ def run_group_evaluation(
254
307
  )
255
308
  logger.info("Marked session %s as evaluated at %d", session_id, evaluated_at)
256
309
 
310
+ # 8. Retrieved-learning evaluation — independent completion; a failure
311
+ # here can never be reported as complete nor force agent-success rows to
312
+ # regenerate.
313
+ return _finish_with_retrieved_evaluation(
314
+ "complete",
315
+ user_id=user_id,
316
+ session_id=session_id,
317
+ agent_version=agent_version,
318
+ request_context=request_context,
319
+ llm_client=llm_client,
320
+ force_regenerate=force_regenerate,
321
+ )
322
+
257
323
 
258
- def _dispatch_shadow_comparison_judge(
324
+ def _finish_with_retrieved_evaluation(
325
+ agent_success_status: AgentSuccessInvocationStatus,
259
326
  *,
260
- storage, # noqa: ANN001 — BaseStorage; imported lazily to avoid cycles
261
- interactions: list[Interaction],
327
+ user_id: str,
262
328
  session_id: str,
263
329
  agent_version: str,
264
330
  request_context: RequestContext,
265
331
  llm_client: LiteLLMClient,
266
- ) -> None:
267
- """F1: grade each shadow-bearing interaction with the per-turn judge.
332
+ force_regenerate: bool,
333
+ ) -> GroupEvaluationOutcome:
334
+ """Run the retrieved-learning phase best-effort and build the outcome."""
335
+ try:
336
+ retrieved_status, fingerprint = _run_retrieved_learning_evaluation(
337
+ user_id=user_id,
338
+ session_id=session_id,
339
+ agent_version=agent_version,
340
+ request_context=request_context,
341
+ llm_client=llm_client,
342
+ force_regenerate=force_regenerate,
343
+ )
344
+ except Exception:
345
+ # Best-effort: never let the retrieved phase break the runner. The
346
+ # generation-guarded state was not advanced to a terminal status, so
347
+ # the next scheduled or forced run retries.
348
+ logger.exception(
349
+ "event=retrieved_learning_eval_failed session_id=%s reason=unexpected_error",
350
+ session_id,
351
+ )
352
+ _eval_health.record_retrieved_outcome("failed")
353
+ _eval_health.record_producer_failure()
354
+ return GroupEvaluationOutcome(agent_success_status, "failed")
355
+ return GroupEvaluationOutcome(agent_success_status, retrieved_status, fingerprint)
268
356
 
269
- Iterates the session's interactions, skips any without ``shadow_content``,
270
- invokes :class:`ShadowComparisonJudge.judge_turn`, and persists each
271
- returned verdict via ``storage.save_shadow_comparison_verdict``. Per-
272
- interaction exceptions are logged and the loop continues — partial
273
- verdict sets are strictly better than nothing for the headline metric.
274
357
 
275
- Args:
276
- storage: The session storage. Must implement
277
- ``save_shadow_comparison_verdict`` (currently SQLite + Supabase
278
- + disk; backends without it surface ``NotImplementedError`` at
279
- save time and the loop logs+continues).
280
- interactions (list[Interaction]): Every interaction in the session,
281
- in chronological order. Only those with non-empty
282
- ``shadow_content`` are graded.
283
- session_id (str): Denormalized onto each verdict.
284
- agent_version (str): Denormalized onto each verdict.
285
- request_context (RequestContext): Provides the configurator (for
286
- the pinned ``shadow_comparison_judge_prompt_version``) and the
287
- shared ``prompt_manager``.
288
- llm_client (LiteLLMClient): The unified LLM client the judge uses
289
- for the structured-output call.
358
+ def _run_retrieved_learning_evaluation(
359
+ *,
360
+ user_id: str,
361
+ session_id: str,
362
+ agent_version: str,
363
+ request_context: RequestContext,
364
+ llm_client: LiteLLMClient,
365
+ force_regenerate: bool,
366
+ ) -> tuple[RetrievedLearningInvocationStatus, str | None]:
367
+ """Evaluate the session's retrieved learnings with fencing.
368
+
369
+ Implements the generation + session-fingerprint protocol: allocate a
370
+ generation, judge the post-allocation snapshot, and atomically replace
371
+ the session's result set only while the fingerprint recomputed under the
372
+ replacement lock still matches. One immediate retry on a stale snapshot,
373
+ then ``pending`` for the next trigger.
290
374
 
291
375
  Returns:
292
- None: Verdicts are persisted as a side effect; the caller does not
293
- need the count for control flow.
376
+ tuple: (invocation status, session fingerprint for terminal/applied
377
+ outcomes else None).
294
378
  """
295
- config = request_context.configurator.get_config() # type: ignore[reportOptionalMemberAccess]
296
- judge = ShadowComparisonJudge(
379
+ storage = request_context.storage
380
+ if storage is None:
381
+ return "skipped", None
382
+
383
+ snapshot = storage.load_bounded_retrieved_learning_snapshot(user_id, session_id)
384
+ fingerprint = session_fingerprint(snapshot)
385
+
386
+ # Fast path: terminal state at this exact fingerprint — nothing changed.
387
+ if not force_regenerate:
388
+ terminal = storage.get_matching_retrieved_learning_terminal_state(
389
+ user_id, session_id, fingerprint
390
+ )
391
+ if terminal:
392
+ status = terminal.get("status")
393
+ if status in ("complete", "not_applicable"):
394
+ return status, fingerprint
395
+
396
+ config = request_context.configurator.get_config()
397
+ agent_success = config.agent_success_config if config else None
398
+ success_definition = (
399
+ agent_success.success_definition_prompt.strip()
400
+ if agent_success and agent_success.success_definition_prompt
401
+ else ""
402
+ )
403
+ evaluator = RetrievedLearningEvaluator(
404
+ request_context=request_context,
297
405
  llm_client=llm_client,
298
- prompt_manager=request_context.prompt_manager, # type: ignore[reportOptionalMemberAccess]
299
- prompt_version=config.shadow_comparison_judge_prompt_version,
406
+ agent_context=(config.agent_context_prompt or "") if config else "",
407
+ success_definition=success_definition,
408
+ )
409
+
410
+ logger.info(
411
+ "event=retrieved_learning_eval_started session_id=%s raw_attachments=%d",
412
+ session_id,
413
+ snapshot.raw_attachment_count,
300
414
  )
301
- rng = random.Random() # noqa: S311 — position randomization, not crypto
302
- saved_count = 0
303
-
304
- for interaction in interactions:
305
- if not interaction.shadow_content:
306
- continue
307
- try:
308
- verdict = judge.judge_turn(
309
- interaction=interaction,
310
- session_id=session_id,
311
- agent_version=agent_version,
312
- rng=rng,
415
+
416
+ generation = 0
417
+ for _stale_attempt in range(2):
418
+ generation = storage.begin_retrieved_learning_evaluation_run(
419
+ user_id, session_id
420
+ )
421
+ # Judge the post-allocation snapshot, never the freshness-check one:
422
+ # every mutation is either visible here or changes the fingerprint
423
+ # that replacement recomputes under lock at commit time.
424
+ snapshot = storage.load_bounded_retrieved_learning_snapshot(user_id, session_id)
425
+ fingerprint = session_fingerprint(snapshot)
426
+ run = evaluator.evaluate(user_id, session_id, agent_version, snapshot)
427
+ if run.outcome == "failed":
428
+ storage.finish_retrieved_learning_evaluation_run(
429
+ user_id, session_id, generation, "failed", run.diagnostics
313
430
  )
314
- except Exception as exc: # noqa: BLE001 — judge failure must not abort batch
315
431
  logger.warning(
316
- "F1 shadow_comparison dispatch failed for interaction %s: %s",
317
- interaction.interaction_id,
318
- exc,
432
+ "event=retrieved_learning_eval_failed session_id=%s reason=%s",
433
+ session_id,
434
+ run.diagnostics.get("error_type", "unknown"),
319
435
  )
320
- continue
321
- if verdict is None:
322
- continue
323
- try:
324
- storage.save_shadow_comparison_verdict(verdict)
325
- saved_count += 1
326
- except Exception as exc: # noqa: BLE001 — single-row save failure must not abort batch
327
- logger.warning(
328
- "F1 shadow_comparison verdict save failed for interaction %s: %s",
329
- interaction.interaction_id,
330
- exc,
436
+ _eval_health.record_retrieved_outcome("failed", diagnostics=run.diagnostics)
437
+ _eval_health.record_producer_failure()
438
+ return "failed", None
439
+ commit = storage.replace_retrieved_learning_evaluation_results(
440
+ user_id,
441
+ session_id,
442
+ generation,
443
+ fingerprint,
444
+ run.proposed_status,
445
+ run.diagnostics,
446
+ run.rows,
447
+ )
448
+ if commit.disposition == "applied":
449
+ # An applied commit's authoritative status is always one of
450
+ # complete/degraded/not_applicable (the storage layer validates
451
+ # proposed_status and may only downgrade to not_applicable).
452
+ final_status: RetrievedLearningInvocationStatus = (
453
+ commit.status
454
+ if commit.status in ("complete", "degraded", "not_applicable")
455
+ else run.proposed_status
331
456
  )
332
-
333
- if saved_count:
457
+ logger.info(
458
+ "event=retrieved_learning_eval_%s session_id=%s candidates=%d"
459
+ " committed=%d failed_relevance_chunks=%s failed_impact_chunks=%s",
460
+ "completed"
461
+ if final_status in ("complete", "not_applicable")
462
+ else final_status,
463
+ session_id,
464
+ len(run.rows),
465
+ commit.committed_count,
466
+ run.diagnostics.get("failed_relevance_chunks", 0),
467
+ run.diagnostics.get("failed_impact_chunks", 0),
468
+ )
469
+ _eval_health.record_retrieved_outcome(
470
+ final_status, diagnostics=run.diagnostics
471
+ )
472
+ return final_status, fingerprint
473
+ if commit.disposition == "superseded":
474
+ logger.info(
475
+ "event=retrieved_learning_eval_superseded session_id=%s generation=%d",
476
+ session_id,
477
+ generation,
478
+ )
479
+ return "superseded", None
334
480
  logger.info(
335
- "F1: saved %d shadow_comparison verdict(s) for session=%s",
336
- saved_count,
481
+ "event=retrieved_learning_eval_stale session_id=%s generation=%d",
337
482
  session_id,
483
+ generation,
338
484
  )
485
+
486
+ # Two stale snapshots in a row: leave pending for the next trigger.
487
+ storage.finish_retrieved_learning_evaluation_run(
488
+ user_id, session_id, generation, "pending", {"error_type": "stale_snapshot"}
489
+ )
490
+ return "pending", None
@@ -13,7 +13,9 @@ import os
13
13
  import threading
14
14
  import time
15
15
  from collections.abc import Callable
16
+ from functools import partial
16
17
 
18
+ from reflexio.server.callback_executor import submit_callback
17
19
  from reflexio.server.services.agent_success_evaluation import _eval_health
18
20
 
19
21
  logger = logging.getLogger(__name__)
@@ -135,14 +137,10 @@ class GroupEvaluationScheduler:
135
137
  # Remove from scheduled map and fire
136
138
  del self._scheduled[key]
137
139
 
138
- # Spawn daemon thread for the callback
139
- t = threading.Thread(
140
- target=self._run_callback,
141
- args=(key, callback),
142
- daemon=True,
143
- name=f"group-eval-{key[2][:20]}",
140
+ submit_callback(
141
+ f"group-eval-{key[2][:20]}",
142
+ partial(self._run_callback, key, callback),
144
143
  )
145
- t.start()
146
144
 
147
145
  except Exception:
148
146
  logger.exception("Error in group evaluation scheduler loop")
@@ -213,6 +213,33 @@ class AgentSuccessEvaluationService(
213
213
  attempt,
214
214
  )
215
215
 
216
+ def _resolve_write_plan(self, results: list) -> None:
217
+ """Compute-half — write the evaluation results HERE (never durable-split).
218
+
219
+ Agent-success evaluation was never split into a durable compute/persist
220
+ pair: its results never flow through the durable worker's fence, so the
221
+ save stays inline in compute exactly as before, routed through the
222
+ permanent ``_finalize_extracted_items`` -> ``_process_results`` path
223
+ (which has its own retry/backoff + EvalHealth accounting). Returns
224
+ ``None`` so ``persist_generation`` invokes no further write. This is the
225
+ concrete default that keeps ``AgentSuccessEvaluationService`` instantiable
226
+ after ``BaseGenerationService`` flipped ``_resolve_write_plan`` /
227
+ ``_persist_write_plan`` to ``@abstractmethod`` (gate b, Task 8).
228
+ """
229
+ for result in results:
230
+ if result:
231
+ self._finalize_extracted_items(result)
232
+ return
233
+
234
+ def _persist_write_plan(self, plan: object) -> None:
235
+ """Persist-half — NO-OP: the write already ran in ``_resolve_write_plan``.
236
+
237
+ ``_resolve_write_plan`` returns ``None`` so ``persist_generation`` never
238
+ reaches this; it exists only to satisfy the abstract contract.
239
+ """
240
+ del plan
241
+ return
242
+
216
243
  def has_run_failures(self) -> bool:
217
244
  """Return True if extractor execution or result persistence failed."""
218
245
  extractor_failed_count = self._last_extractor_run_stats.get("failed", 0)
@@ -31,6 +31,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar
31
31
 
32
32
  from reflexio.server.api_endpoints.request_context import RequestContext
33
33
  from reflexio.server.llm.token_accounting import RunTokenTotals
34
+ from reflexio.server.services.deferred_learning_plan import ExtractorBookmarkAdvance
34
35
  from reflexio.server.services.extraction.outcome import ExtractionOutcome
35
36
  from reflexio.server.services.extractor_config_utils import get_extractor_name
36
37
  from reflexio.server.services.storage.storage_base import AgentRunStatus, BaseStorage
@@ -61,6 +62,7 @@ class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceCo
61
62
  _last_extractor_run_stats: dict[str, int]
62
63
  _last_extraction_run_ids: list[str]
63
64
  _last_token_totals: RunTokenTotals | None
65
+ _last_bookmark_advance: ExtractorBookmarkAdvance | None
64
66
 
65
67
  if TYPE_CHECKING:
66
68
  # Abstract on the base ABC (stays there per SINK-2); declared here type-only so
@@ -117,6 +119,10 @@ class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceCo
117
119
  if result.run_id:
118
120
  self._last_extraction_run_ids.append(result.run_id)
119
121
  self._last_token_totals = result.token_totals
122
+ # Capture the deferred stride-bookmark advance (F1); applied
123
+ # later in ``persist_generation`` (durable fence) or in
124
+ # ``_run_generation``'s persist half for the synchronous path.
125
+ self._last_bookmark_advance = result.bookmark_advance
120
126
  if result.status == "completed" and result.items:
121
127
  return result.items
122
128
  logger.info(
@@ -166,8 +172,8 @@ class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceCo
166
172
  """Mark active agent-run rows failed when the service timeout fires."""
167
173
  if self.storage is None or self.service_config is None:
168
174
  return
169
- request_id = getattr(self.service_config, "request_id", None)
170
- if not request_id:
175
+ generation_request_id = getattr(self.service_config, "request_id", None)
176
+ if not generation_request_id:
171
177
  return
172
178
  user_id = getattr(self.service_config, "user_id", None)
173
179
  try:
@@ -175,7 +181,7 @@ class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceCo
175
181
  org_id=self.request_context.org_id,
176
182
  extractor_kind=extractor_kind,
177
183
  user_id=user_id,
178
- request_id=request_id,
184
+ request_id=generation_request_id,
179
185
  last_error=last_error,
180
186
  )
181
187
  except NotImplementedError:
@@ -189,10 +195,10 @@ class ExtractionRunLifecycleMixin(Generic[TExtractorConfig, TGenerationServiceCo
189
195
  return
190
196
  if failed_count:
191
197
  logger.warning(
192
- "Marked %d timed-out %s agent run(s) failed for request_id=%s",
198
+ "Marked %d timed-out %s agent run(s) failed for generation_request_id=%s",
193
199
  failed_count,
194
200
  extractor_kind,
195
- request_id,
201
+ generation_request_id,
196
202
  )
197
203
 
198
204
  def _finalize_extraction_runs(self) -> None:
@@ -123,10 +123,10 @@ class ShouldRunPrecheckMixin(Generic[TExtractorConfig, TGenerationServiceConfig]
123
123
  )
124
124
  return False
125
125
 
126
- # Cheap pre-filter: reject batches that are structurally unable
127
- # to yield signal (slash-commands only, too-short user turns,
128
- # extractor-prompt echoes) without burning a 5–7s LLM call. See
129
- # _cheap_should_run_reject for the rule set.
126
+ # Cheap pre-filter: apply user-text-only heuristics (slash-commands,
127
+ # short turns, extractor-prompt echoes) without burning a 5–7s LLM
128
+ # call. Batches with no non-empty user text bypass these heuristics;
129
+ # stride/window eligibility was already evaluated independently.
130
130
  from reflexio.server.services.base_generation_service import (
131
131
  _cheap_should_run_reject,
132
132
  )
@@ -189,8 +189,9 @@ class UsageBillingMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): #
189
189
  ``None`` here and resolved enterprise-side at rollup (Phase 1).
190
190
 
191
191
  Gated by ``EMITS_LEARNING_BILLING`` — only profile/playbook generation
192
- services opt in. Evaluation, reflection, consolidation and aggregation
193
- are bundled and must NOT separately meter the ② Learning line.
192
+ services opt in here. Non-extraction learning mutation paths emit their
193
+ own ``learnings_generated`` value-facet events when they durably apply
194
+ revisions/successors.
194
195
 
195
196
  Args:
196
197
  prepared: The prepared generation run (used for input-text computation).
@@ -227,7 +228,10 @@ class UsageBillingMixin(Generic[TExtractorConfig, TGenerationServiceConfig]): #
227
228
  platform_llm=platform_llm,
228
229
  platform_storage=None,
229
230
  pipeline=ctx.get("pipeline"),
231
+ user_id=ctx.get("user_id"),
230
232
  request_id=ctx.get("request_id"),
233
+ source=ctx.get("source"),
234
+ agent_version=ctx.get("agent_version"),
231
235
  )
232
236
 
233
237
  # ② Learning — cost: input-anchored extraction tokens + real provider tokens.