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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/pyproject.toml +1 -1
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +133 -3273
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +29 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +56 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +6 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +6 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +13 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +33 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +44 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
|
@@ -53,12 +53,7 @@ from reflexio.server.services.storage.error import (
|
|
|
53
53
|
StorageError,
|
|
54
54
|
require_non_empty_session_id,
|
|
55
55
|
)
|
|
56
|
-
from reflexio.server.services.storage.
|
|
57
|
-
from reflexio.server.services.storage.retention_mixin import (
|
|
58
|
-
RETENTION_DELETE_CHUNK,
|
|
59
|
-
RetentionMixin,
|
|
60
|
-
chunked,
|
|
61
|
-
)
|
|
56
|
+
from reflexio.server.services.storage.retention_mixin import RetentionMixin
|
|
62
57
|
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
63
58
|
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
64
59
|
|
|
@@ -288,6 +283,42 @@ def _true_rrf_merge(
|
|
|
288
283
|
# explicit status_filter on list/count methods.
|
|
289
284
|
_TOMBSTONE_STATUS_VALUES = (Status.MERGED.value, Status.SUPERSEDED.value)
|
|
290
285
|
|
|
286
|
+
# Profile reads also treat EXPIRED (TTL-expired tombstone) as non-current. Kept
|
|
287
|
+
# separate from _TOMBSTONE_STATUS_VALUES because that tuple is shared with playbook
|
|
288
|
+
# queries (which never carry EXPIRED) via hardcoded placeholders.
|
|
289
|
+
_PROFILE_TOMBSTONE_STATUS_VALUES = (
|
|
290
|
+
Status.MERGED.value,
|
|
291
|
+
Status.SUPERSEDED.value,
|
|
292
|
+
Status.EXPIRED.value,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def parse_status(value: str | None) -> Status | None:
|
|
297
|
+
"""Parse a stored status string into a Status, tolerating unknown values.
|
|
298
|
+
|
|
299
|
+
Unknown non-null values (e.g. a status written by a newer build after a
|
|
300
|
+
rollback) map to a non-current tombstone sentinel and log an anomaly, rather
|
|
301
|
+
than raising ValueError. Never maps to None (which models CURRENT/live).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
value: Raw status string from the database, or None/empty for CURRENT.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
The matching Status enum member, Status.SUPERSEDED for unknown values,
|
|
308
|
+
or None for falsy input (representing the CURRENT/live state).
|
|
309
|
+
"""
|
|
310
|
+
if not value:
|
|
311
|
+
return None
|
|
312
|
+
try:
|
|
313
|
+
return Status(value)
|
|
314
|
+
except ValueError:
|
|
315
|
+
from reflexio.server.tracing import capture_anomaly
|
|
316
|
+
|
|
317
|
+
capture_anomaly(
|
|
318
|
+
"storage.status.unknown_value", level="warning", status_value=value
|
|
319
|
+
)
|
|
320
|
+
return Status.SUPERSEDED
|
|
321
|
+
|
|
291
322
|
|
|
292
323
|
def _status_value(status: Status | None) -> str | None:
|
|
293
324
|
"""Convert a Status enum (or None) to its DB string value."""
|
|
@@ -363,14 +394,19 @@ def _iso_to_epoch(iso_str: str | None) -> int:
|
|
|
363
394
|
# stored row) with a valid value.
|
|
364
395
|
_MAX_SAFE_EPOCH_TS = 253_402_300_799 # 9999-12-31T23:59:59Z
|
|
365
396
|
_MIN_SAFE_EPOCH_TS = 0 # 1970-01-01T00:00:00Z
|
|
397
|
+
_MIN_CONTEMPORARY_MILLISECOND_EPOCH_TS = 1_500_000_000_000
|
|
366
398
|
|
|
367
399
|
|
|
368
400
|
def _epoch_to_iso(ts: int) -> str:
|
|
369
401
|
"""Convert a Unix timestamp (seconds) to an ISO 8601 string.
|
|
370
402
|
|
|
371
|
-
|
|
372
|
-
|
|
403
|
+
Plausible contemporary millisecond timestamps are normalized to seconds.
|
|
404
|
+
Lower values stay in seconds-space so documented sentinel bounds such as
|
|
405
|
+
``to_ts=10**12`` still clamp to the representable range instead of being
|
|
406
|
+
treated as a 2001 millisecond timestamp.
|
|
373
407
|
"""
|
|
408
|
+
if _MIN_CONTEMPORARY_MILLISECOND_EPOCH_TS <= ts <= _MAX_SAFE_EPOCH_TS * 1000:
|
|
409
|
+
ts = ts // 1000
|
|
374
410
|
clamped = max(_MIN_SAFE_EPOCH_TS, min(ts, _MAX_SAFE_EPOCH_TS))
|
|
375
411
|
return datetime.fromtimestamp(clamped, tz=UTC).isoformat()
|
|
376
412
|
|
|
@@ -392,7 +428,7 @@ def _row_to_profile(row: sqlite3.Row) -> UserProfile:
|
|
|
392
428
|
expiration_timestamp=d["expiration_timestamp"],
|
|
393
429
|
custom_features=_json_loads(d.get("custom_features")),
|
|
394
430
|
source=d.get("source") or "",
|
|
395
|
-
status=
|
|
431
|
+
status=parse_status(d.get("status")),
|
|
396
432
|
extractor_names=_json_loads(d.get("extractor_names")),
|
|
397
433
|
expanded_terms=d.get("expanded_terms"),
|
|
398
434
|
source_span=d.get("source_span"),
|
|
@@ -472,7 +508,7 @@ def _row_to_user_playbook(
|
|
|
472
508
|
blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
|
|
473
509
|
if d.get("blocking_issue")
|
|
474
510
|
else None,
|
|
475
|
-
status=
|
|
511
|
+
status=parse_status(d.get("status")),
|
|
476
512
|
source=d.get("source"),
|
|
477
513
|
source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
|
|
478
514
|
tags=_json_loads(d.get("tags")),
|
|
@@ -505,7 +541,7 @@ def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
|
|
|
505
541
|
playbook_metadata=d.get("playbook_metadata") or "",
|
|
506
542
|
tags=_json_loads(d.get("tags")),
|
|
507
543
|
embedding=[],
|
|
508
|
-
status=
|
|
544
|
+
status=parse_status(d.get("status")),
|
|
509
545
|
expanded_terms=d.get("expanded_terms"),
|
|
510
546
|
merged_into=d.get("merged_into"),
|
|
511
547
|
superseded_by=d.get("superseded_by"),
|
|
@@ -546,6 +582,15 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
546
582
|
|
|
547
583
|
supports_embedding: ClassVar[bool] = True
|
|
548
584
|
|
|
585
|
+
# Chunked bulk-delete helpers provided by SQLiteDeletionMixin via the
|
|
586
|
+
# composed SQLiteStorage MRO; declared here for clear_user_data's benefit.
|
|
587
|
+
_delete_in_chunks: Any
|
|
588
|
+
_delete_source_windows_for_user_playbook_ids: Any
|
|
589
|
+
|
|
590
|
+
# FTS/vec index helpers provided by SQLiteFtsVecMixin via the composed
|
|
591
|
+
# SQLiteStorage MRO; declared here for _migrate_vec_tables's benefit.
|
|
592
|
+
_vec_upsert: Any
|
|
593
|
+
|
|
549
594
|
@staticmethod
|
|
550
595
|
def handle_exceptions(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
551
596
|
@functools.wraps(func)
|
|
@@ -671,239 +716,6 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
671
716
|
init_stall_state_table(self.conn)
|
|
672
717
|
return True
|
|
673
718
|
|
|
674
|
-
# -- Retention hooks (see RetentionMixin) --
|
|
675
|
-
|
|
676
|
-
@handle_exceptions
|
|
677
|
-
def _retention_table_exists(self, table_name: str) -> bool:
|
|
678
|
-
row = self._fetchone(
|
|
679
|
-
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
680
|
-
(table_name,),
|
|
681
|
-
)
|
|
682
|
-
return row is not None
|
|
683
|
-
|
|
684
|
-
@handle_exceptions
|
|
685
|
-
def _retention_count_rows(self, target: RetentionTarget) -> int:
|
|
686
|
-
row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {target.table_name}") # noqa: S608
|
|
687
|
-
return int(row["cnt"]) if row else 0
|
|
688
|
-
|
|
689
|
-
@handle_exceptions
|
|
690
|
-
def _retention_select_oldest_keys(
|
|
691
|
-
self, target: RetentionTarget, count: int
|
|
692
|
-
) -> list[tuple[Any, ...]]:
|
|
693
|
-
id_sql = ", ".join(target.id_columns)
|
|
694
|
-
tiebreak_sql = id_sql
|
|
695
|
-
rows = self._fetchall(
|
|
696
|
-
f"SELECT {id_sql} FROM {target.table_name} " # noqa: S608
|
|
697
|
-
f"ORDER BY {target.order_column} ASC, {tiebreak_sql} ASC LIMIT ?",
|
|
698
|
-
(count,),
|
|
699
|
-
)
|
|
700
|
-
return [tuple(row[col] for col in target.id_columns) for row in rows]
|
|
701
|
-
|
|
702
|
-
@handle_exceptions
|
|
703
|
-
def _retention_perform_delete(
|
|
704
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
705
|
-
) -> None:
|
|
706
|
-
# Wrap dependency + target deletes in a single critical section so
|
|
707
|
-
# concurrent writers see either both or neither.
|
|
708
|
-
with self._lock:
|
|
709
|
-
try:
|
|
710
|
-
self._retention_delete_dependencies(target, keys)
|
|
711
|
-
self._retention_delete_target_rows(target, keys)
|
|
712
|
-
self.conn.commit()
|
|
713
|
-
except Exception:
|
|
714
|
-
self.conn.rollback()
|
|
715
|
-
raise
|
|
716
|
-
|
|
717
|
-
def _retention_delete_dependencies(
|
|
718
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
719
|
-
) -> None:
|
|
720
|
-
ids = [key[0] for key in keys]
|
|
721
|
-
target_name = target.name
|
|
722
|
-
if target_name == "requests":
|
|
723
|
-
self._delete_interactions_for_request_ids([str(v) for v in ids])
|
|
724
|
-
elif target_name == "interactions":
|
|
725
|
-
self._delete_interaction_search_rows([int(v) for v in ids])
|
|
726
|
-
elif target_name == "profiles":
|
|
727
|
-
self._delete_profile_search_rows([str(v) for v in ids])
|
|
728
|
-
elif target_name == "user_playbooks":
|
|
729
|
-
self._delete_source_windows_for_user_playbook_ids([int(v) for v in ids])
|
|
730
|
-
self._delete_playbook_search_rows(
|
|
731
|
-
"user", [int(v) for v in ids], commit=False
|
|
732
|
-
)
|
|
733
|
-
elif target_name == "agent_playbooks":
|
|
734
|
-
self._delete_source_windows_for_agent_playbook_ids([int(v) for v in ids])
|
|
735
|
-
self._delete_playbook_search_rows(
|
|
736
|
-
"agent", [int(v) for v in ids], commit=False
|
|
737
|
-
)
|
|
738
|
-
elif target_name == "playbook_optimization_jobs":
|
|
739
|
-
self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
|
|
740
|
-
elif target_name == "playbook_optimization_candidates":
|
|
741
|
-
self._delete_optimizer_evaluations_for_candidate_ids([int(v) for v in ids])
|
|
742
|
-
|
|
743
|
-
def _retention_delete_target_rows(
|
|
744
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
745
|
-
) -> None:
|
|
746
|
-
if len(target.id_columns) == 1:
|
|
747
|
-
self._delete_in_chunks(
|
|
748
|
-
target.table_name,
|
|
749
|
-
target.id_columns[0],
|
|
750
|
-
[key[0] for key in keys],
|
|
751
|
-
)
|
|
752
|
-
return
|
|
753
|
-
# Composite-key delete: chunk by row to bound parameter count.
|
|
754
|
-
params_per_key = len(target.id_columns)
|
|
755
|
-
rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
|
|
756
|
-
for chunk in chunked(keys, rows_per_chunk):
|
|
757
|
-
where = " OR ".join(
|
|
758
|
-
"("
|
|
759
|
-
+ " AND ".join(f"{column} = ?" for column in target.id_columns)
|
|
760
|
-
+ ")"
|
|
761
|
-
for _ in chunk
|
|
762
|
-
)
|
|
763
|
-
params = [value for key in chunk for value in key]
|
|
764
|
-
self.conn.execute(
|
|
765
|
-
f"DELETE FROM {target.table_name} WHERE {where}", # noqa: S608
|
|
766
|
-
params,
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
# -- Chunked-delete primitives shared by the cascade helpers --
|
|
770
|
-
|
|
771
|
-
def _delete_in_chunks(
|
|
772
|
-
self, table_name: str, column_name: str, values: list[Any]
|
|
773
|
-
) -> None:
|
|
774
|
-
"""Chunked ``DELETE FROM table WHERE col IN (...)``.
|
|
775
|
-
|
|
776
|
-
Chunking keeps parameter count under ``SQLITE_MAX_VARIABLE_NUMBER``
|
|
777
|
-
on older sqlite builds (default 999) and avoids degenerate plans
|
|
778
|
-
on very large IN lists.
|
|
779
|
-
"""
|
|
780
|
-
if not values:
|
|
781
|
-
return
|
|
782
|
-
for chunk in chunked(values):
|
|
783
|
-
placeholders = ",".join("?" for _ in chunk)
|
|
784
|
-
self.conn.execute(
|
|
785
|
-
f"DELETE FROM {table_name} WHERE {column_name} IN ({placeholders})", # noqa: S608
|
|
786
|
-
chunk,
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
def _select_in_chunks(self, sql_template: str, values: list[Any]) -> list[Any]:
|
|
790
|
-
"""Run ``sql_template`` (containing ``{placeholders}``) over chunks of
|
|
791
|
-
``values`` and aggregate the result rows."""
|
|
792
|
-
results: list[Any] = []
|
|
793
|
-
for chunk in chunked(values):
|
|
794
|
-
placeholders = ",".join("?" for _ in chunk)
|
|
795
|
-
stmt = sql_template.format(placeholders=placeholders)
|
|
796
|
-
results.extend(self.conn.execute(stmt, chunk).fetchall())
|
|
797
|
-
return results
|
|
798
|
-
|
|
799
|
-
def _delete_interactions_for_request_ids(self, request_ids: list[str]) -> None:
|
|
800
|
-
if not request_ids:
|
|
801
|
-
return
|
|
802
|
-
rows = self._select_in_chunks(
|
|
803
|
-
"SELECT interaction_id FROM interactions WHERE request_id IN ({placeholders})",
|
|
804
|
-
request_ids,
|
|
805
|
-
)
|
|
806
|
-
self._delete_interaction_search_rows(
|
|
807
|
-
[int(row["interaction_id"]) for row in rows]
|
|
808
|
-
)
|
|
809
|
-
self._delete_in_chunks("interactions", "request_id", request_ids)
|
|
810
|
-
|
|
811
|
-
def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
|
|
812
|
-
"""Remove fts + vec index rows for the given interaction IDs.
|
|
813
|
-
|
|
814
|
-
Non-committing: participates in the caller's transaction. Only called
|
|
815
|
-
from inside the retention atomic block (_retention_perform_delete).
|
|
816
|
-
"""
|
|
817
|
-
if not interaction_ids:
|
|
818
|
-
return
|
|
819
|
-
self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
|
|
820
|
-
if self._has_sqlite_vec:
|
|
821
|
-
self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
|
|
822
|
-
|
|
823
|
-
def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
|
|
824
|
-
"""Remove fts + vec index rows for the given profile IDs.
|
|
825
|
-
|
|
826
|
-
Non-committing: participates in the caller's transaction. Only called
|
|
827
|
-
from inside the retention atomic block (_retention_perform_delete).
|
|
828
|
-
profiles_fts is keyed by profile_id (TEXT); profiles_vec by rowid (INT).
|
|
829
|
-
"""
|
|
830
|
-
if not profile_ids:
|
|
831
|
-
return
|
|
832
|
-
self._delete_in_chunks("profiles_fts", "profile_id", profile_ids)
|
|
833
|
-
if self._has_sqlite_vec:
|
|
834
|
-
rows = self._select_in_chunks(
|
|
835
|
-
"SELECT rowid FROM profiles WHERE profile_id IN ({placeholders})",
|
|
836
|
-
profile_ids,
|
|
837
|
-
)
|
|
838
|
-
rowids = [row["rowid"] for row in rows]
|
|
839
|
-
if rowids:
|
|
840
|
-
self._delete_in_chunks("profiles_vec", "rowid", rowids)
|
|
841
|
-
|
|
842
|
-
def _delete_playbook_search_rows(
|
|
843
|
-
self, kind: str, ids: list[int], *, commit: bool = True
|
|
844
|
-
) -> None:
|
|
845
|
-
"""Remove fts + vec index rows for the given playbook IDs.
|
|
846
|
-
|
|
847
|
-
Args:
|
|
848
|
-
kind: ``"user"`` or ``"agent"``.
|
|
849
|
-
ids: Playbook row IDs to remove from the search indexes.
|
|
850
|
-
commit: When ``True`` (default) commits after the deletes so the
|
|
851
|
-
after-commit callers in ``_playbook.py`` get a clean, durable
|
|
852
|
-
cleanup. Pass ``commit=False`` from inside the retention atomic
|
|
853
|
-
block so the deletes participate in the single block-level commit
|
|
854
|
-
(``_retention_perform_delete``).
|
|
855
|
-
|
|
856
|
-
Note: callers may already hold ``self._lock`` when calling this (the
|
|
857
|
-
``commit=False`` retention/atomic-delete call sites do). The internal
|
|
858
|
-
``with self._lock:`` re-acquire is safe ONLY because ``self._lock`` is a
|
|
859
|
-
reentrant ``threading.RLock``; a non-reentrant lock would deadlock here.
|
|
860
|
-
"""
|
|
861
|
-
if not ids:
|
|
862
|
-
return
|
|
863
|
-
with self._lock:
|
|
864
|
-
self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
|
|
865
|
-
if self._has_sqlite_vec:
|
|
866
|
-
self._delete_in_chunks(f"{kind}_playbooks_vec", "rowid", ids)
|
|
867
|
-
if commit:
|
|
868
|
-
self.conn.commit()
|
|
869
|
-
|
|
870
|
-
def _delete_source_windows_for_agent_playbook_ids(
|
|
871
|
-
self, agent_playbook_ids: list[int]
|
|
872
|
-
) -> None:
|
|
873
|
-
self._delete_in_chunks(
|
|
874
|
-
"agent_playbook_source_user_playbooks",
|
|
875
|
-
"agent_playbook_id",
|
|
876
|
-
agent_playbook_ids,
|
|
877
|
-
)
|
|
878
|
-
|
|
879
|
-
def _delete_source_windows_for_user_playbook_ids(
|
|
880
|
-
self, user_playbook_ids: list[int]
|
|
881
|
-
) -> None:
|
|
882
|
-
self._delete_in_chunks(
|
|
883
|
-
"agent_playbook_source_user_playbooks",
|
|
884
|
-
"user_playbook_id",
|
|
885
|
-
user_playbook_ids,
|
|
886
|
-
)
|
|
887
|
-
|
|
888
|
-
def _delete_optimizer_rows_for_job_ids(self, job_ids: list[int]) -> None:
|
|
889
|
-
if not job_ids:
|
|
890
|
-
return
|
|
891
|
-
for table in (
|
|
892
|
-
"playbook_optimization_evaluations",
|
|
893
|
-
"playbook_optimization_events",
|
|
894
|
-
"playbook_optimization_candidates",
|
|
895
|
-
):
|
|
896
|
-
self._delete_in_chunks(table, "job_id", job_ids)
|
|
897
|
-
|
|
898
|
-
def _delete_optimizer_evaluations_for_candidate_ids(
|
|
899
|
-
self, candidate_ids: list[int]
|
|
900
|
-
) -> None:
|
|
901
|
-
self._delete_in_chunks(
|
|
902
|
-
"playbook_optimization_evaluations",
|
|
903
|
-
"candidate_id",
|
|
904
|
-
candidate_ids,
|
|
905
|
-
)
|
|
906
|
-
|
|
907
719
|
def _try_load_sqlite_vec(self) -> bool:
|
|
908
720
|
"""Attempt to load the sqlite-vec extension for native KNN search.
|
|
909
721
|
|
|
@@ -1717,113 +1529,6 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
1717
1529
|
def _current_timestamp(self) -> str:
|
|
1718
1530
|
return datetime.now(UTC).isoformat()
|
|
1719
1531
|
|
|
1720
|
-
# FTS helpers
|
|
1721
|
-
def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
|
|
1722
|
-
"""Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
|
|
1723
|
-
with self._lock:
|
|
1724
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1725
|
-
cols = list(text_fields.keys())
|
|
1726
|
-
vals = [text_fields[c] or "" for c in cols]
|
|
1727
|
-
placeholders = ",".join("?" for _ in cols)
|
|
1728
|
-
col_str = ",".join(cols)
|
|
1729
|
-
self.conn.execute(
|
|
1730
|
-
f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})",
|
|
1731
|
-
[rowid, *vals],
|
|
1732
|
-
)
|
|
1733
|
-
self.conn.commit()
|
|
1734
|
-
|
|
1735
|
-
def _fts_delete(self, table: str, rowid: int) -> None:
|
|
1736
|
-
with self._lock:
|
|
1737
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1738
|
-
self.conn.commit()
|
|
1739
|
-
|
|
1740
|
-
def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
|
|
1741
|
-
"""FTS for profiles uses profile_id TEXT as key column."""
|
|
1742
|
-
with self._lock:
|
|
1743
|
-
self.conn.execute(
|
|
1744
|
-
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1745
|
-
)
|
|
1746
|
-
self.conn.execute(
|
|
1747
|
-
"INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
|
|
1748
|
-
(profile_id, content),
|
|
1749
|
-
)
|
|
1750
|
-
self.conn.commit()
|
|
1751
|
-
|
|
1752
|
-
def _fts_delete_profile(self, profile_id: str) -> None:
|
|
1753
|
-
with self._lock:
|
|
1754
|
-
self.conn.execute(
|
|
1755
|
-
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1756
|
-
)
|
|
1757
|
-
self.conn.commit()
|
|
1758
|
-
|
|
1759
|
-
# Vec helpers (sqlite-vec)
|
|
1760
|
-
def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
1761
|
-
"""Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1762
|
-
if not self._has_sqlite_vec:
|
|
1763
|
-
return
|
|
1764
|
-
with self._lock:
|
|
1765
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1766
|
-
self.conn.execute(
|
|
1767
|
-
f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)",
|
|
1768
|
-
(rowid, json.dumps(embedding)),
|
|
1769
|
-
)
|
|
1770
|
-
self.conn.commit()
|
|
1771
|
-
|
|
1772
|
-
def _vec_delete(self, table: str, rowid: int) -> None:
|
|
1773
|
-
"""Delete a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1774
|
-
if not self._has_sqlite_vec:
|
|
1775
|
-
return
|
|
1776
|
-
with self._lock:
|
|
1777
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1778
|
-
self.conn.commit()
|
|
1779
|
-
|
|
1780
|
-
def _vec_knn_search(
|
|
1781
|
-
self,
|
|
1782
|
-
vec_table: str,
|
|
1783
|
-
main_table: str,
|
|
1784
|
-
query_embedding: list[float],
|
|
1785
|
-
match_count: int,
|
|
1786
|
-
conditions: list[str] | None = None,
|
|
1787
|
-
params: list[Any] | None = None,
|
|
1788
|
-
) -> list[sqlite3.Row]:
|
|
1789
|
-
"""Run a native KNN search via sqlite-vec and join back to the main table.
|
|
1790
|
-
|
|
1791
|
-
Over-fetches from the KNN index (5x ``match_count``) so that post-filter
|
|
1792
|
-
WHERE conditions (org, user, status, etc.) don't silently reduce the
|
|
1793
|
-
result set below the requested count.
|
|
1794
|
-
|
|
1795
|
-
Args:
|
|
1796
|
-
vec_table: Name of the vec0 virtual table.
|
|
1797
|
-
main_table: Name of the main data table.
|
|
1798
|
-
query_embedding: Query embedding vector.
|
|
1799
|
-
match_count: Number of results to return.
|
|
1800
|
-
conditions: Optional WHERE conditions for the main table.
|
|
1801
|
-
params: Parameters for the conditions.
|
|
1802
|
-
|
|
1803
|
-
Returns:
|
|
1804
|
-
Up to ``match_count`` rows from the main table, ordered by vector
|
|
1805
|
-
distance (ascending).
|
|
1806
|
-
"""
|
|
1807
|
-
knn_overfetch = match_count * 5
|
|
1808
|
-
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
1809
|
-
sql = f"""SELECT m.* FROM {main_table} m
|
|
1810
|
-
JOIN (
|
|
1811
|
-
SELECT rowid, distance FROM {vec_table}
|
|
1812
|
-
WHERE embedding MATCH ?
|
|
1813
|
-
ORDER BY distance
|
|
1814
|
-
LIMIT ?
|
|
1815
|
-
) v ON m.rowid = v.rowid
|
|
1816
|
-
WHERE {where_clause}
|
|
1817
|
-
ORDER BY v.distance
|
|
1818
|
-
LIMIT ?"""
|
|
1819
|
-
all_params = [
|
|
1820
|
-
json.dumps(query_embedding),
|
|
1821
|
-
knn_overfetch,
|
|
1822
|
-
*(params or []),
|
|
1823
|
-
match_count,
|
|
1824
|
-
]
|
|
1825
|
-
return self._fetchall(sql, all_params)
|
|
1826
|
-
|
|
1827
1532
|
# ------------------------------------------------------------------
|
|
1828
1533
|
# Per-user data clear
|
|
1829
1534
|
# ------------------------------------------------------------------
|