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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +10 -43
- package/bin/claude-smart.js +105 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -23,6 +23,8 @@ _DELETE_TARGET_NAME_TO_RESULT_KEY = {
|
|
|
23
23
|
"profile": "profiles",
|
|
24
24
|
"request": "requests",
|
|
25
25
|
"agent_success_evaluation_result": "agent_success_evaluation_results",
|
|
26
|
+
"retrieved_learning_evaluation_result": "retrieved_learning_evaluation_results",
|
|
27
|
+
"evaluation_operation_state": "evaluation_operation_states",
|
|
26
28
|
"profile_purge": "purged_profiles",
|
|
27
29
|
"user_playbook_purge": "purged_user_playbooks",
|
|
28
30
|
}
|
|
@@ -28,13 +28,17 @@ import time
|
|
|
28
28
|
from collections.abc import Callable
|
|
29
29
|
|
|
30
30
|
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
31
|
-
from reflexio.server.
|
|
31
|
+
from reflexio.server.env_utils import env_str
|
|
32
|
+
from reflexio.server.org_fanout import iterate_orgs_bounded
|
|
33
|
+
from reflexio.server.scheduling import LeaderGate, ThreadedScheduler
|
|
32
34
|
from reflexio.server.tracing import capture_anomaly
|
|
33
35
|
|
|
34
36
|
logger = logging.getLogger(__name__)
|
|
35
37
|
|
|
36
38
|
_DEFAULT_POLL_INTERVAL_SECONDS = 86400
|
|
37
39
|
_MIN_POLL_SECONDS = 1
|
|
40
|
+
_ORG_SWEEP_TIMEOUT_SECONDS = 60.0
|
|
41
|
+
_DEFAULT_ORG_FANOUT_WORKERS = 8
|
|
38
42
|
|
|
39
43
|
# Window-misconfiguration tripwire: if a single tick deletes more than this
|
|
40
44
|
# many tombstones for one org, something is likely wrong with the grace window.
|
|
@@ -73,6 +77,27 @@ def set_org_id_provider(provider: Callable[[], list[str]] | None) -> None:
|
|
|
73
77
|
_org_id_provider_hook = provider
|
|
74
78
|
|
|
75
79
|
|
|
80
|
+
# Injection seam: enterprise sets the fleet leader gate here at composition
|
|
81
|
+
# time (spec 4.3), exactly like set_org_id_provider. None (OSS default) keeps
|
|
82
|
+
# the always-tick behavior.
|
|
83
|
+
_leader_gate_hook: LeaderGate | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_leader_gate(gate: LeaderGate | None) -> None:
|
|
87
|
+
"""Register the leader gate consulted by ``maybe_start_lineage_gc``.
|
|
88
|
+
|
|
89
|
+
Enterprise deployments call this at app-composition time so the scheduler
|
|
90
|
+
only ticks on the elected leader (``gate.should_run()`` wraps fleet leader
|
|
91
|
+
election, e.g. an advisory-lock gate). ``None`` (OSS default) preserves
|
|
92
|
+
always-tick behavior. Pass ``None`` to clear the hook (tests restore defaults).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
gate (LeaderGate | None): Fleet-coordination gate, or ``None`` to clear.
|
|
96
|
+
"""
|
|
97
|
+
global _leader_gate_hook # noqa: PLW0603
|
|
98
|
+
_leader_gate_hook = gate
|
|
99
|
+
|
|
100
|
+
|
|
76
101
|
# Global sweeps run once per tick (for GLOBAL tables like invitation_codes),
|
|
77
102
|
# gated on expiry_reclamation.enabled. Each fn takes `now` (unix epoch) and
|
|
78
103
|
# returns a deleted-row count. Enterprise registers its sweeps here at startup;
|
|
@@ -109,8 +134,8 @@ _per_org_sweep_hooks: list[Callable[[str, int], int]] = []
|
|
|
109
134
|
def register_per_org_sweep(fn: Callable[[str, int], int]) -> None:
|
|
110
135
|
"""Register a per-org reclamation sweep.
|
|
111
136
|
|
|
112
|
-
The sweep is invoked once per org inside ``
|
|
113
|
-
|
|
137
|
+
The sweep is invoked once per org inside ``_sweep_org``, after the
|
|
138
|
+
Class-B block, unconditionally (not gated on ``lineage_gc`` or
|
|
114
139
|
``expiry_reclamation`` — the real gate lives in the enterprise closure).
|
|
115
140
|
|
|
116
141
|
Note: a registered sweep that absorbs its own exceptions (returns instead
|
|
@@ -138,8 +163,11 @@ class LineageGCScheduler(ThreadedScheduler):
|
|
|
138
163
|
request_context_factory: Callable[[str], RequestContext],
|
|
139
164
|
bootstrap_org_id: str,
|
|
140
165
|
org_id_provider: Callable[[], list[str]] | None = None,
|
|
166
|
+
leader_gate: LeaderGate | None = None,
|
|
141
167
|
) -> None:
|
|
142
|
-
super().__init__(
|
|
168
|
+
super().__init__(
|
|
169
|
+
thread_name="reflexio-lineage-gc-scheduler", leader_gate=leader_gate
|
|
170
|
+
)
|
|
143
171
|
self.request_context_factory = request_context_factory
|
|
144
172
|
self.bootstrap_org_id = bootstrap_org_id
|
|
145
173
|
# Optional injectable org-id source. When set (managed/multi-tenant mode),
|
|
@@ -149,6 +177,8 @@ class LineageGCScheduler(ThreadedScheduler):
|
|
|
149
177
|
# raises NotImplementedError on Supabase. When None (OSS default),
|
|
150
178
|
# discovery falls back to storage.list_org_ids() exactly as before.
|
|
151
179
|
self.org_id_provider = org_id_provider
|
|
180
|
+
# Orgs that timed out on the PREVIOUS tick; a repeat escalates.
|
|
181
|
+
self._prior_timeout_orgs: set[str] = set()
|
|
152
182
|
|
|
153
183
|
def _on_started(self) -> None:
|
|
154
184
|
logger.info("event=lineage_gc_scheduler_started")
|
|
@@ -197,119 +227,163 @@ class LineageGCScheduler(ThreadedScheduler):
|
|
|
197
227
|
org_ids = [bootstrap_ctx.org_id, *org_ids]
|
|
198
228
|
return org_ids
|
|
199
229
|
|
|
200
|
-
def
|
|
201
|
-
"""Run
|
|
230
|
+
def _sweep_org(self, org_id: str) -> None:
|
|
231
|
+
"""Run Class A, Class B, and per-org sweeps for a single org.
|
|
202
232
|
|
|
203
|
-
|
|
233
|
+
Extracted from ``_gc_tick``'s per-org loop body so it can be handed to
|
|
234
|
+
:func:`iterate_orgs_bounded` as the per-org work item; the stop-event
|
|
235
|
+
check that used to guard the loop iteration now lives in that helper.
|
|
204
236
|
|
|
205
237
|
Args:
|
|
206
|
-
|
|
238
|
+
org_id (str): The org to sweep this tick.
|
|
207
239
|
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
try:
|
|
241
|
+
ctx = self.request_context_factory(org_id)
|
|
242
|
+
if ctx.storage is None:
|
|
243
|
+
return
|
|
244
|
+
cfg = ctx.configurator.get_config()
|
|
245
|
+
except Exception:
|
|
246
|
+
capture_anomaly("lineage.gc.run_failed", org_id=org_id)
|
|
247
|
+
logger.exception("event=lineage_gc_org_failed org_id=%s", org_id)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# Class A: profile expiry sweep (requires PII/grace sign-off; gated on
|
|
251
|
+
# lineage_gc.enabled independently of Class B).
|
|
252
|
+
if cfg.lineage_gc.enabled:
|
|
211
253
|
try:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
expired_tombstoned = ctx.storage.expire_active_profiles(
|
|
255
|
+
now=int(time.time())
|
|
256
|
+
)
|
|
257
|
+
if expired_tombstoned:
|
|
258
|
+
logger.info(
|
|
259
|
+
"event=expiry_sweep org_id=%s profiles_tombstoned=%d",
|
|
260
|
+
org_id,
|
|
261
|
+
expired_tombstoned,
|
|
262
|
+
)
|
|
263
|
+
if expired_tombstoned > _HIGH_VOLUME_THRESHOLD:
|
|
264
|
+
capture_anomaly(
|
|
265
|
+
"lineage.expiry_sweep.high_volume",
|
|
266
|
+
org_id=org_id,
|
|
267
|
+
count=expired_tombstoned,
|
|
268
|
+
)
|
|
216
269
|
except Exception:
|
|
217
|
-
capture_anomaly("lineage.
|
|
218
|
-
logger.exception("event=
|
|
219
|
-
continue
|
|
270
|
+
capture_anomaly("lineage.expiry_sweep.failed", org_id=org_id)
|
|
271
|
+
logger.exception("event=lineage_expiry_sweep_failed org_id=%s", org_id)
|
|
220
272
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
273
|
+
# Tombstone GC: each entity type is independent within the loop.
|
|
274
|
+
try:
|
|
275
|
+
older_than_epoch = (
|
|
276
|
+
int(time.time())
|
|
277
|
+
- cfg.lineage_gc.tombstone_grace_window_days * 86400
|
|
278
|
+
)
|
|
279
|
+
tombstone_deleted = 0
|
|
280
|
+
for entity_type in _ENTITY_TYPES:
|
|
281
|
+
tombstone_deleted += ctx.storage.gc_expired_tombstones(
|
|
282
|
+
entity_type=entity_type,
|
|
283
|
+
older_than_epoch=older_than_epoch,
|
|
227
284
|
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
if expired_tombstoned > _HIGH_VOLUME_THRESHOLD:
|
|
235
|
-
capture_anomaly(
|
|
236
|
-
"lineage.expiry_sweep.high_volume",
|
|
237
|
-
org_id=org_id,
|
|
238
|
-
count=expired_tombstoned,
|
|
239
|
-
)
|
|
240
|
-
except Exception:
|
|
241
|
-
capture_anomaly("lineage.expiry_sweep.failed", org_id=org_id)
|
|
242
|
-
logger.exception(
|
|
243
|
-
"event=lineage_expiry_sweep_failed org_id=%s", org_id
|
|
285
|
+
if tombstone_deleted:
|
|
286
|
+
logger.info(
|
|
287
|
+
"event=lineage_gc_tick org_id=%s tombstone_deleted=%d",
|
|
288
|
+
org_id,
|
|
289
|
+
tombstone_deleted,
|
|
244
290
|
)
|
|
291
|
+
if tombstone_deleted > _HIGH_VOLUME_THRESHOLD:
|
|
292
|
+
capture_anomaly(
|
|
293
|
+
"lineage.gc.high_volume",
|
|
294
|
+
org_id=org_id,
|
|
295
|
+
count=tombstone_deleted,
|
|
296
|
+
)
|
|
297
|
+
except Exception:
|
|
298
|
+
capture_anomaly("lineage.gc.tombstone_gc_failed", org_id=org_id)
|
|
299
|
+
logger.exception(
|
|
300
|
+
"event=lineage_gc_tombstone_gc_failed org_id=%s", org_id
|
|
301
|
+
)
|
|
245
302
|
|
|
246
|
-
|
|
303
|
+
# Class B: direct-delete of expired plain rows (no audit/grace
|
|
304
|
+
# obligation; independent of lineage_gc). Each sweep is isolated so
|
|
305
|
+
# one failing method does not skip the rest.
|
|
306
|
+
if (
|
|
307
|
+
getattr(cfg, "expiry_reclamation", None) is not None
|
|
308
|
+
and cfg.expiry_reclamation.enabled
|
|
309
|
+
):
|
|
310
|
+
now = int(time.time())
|
|
311
|
+
for method_name, grace, limit in _CLASS_B_SWEEPS:
|
|
312
|
+
method = getattr(ctx.storage, method_name, None)
|
|
313
|
+
if method is None:
|
|
314
|
+
continue
|
|
247
315
|
try:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
- cfg.lineage_gc.tombstone_grace_window_days * 86400
|
|
251
|
-
)
|
|
252
|
-
tombstone_deleted = 0
|
|
253
|
-
for entity_type in _ENTITY_TYPES:
|
|
254
|
-
tombstone_deleted += ctx.storage.gc_expired_tombstones(
|
|
255
|
-
entity_type=entity_type,
|
|
256
|
-
older_than_epoch=older_than_epoch,
|
|
257
|
-
)
|
|
258
|
-
if tombstone_deleted:
|
|
316
|
+
deleted = method(now=now, grace_seconds=grace, limit=limit)
|
|
317
|
+
if deleted:
|
|
259
318
|
logger.info(
|
|
260
|
-
"event=
|
|
319
|
+
"event=class_b_reclaim org_id=%s method=%s deleted=%d",
|
|
261
320
|
org_id,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if tombstone_deleted > _HIGH_VOLUME_THRESHOLD:
|
|
265
|
-
capture_anomaly(
|
|
266
|
-
"lineage.gc.high_volume",
|
|
267
|
-
org_id=org_id,
|
|
268
|
-
count=tombstone_deleted,
|
|
321
|
+
method_name,
|
|
322
|
+
deleted,
|
|
269
323
|
)
|
|
270
324
|
except Exception:
|
|
271
|
-
capture_anomaly(
|
|
325
|
+
capture_anomaly(
|
|
326
|
+
"lineage.class_b_reclaim.failed",
|
|
327
|
+
org_id=org_id,
|
|
328
|
+
method=method_name,
|
|
329
|
+
)
|
|
272
330
|
logger.exception(
|
|
273
|
-
"event=
|
|
331
|
+
"event=class_b_reclaim_failed org_id=%s method=%s",
|
|
332
|
+
org_id,
|
|
333
|
+
method_name,
|
|
274
334
|
)
|
|
275
335
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
getattr(cfg, "expiry_reclamation", None) is not None
|
|
281
|
-
and cfg.expiry_reclamation.enabled
|
|
282
|
-
):
|
|
283
|
-
now = int(time.time())
|
|
284
|
-
for method_name, grace, limit in _CLASS_B_SWEEPS:
|
|
285
|
-
method = getattr(ctx.storage, method_name, None)
|
|
286
|
-
if method is None:
|
|
287
|
-
continue
|
|
288
|
-
try:
|
|
289
|
-
deleted = method(now=now, grace_seconds=grace, limit=limit)
|
|
290
|
-
if deleted:
|
|
291
|
-
logger.info(
|
|
292
|
-
"event=class_b_reclaim org_id=%s method=%s deleted=%d",
|
|
293
|
-
org_id,
|
|
294
|
-
method_name,
|
|
295
|
-
deleted,
|
|
296
|
-
)
|
|
297
|
-
except Exception:
|
|
298
|
-
capture_anomaly(
|
|
299
|
-
"lineage.class_b_reclaim.failed",
|
|
300
|
-
org_id=org_id,
|
|
301
|
-
method=method_name,
|
|
302
|
-
)
|
|
303
|
-
logger.exception(
|
|
304
|
-
"event=class_b_reclaim_failed org_id=%s method=%s",
|
|
305
|
-
org_id,
|
|
306
|
-
method_name,
|
|
307
|
-
)
|
|
336
|
+
# Per-org sweeps: invoked unconditionally once per org (the real gate
|
|
337
|
+
# lives in each enterprise closure). Extracted to keep _gc_tick's
|
|
338
|
+
# cyclomatic complexity in check and to mirror _run_global_sweeps.
|
|
339
|
+
self._run_per_org_sweeps(org_id)
|
|
308
340
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
341
|
+
def _gc_tick(self, org_ids: list[str], *, max_workers: int = 1) -> None:
|
|
342
|
+
"""Run one GC pass across the given org IDs.
|
|
343
|
+
|
|
344
|
+
Factored out of ``_run_loop`` so tests can exercise it without threads.
|
|
345
|
+
Default ``max_workers=1`` is the serial path (used by existing tests);
|
|
346
|
+
``_run_once`` passes the configured fan-out width.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
org_ids (list[str]): Org IDs to process in this tick.
|
|
350
|
+
max_workers (int): Bounded fan-out width (1 = serial).
|
|
351
|
+
"""
|
|
352
|
+
timed_out = iterate_orgs_bounded(
|
|
353
|
+
org_ids,
|
|
354
|
+
self._sweep_org,
|
|
355
|
+
max_workers=max_workers,
|
|
356
|
+
per_org_timeout_seconds=_ORG_SWEEP_TIMEOUT_SECONDS,
|
|
357
|
+
stop_event=self._stop_event,
|
|
358
|
+
)
|
|
359
|
+
for org_id in set(timed_out) & self._prior_timeout_orgs:
|
|
360
|
+
capture_anomaly("lineage.gc.org_sweep_timeout_repeat", org_id=org_id)
|
|
361
|
+
self._prior_timeout_orgs = set(timed_out)
|
|
362
|
+
|
|
363
|
+
def _org_fanout_workers(self, bootstrap_ctx: RequestContext) -> int:
|
|
364
|
+
"""Resolve the fan-out width: SQLite pins to 1; else env (default 8).
|
|
365
|
+
|
|
366
|
+
SQLite: each worker would construct a fresh storage (connection +
|
|
367
|
+
migrate()) against one shared DB file — contention the serial loop
|
|
368
|
+
never produces — and OSS-local is usually one org anyway (spec 6.1).
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
bootstrap_ctx (RequestContext): This tick's bootstrap context.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
int: The pool width for this tick.
|
|
375
|
+
"""
|
|
376
|
+
storage = getattr(bootstrap_ctx, "storage", None)
|
|
377
|
+
if storage is not None and "sqlite" in type(storage).__name__.lower():
|
|
378
|
+
return 1
|
|
379
|
+
raw = env_str(
|
|
380
|
+
"REFLEXIO_SCHEDULER_ORG_WORKERS", str(_DEFAULT_ORG_FANOUT_WORKERS)
|
|
381
|
+
)
|
|
382
|
+
try:
|
|
383
|
+
value = int(raw)
|
|
384
|
+
except ValueError:
|
|
385
|
+
return _DEFAULT_ORG_FANOUT_WORKERS
|
|
386
|
+
return value if value > 0 else _DEFAULT_ORG_FANOUT_WORKERS
|
|
313
387
|
|
|
314
388
|
def _run_per_org_sweeps(self, org_id: str) -> None:
|
|
315
389
|
"""Invoke each registered per-org sweep once for this org.
|
|
@@ -378,7 +452,7 @@ class LineageGCScheduler(ThreadedScheduler):
|
|
|
378
452
|
cfg = bootstrap_ctx.configurator.get_config()
|
|
379
453
|
poll_interval = cfg.lineage_gc.poll_interval_seconds
|
|
380
454
|
org_ids = self._discover_org_ids(bootstrap_ctx)
|
|
381
|
-
self._gc_tick(org_ids)
|
|
455
|
+
self._gc_tick(org_ids, max_workers=self._org_fanout_workers(bootstrap_ctx))
|
|
382
456
|
self._run_global_sweeps(cfg)
|
|
383
457
|
except Exception:
|
|
384
458
|
logger.exception("event=lineage_gc_scheduler_tick_failed")
|
|
@@ -390,6 +464,7 @@ def maybe_start_lineage_gc(
|
|
|
390
464
|
*,
|
|
391
465
|
bootstrap_org_id: str,
|
|
392
466
|
org_id_provider: Callable[[], list[str]] | None = None,
|
|
467
|
+
leader_gate: LeaderGate | None = None,
|
|
393
468
|
) -> LineageGCScheduler | None:
|
|
394
469
|
"""Start the scheduler when bootstrap config enables tombstone GC or expiry reclamation.
|
|
395
470
|
|
|
@@ -429,6 +504,10 @@ def maybe_start_lineage_gc(
|
|
|
429
504
|
(OSS default), falls back to the module-level provider hook set via
|
|
430
505
|
:func:`set_org_id_provider`; when both are ``None`` discovery uses
|
|
431
506
|
``storage.list_org_ids()`` exactly as before.
|
|
507
|
+
leader_gate: Optional fleet-coordination gate. When ``None`` (OSS
|
|
508
|
+
default), falls back to the module-level hook set via
|
|
509
|
+
:func:`set_leader_gate`; when both are ``None`` the scheduler ticks
|
|
510
|
+
unconditionally (today's behavior).
|
|
432
511
|
|
|
433
512
|
Returns:
|
|
434
513
|
LineageGCScheduler: The started scheduler, or ``None`` if no start
|
|
@@ -495,6 +574,7 @@ def maybe_start_lineage_gc(
|
|
|
495
574
|
org_id_provider=(
|
|
496
575
|
org_id_provider if org_id_provider is not None else _org_id_provider_hook
|
|
497
576
|
),
|
|
577
|
+
leader_gate=leader_gate if leader_gate is not None else _leader_gate_hook,
|
|
498
578
|
)
|
|
499
579
|
scheduler.start()
|
|
500
580
|
return scheduler
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Missing-vector backfill sweep for the shared ``LineageGCScheduler``.
|
|
2
|
+
|
|
3
|
+
When an embedding call fails or degrades, the ingest path stores an empty
|
|
4
|
+
embedding (and writes no vector row), returns success, and nothing ever
|
|
5
|
+
re-embeds the row — so that interaction is invisible to vector/hybrid search
|
|
6
|
+
forever. This is a latent durability hole. This module registers a bounded,
|
|
7
|
+
idempotent, per-org sweep that re-embeds those interactions on the shared GC
|
|
8
|
+
scheduler cadence.
|
|
9
|
+
|
|
10
|
+
Design:
|
|
11
|
+
|
|
12
|
+
- **Per-org**: registered via :func:`register_per_org_sweep`, so it runs once
|
|
13
|
+
per org per tick with the org id supplied by the scheduler (the ``(org_id,
|
|
14
|
+
now) -> int`` seam). The global ``register_global_sweep`` seam only passes
|
|
15
|
+
``now`` and cannot drive a per-org backfill, so it is the wrong hook here.
|
|
16
|
+
- **Bounded**: re-embeds at most ``REFLEXIO_MISSING_VECTOR_BACKFILL_CAP``
|
|
17
|
+
interactions per org per tick (prior art warns unbounded backfills flood
|
|
18
|
+
logs / embedding cost — see ``llm/_litellm_embedding.py``).
|
|
19
|
+
- **Opt-in**: only registered when ``REFLEXIO_MISSING_VECTOR_BACKFILL_ENABLED``
|
|
20
|
+
is truthy, so a default OSS deployment is byte-for-byte unchanged (the
|
|
21
|
+
scheduler does not start on this hook alone). The enable flag is re-read every
|
|
22
|
+
tick so a live disable is honoured without a restart.
|
|
23
|
+
- **Fail-safe**: the storage layer catches ``EmbeddingUnavailableError`` and
|
|
24
|
+
returns 0 (work left for the next tick); this closure additionally captures
|
|
25
|
+
any unexpected error as an anomaly and returns the count so far, so one org's
|
|
26
|
+
failure never stalls the shared loop.
|
|
27
|
+
- **Idempotent**: once an interaction's vector is written it no longer matches
|
|
28
|
+
detection and is skipped next tick.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
|
|
35
|
+
from reflexio.server.env_utils import env_str, env_truthy
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
ENABLE_ENV_VAR = "REFLEXIO_MISSING_VECTOR_BACKFILL_ENABLED"
|
|
40
|
+
CAP_ENV_VAR = "REFLEXIO_MISSING_VECTOR_BACKFILL_CAP"
|
|
41
|
+
_DEFAULT_CAP = 200
|
|
42
|
+
|
|
43
|
+
# Guard against duplicate registration when the app lifespan runs more than once
|
|
44
|
+
# in a single process (e.g. test apps).
|
|
45
|
+
_installed = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_cap() -> int:
|
|
49
|
+
"""Return the per-org per-tick backfill cap from the environment.
|
|
50
|
+
|
|
51
|
+
Falls back to :data:`_DEFAULT_CAP` when unset, blank, or unparseable/<= 0.
|
|
52
|
+
"""
|
|
53
|
+
raw = env_str(CAP_ENV_VAR, str(_DEFAULT_CAP))
|
|
54
|
+
try:
|
|
55
|
+
cap = int(raw)
|
|
56
|
+
except ValueError:
|
|
57
|
+
logger.warning(
|
|
58
|
+
"event=missing_vector_backfill_bad_cap value=%r — using default %d",
|
|
59
|
+
raw,
|
|
60
|
+
_DEFAULT_CAP,
|
|
61
|
+
)
|
|
62
|
+
return _DEFAULT_CAP
|
|
63
|
+
return cap if cap > 0 else _DEFAULT_CAP
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def missing_vector_backfill_sweep(org_id: str, _now: int) -> int:
|
|
67
|
+
"""Re-embed a bounded batch of vector-less interactions for one org.
|
|
68
|
+
|
|
69
|
+
Registered with the OSS ``register_per_org_sweep`` seam. Reads the enable
|
|
70
|
+
flag and cap each tick so live config changes are honoured. Absorbs its own
|
|
71
|
+
errors (returns instead of raising) and therefore emits its own anomaly.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
org_id (str): The org to sweep.
|
|
75
|
+
_now (int): Current unix epoch supplied by the scheduler. Unused — the
|
|
76
|
+
cap, not a time cutoff, bounds the work.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
int: Number of interactions whose vector was backfilled (0 on skip/error).
|
|
80
|
+
"""
|
|
81
|
+
if not env_truthy(env_str(ENABLE_ENV_VAR, "false")):
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
# Imported lazily so importing this module never drags in the request stack.
|
|
85
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
86
|
+
from reflexio.server.tracing import capture_anomaly
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
ctx = RequestContext(org_id=org_id)
|
|
90
|
+
if ctx.storage is None:
|
|
91
|
+
return 0
|
|
92
|
+
backfilled = ctx.storage.backfill_missing_interaction_vectors(_resolve_cap())
|
|
93
|
+
if backfilled:
|
|
94
|
+
logger.info(
|
|
95
|
+
"event=missing_vector_backfill_tick org_id=%s backfilled=%d",
|
|
96
|
+
org_id,
|
|
97
|
+
backfilled,
|
|
98
|
+
)
|
|
99
|
+
return backfilled
|
|
100
|
+
except Exception:
|
|
101
|
+
capture_anomaly(
|
|
102
|
+
"interactions.missing_vector_backfill.run_failed", org_id=org_id
|
|
103
|
+
)
|
|
104
|
+
logger.exception("event=missing_vector_backfill_org_failed org_id=%s", org_id)
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def install_missing_vector_backfill_sweep() -> None:
|
|
109
|
+
"""Register the backfill sweep on the OSS lineage GC scheduler when enabled.
|
|
110
|
+
|
|
111
|
+
Only registers when ``REFLEXIO_MISSING_VECTOR_BACKFILL_ENABLED`` is truthy,
|
|
112
|
+
so an OSS deployment that has not opted in keeps its scheduler start
|
|
113
|
+
conditions unchanged. Idempotent: a second call in the same process is a
|
|
114
|
+
no-op. Non-fatal: a registration failure logs and skips rather than aborting
|
|
115
|
+
app startup.
|
|
116
|
+
|
|
117
|
+
Call this before ``maybe_start_lineage_gc`` so the registered hook is
|
|
118
|
+
visible when the scheduler evaluates its start conditions.
|
|
119
|
+
"""
|
|
120
|
+
global _installed # noqa: PLW0603
|
|
121
|
+
if _installed:
|
|
122
|
+
return
|
|
123
|
+
if not env_truthy(env_str(ENABLE_ENV_VAR, "false")):
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
from reflexio.server.services.lineage.gc_scheduler import (
|
|
127
|
+
register_per_org_sweep,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
register_per_org_sweep(missing_vector_backfill_sweep)
|
|
131
|
+
_installed = True
|
|
132
|
+
logger.info(
|
|
133
|
+
"event=missing_vector_backfill_sweep_registered cap=%d", _resolve_cap()
|
|
134
|
+
)
|
|
135
|
+
except Exception:
|
|
136
|
+
from reflexio.server.tracing import capture_anomaly
|
|
137
|
+
|
|
138
|
+
capture_anomaly("interactions.missing_vector_backfill.install_failed")
|
|
139
|
+
logger.exception("event=missing_vector_backfill_install_failed")
|