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
@@ -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.retention import RetentionTarget
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
- Out-of-range sentinel bounds are clamped to the representable range so that
372
- callers passing "open" window bounds never trigger a ``ValueError``.
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=Status(d["status"]) if d.get("status") else None,
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=Status(d["status"]) if d.get("status") else None,
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=Status(d["status"]) if d.get("status") else None,
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
  # ------------------------------------------------------------------