claude-smart 0.2.46 → 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 (170) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  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/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
  10. package/plugin/dashboard/package-lock.json +61 -390
  11. package/plugin/dashboard/package.json +2 -2
  12. package/plugin/opencode/dist/server.mjs +76 -2
  13. package/plugin/opencode/server.mts +79 -2
  14. package/plugin/pyproject.toml +6 -2
  15. package/plugin/scripts/smart-install.sh +7 -1
  16. package/plugin/src/claude_smart/cli.py +210 -22
  17. package/plugin/src/claude_smart/context_format.py +9 -9
  18. package/plugin/src/claude_smart/cs_cite.py +66 -19
  19. package/plugin/uv.lock +5 -5
  20. package/plugin/vendor/reflexio/.env.example +7 -0
  21. package/plugin/vendor/reflexio/README.md +3 -3
  22. package/plugin/vendor/reflexio/pyproject.toml +2 -1
  23. package/plugin/vendor/reflexio/reflexio/README.md +11 -6
  24. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  25. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  26. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  27. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  28. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  29. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  30. package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
  31. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  32. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
  33. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  34. package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
  35. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  36. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  37. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
  38. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  39. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  40. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
  41. package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
  42. package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
  43. package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
  44. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
  45. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
  46. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  47. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  48. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
  49. package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
  50. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  51. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
  52. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
  53. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
  54. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
  55. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
  56. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
  57. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +73 -1819
  58. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  59. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
  60. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  61. package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
  62. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
  63. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  64. package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
  65. package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
  67. package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
  68. package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
  69. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
  70. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
  71. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +259 -0
  72. package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
  73. package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
  74. package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
  75. package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
  76. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
  77. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/README.md +5 -2
  79. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
  85. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
  86. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
  87. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
  88. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +26 -41
  89. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  90. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
  91. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  92. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  93. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  94. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
  95. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  96. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
  98. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
  99. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
  100. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
  101. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
  102. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  103. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  107. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  109. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
  110. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
  111. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +58 -351
  112. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  113. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
  114. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
  115. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  116. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
  117. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  118. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
  120. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
  122. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
  124. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
  125. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +132 -0
  126. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
  127. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
  129. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
  131. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  133. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  135. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
  139. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +263 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -9
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +73 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
  169. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  170. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -0,0 +1,452 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ import threading
6
+ from typing import Any, Literal, Protocol, cast
7
+
8
+ from reflexio.models.api_schema.domain import AgentPlaybook, AgentPlaybookSourceWindow
9
+ from reflexio.models.api_schema.domain.governance import (
10
+ AuditEvent,
11
+ PurgeOperation,
12
+ PurgeOperationTarget,
13
+ SubjectBarrierStatus,
14
+ SubjectWriteBarrier,
15
+ )
16
+ from reflexio.server.services.storage.governance_validation import (
17
+ _CANONICAL_DELETE_TARGET_NAMES,
18
+ _PREPARE_PHASE,
19
+ _SNAPSHOT_TARGET_NAME,
20
+ _validate_governance_int_list,
21
+ )
22
+
23
+ _LEGACY_AUDIT_REQUEST_REF = "reqref_v1_legacy_unknown"
24
+
25
+ _PURGE_OPERATION_TARGETS_TABLE_DDL = """
26
+ CREATE TABLE IF NOT EXISTS purge_operation_targets (
27
+ org_id TEXT NOT NULL,
28
+ purge_id TEXT NOT NULL,
29
+ target_name TEXT NOT NULL,
30
+ target_ref TEXT NOT NULL DEFAULT '',
31
+ phase TEXT NOT NULL,
32
+ status TEXT NOT NULL DEFAULT 'pending',
33
+ detail TEXT,
34
+ deleted_count INTEGER NOT NULL DEFAULT 0,
35
+ error_detail TEXT,
36
+ started_at INTEGER,
37
+ completed_at INTEGER,
38
+ PRIMARY KEY (org_id, purge_id, target_name, target_ref, phase),
39
+ FOREIGN KEY (org_id, purge_id) REFERENCES purge_operations(org_id, purge_id) ON DELETE CASCADE
40
+ );
41
+ """
42
+
43
+ GOVERNANCE_DDL = f"""
44
+ CREATE TABLE IF NOT EXISTS audit_events (
45
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ org_id TEXT NOT NULL,
47
+ actor_type TEXT NOT NULL DEFAULT 'system',
48
+ actor_ref TEXT,
49
+ operation TEXT NOT NULL,
50
+ entity_type TEXT NOT NULL,
51
+ entity_id TEXT,
52
+ subject_ref TEXT,
53
+ request_ref TEXT NOT NULL,
54
+ idempotency_key TEXT,
55
+ status TEXT NOT NULL DEFAULT 'ok',
56
+ detail TEXT,
57
+ created_at INTEGER NOT NULL
58
+ );
59
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_events_org_idem
60
+ ON audit_events(org_id, idempotency_key)
61
+ WHERE idempotency_key IS NOT NULL;
62
+ CREATE INDEX IF NOT EXISTS idx_audit_events_subject_created
63
+ ON audit_events(org_id, subject_ref, created_at, event_id);
64
+ CREATE INDEX IF NOT EXISTS idx_audit_events_org_created
65
+ ON audit_events(org_id, created_at, event_id);
66
+
67
+ CREATE TABLE IF NOT EXISTS purge_operations (
68
+ org_id TEXT NOT NULL,
69
+ purge_id TEXT NOT NULL,
70
+ operation_type TEXT NOT NULL,
71
+ scope_type TEXT NOT NULL,
72
+ subject_ref TEXT,
73
+ request_ref TEXT NOT NULL,
74
+ idempotency_key TEXT NOT NULL,
75
+ status TEXT NOT NULL DEFAULT 'pending',
76
+ error_code TEXT,
77
+ error_detail TEXT,
78
+ created_at INTEGER NOT NULL,
79
+ updated_at INTEGER NOT NULL,
80
+ completed_at INTEGER,
81
+ PRIMARY KEY (org_id, purge_id)
82
+ );
83
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_purge_operations_org_idem
84
+ ON purge_operations(org_id, idempotency_key);
85
+
86
+ {_PURGE_OPERATION_TARGETS_TABLE_DDL}
87
+ CREATE INDEX IF NOT EXISTS idx_purge_targets_purge_phase
88
+ ON purge_operation_targets(org_id, purge_id, phase, status);
89
+
90
+ CREATE TABLE IF NOT EXISTS subject_write_barriers (
91
+ org_id TEXT NOT NULL,
92
+ subject_ref TEXT NOT NULL,
93
+ purge_id TEXT NOT NULL,
94
+ status TEXT NOT NULL,
95
+ error_code TEXT,
96
+ error_detail TEXT,
97
+ created_at INTEGER NOT NULL,
98
+ updated_at INTEGER NOT NULL,
99
+ PRIMARY KEY (org_id, subject_ref),
100
+ CHECK (status IN ('erasing', 'erased', 'failed'))
101
+ );
102
+ CREATE INDEX IF NOT EXISTS idx_subject_write_barriers_org_purge
103
+ ON subject_write_barriers(org_id, purge_id);
104
+ """
105
+
106
+
107
+ def init_governance_tables(conn: sqlite3.Connection) -> None:
108
+ _upgrade_legacy_purge_operation_targets_table(conn)
109
+ conn.executescript(GOVERNANCE_DDL)
110
+ _enforce_audit_request_ref_not_null(conn)
111
+ _ensure_governance_subject_ref_columns(conn)
112
+
113
+
114
+ def _ensure_governance_subject_ref_columns(conn: sqlite3.Connection) -> None:
115
+ for table in (
116
+ "requests",
117
+ "interactions",
118
+ "profiles",
119
+ "user_playbooks",
120
+ "agent_success_evaluation_result",
121
+ ):
122
+ columns = [row[1] for row in conn.execute(f"PRAGMA table_info({table})")]
123
+ if not columns:
124
+ continue
125
+ if "governance_subject_ref" in columns:
126
+ pass
127
+ else:
128
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN governance_subject_ref TEXT")
129
+ conn.execute(
130
+ f"CREATE INDEX IF NOT EXISTS idx_{table}_governance_subject_ref "
131
+ f"ON {table}(governance_subject_ref)"
132
+ )
133
+
134
+
135
+ def _json_dumps(obj: Any) -> str | None:
136
+ if obj is None:
137
+ return None
138
+ return json.dumps(obj, default=str)
139
+
140
+
141
+ def _json_loads(text: str | None) -> Any:
142
+ if not text:
143
+ return None
144
+ return json.loads(text)
145
+
146
+
147
+ def _upgrade_legacy_purge_operation_targets_table(conn: sqlite3.Connection) -> None:
148
+ target_columns = [
149
+ row[1] for row in conn.execute("PRAGMA table_info(purge_operation_targets)")
150
+ ]
151
+ if not target_columns or "org_id" in target_columns:
152
+ return
153
+ conn.execute(
154
+ "ALTER TABLE purge_operation_targets RENAME TO purge_operation_targets_legacy"
155
+ )
156
+ conn.executescript(_PURGE_OPERATION_TARGETS_TABLE_DDL)
157
+ conn.execute(
158
+ """INSERT INTO purge_operation_targets (
159
+ org_id, purge_id, target_name, target_ref, phase, status, detail,
160
+ deleted_count, error_detail, started_at, completed_at
161
+ )
162
+ SELECT uniquely_mapped_purges.org_id, legacy.purge_id, legacy.target_name,
163
+ legacy.target_ref, legacy.phase, legacy.status, legacy.detail,
164
+ legacy.deleted_count, legacy.error_detail, legacy.started_at,
165
+ legacy.completed_at
166
+ FROM purge_operation_targets_legacy AS legacy
167
+ JOIN (
168
+ SELECT MIN(org_id) AS org_id, purge_id
169
+ FROM purge_operations
170
+ GROUP BY purge_id
171
+ HAVING COUNT(*) = 1
172
+ ) AS uniquely_mapped_purges
173
+ ON uniquely_mapped_purges.purge_id = legacy.purge_id"""
174
+ )
175
+ conn.execute("DROP TABLE purge_operation_targets_legacy")
176
+
177
+
178
+ def _enforce_audit_request_ref_not_null(conn: sqlite3.Connection) -> None:
179
+ audit_columns = [row[1] for row in conn.execute("PRAGMA table_info(audit_events)")]
180
+ if not audit_columns:
181
+ return
182
+ conn.execute(
183
+ "UPDATE audit_events SET request_ref = ? WHERE request_ref IS NULL",
184
+ (_LEGACY_AUDIT_REQUEST_REF,),
185
+ )
186
+ conn.execute(
187
+ """
188
+ CREATE TRIGGER IF NOT EXISTS audit_events_request_ref_not_null
189
+ BEFORE INSERT ON audit_events
190
+ WHEN NEW.request_ref IS NULL
191
+ BEGIN
192
+ SELECT RAISE(ABORT, 'audit_events.request_ref is required');
193
+ END
194
+ """
195
+ )
196
+
197
+
198
+ def _row_to_audit_event(row: sqlite3.Row) -> AuditEvent:
199
+ return AuditEvent(
200
+ org_id=row["org_id"],
201
+ actor_type=row["actor_type"],
202
+ actor_ref=row["actor_ref"],
203
+ operation=row["operation"],
204
+ entity_type=row["entity_type"],
205
+ entity_id=row["entity_id"],
206
+ subject_ref=row["subject_ref"],
207
+ request_ref=row["request_ref"],
208
+ idempotency_key=row["idempotency_key"],
209
+ status=row["status"],
210
+ detail=_json_loads(row["detail"]),
211
+ created_at=row["created_at"],
212
+ )
213
+
214
+
215
+ def _row_to_purge_operation(row: sqlite3.Row) -> PurgeOperation:
216
+ return PurgeOperation(
217
+ purge_id=row["purge_id"],
218
+ org_id=row["org_id"],
219
+ operation_type=row["operation_type"],
220
+ scope_type=row["scope_type"],
221
+ subject_ref=row["subject_ref"],
222
+ request_ref=row["request_ref"],
223
+ idempotency_key=row["idempotency_key"],
224
+ status=row["status"],
225
+ error_code=row["error_code"],
226
+ error_detail=row["error_detail"],
227
+ created_at=row["created_at"],
228
+ updated_at=row["updated_at"],
229
+ completed_at=row["completed_at"],
230
+ )
231
+
232
+
233
+ def _row_to_purge_target(row: sqlite3.Row) -> PurgeOperationTarget:
234
+ return PurgeOperationTarget(
235
+ purge_id=row["purge_id"],
236
+ target_name=row["target_name"],
237
+ target_ref=row["target_ref"],
238
+ phase=row["phase"],
239
+ status=row["status"],
240
+ detail=_json_loads(row["detail"]),
241
+ deleted_count=row["deleted_count"],
242
+ error_detail=row["error_detail"],
243
+ started_at=row["started_at"],
244
+ completed_at=row["completed_at"],
245
+ )
246
+
247
+
248
+ def _row_to_subject_write_barrier(row: sqlite3.Row) -> SubjectWriteBarrier:
249
+ return SubjectWriteBarrier(
250
+ org_id=str(row["org_id"]),
251
+ subject_ref=str(row["subject_ref"]),
252
+ purge_id=str(row["purge_id"]),
253
+ status=cast(SubjectBarrierStatus, str(row["status"])),
254
+ error_code=row["error_code"],
255
+ error_detail=row["error_detail"],
256
+ created_at=int(row["created_at"]),
257
+ updated_at=int(row["updated_at"]),
258
+ )
259
+
260
+
261
+ class _SQLiteGovernanceDeps(Protocol):
262
+ conn: sqlite3.Connection
263
+ _lock: threading.RLock
264
+ org_id: str
265
+ _has_sqlite_vec: bool
266
+
267
+ def _fetchall(
268
+ self, sql: str, params: list[Any] | tuple[Any, ...]
269
+ ) -> list[sqlite3.Row]: ...
270
+
271
+ def _fetchone(
272
+ self, sql: str, params: list[Any] | tuple[Any, ...]
273
+ ) -> sqlite3.Row | None: ...
274
+
275
+ def _partition_purge_vs_delete(
276
+ self, entity_type: Literal["profile", "user_playbook"], ids: list[str]
277
+ ) -> tuple[list[str], list[str]]: ...
278
+
279
+ def _delete_in_chunks(
280
+ self, table_name: str, column_name: str, values: list[Any]
281
+ ) -> None: ...
282
+
283
+ def _delete_source_windows_for_user_playbook_ids(
284
+ self, user_playbook_ids: list[int]
285
+ ) -> None: ...
286
+
287
+ def _get_embedding(self, text: str) -> list[float]: ...
288
+
289
+ def set_source_windows_for_agent_playbook(
290
+ self, agent_playbook_id: int, windows: list[AgentPlaybookSourceWindow]
291
+ ) -> None: ...
292
+
293
+ def get_source_windows_for_agent_playbook(
294
+ self, agent_playbook_id: int
295
+ ) -> list[AgentPlaybookSourceWindow]: ...
296
+
297
+ def get_agent_playbook_by_id(
298
+ self,
299
+ agent_playbook_id: int,
300
+ *,
301
+ include_tombstones: bool = False,
302
+ ) -> AgentPlaybook | None: ...
303
+
304
+ def _index_agent_playbook_fts_vec(self, ap: AgentPlaybook) -> None: ...
305
+
306
+
307
+ class SQLiteGovernanceMixin:
308
+ """SQLite governance storage primitives."""
309
+
310
+ conn: sqlite3.Connection
311
+ _lock: threading.RLock
312
+ org_id: str
313
+
314
+ def _deps(self) -> _SQLiteGovernanceDeps:
315
+ return cast(_SQLiteGovernanceDeps, self)
316
+
317
+ def _validate_prepared_delete_target_matrix_locked(self, purge_id: str) -> None:
318
+ snapshot = self.conn.execute(
319
+ """SELECT 1 FROM purge_operation_targets
320
+ WHERE org_id = ? AND purge_id = ? AND target_name = ? AND target_ref = 'all'
321
+ AND phase = ? AND status = 'complete'""",
322
+ (self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
323
+ ).fetchone()
324
+ if snapshot is None:
325
+ raise ValueError("Cannot delete user data without target snapshot marker")
326
+ delete_rows = self.conn.execute(
327
+ """SELECT target_name, status FROM purge_operation_targets
328
+ WHERE org_id = ? AND purge_id = ? AND phase = 'delete'
329
+ AND target_ref = 'all'""",
330
+ (self.org_id, purge_id),
331
+ ).fetchall()
332
+ delete_statuses = {
333
+ str(row["target_name"]): str(row["status"]) for row in delete_rows
334
+ }
335
+ missing_delete_targets = [
336
+ target_name
337
+ for target_name in _CANONICAL_DELETE_TARGET_NAMES
338
+ if delete_statuses.get(target_name) not in {"pending", "complete"}
339
+ ]
340
+ if missing_delete_targets:
341
+ raise ValueError(
342
+ "Cannot delete user data without complete delete target matrix: "
343
+ + ", ".join(missing_delete_targets)
344
+ )
345
+
346
+ def _validate_hide_for_rebuild_targets_locked(self, purge_id: str) -> None:
347
+ rebuild_rows = self.conn.execute(
348
+ """SELECT DISTINCT target_ref
349
+ FROM purge_operation_targets
350
+ WHERE org_id = ? AND purge_id = ? AND target_name = 'agent_playbook'
351
+ AND phase = 'rebuild_without_erased_sources' AND target_ref != ''
352
+ ORDER BY target_ref ASC""",
353
+ (self.org_id, purge_id),
354
+ ).fetchall()
355
+ if not rebuild_rows:
356
+ return
357
+ hidden_refs = {
358
+ str(row["target_ref"])
359
+ for row in self.conn.execute(
360
+ """SELECT target_ref
361
+ FROM purge_operation_targets
362
+ WHERE org_id = ? AND purge_id = ? AND target_name = 'agent_playbook'
363
+ AND phase = 'hide_for_rebuild' AND status = 'complete'""",
364
+ (self.org_id, purge_id),
365
+ ).fetchall()
366
+ }
367
+ missing_hidden_refs = [
368
+ str(row["target_ref"])
369
+ for row in rebuild_rows
370
+ if str(row["target_ref"]) not in hidden_refs
371
+ ]
372
+ if missing_hidden_refs:
373
+ raise ValueError(
374
+ "Cannot delete user data before hide_for_rebuild completes for "
375
+ f"planned agent_playbooks: {', '.join(missing_hidden_refs)}"
376
+ )
377
+
378
+ def _planned_governance_delete_counts(
379
+ self, user_id: str, owned_user_playbook_ids: set[int]
380
+ ) -> dict[str, int]:
381
+ request_row = self.conn.execute(
382
+ "SELECT COUNT(*) AS cnt FROM requests WHERE user_id = ?",
383
+ (user_id,),
384
+ ).fetchone()
385
+ interaction_row = self.conn.execute(
386
+ "SELECT COUNT(*) AS cnt FROM interactions WHERE user_id = ?",
387
+ (user_id,),
388
+ ).fetchone()
389
+ eval_result_row = self.conn.execute(
390
+ """SELECT COUNT(*) AS cnt
391
+ FROM agent_success_evaluation_result
392
+ WHERE user_id = ?""",
393
+ (user_id,),
394
+ ).fetchone()
395
+ profile_rows = self.conn.execute(
396
+ "SELECT profile_id FROM profiles WHERE user_id = ?",
397
+ (user_id,),
398
+ ).fetchall()
399
+ if request_row is None or interaction_row is None or eval_result_row is None:
400
+ raise ValueError("Missing governance count rows")
401
+ profile_ids = [str(row["profile_id"]) for row in profile_rows]
402
+ purge_profile_ids, delete_profile_ids = self._deps()._partition_purge_vs_delete(
403
+ "profile",
404
+ profile_ids,
405
+ )
406
+ playbook_ids = [
407
+ str(user_playbook_id)
408
+ for user_playbook_id in sorted(owned_user_playbook_ids)
409
+ ]
410
+ purge_playbook_ids, delete_playbook_ids = (
411
+ self._deps()._partition_purge_vs_delete(
412
+ "user_playbook",
413
+ playbook_ids,
414
+ )
415
+ )
416
+ return {
417
+ "request": int(request_row["cnt"]),
418
+ "interaction": int(interaction_row["cnt"]),
419
+ "profile": len(delete_profile_ids),
420
+ "profile_purge": len(purge_profile_ids),
421
+ "user_playbook": len(delete_playbook_ids),
422
+ "agent_success_evaluation_result": int(eval_result_row["cnt"]),
423
+ "user_playbook_purge": len(purge_playbook_ids),
424
+ }
425
+
426
+ def _owned_user_playbook_ids_locked(self, user_id: str) -> set[int]:
427
+ return {
428
+ int(row["user_playbook_id"])
429
+ for row in self.conn.execute(
430
+ "SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
431
+ (user_id,),
432
+ ).fetchall()
433
+ }
434
+
435
+ def _prepared_owned_user_playbook_ids_locked(self, purge_id: str) -> set[int]:
436
+ row = self.conn.execute(
437
+ """SELECT detail FROM purge_operation_targets
438
+ WHERE org_id = ? AND purge_id = ? AND target_name = ?
439
+ AND target_ref = 'all' AND phase = ? AND status = 'complete'""",
440
+ (self.org_id, purge_id, _SNAPSHOT_TARGET_NAME, _PREPARE_PHASE),
441
+ ).fetchone()
442
+ if row is None:
443
+ raise ValueError("Prepared target snapshot is missing")
444
+ detail = _json_loads(row["detail"])
445
+ if not isinstance(detail, dict):
446
+ raise ValueError("Prepared target snapshot detail is missing")
447
+ return set(
448
+ _validate_governance_int_list(
449
+ "owned_user_playbook_ids",
450
+ detail.get("owned_user_playbook_ids"),
451
+ )
452
+ )
@@ -17,7 +17,12 @@ EntityType = Literal["user_playbook", "agent_playbook", "profile"]
17
17
  # Also used as the merge guard: a source that already carries any of these
18
18
  # statuses is skipped (no re-tombstone, no clock reset).
19
19
  _GC_ELIGIBLE_STATUSES: frozenset[str] = frozenset(
20
- {Status.MERGED.value, Status.SUPERSEDED.value, Status.ARCHIVED.value}
20
+ {
21
+ Status.MERGED.value,
22
+ Status.SUPERSEDED.value,
23
+ Status.ARCHIVED.value,
24
+ Status.EXPIRED.value,
25
+ }
21
26
  )
22
27
 
23
28
  # Mapping from entity_type string to (table_name, primary_key_column).
@@ -95,15 +100,17 @@ _PROFILE_PURGE_SQL = (
95
100
  "UPDATE profiles SET "
96
101
  "content='', user_id='', generated_from_request_id='', source='', "
97
102
  "embedding=NULL, extractor_names=NULL, expanded_terms=NULL, tags=NULL, "
98
- "custom_features=NULL, notes=NULL, source_span=NULL, reader_angle=NULL "
103
+ "custom_features=NULL, notes=NULL, source_span=NULL, reader_angle=NULL, "
104
+ "governance_subject_ref=NULL "
99
105
  "WHERE profile_id=?"
100
106
  )
101
107
  _USER_PLAYBOOK_PURGE_SQL = (
102
108
  "UPDATE user_playbooks SET "
103
- "content='', user_id=NULL, source=NULL, "
109
+ "content='', user_id=NULL, request_id='', source=NULL, "
104
110
  "trigger=NULL, rationale=NULL, blocking_issue=NULL, "
105
111
  "source_interaction_ids=NULL, embedding=NULL, expanded_terms=NULL, "
106
- "tags=NULL, source_span=NULL, notes=NULL, reader_angle=NULL "
112
+ "tags=NULL, source_span=NULL, notes=NULL, reader_angle=NULL, "
113
+ "governance_subject_ref=NULL "
107
114
  "WHERE user_playbook_id=?"
108
115
  )
109
116
  # agent_playbook purge not yet required; added when Task 3/4 needs it.