claude-smart 0.2.47 → 0.2.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  8. package/plugin/dashboard/package-lock.json +61 -390
  9. package/plugin/dashboard/package.json +2 -2
  10. package/plugin/opencode/dist/server.mjs +60 -2
  11. package/plugin/opencode/server.mts +64 -2
  12. package/plugin/pyproject.toml +2 -2
  13. package/plugin/scripts/_lib.sh +58 -6
  14. package/plugin/scripts/backend-service.sh +15 -10
  15. package/plugin/scripts/codex-hook.js +16 -4
  16. package/plugin/scripts/dashboard-service.sh +1 -1
  17. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  18. package/plugin/scripts/opencode-claude-compat.js +8 -1
  19. package/plugin/scripts/smart-install.sh +101 -20
  20. package/plugin/src/README.md +1 -1
  21. package/plugin/src/claude_smart/cli.py +79 -29
  22. package/plugin/src/claude_smart/env_config.py +53 -11
  23. package/plugin/uv.lock +5 -5
  24. package/plugin/vendor/reflexio/.env.example +7 -0
  25. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  26. package/plugin/vendor/reflexio/reflexio/README.md +8 -5
  27. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  28. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  29. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  30. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  31. package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  33. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  34. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
  35. package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
  36. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  37. package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
  38. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  39. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  40. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  41. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  42. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  43. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  44. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  45. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  46. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  47. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  48. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
  49. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
  50. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  51. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  52. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  53. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  54. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  55. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  56. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  57. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  58. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  59. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  60. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
  61. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  62. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  63. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  64. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  66. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
  68. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  73. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  74. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  76. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  77. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
  81. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
  82. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
  83. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
  84. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  87. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  88. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  89. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
  91. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  92. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
  93. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
  94. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  95. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
  96. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
  97. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  98. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  103. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  104. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
@@ -0,0 +1,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
@@ -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)