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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
|
@@ -0,0 +1,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)
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py
CHANGED
|
@@ -87,6 +87,7 @@ class AgentPlaybookStoreMixin:
|
|
|
87
87
|
_fts_upsert: Any
|
|
88
88
|
_vec_upsert: Any
|
|
89
89
|
_delete_playbook_search_rows: Any
|
|
90
|
+
_own_transaction: Any
|
|
90
91
|
|
|
91
92
|
def _index_agent_playbook_fts_vec(self, ap: AgentPlaybook) -> None:
|
|
92
93
|
"""Update the FTS and vector indexes for a single agent playbook row.
|
|
@@ -178,7 +179,8 @@ class AgentPlaybookStoreMixin:
|
|
|
178
179
|
created_at_iso = _epoch_to_iso(ap.created_at)
|
|
179
180
|
with self._lock:
|
|
180
181
|
self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
|
|
181
|
-
self.
|
|
182
|
+
if self._own_transaction():
|
|
183
|
+
self.conn.commit()
|
|
182
184
|
|
|
183
185
|
self._index_agent_playbook_fts_vec(ap)
|
|
184
186
|
saved.append(ap)
|
|
@@ -228,6 +230,7 @@ class AgentPlaybookStoreMixin:
|
|
|
228
230
|
|
|
229
231
|
created_at_iso = _epoch_to_iso(ap.created_at)
|
|
230
232
|
with self._lock:
|
|
233
|
+
own_txn = self._own_transaction()
|
|
231
234
|
try:
|
|
232
235
|
self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
|
|
233
236
|
_append_event_stmt(
|
|
@@ -242,9 +245,11 @@ class AgentPlaybookStoreMixin:
|
|
|
242
245
|
request_id=request_id,
|
|
243
246
|
reason=f"{AGGREGATE_REASON_PREFIX}{run_mode}",
|
|
244
247
|
)
|
|
245
|
-
|
|
248
|
+
if own_txn:
|
|
249
|
+
self.conn.commit()
|
|
246
250
|
except Exception:
|
|
247
|
-
|
|
251
|
+
if own_txn:
|
|
252
|
+
self.conn.rollback()
|
|
248
253
|
raise
|
|
249
254
|
|
|
250
255
|
# FTS/vec indexing AFTER commit — these helpers self-commit and must
|
|
@@ -329,7 +334,8 @@ class AgentPlaybookStoreMixin:
|
|
|
329
334
|
) -> AgentPlaybook | None:
|
|
330
335
|
sql = "SELECT * FROM agent_playbooks WHERE agent_playbook_id = ?"
|
|
331
336
|
if not include_tombstones:
|
|
332
|
-
|
|
337
|
+
_ph = ",".join("?" * len(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES))
|
|
338
|
+
sql += f" AND (status IS NULL OR status NOT IN ({_ph}))"
|
|
333
339
|
row = self._fetchone(
|
|
334
340
|
sql,
|
|
335
341
|
(agent_playbook_id, *_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES),
|
|
@@ -608,7 +614,8 @@ class AgentPlaybookStoreMixin:
|
|
|
608
614
|
to_status="archived",
|
|
609
615
|
status_namespace="lifecycle_status",
|
|
610
616
|
)
|
|
611
|
-
self.
|
|
617
|
+
if self._own_transaction():
|
|
618
|
+
self.conn.commit()
|
|
612
619
|
|
|
613
620
|
@SQLiteStorageBase.handle_exceptions
|
|
614
621
|
def archive_agent_playbooks_by_ids(self, agent_playbook_ids: list[int]) -> None:
|
|
@@ -647,7 +654,8 @@ class AgentPlaybookStoreMixin:
|
|
|
647
654
|
to_status="archived",
|
|
648
655
|
status_namespace="lifecycle_status",
|
|
649
656
|
)
|
|
650
|
-
self.
|
|
657
|
+
if self._own_transaction():
|
|
658
|
+
self.conn.commit()
|
|
651
659
|
|
|
652
660
|
@SQLiteStorageBase.handle_exceptions
|
|
653
661
|
def supersede_agent_playbooks_by_ids(
|
|
@@ -689,10 +697,11 @@ class AgentPlaybookStoreMixin:
|
|
|
689
697
|
# would require adding playbook_status to the SELECT. The UPDATE WHERE
|
|
690
698
|
# clause already excludes those rows atomically; the extra continue here
|
|
691
699
|
# would be a no-op and not worth the added complexity.
|
|
700
|
+
_ph = ",".join("?" * len(_TOMBSTONE_STATUS_VALUES))
|
|
692
701
|
cur = self.conn.execute(
|
|
693
702
|
"UPDATE agent_playbooks SET status = ?, retired_at = ?"
|
|
694
703
|
" WHERE agent_playbook_id = ? AND playbook_status != ?"
|
|
695
|
-
" AND (status IS NULL OR status NOT IN (
|
|
704
|
+
f" AND (status IS NULL OR status NOT IN ({_ph}))",
|
|
696
705
|
(
|
|
697
706
|
Status.SUPERSEDED.value,
|
|
698
707
|
now_ts,
|
|
@@ -710,7 +719,8 @@ class AgentPlaybookStoreMixin:
|
|
|
710
719
|
request_id=request_id,
|
|
711
720
|
)
|
|
712
721
|
updated += 1
|
|
713
|
-
self.
|
|
722
|
+
if self._own_transaction():
|
|
723
|
+
self.conn.commit()
|
|
714
724
|
return updated
|
|
715
725
|
|
|
716
726
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -771,7 +781,8 @@ class AgentPlaybookStoreMixin:
|
|
|
771
781
|
request_id=request_id,
|
|
772
782
|
)
|
|
773
783
|
updated += 1
|
|
774
|
-
self.
|
|
784
|
+
if self._own_transaction():
|
|
785
|
+
self.conn.commit()
|
|
775
786
|
return updated
|
|
776
787
|
|
|
777
788
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -882,7 +893,8 @@ class AgentPlaybookStoreMixin:
|
|
|
882
893
|
conditions.append(frag)
|
|
883
894
|
params.extend(sparams)
|
|
884
895
|
else:
|
|
885
|
-
|
|
896
|
+
_ph = ",".join("?" * len(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES))
|
|
897
|
+
conditions.append(f"(ap.status IS NULL OR ap.status NOT IN ({_ph}))")
|
|
886
898
|
params.extend(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES)
|
|
887
899
|
tag_frag, tag_params = _build_tags_sql("ap", request.tags)
|
|
888
900
|
if tag_frag:
|
|
@@ -22,6 +22,7 @@ class PlaybookSourceLinkageMixin:
|
|
|
22
22
|
_fetchall: Any
|
|
23
23
|
_subject_ref_for_user_id: Any
|
|
24
24
|
_assert_subject_writable_locked: Any
|
|
25
|
+
_own_transaction: Any
|
|
25
26
|
|
|
26
27
|
@SQLiteStorageBase.handle_exceptions
|
|
27
28
|
def set_source_user_playbook_ids_for_agent_playbook(
|
|
@@ -87,8 +88,10 @@ class PlaybookSourceLinkageMixin:
|
|
|
87
88
|
ids.append(source_id)
|
|
88
89
|
seen.add(source_id)
|
|
89
90
|
with self._lock:
|
|
91
|
+
own_txn = self._own_transaction()
|
|
90
92
|
try:
|
|
91
|
-
|
|
93
|
+
if own_txn:
|
|
94
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
92
95
|
for user_playbook_id in by_id:
|
|
93
96
|
row = self.conn.execute(
|
|
94
97
|
"""SELECT user_id, governance_subject_ref FROM user_playbooks
|
|
@@ -120,9 +123,11 @@ class PlaybookSourceLinkageMixin:
|
|
|
120
123
|
for upid, source_interaction_ids in by_id.items()
|
|
121
124
|
],
|
|
122
125
|
)
|
|
123
|
-
|
|
126
|
+
if own_txn:
|
|
127
|
+
self.conn.commit()
|
|
124
128
|
except Exception:
|
|
125
|
-
|
|
129
|
+
if own_txn:
|
|
130
|
+
self.conn.rollback()
|
|
126
131
|
raise
|
|
127
132
|
|
|
128
133
|
@SQLiteStorageBase.handle_exceptions
|