claude-smart 0.2.47 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  8. package/plugin/dashboard/package-lock.json +61 -390
  9. package/plugin/dashboard/package.json +2 -2
  10. package/plugin/opencode/dist/server.mjs +60 -2
  11. package/plugin/opencode/server.mts +64 -2
  12. package/plugin/pyproject.toml +2 -2
  13. package/plugin/scripts/_lib.sh +58 -6
  14. package/plugin/scripts/backend-service.sh +15 -10
  15. package/plugin/scripts/codex-hook.js +16 -4
  16. package/plugin/scripts/dashboard-service.sh +1 -1
  17. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  18. package/plugin/scripts/opencode-claude-compat.js +8 -1
  19. package/plugin/scripts/smart-install.sh +101 -20
  20. package/plugin/src/README.md +1 -1
  21. package/plugin/src/claude_smart/cli.py +79 -29
  22. package/plugin/src/claude_smart/env_config.py +53 -11
  23. package/plugin/uv.lock +5 -5
  24. package/plugin/vendor/reflexio/.env.example +7 -0
  25. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  26. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  27. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  28. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  29. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  30. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  31. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  33. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  34. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  35. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  36. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  37. package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
  38. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  39. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  40. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  41. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  42. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  43. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  44. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  45. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  46. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  47. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  48. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
  49. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  50. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  51. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  52. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  53. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  54. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  55. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  56. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  57. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  58. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  59. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  60. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
  61. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  62. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  63. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  64. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  66. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  68. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  73. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  74. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  76. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  77. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
  81. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
  82. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  83. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  84. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  88. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  89. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -0,0 +1,320 @@
1
+ """Interaction route handlers (extracted from api.py, Tier3 A2)."""
2
+
3
+ import logging
4
+ import uuid
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ pass
9
+
10
+ from fastapi import (
11
+ APIRouter,
12
+ BackgroundTasks,
13
+ Depends,
14
+ HTTPException,
15
+ Request,
16
+ )
17
+
18
+ from reflexio.models.api_schema.retriever_schema import (
19
+ GetInteractionsRequest,
20
+ GetInteractionsViewResponse,
21
+ GetRequestsRequest,
22
+ GetRequestsViewResponse,
23
+ RequestDataView,
24
+ SessionView,
25
+ )
26
+ from reflexio.models.api_schema.service_schemas import (
27
+ BulkDeleteResponse,
28
+ DeleteRequestRequest,
29
+ DeleteRequestResponse,
30
+ DeleteRequestsByIdsRequest,
31
+ DeleteSessionRequest,
32
+ DeleteSessionResponse,
33
+ DeleteUserInteractionRequest,
34
+ DeleteUserInteractionResponse,
35
+ LearningStatusResponse,
36
+ PublishUserInteractionRequest,
37
+ PublishUserInteractionResponse,
38
+ )
39
+ from reflexio.models.api_schema.ui.converters import (
40
+ to_interaction_view,
41
+ )
42
+ from reflexio.server.api_endpoints import (
43
+ publisher_api,
44
+ )
45
+ from reflexio.server.auth import (
46
+ default_billing_gate,
47
+ default_get_org_id,
48
+ )
49
+ from reflexio.server.cache import reflexio_cache
50
+ from reflexio.server.rate_limit import limiter
51
+ from reflexio.server.routes._common import _run_limited_api
52
+
53
+ logger = logging.getLogger(__name__)
54
+ router = APIRouter()
55
+
56
+
57
+ @router.post(
58
+ "/api/publish_interaction",
59
+ response_model=PublishUserInteractionResponse,
60
+ response_model_exclude_none=True,
61
+ )
62
+ @limiter.limit("60/minute") # Rate limit for write operations
63
+ def publish_user_interaction(
64
+ request: Request,
65
+ payload: PublishUserInteractionRequest,
66
+ background_tasks: BackgroundTasks,
67
+ org_id: str = Depends(default_get_org_id),
68
+ wait_for_response: bool = False,
69
+ _gate: None = Depends(default_billing_gate("learnings_generated")), # noqa: B008
70
+ ) -> PublishUserInteractionResponse:
71
+ if wait_for_response:
72
+ # Sync callers wait for the real result, so preserve bounded backpressure
73
+ # before any storage side effects. The inner service limiter is disabled
74
+ # because this route already owns the publish slot.
75
+ return _run_limited_api(
76
+ org_id,
77
+ "publish",
78
+ lambda: publisher_api.add_user_interaction(
79
+ org_id=org_id,
80
+ request=payload,
81
+ use_publish_limiter=False,
82
+ ),
83
+ )
84
+
85
+ # Resolve the request_id BEFORE backgrounding so we can return it to the
86
+ # caller for polling. GenerationService uses a caller-supplied request_id
87
+ # verbatim (and generates one only when absent), so pinning it on the
88
+ # payload guarantees the background task stores its status under the same
89
+ # id we hand back here.
90
+ request_id = payload.request_id or str(uuid.uuid4())
91
+ payload.request_id = request_id
92
+
93
+ def _publish_task() -> None:
94
+ try:
95
+ publisher_api.add_user_interaction(
96
+ org_id=org_id,
97
+ request=payload,
98
+ defer_learning=True,
99
+ )
100
+ except Exception:
101
+ logger.exception("Background publish failed for org %s", org_id)
102
+
103
+ # Run in background — caller gets immediate acknowledgement.
104
+ # learning_status="deferred" tells the caller that extraction has not yet
105
+ # run; they can poll GET /api/learning_status?request_id=... (using the
106
+ # request_id returned here) to track it.
107
+ background_tasks.add_task(_publish_task)
108
+ return PublishUserInteractionResponse(
109
+ success=True,
110
+ message="Interaction queued for processing",
111
+ request_id=request_id,
112
+ learning_status="deferred",
113
+ )
114
+
115
+
116
+ @router.delete(
117
+ "/api/delete_interaction",
118
+ response_model=DeleteUserInteractionResponse,
119
+ response_model_exclude_none=True,
120
+ )
121
+ def delete_interaction(
122
+ request: DeleteUserInteractionRequest,
123
+ org_id: str = Depends(default_get_org_id),
124
+ ) -> DeleteUserInteractionResponse:
125
+ return publisher_api.delete_user_interaction(org_id=org_id, request=request)
126
+
127
+
128
+ @router.delete(
129
+ "/api/delete_request",
130
+ response_model=DeleteRequestResponse,
131
+ response_model_exclude_none=True,
132
+ )
133
+ def delete_request(
134
+ request: DeleteRequestRequest,
135
+ org_id: str = Depends(default_get_org_id),
136
+ ) -> DeleteRequestResponse:
137
+ return publisher_api.delete_request(org_id=org_id, request=request)
138
+
139
+
140
+ @router.delete(
141
+ "/api/delete_session",
142
+ response_model=DeleteSessionResponse,
143
+ response_model_exclude_none=True,
144
+ )
145
+ def delete_session(
146
+ request: DeleteSessionRequest,
147
+ org_id: str = Depends(default_get_org_id),
148
+ ) -> DeleteSessionResponse:
149
+ return publisher_api.delete_session(org_id=org_id, request=request)
150
+
151
+
152
+ @router.delete(
153
+ "/api/delete_requests_by_ids",
154
+ response_model=BulkDeleteResponse,
155
+ response_model_exclude_none=True,
156
+ )
157
+ def delete_requests_by_ids(
158
+ request: DeleteRequestsByIdsRequest,
159
+ org_id: str = Depends(default_get_org_id),
160
+ ) -> BulkDeleteResponse:
161
+ """Delete multiple requests by their IDs.
162
+
163
+ Args:
164
+ request (DeleteRequestsByIdsRequest): Request containing list of request IDs to delete
165
+ org_id (str): Organization ID
166
+
167
+ Returns:
168
+ BulkDeleteResponse: Response containing success status and deleted count
169
+ """
170
+ return publisher_api.delete_requests_by_ids(org_id=org_id, request=request)
171
+
172
+
173
+ @router.delete(
174
+ "/api/delete_all_interactions",
175
+ response_model=BulkDeleteResponse,
176
+ response_model_exclude_none=True,
177
+ )
178
+ @limiter.limit("10/minute")
179
+ def delete_all_interactions(
180
+ request: Request,
181
+ org_id: str = Depends(default_get_org_id),
182
+ ) -> BulkDeleteResponse:
183
+ """Delete all requests and their associated interactions.
184
+
185
+ Args:
186
+ org_id (str): Organization ID
187
+
188
+ Returns:
189
+ BulkDeleteResponse: Response containing success status and deleted count
190
+ """
191
+ return publisher_api.delete_all_interactions_bulk(org_id=org_id)
192
+
193
+
194
+ @router.post(
195
+ "/api/get_interactions",
196
+ response_model=GetInteractionsViewResponse,
197
+ response_model_exclude_none=True,
198
+ )
199
+ def get_interactions(
200
+ request: GetInteractionsRequest,
201
+ org_id: str = Depends(default_get_org_id),
202
+ ) -> GetInteractionsViewResponse:
203
+ response = reflexio_cache.get_reflexio(org_id=org_id).get_interactions(request)
204
+ return GetInteractionsViewResponse(
205
+ success=response.success,
206
+ interactions=[to_interaction_view(i) for i in response.interactions],
207
+ msg=response.msg,
208
+ )
209
+
210
+
211
+ @router.get(
212
+ "/api/get_all_interactions",
213
+ response_model=GetInteractionsViewResponse,
214
+ response_model_exclude_none=True,
215
+ )
216
+ @limiter.limit("30/minute")
217
+ def get_all_interactions(
218
+ request: Request,
219
+ limit: int = 100,
220
+ org_id: str = Depends(default_get_org_id),
221
+ ) -> GetInteractionsViewResponse:
222
+ """Get all user interactions across all users.
223
+
224
+ Args:
225
+ limit (int, optional): Maximum number of interactions to return. Defaults to 100.
226
+ org_id (str): Organization ID
227
+
228
+ Returns:
229
+ GetInteractionsViewResponse: Response containing all user interactions
230
+ """
231
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
232
+ response = reflexio.get_all_interactions(limit=limit)
233
+ return GetInteractionsViewResponse(
234
+ success=response.success,
235
+ interactions=[to_interaction_view(i) for i in response.interactions],
236
+ msg=response.msg,
237
+ )
238
+
239
+
240
+ @router.post(
241
+ "/api/get_requests",
242
+ response_model=GetRequestsViewResponse,
243
+ response_model_exclude_none=True,
244
+ )
245
+ def get_requests_endpoint(
246
+ request: GetRequestsRequest,
247
+ org_id: str = Depends(default_get_org_id),
248
+ ) -> GetRequestsViewResponse:
249
+ """Get requests with their associated interactions.
250
+
251
+ Args:
252
+ request (GetRequestsRequest): The get request
253
+ org_id (str): Organization ID
254
+
255
+ Returns:
256
+ GetRequestsViewResponse: Response containing requests with their interactions
257
+ """
258
+ internal_response = reflexio_cache.get_reflexio(org_id=org_id).get_requests(request)
259
+ return GetRequestsViewResponse(
260
+ success=internal_response.success,
261
+ sessions=[
262
+ SessionView(
263
+ session_id=s.session_id,
264
+ requests=[
265
+ RequestDataView(
266
+ request=rd.request,
267
+ interactions=[to_interaction_view(i) for i in rd.interactions],
268
+ )
269
+ for rd in s.requests
270
+ ],
271
+ )
272
+ for s in internal_response.sessions
273
+ ],
274
+ has_more=internal_response.has_more,
275
+ msg=internal_response.msg,
276
+ )
277
+
278
+
279
+ @router.get(
280
+ "/api/learning_status",
281
+ response_model=LearningStatusResponse,
282
+ response_model_exclude_none=True,
283
+ )
284
+ @limiter.limit("120/minute")
285
+ def get_learning_status(
286
+ request: Request,
287
+ request_id: str,
288
+ org_id: str = Depends(default_get_org_id),
289
+ ) -> LearningStatusResponse:
290
+ """Return the coverage-based learning status for a published request.
291
+
292
+ The status reflects whether a durable learning job has processed through
293
+ the request's creation timestamp:
294
+
295
+ - ``pending``: not yet picked up.
296
+ - ``processing``: a worker currently holds the job.
297
+ - ``done``: at least one completed job covers this request.
298
+ - ``failed``: a dead job covers this request and no done job does.
299
+
300
+ Note: this endpoint reads ``learning_jobs`` rows written by the durable
301
+ queue. When the durable queue is OFF (in-memory deferred path) it returns
302
+ absence-based status (``pending`` for recent requests, ``done`` for old
303
+ ones) — acceptable for v1; the poll contract is tied to the durable queue.
304
+
305
+ Raises:
306
+ HTTPException: 404 when ``request_id`` is not found for this org.
307
+ Never reports ``done`` for a request that never existed.
308
+ """
309
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
310
+ storage = reflexio.request_context.storage
311
+ if storage is None:
312
+ raise HTTPException(status_code=503, detail="Storage not configured")
313
+ req = storage.get_request(request_id)
314
+ if req is None:
315
+ raise HTTPException(status_code=404, detail="request not found")
316
+ status = storage.get_learning_status_for_request(
317
+ user_id=req.user_id,
318
+ request_created_at=float(req.created_at),
319
+ )
320
+ return LearningStatusResponse(status=status)