claude-smart 0.2.47 → 0.2.48

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 (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  7. package/plugin/dashboard/package-lock.json +61 -390
  8. package/plugin/dashboard/package.json +2 -2
  9. package/plugin/pyproject.toml +1 -1
  10. package/plugin/uv.lock +1 -1
  11. package/plugin/vendor/reflexio/.env.example +7 -0
  12. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  13. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  14. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  15. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  16. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  17. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  18. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  19. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  20. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  21. package/plugin/vendor/reflexio/reflexio/server/api.py +133 -3273
  22. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  23. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  24. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  25. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  26. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  27. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  28. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  29. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  30. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  31. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  32. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  33. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  34. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  35. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  36. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  37. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  38. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  39. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  40. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  41. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  42. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  43. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  44. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  45. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  46. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  47. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  48. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  49. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  50. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  51. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  52. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  53. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  54. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  56. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  59. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  60. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  61. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  62. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  63. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  64. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  65. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  66. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  68. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -4
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +56 -351
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  88. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  89. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +6 -3
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +33 -8
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -0,0 +1,261 @@
1
+ """System/meta/stats/operations route handlers (extracted from api.py, Tier3 A2)."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ pass
8
+
9
+ from fastapi import (
10
+ APIRouter,
11
+ Depends,
12
+ HTTPException,
13
+ Request,
14
+ status,
15
+ )
16
+
17
+ from reflexio.models.api_schema.retriever_schema import (
18
+ GetDashboardStatsRequest,
19
+ GetDashboardStatsResponse,
20
+ StorageStatsRequest,
21
+ StorageStatsResponse,
22
+ )
23
+ from reflexio.models.api_schema.service_schemas import (
24
+ AdminInvalidateCacheRequest,
25
+ AdminInvalidateCacheResponse,
26
+ CancelOperationRequest,
27
+ CancelOperationResponse,
28
+ ClearUserDataRequest,
29
+ ClearUserDataResponse,
30
+ GetOperationStatusRequest,
31
+ GetOperationStatusResponse,
32
+ WhoamiResponse,
33
+ )
34
+ from reflexio.server.api_endpoints import (
35
+ account_api,
36
+ publisher_api,
37
+ )
38
+ from reflexio.server.auth import (
39
+ default_get_org_id,
40
+ )
41
+ from reflexio.server.cache import reflexio_cache
42
+ from reflexio.server.rate_limit import limiter
43
+ from reflexio.server.routes._common import _run_limited_api
44
+
45
+ logger = logging.getLogger(__name__)
46
+ router = APIRouter()
47
+
48
+
49
+ @router.get("/")
50
+ def root() -> dict[str, str]:
51
+ return {
52
+ "service": "Reflexio API",
53
+ "docs": "/docs",
54
+ "health": "/health",
55
+ }
56
+
57
+
58
+ @router.get("/health")
59
+ async def health_check() -> dict[str, str]:
60
+ """Health check endpoint for ECS/container orchestration."""
61
+ return {"status": "healthy"}
62
+
63
+
64
+ @router.get(
65
+ "/api/whoami",
66
+ response_model=WhoamiResponse,
67
+ response_model_exclude_none=True,
68
+ )
69
+ def whoami_endpoint(
70
+ org_id: str = Depends(default_get_org_id),
71
+ ) -> WhoamiResponse:
72
+ """Return the caller's org and masked storage routing.
73
+
74
+ Powers ``reflexio status``. Safe to call unauthenticated in
75
+ self-host mode; the enterprise server wraps this in Bearer auth.
76
+ """
77
+ return account_api.whoami(org_id=org_id)
78
+
79
+
80
+ @router.get(
81
+ "/api/storage_stats",
82
+ response_model=StorageStatsResponse,
83
+ response_model_exclude_none=True,
84
+ )
85
+ @limiter.limit("120/minute") # Rate limit for read operations
86
+ def storage_stats(
87
+ request: Request,
88
+ user_id: str,
89
+ org_id: str = Depends(default_get_org_id),
90
+ ) -> StorageStatsResponse:
91
+ """Return lightweight metadata about a user's profiles and playbooks.
92
+
93
+ Args:
94
+ request (Request): The HTTP request object (for rate limiting)
95
+ user_id (str): Target user id, passed as a query parameter so this is
96
+ a cacheable, idempotent GET.
97
+ org_id (str): Organization ID
98
+
99
+ Returns:
100
+ StorageStatsResponse: Counts and timestamp range for the user.
101
+ """
102
+ return _run_limited_api(
103
+ org_id,
104
+ "search",
105
+ lambda: reflexio_cache.get_reflexio(org_id=org_id).storage_stats(
106
+ StorageStatsRequest(user_id=user_id)
107
+ ),
108
+ )
109
+
110
+
111
+ @router.post(
112
+ "/api/clear_user_data",
113
+ response_model=ClearUserDataResponse,
114
+ response_model_exclude_none=True,
115
+ )
116
+ @limiter.limit("10/minute")
117
+ def clear_user_data(
118
+ request: Request,
119
+ payload: ClearUserDataRequest,
120
+ org_id: str = Depends(default_get_org_id),
121
+ ) -> ClearUserDataResponse:
122
+ """Delete all rows scoped to a single ``user_id``.
123
+
124
+ Removes the user's interactions, user playbooks, profiles, and
125
+ requests. Does NOT touch ``agent_playbooks`` — they are
126
+ intentionally shared cross-project. Used by paired-protocol
127
+ harnesses (e.g. SWE-bench) to isolate per-task data on a shared
128
+ backend without one task's clear-all nuking another in-flight
129
+ task's rows.
130
+
131
+ Args:
132
+ request (ClearUserDataRequest): Request containing the target user_id
133
+ org_id (str): Organization ID
134
+
135
+ Returns:
136
+ ClearUserDataResponse: Response with per-entity deletion counts
137
+ """
138
+ return publisher_api.clear_user_data(org_id=org_id, request=payload)
139
+
140
+
141
+ @router.post("/api/admin/cache/invalidate")
142
+ def admin_invalidate_cache(
143
+ payload: AdminInvalidateCacheRequest,
144
+ org_id: str = Depends(default_get_org_id),
145
+ ) -> AdminInvalidateCacheResponse:
146
+ """Explicitly evict the per-org Reflexio cache entry.
147
+
148
+ Necessary when the running config has been mutated through a
149
+ channel the server can't observe — e.g. another replica wrote to
150
+ the shared DB, or an operator hand-edited a self-host config file
151
+ on a backend that doesn't support cheap version probing. The
152
+ file-mtime check (Phase 1) and DB version check (Phase 3) cover
153
+ most cases automatically; this endpoint is the manual escape hatch.
154
+
155
+ Auth uses the same dependency as ``/api/set_config`` — callers
156
+ can only invalidate their own org's cache. If the request body
157
+ supplies ``org_id`` it must match the dep-resolved value;
158
+ cross-org invalidation is intentionally NOT exposed here.
159
+
160
+ Args:
161
+ payload: Optional ``org_id`` (verification only — must match
162
+ the caller's authenticated org if provided).
163
+ org_id: Organization ID resolved by the auth layer.
164
+
165
+ Returns:
166
+ AdminInvalidateCacheResponse: ``invalidated`` is True iff an
167
+ entry was evicted (False is a successful no-op when nothing
168
+ was cached).
169
+
170
+ Raises:
171
+ HTTPException: 403 when the body's ``org_id`` differs from the
172
+ caller's authenticated org.
173
+ """
174
+ if payload.org_id is not None and payload.org_id != org_id:
175
+ raise HTTPException(
176
+ status_code=status.HTTP_403_FORBIDDEN,
177
+ detail=(
178
+ "Cross-org cache invalidation is not supported; "
179
+ "omit org_id or pass your own."
180
+ ),
181
+ )
182
+ invalidated = reflexio_cache.invalidate_reflexio_cache(org_id=org_id)
183
+ return AdminInvalidateCacheResponse(invalidated=invalidated, org_id=org_id)
184
+
185
+
186
+ @router.post(
187
+ "/api/get_dashboard_stats",
188
+ response_model=GetDashboardStatsResponse,
189
+ response_model_exclude_none=True,
190
+ )
191
+ @limiter.limit("30/minute")
192
+ def get_dashboard_stats(
193
+ request: Request,
194
+ payload: GetDashboardStatsRequest,
195
+ org_id: str = Depends(default_get_org_id),
196
+ ) -> GetDashboardStatsResponse:
197
+ """Get comprehensive dashboard statistics including counts and time-series data.
198
+
199
+ Args:
200
+ request (GetDashboardStatsRequest): Request containing days_back and granularity
201
+ org_id (str): Organization ID
202
+
203
+ Returns:
204
+ GetDashboardStatsResponse: Response containing dashboard statistics
205
+ """
206
+ # Create Reflexio instance
207
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
208
+
209
+ # Get dashboard stats using Reflexio's method
210
+ return reflexio.get_dashboard_stats(payload)
211
+
212
+
213
+ @router.get(
214
+ "/api/get_operation_status",
215
+ response_model=GetOperationStatusResponse,
216
+ response_model_exclude_none=True,
217
+ )
218
+ def get_operation_status_endpoint(
219
+ service_name: str = "profile_generation",
220
+ org_id: str = Depends(default_get_org_id),
221
+ ) -> GetOperationStatusResponse:
222
+ """Get the status of an operation (e.g., profile generation rerun or manual).
223
+
224
+ Args:
225
+ service_name (str): The service name to query. Defaults to "profile_generation"
226
+ org_id (str): Organization ID
227
+
228
+ Returns:
229
+ GetOperationStatusResponse: Response containing operation status info
230
+ """
231
+ # Create Reflexio instance
232
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
233
+
234
+ # Get operation status
235
+ request = GetOperationStatusRequest(service_name=service_name)
236
+ return reflexio.get_operation_status(request)
237
+
238
+
239
+ @router.post(
240
+ "/api/cancel_operation",
241
+ response_model=CancelOperationResponse,
242
+ response_model_exclude_none=True,
243
+ )
244
+ @limiter.limit("10/minute")
245
+ def cancel_operation_endpoint(
246
+ request: Request,
247
+ payload: CancelOperationRequest,
248
+ org_id: str = Depends(default_get_org_id),
249
+ ) -> CancelOperationResponse:
250
+ """Cancel an in-progress operation (rerun or manual generation).
251
+
252
+ Args:
253
+ request (Request): The HTTP request object (for rate limiting)
254
+ payload (CancelOperationRequest): Request containing optional service_name
255
+ org_id (str): Organization ID
256
+
257
+ Returns:
258
+ CancelOperationResponse: Response with list of services that were cancelled
259
+ """
260
+ reflexio = reflexio_cache.get_reflexio(org_id=org_id)
261
+ return reflexio.cancel_operation(payload)
@@ -0,0 +1,132 @@
1
+ """Shared thread-lifecycle base for the process-local polling schedulers.
2
+
3
+ Reflexio runs a family of background daemons — extraction resume, lineage GC,
4
+ and (in the enterprise layer) billing ship / lease / period-close / integrity /
5
+ audit / offline-tuner / governance-retention / invitation-reclamation. Every one
6
+ of them hand-rolled the SAME thread mechanics: a :class:`threading.Event`
7
+ stop-signal, a daemon :class:`threading.Thread` running a ``while not
8
+ stop.is_set()`` loop, and a ``.join(timeout)`` graceful stop.
9
+
10
+ :class:`ThreadedScheduler` captures ONLY those mechanics. Each scheduler keeps
11
+ its own poll interval, work body, gating, and log lines — supplied via a small
12
+ set of hooks so no observable behaviour changes:
13
+
14
+ - :meth:`_run_once` performs one tick (catching its own errors) and returns the
15
+ number of seconds to wait before the next tick. A scheduler that reads its
16
+ interval from config each tick keeps doing exactly that by returning the fresh
17
+ value.
18
+ - :meth:`_should_start` can veto startup (the enterprise schedulers whose
19
+ ``start`` is a logged no-op when a feature gate is off).
20
+ - :meth:`_on_started` / :meth:`_on_stopped` emit each scheduler's own start/stop
21
+ log line (through its own module logger, so output is byte-identical).
22
+
23
+ A scheduler whose loop is materially different (e.g. a bounded-attempt retrier
24
+ that waits *before* each attempt and exits on first success) may override
25
+ :meth:`_run_loop` wholesale and still reuse :meth:`start` / :meth:`stop` /
26
+ :meth:`is_running`.
27
+
28
+ This module is an OSS public seam: it imports only the standard library and
29
+ nothing from ``reflexio_ext``.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import threading
35
+
36
+
37
+ class ThreadedScheduler:
38
+ """Base for a process-local daemon that ticks on a background thread.
39
+
40
+ Owns the shared thread mechanics only: a stop :class:`threading.Event`, a
41
+ daemon thread, an idempotent :meth:`start`, and a join-with-timeout
42
+ :meth:`stop`. Subclasses supply the per-tick work + cadence via
43
+ :meth:`_run_once` (or override :meth:`_run_loop` for a bespoke loop shape).
44
+
45
+ Args:
46
+ thread_name (str): OS thread name for the daemon (aids debugging / logs).
47
+ """
48
+
49
+ def __init__(self, *, thread_name: str) -> None:
50
+ self._thread_name = thread_name
51
+ self._stop_event = threading.Event()
52
+ self._thread: threading.Thread | None = None
53
+
54
+ def start(self) -> None:
55
+ """Start the daemon thread, unless it is gated off or already running.
56
+
57
+ Calls :meth:`_should_start` first (a subclass gate that can log + veto),
58
+ then the idempotent alive-check, then spawns the thread and calls
59
+ :meth:`_on_started`.
60
+ """
61
+ if not self._should_start():
62
+ return
63
+ if self._thread is not None and self._thread.is_alive():
64
+ return
65
+ self._stop_event.clear()
66
+ self._thread = threading.Thread(
67
+ target=self._run_loop,
68
+ name=self._thread_name,
69
+ daemon=True,
70
+ )
71
+ self._thread.start()
72
+ self._on_started()
73
+
74
+ def stop(self, *, timeout_seconds: float = 5.0) -> None:
75
+ """Signal the loop to stop and join the thread.
76
+
77
+ Args:
78
+ timeout_seconds (float): Max seconds to wait for the thread to exit.
79
+ """
80
+ self._stop_event.set()
81
+ if self._thread is not None:
82
+ self._thread.join(timeout=timeout_seconds)
83
+ # Only drop the reference once the thread has actually exited. If the
84
+ # join timed out (a slow ``_run_once`` still running), keep it so
85
+ # ``is_running()`` stays true and a subsequent ``start()`` refuses to
86
+ # spawn a second loop while the first is alive.
87
+ if not self._thread.is_alive():
88
+ self._thread = None
89
+ self._on_stopped()
90
+
91
+ def is_running(self) -> bool:
92
+ """Return whether the background thread exists and is alive.
93
+
94
+ Returns:
95
+ bool: True when the loop thread is running.
96
+ """
97
+ return self._thread is not None and self._thread.is_alive()
98
+
99
+ # -- Overridable hooks ---------------------------------------------------
100
+
101
+ def _should_start(self) -> bool:
102
+ """Return whether :meth:`start` should spawn the thread (default True).
103
+
104
+ Override to gate startup behind a feature flag; the override owns any
105
+ "not starting" log line.
106
+ """
107
+ return True
108
+
109
+ def _on_started(self) -> None:
110
+ """Hook run after the thread starts (default no-op); override to log."""
111
+
112
+ def _on_stopped(self) -> None:
113
+ """Hook run after the thread joins (default no-op); override to log."""
114
+
115
+ def _run_once(self) -> float:
116
+ """Run one tick and return the seconds to wait before the next.
117
+
118
+ Implementations MUST catch their own per-tick errors — a raise here would
119
+ kill the daemon thread. The returned value is passed straight to
120
+ ``stop_event.wait`` as the inter-tick delay, so a subclass may compute a
121
+ fresh interval each tick (e.g. from config).
122
+
123
+ Returns:
124
+ float: Seconds to wait before the next tick.
125
+ """
126
+ raise NotImplementedError
127
+
128
+ def _run_loop(self) -> None:
129
+ """Drive :meth:`_run_once` until stopped, waiting its returned interval."""
130
+ while not self._stop_event.is_set():
131
+ interval = self._run_once()
132
+ self._stop_event.wait(interval)
@@ -23,7 +23,8 @@ strings before deleting old import paths in the same PR.
23
23
  | File | Purpose |
24
24
  |------|---------|
25
25
  | `generation_service.py` | `GenerationService` — saves interactions, runs profile + playbook generation in parallel (ThreadPoolExecutor), schedules deferred evaluation when `session_id` is present. |
26
- | `base_generation_service.py` | `BaseGenerationService` abstract base; the **Service Pattern** (load configs create actors run in parallel save results). Per-extractor timeout `EXTRACTOR_TIMEOUT_SECONDS = 300`. |
26
+ | `publish_learning_worker.py` | Deferred post-persist publish learning worker queues async publish learning after durable interaction writes and requeues under publish limiter pressure. |
27
+ | `base_generation_service.py` + `base_generation/` | `BaseGenerationService` stable import surface plus mixins for batch progress, config filtering, extraction lifecycle, should-run prechecks, status transitions, and usage billing. Per-extractor timeout `EXTRACTOR_TIMEOUT_SECONDS = 300`. |
27
28
  | `operation_state_utils.py` | `OperationStateManager` — all `_operation_state` access (progress, concurrency locks, extractor/aggregator bookmarks, cluster fingerprints, cancellation). |
28
29
  | `extractor_config_utils.py`, `extractor_interaction_utils.py` | Filter extractors by source / `allow_manual_trigger` / names; per-extractor stride + window + bookmark handling. |
29
30
  | `deduplication_utils.py`, `service_utils.py`, `embedding_text.py` | LLM dedup helpers (used by `ProfileConsolidator` + `PlaybookConsolidator`), message construction / JSON extraction / response logging, embedding text builders. |
@@ -62,7 +63,7 @@ strings before deleting old import paths in the same PR.
62
63
 
63
64
  | Path | Purpose |
64
65
  |------|---------|
65
- | `storage/` | `storage_base/` (`BaseStorage` split by domain, including `_governance.py` and `_lineage.py`) + `sqlite_storage/` (governance validation, retention barriers, lineage/tombstones) + `governance_validation.py` + `retention*.py`. Access via `request_context.storage` only. |
66
+ | `storage/` | `storage_base/` and `sqlite_storage/` keep legacy domain facades while focused subpackages own `profiles/`, `playbook/`, `agent_run/`, `governance/`, and SQLite `base/` helpers; plus `governance_validation.py` and `retention*.py`. Access via `request_context.storage` only. |
66
67
  | `configurator/` | `DefaultConfigurator` — loads YAML config and creates the storage backend. |
67
68
 
68
69
  ## Key Rules
@@ -0,0 +1,38 @@
1
+ """Sub-mixins composed into ``BaseGenerationService`` (Tier-1b decomposition).
2
+
3
+ Each mixin holds a cohesive bucket of the ``BaseGenerationService`` behaviour and
4
+ is composed into the base via MRO in
5
+ ``reflexio.server.services.base_generation_service``. The residual base keeps the
6
+ ABC skeleton (all abstract methods), ``__init__`` (per-run field inits), the ``run``
7
+ FIFO drain, ``_run_generation`` billing emit-ordering, and the lock/queue helpers.
8
+ """
9
+
10
+ from reflexio.server.services.base_generation._batch_progress import (
11
+ BatchProgressMixin,
12
+ )
13
+ from reflexio.server.services.base_generation._config_filter import (
14
+ ConfigFilterMixin,
15
+ )
16
+ from reflexio.server.services.base_generation._extraction_lifecycle import (
17
+ ExtractionRunLifecycleMixin,
18
+ )
19
+ from reflexio.server.services.base_generation._should_run import (
20
+ ShouldRunPrecheckMixin,
21
+ )
22
+ from reflexio.server.services.base_generation._status_change import (
23
+ StatusChangeMixin,
24
+ StatusChangeOperation,
25
+ )
26
+ from reflexio.server.services.base_generation._usage_billing import (
27
+ UsageBillingMixin,
28
+ )
29
+
30
+ __all__ = [
31
+ "BatchProgressMixin",
32
+ "ConfigFilterMixin",
33
+ "ExtractionRunLifecycleMixin",
34
+ "ShouldRunPrecheckMixin",
35
+ "StatusChangeMixin",
36
+ "StatusChangeOperation",
37
+ "UsageBillingMixin",
38
+ ]