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
@@ -0,0 +1,465 @@
1
+ """SQLite governance-erase-execution store methods.
2
+
3
+ Extracted verbatim from ``_governance.py`` (the GovernanceEraseExecution bucket):
4
+ the two public methods (``apply_governance_user_data_delete``,
5
+ ``complete_purge_operation_with_audit``) plus the two EraseExecution-owned
6
+ privates (``_purge_governance_entity_content_locked``,
7
+ ``_clear_user_data_for_governance_locked``).
8
+
9
+ ``complete_purge_operation_with_audit`` is one of the two methods authorized to
10
+ write a successful-ERASE audit row — the ERASE-audit-idempotency block, the
11
+ re-read+identity-verify, and the ``status='complete'`` flip are preserved
12
+ byte-for-byte, committing together with the audit insert.
13
+ ``_purge_governance_entity_content_locked`` emits the ``wasPurged`` lineage event
14
+ only on an actual content purge (``rowcount <= 0 -> return False`` before
15
+ ``_append_event_stmt``); the ``_PURGE_SQL`` / ``_append_event_stmt`` symbols are
16
+ function-imported LOCAL-in-method (from ``.._lineage``) to avoid an import cycle.
17
+
18
+ The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
19
+ permanently holds the shared infra (``conn``, ``_lock``, ``org_id``,
20
+ ``_deps()``), the delete-target / hide-for-rebuild / prepared-snapshot
21
+ validators it reaches cross-bucket
22
+ (``_validate_prepared_delete_target_matrix_locked``,
23
+ ``_validate_hide_for_rebuild_targets_locked``,
24
+ ``_prepared_owned_user_playbook_ids_locked``), and the module-level helpers
25
+ (``_row_to_audit_event``, ``_row_to_purge_operation``), which are imported here
26
+ rather than duplicated. ``_append_audit_event_with_cursor`` (AuditEventStore),
27
+ ``get_purge_operation`` / ``_record_purge_target_locked`` (PurgeOperationStore)
28
+ resolve through the composed MRO.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import sqlite3
35
+ import threading
36
+ from collections.abc import Callable
37
+ from typing import TYPE_CHECKING, Literal
38
+
39
+ from reflexio.models.api_schema.domain.governance import (
40
+ AuditEvent,
41
+ PurgeOperation,
42
+ )
43
+ from reflexio.server.services.storage.governance_validation import (
44
+ _CANONICAL_DELETE_TARGET_NAMES,
45
+ _PREPARE_PHASE,
46
+ _SNAPSHOT_TARGET_NAME,
47
+ _canonicalize_audit_event_for_persistence,
48
+ _epoch_now,
49
+ _is_successful_erase_event,
50
+ _successful_erase_identity,
51
+ _validate_governance_purge_id,
52
+ )
53
+
54
+ from .._governance import _row_to_audit_event, _row_to_purge_operation
55
+
56
+ if TYPE_CHECKING:
57
+ from .._governance import _SQLiteGovernanceDeps
58
+
59
+
60
+ class GovernanceEraseExecutionMixin:
61
+ """SQLite governance-erase-execution store primitives."""
62
+
63
+ # Type hints for instance attributes/methods provided via MRO by the
64
+ # co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
65
+ conn: sqlite3.Connection
66
+ _lock: threading.RLock
67
+ org_id: str
68
+ _deps: Callable[[], _SQLiteGovernanceDeps]
69
+
70
+ # Cross-bucket residents reached via MRO: the delete-target / hide /
71
+ # prepared-snapshot validators live on the residual SQLiteGovernanceMixin.
72
+ _validate_prepared_delete_target_matrix_locked: Callable[[str], None]
73
+ _validate_hide_for_rebuild_targets_locked: Callable[[str], None]
74
+ _prepared_owned_user_playbook_ids_locked: Callable[[str], set[int]]
75
+
76
+ # Provided via MRO by the co-composed AuditEventStoreMixin (audit bucket)
77
+ # and PurgeOperationStoreMixin (purge bucket); reached here by the
78
+ # cross-bucket erase-execution / purge completion methods.
79
+ _append_audit_event_with_cursor: Callable[
80
+ [sqlite3.Connection | sqlite3.Cursor, AuditEvent], bool
81
+ ]
82
+ get_purge_operation: Callable[[str], PurgeOperation]
83
+ _record_purge_target_locked: Callable[..., None]
84
+
85
+ def _purge_governance_entity_content_locked(
86
+ self,
87
+ *,
88
+ entity_type: Literal["profile", "user_playbook"],
89
+ entity_id: str,
90
+ rowid: int,
91
+ ) -> bool:
92
+ from .._lineage import _PURGE_SQL, _append_event_stmt
93
+
94
+ sql = _PURGE_SQL[entity_type]
95
+ cur = self.conn.execute(sql, (entity_id,))
96
+ if cur.rowcount <= 0:
97
+ return False
98
+ _append_event_stmt(
99
+ self.conn,
100
+ org_id=self.org_id,
101
+ entity_type=entity_type,
102
+ entity_id=entity_id,
103
+ op="purge",
104
+ prov="wasPurged",
105
+ source_ids=[],
106
+ actor="erasure",
107
+ request_id=f"purge_{entity_id}",
108
+ reason="content_purge",
109
+ )
110
+ if entity_type == "profile":
111
+ self.conn.execute(
112
+ "DELETE FROM profiles_fts WHERE profile_id = ?",
113
+ (entity_id,),
114
+ )
115
+ if self._deps()._has_sqlite_vec:
116
+ self.conn.execute(
117
+ "DELETE FROM profiles_vec WHERE rowid = ?",
118
+ (rowid,),
119
+ )
120
+ else:
121
+ self.conn.execute(
122
+ "DELETE FROM user_playbooks_fts WHERE rowid = ?",
123
+ (rowid,),
124
+ )
125
+ if self._deps()._has_sqlite_vec:
126
+ self.conn.execute(
127
+ "DELETE FROM user_playbooks_vec WHERE rowid = ?",
128
+ (rowid,),
129
+ )
130
+ return True
131
+
132
+ def _clear_user_data_for_governance_locked(
133
+ self,
134
+ user_id: str,
135
+ *,
136
+ expected_user_playbook_ids: set[int] | None = None,
137
+ ) -> dict[str, int]:
138
+ deps = self._deps()
139
+ interaction_ids = [
140
+ int(row["interaction_id"])
141
+ for row in self.conn.execute(
142
+ "SELECT interaction_id FROM interactions WHERE user_id = ?",
143
+ (user_id,),
144
+ ).fetchall()
145
+ ]
146
+ raw_upb_ids = [
147
+ int(row["user_playbook_id"])
148
+ for row in self.conn.execute(
149
+ "SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
150
+ (user_id,),
151
+ ).fetchall()
152
+ ]
153
+ if (
154
+ expected_user_playbook_ids is not None
155
+ and set(raw_upb_ids) != expected_user_playbook_ids
156
+ ):
157
+ raise ValueError(
158
+ "Current user playbooks no longer match prepared purge snapshot"
159
+ )
160
+ request_ids = [
161
+ str(row["request_id"])
162
+ for row in self.conn.execute(
163
+ "SELECT request_id FROM requests WHERE user_id = ?",
164
+ (user_id,),
165
+ ).fetchall()
166
+ ]
167
+ profile_rows = self.conn.execute(
168
+ "SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
169
+ (user_id,),
170
+ ).fetchall()
171
+ profile_rowid_by_id = {
172
+ str(row["profile_id"]): int(row["rowid"]) for row in profile_rows
173
+ }
174
+ all_profile_ids = list(profile_rowid_by_id)
175
+
176
+ purge_profile_ids, delete_profile_ids = deps._partition_purge_vs_delete(
177
+ "profile",
178
+ all_profile_ids,
179
+ )
180
+ purge_upb_str_ids, delete_upb_str_ids = deps._partition_purge_vs_delete(
181
+ "user_playbook",
182
+ [str(user_playbook_id) for user_playbook_id in raw_upb_ids],
183
+ )
184
+ purge_upb_ids = [int(entity_id) for entity_id in purge_upb_str_ids]
185
+ delete_upb_ids = [int(entity_id) for entity_id in delete_upb_str_ids]
186
+ erased_entity_ids = [
187
+ *request_ids,
188
+ *[str(interaction_id) for interaction_id in interaction_ids],
189
+ *all_profile_ids,
190
+ *[str(user_playbook_id) for user_playbook_id in raw_upb_ids],
191
+ ]
192
+ if erased_entity_ids:
193
+ erased_entity_id_set = set(erased_entity_ids)
194
+ lineage_source_event_ids: list[int] = []
195
+ for row in self.conn.execute(
196
+ "SELECT event_id, source_ids FROM lineage_event WHERE org_id = ?",
197
+ (self.org_id,),
198
+ ).fetchall():
199
+ try:
200
+ source_ids = json.loads(str(row["source_ids"] or "[]"))
201
+ except json.JSONDecodeError:
202
+ source_ids = []
203
+ if any(
204
+ str(source_id) in erased_entity_id_set for source_id in source_ids
205
+ ):
206
+ lineage_source_event_ids.append(int(row["event_id"]))
207
+ deps._delete_in_chunks(
208
+ "lineage_event", "event_id", lineage_source_event_ids
209
+ )
210
+ placeholders = ",".join("?" for _ in erased_entity_ids)
211
+ self.conn.execute(
212
+ f"""DELETE FROM lineage_event
213
+ WHERE org_id = ?
214
+ AND (
215
+ request_id IN ({placeholders})
216
+ OR entity_id IN ({placeholders})
217
+ )""", # noqa: S608
218
+ [self.org_id, *erased_entity_ids, *erased_entity_ids],
219
+ )
220
+ delete_profile_rowids = [
221
+ profile_rowid_by_id[profile_id]
222
+ for profile_id in delete_profile_ids
223
+ if profile_id in profile_rowid_by_id
224
+ ]
225
+
226
+ deps._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
227
+ deps._delete_in_chunks("user_playbooks_fts", "rowid", delete_upb_ids)
228
+ deps._delete_in_chunks("profiles_fts", "profile_id", delete_profile_ids)
229
+ if deps._has_sqlite_vec:
230
+ deps._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
231
+ deps._delete_in_chunks("user_playbooks_vec", "rowid", delete_upb_ids)
232
+ deps._delete_in_chunks("profiles_vec", "rowid", delete_profile_rowids)
233
+
234
+ interactions_cur = self.conn.execute(
235
+ "DELETE FROM interactions WHERE user_id = ?",
236
+ (user_id,),
237
+ )
238
+ eval_results_cur = self.conn.execute(
239
+ """DELETE FROM agent_success_evaluation_result
240
+ WHERE user_id = ?""",
241
+ (user_id,),
242
+ )
243
+ requests_cur = self.conn.execute(
244
+ "DELETE FROM requests WHERE user_id = ?",
245
+ (user_id,),
246
+ )
247
+ if raw_upb_ids:
248
+ deps._delete_source_windows_for_user_playbook_ids(raw_upb_ids)
249
+ if delete_upb_ids:
250
+ deps._delete_in_chunks("user_playbooks", "user_playbook_id", delete_upb_ids)
251
+ if delete_profile_ids:
252
+ deps._delete_in_chunks("profiles", "profile_id", delete_profile_ids)
253
+
254
+ purged_profiles = 0
255
+ for profile_id in purge_profile_ids:
256
+ rowid = profile_rowid_by_id.get(profile_id)
257
+ if rowid is None:
258
+ continue
259
+ purged_profiles += int(
260
+ self._purge_governance_entity_content_locked(
261
+ entity_type="profile",
262
+ entity_id=profile_id,
263
+ rowid=rowid,
264
+ )
265
+ )
266
+
267
+ purged_user_playbooks = 0
268
+ for user_playbook_id in purge_upb_ids:
269
+ purged_user_playbooks += int(
270
+ self._purge_governance_entity_content_locked(
271
+ entity_type="user_playbook",
272
+ entity_id=str(user_playbook_id),
273
+ rowid=user_playbook_id,
274
+ )
275
+ )
276
+
277
+ return {
278
+ "interactions": interactions_cur.rowcount,
279
+ "user_playbooks": len(delete_upb_ids),
280
+ "profiles": len(delete_profile_ids),
281
+ "requests": requests_cur.rowcount,
282
+ "agent_success_evaluation_results": eval_results_cur.rowcount,
283
+ "purged_profiles": purged_profiles,
284
+ "purged_user_playbooks": purged_user_playbooks,
285
+ }
286
+
287
+ def apply_governance_user_data_delete(
288
+ self, purge_id: str, user_id: str
289
+ ) -> dict[str, int]:
290
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
291
+ name_map = {
292
+ "interactions": "interaction",
293
+ "user_playbooks": "user_playbook",
294
+ "profiles": "profile",
295
+ "requests": "request",
296
+ "agent_success_evaluation_results": "agent_success_evaluation_result",
297
+ "purged_profiles": "profile_purge",
298
+ "purged_user_playbooks": "user_playbook_purge",
299
+ }
300
+ with self._lock:
301
+ try:
302
+ self.conn.execute("BEGIN")
303
+ self._validate_prepared_delete_target_matrix_locked(purge_id)
304
+ self._validate_hide_for_rebuild_targets_locked(purge_id)
305
+ expected_user_playbook_ids = (
306
+ self._prepared_owned_user_playbook_ids_locked(purge_id)
307
+ )
308
+ counts = self._clear_user_data_for_governance_locked(
309
+ user_id,
310
+ expected_user_playbook_ids=expected_user_playbook_ids,
311
+ )
312
+ for key, value in counts.items():
313
+ self._record_purge_target_locked(
314
+ purge_id=purge_id,
315
+ target_name=name_map.get(key, key),
316
+ target_ref="all",
317
+ phase="delete",
318
+ status="complete",
319
+ detail={"count": int(value)},
320
+ deleted_count=int(value),
321
+ error_detail=None,
322
+ )
323
+ self.conn.commit()
324
+ except Exception:
325
+ self.conn.rollback()
326
+ raise
327
+ return counts
328
+
329
+ def complete_purge_operation_with_audit(
330
+ self, purge_id: str, audit_event: AuditEvent
331
+ ) -> PurgeOperation:
332
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
333
+ if audit_event.org_id != self.org_id:
334
+ raise ValueError("Audit event org_id must match storage org_id")
335
+ if audit_event.idempotency_key != purge_id:
336
+ raise ValueError("Audit event idempotency key must match purge_id")
337
+ if not _is_successful_erase_event(audit_event, purge_id=purge_id):
338
+ raise ValueError(
339
+ "Completion requires a successful ERASE audit event for this purge"
340
+ )
341
+ audit_event = _canonicalize_audit_event_for_persistence(audit_event)
342
+ now = _epoch_now()
343
+ with self._lock:
344
+ try:
345
+ self.conn.execute("BEGIN IMMEDIATE")
346
+ row = self.conn.execute(
347
+ "SELECT * FROM purge_operations WHERE purge_id = ? AND org_id = ?",
348
+ (purge_id, self.org_id),
349
+ ).fetchone()
350
+ if row is None:
351
+ raise ValueError(f"Purge operation {purge_id!r} not found")
352
+ purge_operation = _row_to_purge_operation(row)
353
+ if purge_operation.subject_ref != audit_event.subject_ref:
354
+ raise ValueError(
355
+ "Audit event subject_ref must match purge operation subject_ref"
356
+ )
357
+ if purge_operation.request_ref != audit_event.request_ref:
358
+ raise ValueError(
359
+ "Audit event request_ref must match purge operation request_ref"
360
+ )
361
+ barrier_row = self.conn.execute(
362
+ """SELECT status FROM subject_write_barriers
363
+ WHERE org_id = ? AND subject_ref = ? AND purge_id = ?""",
364
+ (self.org_id, audit_event.subject_ref, purge_id),
365
+ ).fetchone()
366
+ if barrier_row is None or barrier_row["status"] != "erasing":
367
+ raise ValueError("subject erasure barrier is missing")
368
+ snapshot = self.conn.execute(
369
+ """SELECT 1 FROM purge_operation_targets
370
+ WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
371
+ AND phase = ? AND status = 'complete'""",
372
+ (self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
373
+ ).fetchone()
374
+ if snapshot is None:
375
+ raise ValueError(
376
+ "Cannot complete purge without target snapshot marker"
377
+ )
378
+ delete_rows = self.conn.execute(
379
+ """SELECT target_name, status FROM purge_operation_targets
380
+ WHERE org_id = ? AND purge_id = ? AND phase = 'delete'
381
+ AND target_ref = 'all'""",
382
+ (self.org_id, purge_id),
383
+ ).fetchall()
384
+ delete_statuses = {
385
+ str(row["target_name"]): str(row["status"]) for row in delete_rows
386
+ }
387
+ missing_delete_targets = [
388
+ target_name
389
+ for target_name in _CANONICAL_DELETE_TARGET_NAMES
390
+ if delete_statuses.get(target_name) != "complete"
391
+ ]
392
+ if missing_delete_targets:
393
+ raise ValueError(
394
+ "Cannot complete purge without complete delete target matrix: "
395
+ + ", ".join(missing_delete_targets)
396
+ )
397
+ incomplete = self.conn.execute(
398
+ """SELECT 1 FROM purge_operation_targets
399
+ WHERE org_id = ? AND purge_id = ? AND status != 'complete'
400
+ LIMIT 1""",
401
+ (self.org_id, purge_id),
402
+ ).fetchone()
403
+ if incomplete is not None:
404
+ raise ValueError("Cannot complete purge with incomplete targets")
405
+ existing_audit_row = self.conn.execute(
406
+ """SELECT * FROM audit_events
407
+ WHERE org_id = ? AND idempotency_key = ?""",
408
+ (self.org_id, purge_id),
409
+ ).fetchone()
410
+ if existing_audit_row is not None:
411
+ existing_event = _row_to_audit_event(existing_audit_row)
412
+ if not _is_successful_erase_event(
413
+ existing_event, purge_id=purge_id
414
+ ):
415
+ raise ValueError(
416
+ "Existing audit row for purge_id must be the matching "
417
+ "successful ERASE row"
418
+ )
419
+ if _successful_erase_identity(
420
+ existing_event
421
+ ) != _successful_erase_identity(audit_event):
422
+ raise ValueError(
423
+ "Existing audit row for purge_id must be the matching "
424
+ "successful ERASE row"
425
+ )
426
+ else:
427
+ self._append_audit_event_with_cursor(self.conn, audit_event)
428
+ existing_audit_row = self.conn.execute(
429
+ """SELECT * FROM audit_events
430
+ WHERE org_id = ? AND idempotency_key = ?""",
431
+ (self.org_id, purge_id),
432
+ ).fetchone()
433
+ if existing_audit_row is None:
434
+ raise ValueError(
435
+ "Completion requires exactly one successful ERASE audit row "
436
+ "for the purge_id"
437
+ )
438
+ existing_event = _row_to_audit_event(existing_audit_row)
439
+ if not _is_successful_erase_event(existing_event, purge_id=purge_id):
440
+ raise ValueError(
441
+ "Completion requires exactly one matching successful ERASE "
442
+ "audit row for the purge_id"
443
+ )
444
+ if _successful_erase_identity(
445
+ existing_event
446
+ ) != _successful_erase_identity(audit_event):
447
+ raise ValueError(
448
+ "Completion requires exactly one matching successful ERASE "
449
+ "audit row for the purge_id"
450
+ )
451
+ self.conn.execute(
452
+ """UPDATE purge_operations
453
+ SET status = 'complete',
454
+ error_code = NULL,
455
+ error_detail = NULL,
456
+ updated_at = ?,
457
+ completed_at = ?
458
+ WHERE purge_id = ? AND org_id = ?""",
459
+ (now, now, purge_id, self.org_id),
460
+ )
461
+ self.conn.commit()
462
+ except Exception:
463
+ self.conn.rollback()
464
+ raise
465
+ return self.get_purge_operation(purge_id)