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
|
@@ -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)
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py
ADDED
|
@@ -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
|
+
)
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sqlite3
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SQLiteFtsVecMixin:
|
|
19
|
+
"""FTS5 + sqlite-vec index-maintenance helpers for SQLite storage."""
|
|
20
|
+
|
|
21
|
+
# Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
|
|
22
|
+
_lock: Any
|
|
23
|
+
conn: sqlite3.Connection
|
|
24
|
+
_fetchall: Any
|
|
25
|
+
_has_sqlite_vec: bool
|
|
26
|
+
|
|
27
|
+
# FTS helpers
|
|
28
|
+
def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
|
|
29
|
+
"""Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
|
|
30
|
+
with self._lock:
|
|
31
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
32
|
+
cols = list(text_fields.keys())
|
|
33
|
+
vals = [text_fields[c] or "" for c in cols]
|
|
34
|
+
placeholders = ",".join("?" for _ in cols)
|
|
35
|
+
col_str = ",".join(cols)
|
|
36
|
+
self.conn.execute(
|
|
37
|
+
f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})", # noqa: S608
|
|
38
|
+
[rowid, *vals],
|
|
39
|
+
)
|
|
40
|
+
self.conn.commit()
|
|
41
|
+
|
|
42
|
+
def _fts_delete(self, table: str, rowid: int) -> None:
|
|
43
|
+
with self._lock:
|
|
44
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
45
|
+
self.conn.commit()
|
|
46
|
+
|
|
47
|
+
def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
|
|
48
|
+
"""FTS for profiles uses profile_id TEXT as key column."""
|
|
49
|
+
with self._lock:
|
|
50
|
+
self.conn.execute(
|
|
51
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
52
|
+
)
|
|
53
|
+
self.conn.execute(
|
|
54
|
+
"INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
|
|
55
|
+
(profile_id, content),
|
|
56
|
+
)
|
|
57
|
+
self.conn.commit()
|
|
58
|
+
|
|
59
|
+
def _fts_delete_profile(self, profile_id: str) -> None:
|
|
60
|
+
with self._lock:
|
|
61
|
+
self.conn.execute(
|
|
62
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
63
|
+
)
|
|
64
|
+
self.conn.commit()
|
|
65
|
+
|
|
66
|
+
# Vec helpers (sqlite-vec)
|
|
67
|
+
def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
68
|
+
"""Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
|
|
69
|
+
if not self._has_sqlite_vec:
|
|
70
|
+
return
|
|
71
|
+
with self._lock:
|
|
72
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
73
|
+
self.conn.execute(
|
|
74
|
+
f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)", # noqa: S608
|
|
75
|
+
(rowid, json.dumps(embedding)),
|
|
76
|
+
)
|
|
77
|
+
self.conn.commit()
|
|
78
|
+
|
|
79
|
+
def _vec_delete(self, table: str, rowid: int) -> None:
|
|
80
|
+
"""Delete a vec table row. No-op when sqlite-vec is unavailable."""
|
|
81
|
+
if not self._has_sqlite_vec:
|
|
82
|
+
return
|
|
83
|
+
with self._lock:
|
|
84
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
85
|
+
self.conn.commit()
|
|
86
|
+
|
|
87
|
+
def _vec_knn_search(
|
|
88
|
+
self,
|
|
89
|
+
vec_table: str,
|
|
90
|
+
main_table: str,
|
|
91
|
+
query_embedding: list[float],
|
|
92
|
+
match_count: int,
|
|
93
|
+
conditions: list[str] | None = None,
|
|
94
|
+
params: list[Any] | None = None,
|
|
95
|
+
) -> list[sqlite3.Row]:
|
|
96
|
+
"""Run a native KNN search via sqlite-vec and join back to the main table.
|
|
97
|
+
|
|
98
|
+
Over-fetches from the KNN index (5x ``match_count``) so that post-filter
|
|
99
|
+
WHERE conditions (org, user, status, etc.) don't silently reduce the
|
|
100
|
+
result set below the requested count.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
vec_table: Name of the vec0 virtual table.
|
|
104
|
+
main_table: Name of the main data table.
|
|
105
|
+
query_embedding: Query embedding vector.
|
|
106
|
+
match_count: Number of results to return.
|
|
107
|
+
conditions: Optional WHERE conditions for the main table.
|
|
108
|
+
params: Parameters for the conditions.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Up to ``match_count`` rows from the main table, ordered by vector
|
|
112
|
+
distance (ascending).
|
|
113
|
+
"""
|
|
114
|
+
knn_overfetch = match_count * 5
|
|
115
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
116
|
+
sql = f"""SELECT m.* FROM {main_table} m
|
|
117
|
+
JOIN (
|
|
118
|
+
SELECT rowid, distance FROM {vec_table}
|
|
119
|
+
WHERE embedding MATCH ?
|
|
120
|
+
ORDER BY distance
|
|
121
|
+
LIMIT ?
|
|
122
|
+
) v ON m.rowid = v.rowid
|
|
123
|
+
WHERE {where_clause}
|
|
124
|
+
ORDER BY v.distance
|
|
125
|
+
LIMIT ?""" # noqa: S608
|
|
126
|
+
all_params = [
|
|
127
|
+
json.dumps(query_embedding),
|
|
128
|
+
knn_overfetch,
|
|
129
|
+
*(params or []),
|
|
130
|
+
match_count,
|
|
131
|
+
]
|
|
132
|
+
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
|
+
]
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""SQLite audit-event store methods.
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_governance.py`` (the AuditEventStore bucket): the
|
|
4
|
+
three public methods (``append_audit_event``, ``list_audit_events``,
|
|
5
|
+
``gc_governance_retention``) plus the Audit-owned private
|
|
6
|
+
``_append_audit_event_with_cursor`` (called cross-bucket by the residual purge /
|
|
7
|
+
barrier completion methods, which reach it via MRO co-composition).
|
|
8
|
+
|
|
9
|
+
The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
|
|
10
|
+
permanently holds the shared infra (``conn``, ``_lock``, ``org_id``, ``_deps()``)
|
|
11
|
+
and the module-level helpers (``_json_dumps``, ``_row_to_audit_event``), which
|
|
12
|
+
are imported here rather than duplicated.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sqlite3
|
|
18
|
+
import threading
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
from reflexio.models.api_schema.domain.governance import AuditEvent
|
|
23
|
+
from reflexio.models.config_schema import GovernanceRetentionConfig
|
|
24
|
+
from reflexio.server.services.storage.governance_validation import (
|
|
25
|
+
_canonicalize_audit_event_for_persistence,
|
|
26
|
+
_epoch_now,
|
|
27
|
+
_is_successful_erase_event,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .._governance import _json_dumps, _row_to_audit_event
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .._governance import _SQLiteGovernanceDeps
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuditEventStoreMixin:
|
|
37
|
+
"""SQLite audit-event store primitives."""
|
|
38
|
+
|
|
39
|
+
# Type hints for instance attributes/methods provided via MRO by the
|
|
40
|
+
# co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
|
|
41
|
+
conn: sqlite3.Connection
|
|
42
|
+
_lock: threading.RLock
|
|
43
|
+
org_id: str
|
|
44
|
+
_deps: Callable[[], _SQLiteGovernanceDeps]
|
|
45
|
+
|
|
46
|
+
def _append_audit_event_with_cursor(
|
|
47
|
+
self, cur: sqlite3.Connection | sqlite3.Cursor, event: AuditEvent
|
|
48
|
+
) -> bool:
|
|
49
|
+
inserted = cur.execute(
|
|
50
|
+
"""INSERT OR IGNORE INTO audit_events (
|
|
51
|
+
org_id, actor_type, actor_ref, operation, entity_type, entity_id,
|
|
52
|
+
subject_ref, request_ref, idempotency_key, status, detail, created_at
|
|
53
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
54
|
+
(
|
|
55
|
+
event.org_id,
|
|
56
|
+
event.actor_type,
|
|
57
|
+
event.actor_ref,
|
|
58
|
+
event.operation,
|
|
59
|
+
event.entity_type,
|
|
60
|
+
event.entity_id,
|
|
61
|
+
event.subject_ref,
|
|
62
|
+
event.request_ref,
|
|
63
|
+
event.idempotency_key,
|
|
64
|
+
event.status,
|
|
65
|
+
_json_dumps(event.detail),
|
|
66
|
+
event.created_at,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
return inserted.rowcount > 0
|
|
70
|
+
|
|
71
|
+
def append_audit_event(self, event: AuditEvent) -> bool:
|
|
72
|
+
if _is_successful_erase_event(event):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"Successful ERASE audit rows may only be written by "
|
|
75
|
+
"complete_purge_operation_with_audit()"
|
|
76
|
+
)
|
|
77
|
+
if event.org_id != self.org_id:
|
|
78
|
+
raise ValueError("Audit event org_id must match storage org_id")
|
|
79
|
+
event = _canonicalize_audit_event_for_persistence(event)
|
|
80
|
+
with self._lock:
|
|
81
|
+
inserted = self._append_audit_event_with_cursor(self.conn, event)
|
|
82
|
+
self.conn.commit()
|
|
83
|
+
return inserted
|
|
84
|
+
|
|
85
|
+
def list_audit_events(
|
|
86
|
+
self, subject_ref: str | None = None, *, org_id: str | None = None
|
|
87
|
+
) -> list[AuditEvent]:
|
|
88
|
+
deps = self._deps()
|
|
89
|
+
if org_id is not None and org_id != self.org_id:
|
|
90
|
+
raise ValueError("Audit event org_id must match storage org_id")
|
|
91
|
+
sql = "SELECT * FROM audit_events WHERE org_id = ?"
|
|
92
|
+
params: list[Any] = [self.org_id]
|
|
93
|
+
if subject_ref is not None:
|
|
94
|
+
sql += " AND subject_ref = ?"
|
|
95
|
+
params.append(subject_ref)
|
|
96
|
+
sql += " ORDER BY created_at ASC, event_id ASC"
|
|
97
|
+
rows = deps._fetchall(sql, params)
|
|
98
|
+
return [_row_to_audit_event(row) for row in rows]
|
|
99
|
+
|
|
100
|
+
def gc_governance_retention(self, *, config: GovernanceRetentionConfig) -> int:
|
|
101
|
+
if not config.audit_events_retention_enabled:
|
|
102
|
+
return 0
|
|
103
|
+
cutoff_epoch = _epoch_now() - config.audit_events_retention_days * 24 * 60 * 60
|
|
104
|
+
with self._lock:
|
|
105
|
+
cur = self.conn.execute(
|
|
106
|
+
"""DELETE FROM audit_events
|
|
107
|
+
WHERE event_id IN (
|
|
108
|
+
SELECT event_id
|
|
109
|
+
FROM audit_events
|
|
110
|
+
WHERE org_id = ? AND created_at < ?
|
|
111
|
+
ORDER BY created_at ASC, event_id ASC
|
|
112
|
+
LIMIT ?
|
|
113
|
+
)""",
|
|
114
|
+
(
|
|
115
|
+
self.org_id,
|
|
116
|
+
cutoff_epoch,
|
|
117
|
+
config.audit_events_delete_batch_limit,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
deleted = int(cur.rowcount or 0)
|
|
121
|
+
self.conn.commit()
|
|
122
|
+
return deleted
|