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,511 @@
1
+ """SQLite subject-erasure-barrier store methods.
2
+
3
+ Extracted verbatim from ``_governance.py`` (the SubjectBarrier bucket): the five
4
+ public methods (``begin_subject_erasure_barrier``, ``assert_subject_writable``,
5
+ ``complete_subject_erasure_barrier_after_empty_check``,
6
+ ``fail_subject_erasure_barrier``, ``get_subject_write_barrier``) plus the seven
7
+ SubjectBarrier-owned privates (``_barrier_from_purge``,
8
+ ``_active_subject_barrier_locked``, ``_assert_subject_writable_locked``,
9
+ ``_subject_ref_for_user_id``, ``_legacy_request_ids_for_subject_locked``,
10
+ ``_legacy_user_id_rows_remain_locked``, ``_same_subject_rows_remain_locked``).
11
+
12
+ ``complete_subject_erasure_barrier_after_empty_check`` is one of the two methods
13
+ authorized to write a successful-ERASE audit row — the ERASE-audit-idempotency
14
+ block and the ``rowcount != 1``-guarded barrier flip are preserved byte-for-byte.
15
+
16
+ The residual ``SQLiteGovernanceMixin`` stays composed alongside this mixin and
17
+ permanently holds the shared infra (``conn``, ``_lock``, ``org_id``) and the
18
+ module-level helpers (``_row_to_audit_event``, ``_row_to_purge_operation``,
19
+ ``_row_to_subject_write_barrier``), which are imported here rather than
20
+ duplicated. ``_append_audit_event_with_cursor`` (AuditEventStore) and
21
+ ``get_purge_operation`` (PurgeOperationStore) resolve through the composed MRO.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import sqlite3
27
+ import threading
28
+ from collections.abc import Callable
29
+
30
+ from reflexio.models.api_schema.domain.governance import (
31
+ AuditEvent,
32
+ PurgeOperation,
33
+ SubjectBarrierStatus,
34
+ SubjectWriteBarrier,
35
+ )
36
+ from reflexio.server.services.governance.config import (
37
+ get_governance_ref_secret,
38
+ governance_subject_ref,
39
+ )
40
+ from reflexio.server.services.storage.error import SubjectWriteBarrierError
41
+ from reflexio.server.services.storage.governance_validation import (
42
+ _CANONICAL_DELETE_TARGET_NAMES,
43
+ _PREPARE_PHASE,
44
+ _SNAPSHOT_TARGET_NAME,
45
+ _canonicalize_audit_event_for_persistence,
46
+ _epoch_now,
47
+ _is_successful_erase_event,
48
+ _successful_erase_identity,
49
+ _validate_governance_error_code,
50
+ _validate_governance_error_detail,
51
+ _validate_governance_prefixed_ref,
52
+ _validate_governance_purge_id,
53
+ )
54
+
55
+ from .._governance import (
56
+ _row_to_audit_event,
57
+ _row_to_purge_operation,
58
+ _row_to_subject_write_barrier,
59
+ )
60
+
61
+
62
+ class SubjectBarrierMixin:
63
+ """SQLite subject-erasure-barrier store primitives."""
64
+
65
+ # Type hints for instance attributes/methods provided via MRO by the
66
+ # co-composed residual SQLiteGovernanceMixin / SQLiteStorageBase.
67
+ conn: sqlite3.Connection
68
+ _lock: threading.RLock
69
+ org_id: str
70
+
71
+ # Provided via MRO by the co-composed AuditEventStoreMixin / the residual
72
+ # PurgeOperationStoreMixin; reached here by the cross-bucket barrier
73
+ # completion method.
74
+ _append_audit_event_with_cursor: Callable[
75
+ [sqlite3.Connection | sqlite3.Cursor, AuditEvent], bool
76
+ ]
77
+ get_purge_operation: Callable[[str], PurgeOperation]
78
+
79
+ def _barrier_from_purge(
80
+ self,
81
+ purge_operation: PurgeOperation,
82
+ *,
83
+ subject_ref: str,
84
+ ) -> SubjectWriteBarrier:
85
+ if purge_operation.subject_ref != subject_ref:
86
+ raise ValueError(
87
+ "Purge operation subject_ref must match the barrier subject_ref"
88
+ )
89
+ status_by_purge_status: dict[str, SubjectBarrierStatus] = {
90
+ "pending": "erasing",
91
+ "running": "erasing",
92
+ "complete": "erased",
93
+ "failed": "failed",
94
+ }
95
+ return SubjectWriteBarrier(
96
+ org_id=purge_operation.org_id,
97
+ subject_ref=subject_ref,
98
+ purge_id=purge_operation.purge_id,
99
+ status=status_by_purge_status[purge_operation.status],
100
+ error_code=purge_operation.error_code,
101
+ error_detail=purge_operation.error_detail,
102
+ created_at=purge_operation.created_at,
103
+ updated_at=purge_operation.updated_at,
104
+ )
105
+
106
+ def _active_subject_barrier_locked(self, subject_ref: str) -> sqlite3.Row | None:
107
+ return self.conn.execute(
108
+ """SELECT * FROM subject_write_barriers
109
+ WHERE org_id = ? AND subject_ref = ? AND status IN ('erasing', 'erased')""",
110
+ (self.org_id, subject_ref),
111
+ ).fetchone()
112
+
113
+ def _assert_subject_writable_locked(self, subject_ref: str) -> None:
114
+ row = self._active_subject_barrier_locked(subject_ref)
115
+ if row is not None:
116
+ raise SubjectWriteBarrierError(
117
+ f"subject {subject_ref} is blocked by erasure barrier {row['purge_id']}"
118
+ )
119
+
120
+ def _subject_ref_for_user_id(self, user_id: str) -> str:
121
+ return governance_subject_ref(
122
+ self.org_id,
123
+ user_id,
124
+ get_governance_ref_secret(),
125
+ )
126
+
127
+ def _legacy_request_ids_for_subject_locked(self, subject_ref: str) -> set[str]:
128
+ request_ids: set[str] = set()
129
+ for row in self.conn.execute(
130
+ """SELECT request_id, user_id
131
+ FROM requests
132
+ WHERE governance_subject_ref IS NULL"""
133
+ ):
134
+ user_id = str(row["user_id"])
135
+ if self._subject_ref_for_user_id(user_id) != subject_ref:
136
+ continue
137
+ request_ids.add(str(row["request_id"]))
138
+ return request_ids
139
+
140
+ def _legacy_user_id_rows_remain_locked(
141
+ self,
142
+ *,
143
+ table: str,
144
+ subject_ref: str,
145
+ request_ids: set[str] | None = None,
146
+ request_id_column: str | None = None,
147
+ ) -> bool:
148
+ sql = f"SELECT user_id{', ' + request_id_column if request_id_column else ''} FROM {table} WHERE governance_subject_ref IS NULL" # noqa: S608
149
+ for row in self.conn.execute(sql):
150
+ user_id = str(row["user_id"])
151
+ if self._subject_ref_for_user_id(user_id) == subject_ref:
152
+ return True
153
+ if (
154
+ request_ids
155
+ and request_id_column is not None
156
+ and str(row[request_id_column]) in request_ids
157
+ ):
158
+ return True
159
+ return False
160
+
161
+ def _same_subject_rows_remain_locked(self, subject_ref: str) -> bool:
162
+ legacy_request_ids = self._legacy_request_ids_for_subject_locked(subject_ref)
163
+ for table in (
164
+ "requests",
165
+ "interactions",
166
+ "profiles",
167
+ "user_playbooks",
168
+ "agent_success_evaluation_result",
169
+ ):
170
+ row = self.conn.execute(
171
+ f"""SELECT 1 FROM {table}
172
+ WHERE governance_subject_ref = ?
173
+ LIMIT 1""",
174
+ (subject_ref,),
175
+ ).fetchone()
176
+ if row is not None:
177
+ return True
178
+ if legacy_request_ids:
179
+ return True
180
+ if self._legacy_user_id_rows_remain_locked(
181
+ table="interactions",
182
+ subject_ref=subject_ref,
183
+ request_ids=legacy_request_ids,
184
+ request_id_column="request_id",
185
+ ):
186
+ return True
187
+ if self._legacy_user_id_rows_remain_locked(
188
+ table="profiles",
189
+ subject_ref=subject_ref,
190
+ request_ids=legacy_request_ids,
191
+ request_id_column="generated_from_request_id",
192
+ ):
193
+ return True
194
+ if self._legacy_user_id_rows_remain_locked(
195
+ table="user_playbooks",
196
+ subject_ref=subject_ref,
197
+ request_ids=legacy_request_ids,
198
+ request_id_column="request_id",
199
+ ):
200
+ return True
201
+ return self._legacy_user_id_rows_remain_locked(
202
+ table="agent_success_evaluation_result",
203
+ subject_ref=subject_ref,
204
+ )
205
+
206
+ def begin_subject_erasure_barrier(
207
+ self, subject_ref: str, purge_id: str
208
+ ) -> SubjectWriteBarrier:
209
+ _validate_governance_prefixed_ref(
210
+ "subject_ref", subject_ref, prefix="subref_v1_"
211
+ )
212
+ validated_purge_id = _validate_governance_purge_id("purge_id", purge_id)
213
+ now = _epoch_now()
214
+ with self._lock:
215
+ try:
216
+ self.conn.execute("BEGIN IMMEDIATE")
217
+ purge_row = self.conn.execute(
218
+ """SELECT * FROM purge_operations
219
+ WHERE purge_id = ? AND org_id = ?""",
220
+ (validated_purge_id, self.org_id),
221
+ ).fetchone()
222
+ if purge_row is None:
223
+ raise ValueError(
224
+ f"Purge operation {validated_purge_id!r} not found"
225
+ )
226
+ purge_operation = _row_to_purge_operation(purge_row)
227
+ if purge_operation.subject_ref != subject_ref:
228
+ raise ValueError(
229
+ "Purge operation subject_ref must match the barrier subject_ref"
230
+ )
231
+ existing_barrier = self.conn.execute(
232
+ """SELECT * FROM subject_write_barriers
233
+ WHERE org_id = ? AND subject_ref = ?""",
234
+ (self.org_id, subject_ref),
235
+ ).fetchone()
236
+ if (
237
+ existing_barrier is not None
238
+ and str(existing_barrier["purge_id"]) != validated_purge_id
239
+ ):
240
+ raise ValueError(
241
+ "Existing barrier purge_id must match the requested purge_id"
242
+ )
243
+ if (
244
+ existing_barrier is not None
245
+ and str(existing_barrier["status"]) == "erased"
246
+ ):
247
+ row = existing_barrier
248
+ self.conn.commit()
249
+ return _row_to_subject_write_barrier(row)
250
+ self.conn.execute(
251
+ """INSERT INTO subject_write_barriers
252
+ (org_id, subject_ref, purge_id, status, created_at, updated_at)
253
+ VALUES (?, ?, ?, 'erasing', ?, ?)
254
+ ON CONFLICT(org_id, subject_ref) DO UPDATE SET
255
+ purge_id = excluded.purge_id,
256
+ status = 'erasing',
257
+ error_code = NULL,
258
+ error_detail = NULL,
259
+ updated_at = excluded.updated_at""",
260
+ (self.org_id, subject_ref, validated_purge_id, now, now),
261
+ )
262
+ row = self.conn.execute(
263
+ """SELECT * FROM subject_write_barriers
264
+ WHERE org_id = ? AND subject_ref = ?""",
265
+ (self.org_id, subject_ref),
266
+ ).fetchone()
267
+ self.conn.commit()
268
+ except Exception:
269
+ self.conn.rollback()
270
+ raise
271
+ if row is None:
272
+ raise ValueError("subject erasure barrier insert failed")
273
+ return _row_to_subject_write_barrier(row)
274
+
275
+ def assert_subject_writable(self, subject_ref: str) -> None:
276
+ _validate_governance_prefixed_ref(
277
+ "subject_ref", subject_ref, prefix="subref_v1_"
278
+ )
279
+ with self._lock:
280
+ try:
281
+ self.conn.execute("BEGIN IMMEDIATE")
282
+ self._assert_subject_writable_locked(subject_ref)
283
+ self.conn.commit()
284
+ except Exception:
285
+ self.conn.rollback()
286
+ raise
287
+
288
+ def complete_subject_erasure_barrier_after_empty_check(
289
+ self, purge_id: str, audit_event: AuditEvent
290
+ ) -> PurgeOperation:
291
+ purge_id = _validate_governance_purge_id("purge_id", purge_id)
292
+ if audit_event.org_id != self.org_id:
293
+ raise ValueError("Audit event org_id must match storage org_id")
294
+ if audit_event.idempotency_key != purge_id:
295
+ raise ValueError("Audit event idempotency key must match purge_id")
296
+ if not _is_successful_erase_event(audit_event, purge_id=purge_id):
297
+ raise ValueError(
298
+ "Completion requires a successful ERASE audit event for this purge"
299
+ )
300
+ audit_event = _canonicalize_audit_event_for_persistence(audit_event)
301
+ now = _epoch_now()
302
+ with self._lock:
303
+ try:
304
+ self.conn.execute("BEGIN IMMEDIATE")
305
+ row = self.conn.execute(
306
+ "SELECT * FROM purge_operations WHERE purge_id = ? AND org_id = ?",
307
+ (purge_id, self.org_id),
308
+ ).fetchone()
309
+ if row is None:
310
+ raise ValueError(f"Purge operation {purge_id!r} not found")
311
+ purge_operation = _row_to_purge_operation(row)
312
+ if purge_operation.subject_ref != audit_event.subject_ref:
313
+ raise ValueError(
314
+ "Audit event subject_ref must match purge operation subject_ref"
315
+ )
316
+ if purge_operation.request_ref != audit_event.request_ref:
317
+ raise ValueError(
318
+ "Audit event request_ref must match purge operation request_ref"
319
+ )
320
+ snapshot = self.conn.execute(
321
+ """SELECT 1 FROM purge_operation_targets
322
+ WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
323
+ AND phase = ? AND status = 'complete'""",
324
+ (self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
325
+ ).fetchone()
326
+ if snapshot is None:
327
+ raise ValueError(
328
+ "Cannot complete purge without target snapshot marker"
329
+ )
330
+ if self._same_subject_rows_remain_locked(audit_event.subject_ref or ""):
331
+ raise ValueError("same-subject rows remain")
332
+ delete_rows = self.conn.execute(
333
+ """SELECT target_name, status FROM purge_operation_targets
334
+ WHERE org_id = ? AND purge_id = ? AND phase = 'delete'
335
+ AND target_ref = 'all'""",
336
+ (self.org_id, purge_id),
337
+ ).fetchall()
338
+ delete_statuses = {
339
+ str(target_row["target_name"]): str(target_row["status"])
340
+ for target_row in delete_rows
341
+ }
342
+ missing_delete_targets = [
343
+ target_name
344
+ for target_name in _CANONICAL_DELETE_TARGET_NAMES
345
+ if delete_statuses.get(target_name) != "complete"
346
+ ]
347
+ if missing_delete_targets:
348
+ raise ValueError(
349
+ "Cannot complete purge without complete delete target matrix: "
350
+ + ", ".join(missing_delete_targets)
351
+ )
352
+ incomplete = self.conn.execute(
353
+ """SELECT 1 FROM purge_operation_targets
354
+ WHERE org_id = ? AND purge_id = ? AND status != 'complete'
355
+ LIMIT 1""",
356
+ (self.org_id, purge_id),
357
+ ).fetchone()
358
+ if incomplete is not None:
359
+ raise ValueError("Cannot complete purge with incomplete targets")
360
+ existing_audit_row = self.conn.execute(
361
+ """SELECT * FROM audit_events
362
+ WHERE org_id = ? AND idempotency_key = ?""",
363
+ (self.org_id, purge_id),
364
+ ).fetchone()
365
+ if existing_audit_row is not None:
366
+ existing_event = _row_to_audit_event(existing_audit_row)
367
+ if not _is_successful_erase_event(
368
+ existing_event, purge_id=purge_id
369
+ ):
370
+ raise ValueError(
371
+ "Existing audit row for purge_id must be the matching "
372
+ "successful ERASE row"
373
+ )
374
+ if _successful_erase_identity(
375
+ existing_event
376
+ ) != _successful_erase_identity(audit_event):
377
+ raise ValueError(
378
+ "Existing audit row for purge_id must be the matching "
379
+ "successful ERASE row"
380
+ )
381
+ else:
382
+ self._append_audit_event_with_cursor(self.conn, audit_event)
383
+ existing_audit_row = self.conn.execute(
384
+ """SELECT * FROM audit_events
385
+ WHERE org_id = ? AND idempotency_key = ?""",
386
+ (self.org_id, purge_id),
387
+ ).fetchone()
388
+ if existing_audit_row is None:
389
+ raise ValueError(
390
+ "Completion requires exactly one successful ERASE audit row "
391
+ "for the purge_id"
392
+ )
393
+ existing_event = _row_to_audit_event(existing_audit_row)
394
+ if not _is_successful_erase_event(existing_event, purge_id=purge_id):
395
+ raise ValueError(
396
+ "Completion requires exactly one matching successful ERASE "
397
+ "audit row for the purge_id"
398
+ )
399
+ if _successful_erase_identity(
400
+ existing_event
401
+ ) != _successful_erase_identity(audit_event):
402
+ raise ValueError(
403
+ "Completion requires exactly one matching successful ERASE "
404
+ "audit row for the purge_id"
405
+ )
406
+ barrier_update = self.conn.execute(
407
+ """UPDATE subject_write_barriers
408
+ SET status = 'erased', error_code = NULL, error_detail = NULL, updated_at = ?
409
+ WHERE org_id = ? AND subject_ref = ? AND purge_id = ? AND status = 'erasing'""",
410
+ (now, self.org_id, audit_event.subject_ref, purge_id),
411
+ )
412
+ if barrier_update.rowcount != 1:
413
+ raise ValueError("subject erasure barrier is missing")
414
+ self.conn.execute(
415
+ """UPDATE purge_operations
416
+ SET status = 'complete',
417
+ error_code = NULL,
418
+ error_detail = NULL,
419
+ updated_at = ?,
420
+ completed_at = ?
421
+ WHERE purge_id = ? AND org_id = ?""",
422
+ (now, now, purge_id, self.org_id),
423
+ )
424
+ self.conn.commit()
425
+ except Exception:
426
+ self.conn.rollback()
427
+ raise
428
+ return self.get_purge_operation(purge_id)
429
+
430
+ def fail_subject_erasure_barrier(
431
+ self,
432
+ subject_ref: str,
433
+ purge_id: str,
434
+ error_code: str,
435
+ error_detail: str,
436
+ ) -> SubjectWriteBarrier:
437
+ _validate_governance_prefixed_ref(
438
+ "subject_ref", subject_ref, prefix="subref_v1_"
439
+ )
440
+ validated_purge_id = _validate_governance_purge_id("purge_id", purge_id)
441
+ validated_error_code = _validate_governance_error_code(error_code)
442
+ validated_error_detail = _validate_governance_error_detail(error_detail)
443
+ now = _epoch_now()
444
+ with self._lock:
445
+ try:
446
+ self.conn.execute("BEGIN IMMEDIATE")
447
+ update_cursor = self.conn.execute(
448
+ """UPDATE subject_write_barriers
449
+ SET status = 'failed',
450
+ error_code = ?,
451
+ error_detail = ?,
452
+ updated_at = ?
453
+ WHERE org_id = ? AND subject_ref = ? AND purge_id = ?
454
+ AND status = 'erasing'""",
455
+ (
456
+ validated_error_code,
457
+ validated_error_detail,
458
+ now,
459
+ self.org_id,
460
+ subject_ref,
461
+ validated_purge_id,
462
+ ),
463
+ )
464
+ if update_cursor.rowcount != 1:
465
+ raise ValueError(
466
+ "subject erasure barrier failure requires a matching barrier"
467
+ )
468
+ purge_row = self.conn.execute(
469
+ "SELECT 1 FROM purge_operations WHERE purge_id = ? AND org_id = ?",
470
+ (validated_purge_id, self.org_id),
471
+ ).fetchone()
472
+ if purge_row is not None:
473
+ self.conn.execute(
474
+ """UPDATE purge_operations
475
+ SET status = 'failed', error_code = ?, error_detail = ?,
476
+ updated_at = ?, completed_at = ?
477
+ WHERE purge_id = ? AND org_id = ?""",
478
+ (
479
+ validated_error_code,
480
+ validated_error_detail,
481
+ now,
482
+ now,
483
+ validated_purge_id,
484
+ self.org_id,
485
+ ),
486
+ )
487
+ row = self.conn.execute(
488
+ """SELECT * FROM subject_write_barriers
489
+ WHERE org_id = ? AND subject_ref = ? AND purge_id = ?""",
490
+ (self.org_id, subject_ref, validated_purge_id),
491
+ ).fetchone()
492
+ self.conn.commit()
493
+ except Exception:
494
+ self.conn.rollback()
495
+ raise
496
+ if row is None:
497
+ raise ValueError("subject erasure barrier update failed")
498
+ return _row_to_subject_write_barrier(row)
499
+
500
+ def get_subject_write_barrier(self, subject_ref: str) -> SubjectWriteBarrier | None:
501
+ _validate_governance_prefixed_ref(
502
+ "subject_ref", subject_ref, prefix="subref_v1_"
503
+ )
504
+ row = self.conn.execute(
505
+ """SELECT * FROM subject_write_barriers
506
+ WHERE org_id = ? AND subject_ref = ?""",
507
+ (self.org_id, subject_ref),
508
+ ).fetchone()
509
+ if row is None:
510
+ return None
511
+ return _row_to_subject_write_barrier(row)
@@ -329,7 +329,8 @@ class AgentPlaybookStoreMixin:
329
329
  ) -> AgentPlaybook | None:
330
330
  sql = "SELECT * FROM agent_playbooks WHERE agent_playbook_id = ?"
331
331
  if not include_tombstones:
332
- sql += " AND (status IS NULL OR status NOT IN (?, ?, ?))"
332
+ _ph = ",".join("?" * len(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES))
333
+ sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
333
334
  row = self._fetchone(
334
335
  sql,
335
336
  (agent_playbook_id, *_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES),
@@ -689,10 +690,11 @@ class AgentPlaybookStoreMixin:
689
690
  # would require adding playbook_status to the SELECT. The UPDATE WHERE
690
691
  # clause already excludes those rows atomically; the extra continue here
691
692
  # would be a no-op and not worth the added complexity.
693
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
692
694
  cur = self.conn.execute(
693
695
  "UPDATE agent_playbooks SET status = ?, retired_at = ?"
694
696
  " WHERE agent_playbook_id = ? AND playbook_status != ?"
695
- " AND (status IS NULL OR status NOT IN (?, ?))",
697
+ f" AND (status IS NULL OR status NOT IN ({_ph}))",
696
698
  (
697
699
  Status.SUPERSEDED.value,
698
700
  now_ts,
@@ -882,7 +884,8 @@ class AgentPlaybookStoreMixin:
882
884
  conditions.append(frag)
883
885
  params.extend(sparams)
884
886
  else:
885
- conditions.append("(ap.status IS NULL OR ap.status NOT IN (?, ?, ?))")
887
+ _ph = ",".join("?" * len(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES))
888
+ conditions.append(f"(ap.status IS NULL OR ap.status NOT IN ({_ph}))")
886
889
  params.extend(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES)
887
890
  tag_frag, tag_params = _build_tags_sql("ap", request.tags)
888
891
  if tag_frag:
@@ -230,7 +230,8 @@ class UserPlaybookStoreMixin:
230
230
  params.extend(sparams)
231
231
  else:
232
232
  # Default: exclude tombstone statuses (MERGED/SUPERSEDED)
233
- sql += " AND (status IS NULL OR status NOT IN (?, ?))"
233
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
234
+ sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
234
235
  params.extend(_TOMBSTONE_STATUS_VALUES)
235
236
  tag_frag, tag_params = _build_tags_sql("user_playbooks", tags)
236
237
  if tag_frag:
@@ -274,7 +275,8 @@ class UserPlaybookStoreMixin:
274
275
  params.extend(sparams)
275
276
  else:
276
277
  # Default: exclude tombstone statuses (MERGED/SUPERSEDED)
277
- sql += " AND (status IS NULL OR status NOT IN (?, ?))"
278
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
279
+ sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
278
280
  params.extend(_TOMBSTONE_STATUS_VALUES)
279
281
 
280
282
  row = self._fetchone(sql, params)
@@ -282,11 +284,12 @@ class UserPlaybookStoreMixin:
282
284
 
283
285
  @SQLiteStorageBase.handle_exceptions
284
286
  def count_user_playbooks_by_session(self, session_id: str) -> int:
287
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
285
288
  row = self._fetchone(
286
- """SELECT COUNT(*) as cnt FROM user_playbooks up
289
+ f"""SELECT COUNT(*) as cnt FROM user_playbooks up
287
290
  JOIN requests r ON up.request_id = r.request_id
288
291
  WHERE r.session_id = ?
289
- AND (up.status IS NULL OR up.status NOT IN (?, ?))""",
292
+ AND (up.status IS NULL OR up.status NOT IN ({_ph}))""", # noqa: S608
290
293
  (session_id, *_TOMBSTONE_STATUS_VALUES),
291
294
  )
292
295
  return row["cnt"] if row else 0
@@ -620,7 +623,8 @@ class UserPlaybookStoreMixin:
620
623
  params.extend(sparams)
621
624
  else:
622
625
  # Default: exclude tombstone statuses (MERGED/SUPERSEDED)
623
- conditions.append("(up.status IS NULL OR up.status NOT IN (?, ?))")
626
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
627
+ conditions.append(f"(up.status IS NULL OR up.status NOT IN ({_ph}))")
624
628
  params.extend(_TOMBSTONE_STATUS_VALUES)
625
629
  tag_frag, tag_params = _build_tags_sql("up", request.tags)
626
630
  if tag_frag:
@@ -695,7 +699,8 @@ class UserPlaybookStoreMixin:
695
699
  ) -> UserPlaybook | None:
696
700
  sql = "SELECT * FROM user_playbooks WHERE user_playbook_id = ?"
697
701
  if not include_tombstones:
698
- sql += " AND (status IS NULL OR status NOT IN (?, ?))"
702
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
703
+ sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
699
704
  row = self._fetchone(sql, (user_playbook_id, *_TOMBSTONE_STATUS_VALUES))
700
705
  else:
701
706
  row = self._fetchone(sql, (user_playbook_id,))
@@ -814,10 +819,11 @@ class UserPlaybookStoreMixin:
814
819
  self._subject_ref_from_user_playbook_row(row)
815
820
  )
816
821
  old_status = row["status"]
822
+ _ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
817
823
  cur = self.conn.execute(
818
824
  "UPDATE user_playbooks SET status = ?, retired_at = ?"
819
825
  " WHERE user_playbook_id = ?"
820
- " AND (status IS NULL OR status NOT IN (?, ?))",
826
+ f" AND (status IS NULL OR status NOT IN ({_ph}))",
821
827
  (
822
828
  Status.SUPERSEDED.value,
823
829
  now_ts,
@@ -0,0 +1,9 @@
1
+ from ._interaction_store import InteractionStoreMixin
2
+ from ._profile_store import ProfileStoreMixin
3
+ from ._search import ProfileSearchMixin
4
+
5
+ __all__ = [
6
+ "InteractionStoreMixin",
7
+ "ProfileSearchMixin",
8
+ "ProfileStoreMixin",
9
+ ]