claude-smart 0.2.47 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  8. package/plugin/dashboard/package-lock.json +61 -390
  9. package/plugin/dashboard/package.json +2 -2
  10. package/plugin/opencode/dist/server.mjs +60 -2
  11. package/plugin/opencode/server.mts +64 -2
  12. package/plugin/pyproject.toml +2 -2
  13. package/plugin/scripts/_lib.sh +58 -6
  14. package/plugin/scripts/backend-service.sh +15 -10
  15. package/plugin/scripts/codex-hook.js +16 -4
  16. package/plugin/scripts/dashboard-service.sh +1 -1
  17. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  18. package/plugin/scripts/opencode-claude-compat.js +8 -1
  19. package/plugin/scripts/smart-install.sh +101 -20
  20. package/plugin/src/README.md +1 -1
  21. package/plugin/src/claude_smart/cli.py +79 -29
  22. package/plugin/src/claude_smart/env_config.py +53 -11
  23. package/plugin/uv.lock +5 -5
  24. package/plugin/vendor/reflexio/.env.example +7 -0
  25. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  26. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  27. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  28. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  29. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  30. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  31. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  33. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  34. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  35. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  36. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  37. package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
  38. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  39. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  40. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  41. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  42. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  43. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  44. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  45. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  46. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  47. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  48. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
  49. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  50. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  51. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  52. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  53. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  54. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  55. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  56. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  57. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  58. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  59. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  60. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
  61. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  62. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  63. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  64. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  66. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  68. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  73. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  74. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  76. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  77. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
  81. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
  82. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  83. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  84. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  88. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  89. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -0,0 +1,123 @@
1
+ """SQLite RunToolDependencyStore methods (the RunToolDependencyStore bucket).
2
+
3
+ Extracted verbatim from ``_agent_run.py`` (the RunToolDependencyStore bucket):
4
+ the four run-tool-dependency methods. This is the LAST bucket — the residual
5
+ ``_agent_run.py`` module is now helpers-only (it no longer defines a mixin
6
+ class), and permanently holds the shared row/datetime helpers (``_dt_str``,
7
+ ``_row_to_run_tool_dependency`` …), which are imported here rather than
8
+ duplicated.
9
+
10
+ ``consume_run_tool_dependencies`` keeps its resolved-unconsumed ``WHERE`` guard
11
+ and ``rowcount`` return; ``count_unresolved_followup_dependencies`` keeps its
12
+ three-table JOIN. Both are moved WHOLE, byte-identically.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sqlite3
18
+ from typing import Any
19
+
20
+ from reflexio.server.services.storage.storage_base import (
21
+ PendingToolCallStatus,
22
+ RunToolDependencyRecord,
23
+ )
24
+
25
+ from .._agent_run import _dt_str, _row_to_run_tool_dependency
26
+ from .._base import SQLiteStorageBase
27
+
28
+
29
+ class SQLiteRunToolDependencyStoreMixin:
30
+ """SQLite-backed run-tool-dependency store primitives."""
31
+
32
+ _lock: Any
33
+ conn: sqlite3.Connection
34
+ _fetchone: Any
35
+ _fetchall: Any
36
+ _current_timestamp: Any
37
+
38
+ @SQLiteStorageBase.handle_exceptions
39
+ def attach_run_tool_dependency(
40
+ self, record: RunToolDependencyRecord
41
+ ) -> RunToolDependencyRecord:
42
+ with self._lock:
43
+ self.conn.execute(
44
+ """
45
+ INSERT OR IGNORE INTO _run_tool_dependencies (
46
+ run_id, pending_tool_call_id, dependency_kind, resolved_at,
47
+ consumed_at
48
+ ) VALUES (?,?,?,?,?)
49
+ """,
50
+ (
51
+ record.run_id,
52
+ record.pending_tool_call_id,
53
+ record.dependency_kind.value,
54
+ _dt_str(record.resolved_at),
55
+ _dt_str(record.consumed_at),
56
+ ),
57
+ )
58
+ self.conn.commit()
59
+ row = self._fetchone(
60
+ """
61
+ SELECT * FROM _run_tool_dependencies
62
+ WHERE run_id = ? AND pending_tool_call_id = ?
63
+ """,
64
+ (record.run_id, record.pending_tool_call_id),
65
+ )
66
+ if row is None: # pragma: no cover
67
+ raise RuntimeError("Failed to attach run tool dependency")
68
+ return _row_to_run_tool_dependency(row)
69
+
70
+ @SQLiteStorageBase.handle_exceptions
71
+ def count_unresolved_followup_dependencies(
72
+ self,
73
+ *,
74
+ org_id: str,
75
+ extractor_kind: str,
76
+ tool_name: str,
77
+ ) -> int:
78
+ row = self._fetchone(
79
+ """
80
+ SELECT COUNT(*) AS count
81
+ FROM _run_tool_dependencies d
82
+ JOIN _agent_runs r ON r.id = d.run_id
83
+ JOIN _pending_tool_calls p ON p.id = d.pending_tool_call_id
84
+ WHERE r.org_id = ?
85
+ AND r.extractor_kind = ?
86
+ AND p.tool_name = ?
87
+ AND p.status = ?
88
+ AND d.resolved_at IS NULL
89
+ AND d.consumed_at IS NULL
90
+ """,
91
+ (
92
+ org_id,
93
+ extractor_kind,
94
+ tool_name,
95
+ PendingToolCallStatus.PENDING.value,
96
+ ),
97
+ )
98
+ return int(row["count"]) if row is not None else 0
99
+
100
+ @SQLiteStorageBase.handle_exceptions
101
+ def list_run_tool_dependencies(self, run_id: str) -> list[RunToolDependencyRecord]:
102
+ rows = self._fetchall(
103
+ "SELECT * FROM _run_tool_dependencies WHERE run_id = ?",
104
+ (run_id,),
105
+ )
106
+ return [_row_to_run_tool_dependency(row) for row in rows]
107
+
108
+ @SQLiteStorageBase.handle_exceptions
109
+ def consume_run_tool_dependencies(self, run_id: str) -> int:
110
+ consumed_at = self._current_timestamp()
111
+ with self._lock:
112
+ cur = self.conn.execute(
113
+ """
114
+ UPDATE _run_tool_dependencies
115
+ SET consumed_at = ?
116
+ WHERE run_id = ?
117
+ AND resolved_at IS NOT NULL
118
+ AND consumed_at IS NULL
119
+ """,
120
+ (consumed_at, run_id),
121
+ )
122
+ self.conn.commit()
123
+ return int(cur.rowcount)
@@ -0,0 +1,6 @@
1
+ """SQLite storage base sub-mixins (peeled from _base.py)."""
2
+
3
+ from ._deletion import SQLiteDeletionMixin
4
+ from ._fts_vec import SQLiteFtsVecMixin
5
+
6
+ __all__ = ["SQLiteDeletionMixin", "SQLiteFtsVecMixin"]
@@ -0,0 +1,263 @@
1
+ """SQLite retention-hook + chunked bulk-delete helpers.
2
+
3
+ Peeled verbatim from ``_base.py`` (Tier-1 storage decomposition). Composed into
4
+ ``SQLiteStorage`` ahead of ``SQLiteStorageBase`` so the atomic
5
+ ``_retention_perform_delete`` override wins over ``RetentionMixin``'s default.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ from typing import Any
12
+
13
+ from reflexio.server.services.storage.retention import RetentionTarget
14
+ from reflexio.server.services.storage.retention_mixin import (
15
+ RETENTION_DELETE_CHUNK,
16
+ chunked,
17
+ )
18
+
19
+ from .._base import SQLiteStorageBase
20
+
21
+
22
+ class SQLiteDeletionMixin:
23
+ """Retention-hook impls + chunked bulk-delete helpers for SQLite storage."""
24
+
25
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
26
+ _lock: Any
27
+ conn: sqlite3.Connection
28
+ _fetchone: Any
29
+ _fetchall: Any
30
+ _has_sqlite_vec: bool
31
+
32
+ # -- Retention hooks (see RetentionMixin) --
33
+
34
+ @SQLiteStorageBase.handle_exceptions
35
+ def _retention_table_exists(self, table_name: str) -> bool:
36
+ row = self._fetchone(
37
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
38
+ (table_name,),
39
+ )
40
+ return row is not None
41
+
42
+ @SQLiteStorageBase.handle_exceptions
43
+ def _retention_count_rows(self, target: RetentionTarget) -> int:
44
+ row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {target.table_name}") # noqa: S608
45
+ return int(row["cnt"]) if row else 0
46
+
47
+ @SQLiteStorageBase.handle_exceptions
48
+ def _retention_select_oldest_keys(
49
+ self, target: RetentionTarget, count: int
50
+ ) -> list[tuple[Any, ...]]:
51
+ id_sql = ", ".join(target.id_columns)
52
+ tiebreak_sql = id_sql
53
+ rows = self._fetchall(
54
+ f"SELECT {id_sql} FROM {target.table_name} " # noqa: S608
55
+ f"ORDER BY {target.order_column} ASC, {tiebreak_sql} ASC LIMIT ?",
56
+ (count,),
57
+ )
58
+ return [tuple(row[col] for col in target.id_columns) for row in rows]
59
+
60
+ @SQLiteStorageBase.handle_exceptions
61
+ def _retention_perform_delete(
62
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
63
+ ) -> None:
64
+ # Wrap dependency + target deletes in a single critical section so
65
+ # concurrent writers see either both or neither.
66
+ with self._lock:
67
+ try:
68
+ self._retention_delete_dependencies(target, keys)
69
+ self._retention_delete_target_rows(target, keys)
70
+ self.conn.commit()
71
+ except Exception:
72
+ self.conn.rollback()
73
+ raise
74
+
75
+ def _retention_delete_dependencies(
76
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
77
+ ) -> None:
78
+ ids = [key[0] for key in keys]
79
+ target_name = target.name
80
+ if target_name == "requests":
81
+ self._delete_interactions_for_request_ids([str(v) for v in ids])
82
+ elif target_name == "interactions":
83
+ self._delete_interaction_search_rows([int(v) for v in ids])
84
+ elif target_name == "profiles":
85
+ self._delete_profile_search_rows([str(v) for v in ids])
86
+ elif target_name == "user_playbooks":
87
+ self._delete_source_windows_for_user_playbook_ids([int(v) for v in ids])
88
+ self._delete_playbook_search_rows(
89
+ "user", [int(v) for v in ids], commit=False
90
+ )
91
+ elif target_name == "agent_playbooks":
92
+ self._delete_source_windows_for_agent_playbook_ids([int(v) for v in ids])
93
+ self._delete_playbook_search_rows(
94
+ "agent", [int(v) for v in ids], commit=False
95
+ )
96
+ elif target_name == "playbook_optimization_jobs":
97
+ self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
98
+ elif target_name == "playbook_optimization_candidates":
99
+ self._delete_optimizer_evaluations_for_candidate_ids([int(v) for v in ids])
100
+
101
+ def _retention_delete_target_rows(
102
+ self, target: RetentionTarget, keys: list[tuple[Any, ...]]
103
+ ) -> None:
104
+ if len(target.id_columns) == 1:
105
+ self._delete_in_chunks(
106
+ target.table_name,
107
+ target.id_columns[0],
108
+ [key[0] for key in keys],
109
+ )
110
+ return
111
+ # Composite-key delete: chunk by row to bound parameter count.
112
+ params_per_key = len(target.id_columns)
113
+ rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
114
+ for chunk in chunked(keys, rows_per_chunk):
115
+ where = " OR ".join(
116
+ "("
117
+ + " AND ".join(f"{column} = ?" for column in target.id_columns)
118
+ + ")"
119
+ for _ in chunk
120
+ )
121
+ params = [value for key in chunk for value in key]
122
+ self.conn.execute(
123
+ f"DELETE FROM {target.table_name} WHERE {where}", # noqa: S608
124
+ params,
125
+ )
126
+
127
+ # -- Chunked-delete primitives shared by the cascade helpers --
128
+
129
+ def _delete_in_chunks(
130
+ self, table_name: str, column_name: str, values: list[Any]
131
+ ) -> None:
132
+ """Chunked ``DELETE FROM table WHERE col IN (...)``.
133
+
134
+ Chunking keeps parameter count under ``SQLITE_MAX_VARIABLE_NUMBER``
135
+ on older sqlite builds (default 999) and avoids degenerate plans
136
+ on very large IN lists.
137
+ """
138
+ if not values:
139
+ return
140
+ for chunk in chunked(values):
141
+ placeholders = ",".join("?" for _ in chunk)
142
+ self.conn.execute(
143
+ f"DELETE FROM {table_name} WHERE {column_name} IN ({placeholders})", # noqa: S608
144
+ chunk,
145
+ )
146
+
147
+ def _select_in_chunks(self, sql_template: str, values: list[Any]) -> list[Any]:
148
+ """Run ``sql_template`` (containing ``{placeholders}``) over chunks of
149
+ ``values`` and aggregate the result rows."""
150
+ results: list[Any] = []
151
+ for chunk in chunked(values):
152
+ placeholders = ",".join("?" for _ in chunk)
153
+ stmt = sql_template.format(placeholders=placeholders)
154
+ results.extend(self.conn.execute(stmt, chunk).fetchall())
155
+ return results
156
+
157
+ def _delete_interactions_for_request_ids(self, request_ids: list[str]) -> None:
158
+ if not request_ids:
159
+ return
160
+ rows = self._select_in_chunks(
161
+ "SELECT interaction_id FROM interactions WHERE request_id IN ({placeholders})",
162
+ request_ids,
163
+ )
164
+ self._delete_interaction_search_rows(
165
+ [int(row["interaction_id"]) for row in rows]
166
+ )
167
+ self._delete_in_chunks("interactions", "request_id", request_ids)
168
+
169
+ def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
170
+ """Remove fts + vec index rows for the given interaction IDs.
171
+
172
+ Non-committing: participates in the caller's transaction. Only called
173
+ from inside the retention atomic block (_retention_perform_delete).
174
+ """
175
+ if not interaction_ids:
176
+ return
177
+ self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
178
+ if self._has_sqlite_vec:
179
+ self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
180
+
181
+ def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
182
+ """Remove fts + vec index rows for the given profile IDs.
183
+
184
+ Non-committing: participates in the caller's transaction. Only called
185
+ from inside the retention atomic block (_retention_perform_delete).
186
+ profiles_fts is keyed by profile_id (TEXT); profiles_vec by rowid (INT).
187
+ """
188
+ if not profile_ids:
189
+ return
190
+ self._delete_in_chunks("profiles_fts", "profile_id", profile_ids)
191
+ if self._has_sqlite_vec:
192
+ rows = self._select_in_chunks(
193
+ "SELECT rowid FROM profiles WHERE profile_id IN ({placeholders})",
194
+ profile_ids,
195
+ )
196
+ rowids = [row["rowid"] for row in rows]
197
+ if rowids:
198
+ self._delete_in_chunks("profiles_vec", "rowid", rowids)
199
+
200
+ def _delete_playbook_search_rows(
201
+ self, kind: str, ids: list[int], *, commit: bool = True
202
+ ) -> None:
203
+ """Remove fts + vec index rows for the given playbook IDs.
204
+
205
+ Args:
206
+ kind: ``"user"`` or ``"agent"``.
207
+ ids: Playbook row IDs to remove from the search indexes.
208
+ commit: When ``True`` (default) commits after the deletes so the
209
+ after-commit callers in ``_playbook.py`` get a clean, durable
210
+ cleanup. Pass ``commit=False`` from inside the retention atomic
211
+ block so the deletes participate in the single block-level commit
212
+ (``_retention_perform_delete``).
213
+
214
+ Note: callers may already hold ``self._lock`` when calling this (the
215
+ ``commit=False`` retention/atomic-delete call sites do). The internal
216
+ ``with self._lock:`` re-acquire is safe ONLY because ``self._lock`` is a
217
+ reentrant ``threading.RLock``; a non-reentrant lock would deadlock here.
218
+ """
219
+ if not ids:
220
+ return
221
+ with self._lock:
222
+ self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
223
+ if self._has_sqlite_vec:
224
+ self._delete_in_chunks(f"{kind}_playbooks_vec", "rowid", ids)
225
+ if commit:
226
+ self.conn.commit()
227
+
228
+ def _delete_source_windows_for_agent_playbook_ids(
229
+ self, agent_playbook_ids: list[int]
230
+ ) -> None:
231
+ self._delete_in_chunks(
232
+ "agent_playbook_source_user_playbooks",
233
+ "agent_playbook_id",
234
+ agent_playbook_ids,
235
+ )
236
+
237
+ def _delete_source_windows_for_user_playbook_ids(
238
+ self, user_playbook_ids: list[int]
239
+ ) -> None:
240
+ self._delete_in_chunks(
241
+ "agent_playbook_source_user_playbooks",
242
+ "user_playbook_id",
243
+ user_playbook_ids,
244
+ )
245
+
246
+ def _delete_optimizer_rows_for_job_ids(self, job_ids: list[int]) -> None:
247
+ if not job_ids:
248
+ return
249
+ for table in (
250
+ "playbook_optimization_evaluations",
251
+ "playbook_optimization_events",
252
+ "playbook_optimization_candidates",
253
+ ):
254
+ self._delete_in_chunks(table, "job_id", job_ids)
255
+
256
+ def _delete_optimizer_evaluations_for_candidate_ids(
257
+ self, candidate_ids: list[int]
258
+ ) -> None:
259
+ self._delete_in_chunks(
260
+ "playbook_optimization_evaluations",
261
+ "candidate_id",
262
+ candidate_ids,
263
+ )
@@ -0,0 +1,216 @@
1
+ """SQLite FTS5 + sqlite-vec index maintenance helpers.
2
+
3
+ Peeled verbatim from ``_base.py`` (Tier-1 storage decomposition). These helpers
4
+ are **self-committing**: each calls ``self.conn.commit()`` internally so the
5
+ index sidecar update is durable. They are sqlite-only — there is no
6
+ supabase/postgres counterpart. Composed into ``SQLiteStorage`` ahead of
7
+ ``SQLiteStorageBase``; the boot-time ``_migrate_vec_tables`` (residual in
8
+ ``_base.py``) resolves ``self._vec_upsert`` through the composed MRO.
9
+
10
+ When a ``commit_scope`` is active (``_scope_depth > 0``), the public helpers
11
+ buffer the operation into ``_deferred_index_ops`` for post-commit flushing;
12
+ the ``_..._now`` variants always execute immediately and are called by
13
+ ``_flush_index_op`` after the outer commit.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import sqlite3
20
+ from typing import Any
21
+
22
+
23
+ class SQLiteFtsVecMixin:
24
+ """FTS5 + sqlite-vec index-maintenance helpers for SQLite storage."""
25
+
26
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
27
+ _lock: Any
28
+ conn: sqlite3.Connection
29
+ _fetchall: Any
30
+ _has_sqlite_vec: bool
31
+ _scope_depth: int
32
+ _deferred_index_ops: list[tuple[str, Any]]
33
+
34
+ # ------------------------------------------------------------------
35
+ # Dispatcher — called by commit_scope after the outer commit
36
+ # ------------------------------------------------------------------
37
+
38
+ def _flush_index_op(self, kind: str, args: Any) -> None:
39
+ """Dispatch a buffered index op after the outer commit."""
40
+ if kind == "fts_upsert":
41
+ table, rowid, text_fields = args
42
+ self._fts_upsert_now(table, rowid, **text_fields)
43
+ elif kind == "fts_delete":
44
+ table, rowid = args
45
+ self._fts_delete_now(table, rowid)
46
+ elif kind == "fts_upsert_profile":
47
+ profile_id, content = args
48
+ self._fts_upsert_profile_now(profile_id, content)
49
+ elif kind == "fts_delete_profile":
50
+ (profile_id,) = args
51
+ self._fts_delete_profile_now(profile_id)
52
+ elif kind == "vec_upsert":
53
+ table, rowid, embedding = args
54
+ self._vec_upsert_now(table, rowid, embedding)
55
+ elif kind == "vec_delete":
56
+ table, rowid = args
57
+ self._vec_delete_now(table, rowid)
58
+ else:
59
+ raise ValueError(f"unknown index op kind: {kind!r}")
60
+
61
+ # ------------------------------------------------------------------
62
+ # FTS helpers — public (defer when in scope) + _now (immediate)
63
+ # ------------------------------------------------------------------
64
+
65
+ def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
66
+ """Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
67
+ if self._scope_depth > 0:
68
+ self._deferred_index_ops.append(("fts_upsert", (table, rowid, text_fields)))
69
+ return
70
+ self._fts_upsert_now(table, rowid, **text_fields)
71
+
72
+ def _fts_upsert_now(
73
+ self, table: str, rowid: int, **text_fields: str | None
74
+ ) -> None:
75
+ with self._lock:
76
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
77
+ cols = list(text_fields.keys())
78
+ vals = [text_fields[c] or "" for c in cols]
79
+ placeholders = ",".join("?" for _ in cols)
80
+ col_str = ",".join(cols)
81
+ self.conn.execute(
82
+ f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})", # noqa: S608
83
+ [rowid, *vals],
84
+ )
85
+ self.conn.commit()
86
+
87
+ def _fts_delete(self, table: str, rowid: int) -> None:
88
+ if self._scope_depth > 0:
89
+ self._deferred_index_ops.append(("fts_delete", (table, rowid)))
90
+ return
91
+ self._fts_delete_now(table, rowid)
92
+
93
+ def _fts_delete_now(self, table: str, rowid: int) -> None:
94
+ with self._lock:
95
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
96
+ self.conn.commit()
97
+
98
+ def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
99
+ """FTS for profiles uses profile_id TEXT as key column."""
100
+ if self._scope_depth > 0:
101
+ self._deferred_index_ops.append(
102
+ ("fts_upsert_profile", (profile_id, content))
103
+ )
104
+ return
105
+ self._fts_upsert_profile_now(profile_id, content)
106
+
107
+ def _fts_upsert_profile_now(self, profile_id: str, content: str) -> None:
108
+ with self._lock:
109
+ self.conn.execute(
110
+ "DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
111
+ )
112
+ self.conn.execute(
113
+ "INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
114
+ (profile_id, content),
115
+ )
116
+ self.conn.commit()
117
+
118
+ def _fts_delete_profile(self, profile_id: str) -> None:
119
+ if self._scope_depth > 0:
120
+ self._deferred_index_ops.append(("fts_delete_profile", (profile_id,)))
121
+ return
122
+ self._fts_delete_profile_now(profile_id)
123
+
124
+ def _fts_delete_profile_now(self, profile_id: str) -> None:
125
+ with self._lock:
126
+ self.conn.execute(
127
+ "DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
128
+ )
129
+ self.conn.commit()
130
+
131
+ # ------------------------------------------------------------------
132
+ # Vec helpers (sqlite-vec) — public (defer when in scope) + _now (immediate)
133
+ # ------------------------------------------------------------------
134
+
135
+ def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
136
+ """Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
137
+ if not self._has_sqlite_vec:
138
+ return
139
+ if self._scope_depth > 0:
140
+ self._deferred_index_ops.append(("vec_upsert", (table, rowid, embedding)))
141
+ return
142
+ self._vec_upsert_now(table, rowid, embedding)
143
+
144
+ def _vec_upsert_now(self, table: str, rowid: int, embedding: list[float]) -> None:
145
+ if not self._has_sqlite_vec:
146
+ return
147
+ with self._lock:
148
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
149
+ self.conn.execute(
150
+ f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)", # noqa: S608
151
+ (rowid, json.dumps(embedding)),
152
+ )
153
+ self.conn.commit()
154
+
155
+ def _vec_delete(self, table: str, rowid: int) -> None:
156
+ """Delete a vec table row. No-op when sqlite-vec is unavailable."""
157
+ if not self._has_sqlite_vec:
158
+ return
159
+ if self._scope_depth > 0:
160
+ self._deferred_index_ops.append(("vec_delete", (table, rowid)))
161
+ return
162
+ self._vec_delete_now(table, rowid)
163
+
164
+ def _vec_delete_now(self, table: str, rowid: int) -> None:
165
+ if not self._has_sqlite_vec:
166
+ return
167
+ with self._lock:
168
+ self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
169
+ self.conn.commit()
170
+
171
+ def _vec_knn_search(
172
+ self,
173
+ vec_table: str,
174
+ main_table: str,
175
+ query_embedding: list[float],
176
+ match_count: int,
177
+ conditions: list[str] | None = None,
178
+ params: list[Any] | None = None,
179
+ ) -> list[sqlite3.Row]:
180
+ """Run a native KNN search via sqlite-vec and join back to the main table.
181
+
182
+ Over-fetches from the KNN index (5x ``match_count``) so that post-filter
183
+ WHERE conditions (org, user, status, etc.) don't silently reduce the
184
+ result set below the requested count.
185
+
186
+ Args:
187
+ vec_table: Name of the vec0 virtual table.
188
+ main_table: Name of the main data table.
189
+ query_embedding: Query embedding vector.
190
+ match_count: Number of results to return.
191
+ conditions: Optional WHERE conditions for the main table.
192
+ params: Parameters for the conditions.
193
+
194
+ Returns:
195
+ Up to ``match_count`` rows from the main table, ordered by vector
196
+ distance (ascending).
197
+ """
198
+ knn_overfetch = match_count * 5
199
+ where_clause = " AND ".join(conditions) if conditions else "1=1"
200
+ sql = f"""SELECT m.* FROM {main_table} m
201
+ JOIN (
202
+ SELECT rowid, distance FROM {vec_table}
203
+ WHERE embedding MATCH ?
204
+ ORDER BY distance
205
+ LIMIT ?
206
+ ) v ON m.rowid = v.rowid
207
+ WHERE {where_clause}
208
+ ORDER BY v.distance
209
+ LIMIT ?""" # noqa: S608
210
+ all_params = [
211
+ json.dumps(query_embedding),
212
+ knn_overfetch,
213
+ *(params or []),
214
+ match_count,
215
+ ]
216
+ return self._fetchall(sql, all_params)
@@ -0,0 +1,13 @@
1
+ from ._audit import AuditEventStoreMixin
2
+ from ._erase_execution import GovernanceEraseExecutionMixin
3
+ from ._purge import PurgeOperationStoreMixin
4
+ from ._rebuild_hide import RebuildHideMixin
5
+ from ._subject_barrier import SubjectBarrierMixin
6
+
7
+ __all__ = [
8
+ "AuditEventStoreMixin",
9
+ "GovernanceEraseExecutionMixin",
10
+ "PurgeOperationStoreMixin",
11
+ "RebuildHideMixin",
12
+ "SubjectBarrierMixin",
13
+ ]