claude-smart 0.2.48 → 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/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/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +15 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- 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 +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +150 -37
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +140 -2
- 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 +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +16 -7
- 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 +12 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +52 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +28 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- 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/profiles/_interaction_store.py +23 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +22 -0
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py
CHANGED
|
@@ -6,6 +6,11 @@ index sidecar update is durable. They are sqlite-only — there is no
|
|
|
6
6
|
supabase/postgres counterpart. Composed into ``SQLiteStorage`` ahead of
|
|
7
7
|
``SQLiteStorageBase``; the boot-time ``_migrate_vec_tables`` (residual in
|
|
8
8
|
``_base.py``) resolves ``self._vec_upsert`` through the composed MRO.
|
|
9
|
+
|
|
10
|
+
When a ``commit_scope`` is active (``_scope_depth > 0``), the public helpers
|
|
11
|
+
buffer the operation into ``_deferred_index_ops`` for post-commit flushing;
|
|
12
|
+
the ``_..._now`` variants always execute immediately and are called by
|
|
13
|
+
``_flush_index_op`` after the outer commit.
|
|
9
14
|
"""
|
|
10
15
|
|
|
11
16
|
from __future__ import annotations
|
|
@@ -23,10 +28,50 @@ class SQLiteFtsVecMixin:
|
|
|
23
28
|
conn: sqlite3.Connection
|
|
24
29
|
_fetchall: Any
|
|
25
30
|
_has_sqlite_vec: bool
|
|
31
|
+
_scope_depth: int
|
|
32
|
+
_deferred_index_ops: list[tuple[str, Any]]
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------
|
|
35
|
+
# Dispatcher — called by commit_scope after the outer commit
|
|
36
|
+
# ------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def _flush_index_op(self, kind: str, args: Any) -> None:
|
|
39
|
+
"""Dispatch a buffered index op after the outer commit."""
|
|
40
|
+
if kind == "fts_upsert":
|
|
41
|
+
table, rowid, text_fields = args
|
|
42
|
+
self._fts_upsert_now(table, rowid, **text_fields)
|
|
43
|
+
elif kind == "fts_delete":
|
|
44
|
+
table, rowid = args
|
|
45
|
+
self._fts_delete_now(table, rowid)
|
|
46
|
+
elif kind == "fts_upsert_profile":
|
|
47
|
+
profile_id, content = args
|
|
48
|
+
self._fts_upsert_profile_now(profile_id, content)
|
|
49
|
+
elif kind == "fts_delete_profile":
|
|
50
|
+
(profile_id,) = args
|
|
51
|
+
self._fts_delete_profile_now(profile_id)
|
|
52
|
+
elif kind == "vec_upsert":
|
|
53
|
+
table, rowid, embedding = args
|
|
54
|
+
self._vec_upsert_now(table, rowid, embedding)
|
|
55
|
+
elif kind == "vec_delete":
|
|
56
|
+
table, rowid = args
|
|
57
|
+
self._vec_delete_now(table, rowid)
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"unknown index op kind: {kind!r}")
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# FTS helpers — public (defer when in scope) + _now (immediate)
|
|
63
|
+
# ------------------------------------------------------------------
|
|
26
64
|
|
|
27
|
-
# FTS helpers
|
|
28
65
|
def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
|
|
29
66
|
"""Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
|
|
67
|
+
if self._scope_depth > 0:
|
|
68
|
+
self._deferred_index_ops.append(("fts_upsert", (table, rowid, text_fields)))
|
|
69
|
+
return
|
|
70
|
+
self._fts_upsert_now(table, rowid, **text_fields)
|
|
71
|
+
|
|
72
|
+
def _fts_upsert_now(
|
|
73
|
+
self, table: str, rowid: int, **text_fields: str | None
|
|
74
|
+
) -> None:
|
|
30
75
|
with self._lock:
|
|
31
76
|
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
32
77
|
cols = list(text_fields.keys())
|
|
@@ -40,12 +85,26 @@ class SQLiteFtsVecMixin:
|
|
|
40
85
|
self.conn.commit()
|
|
41
86
|
|
|
42
87
|
def _fts_delete(self, table: str, rowid: int) -> None:
|
|
88
|
+
if self._scope_depth > 0:
|
|
89
|
+
self._deferred_index_ops.append(("fts_delete", (table, rowid)))
|
|
90
|
+
return
|
|
91
|
+
self._fts_delete_now(table, rowid)
|
|
92
|
+
|
|
93
|
+
def _fts_delete_now(self, table: str, rowid: int) -> None:
|
|
43
94
|
with self._lock:
|
|
44
95
|
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,)) # noqa: S608
|
|
45
96
|
self.conn.commit()
|
|
46
97
|
|
|
47
98
|
def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
|
|
48
99
|
"""FTS for profiles uses profile_id TEXT as key column."""
|
|
100
|
+
if self._scope_depth > 0:
|
|
101
|
+
self._deferred_index_ops.append(
|
|
102
|
+
("fts_upsert_profile", (profile_id, content))
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
self._fts_upsert_profile_now(profile_id, content)
|
|
106
|
+
|
|
107
|
+
def _fts_upsert_profile_now(self, profile_id: str, content: str) -> None:
|
|
49
108
|
with self._lock:
|
|
50
109
|
self.conn.execute(
|
|
51
110
|
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
@@ -57,15 +116,32 @@ class SQLiteFtsVecMixin:
|
|
|
57
116
|
self.conn.commit()
|
|
58
117
|
|
|
59
118
|
def _fts_delete_profile(self, profile_id: str) -> None:
|
|
119
|
+
if self._scope_depth > 0:
|
|
120
|
+
self._deferred_index_ops.append(("fts_delete_profile", (profile_id,)))
|
|
121
|
+
return
|
|
122
|
+
self._fts_delete_profile_now(profile_id)
|
|
123
|
+
|
|
124
|
+
def _fts_delete_profile_now(self, profile_id: str) -> None:
|
|
60
125
|
with self._lock:
|
|
61
126
|
self.conn.execute(
|
|
62
127
|
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
63
128
|
)
|
|
64
129
|
self.conn.commit()
|
|
65
130
|
|
|
66
|
-
#
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Vec helpers (sqlite-vec) — public (defer when in scope) + _now (immediate)
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
67
135
|
def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
68
136
|
"""Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
|
|
137
|
+
if not self._has_sqlite_vec:
|
|
138
|
+
return
|
|
139
|
+
if self._scope_depth > 0:
|
|
140
|
+
self._deferred_index_ops.append(("vec_upsert", (table, rowid, embedding)))
|
|
141
|
+
return
|
|
142
|
+
self._vec_upsert_now(table, rowid, embedding)
|
|
143
|
+
|
|
144
|
+
def _vec_upsert_now(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
69
145
|
if not self._has_sqlite_vec:
|
|
70
146
|
return
|
|
71
147
|
with self._lock:
|
|
@@ -78,6 +154,14 @@ class SQLiteFtsVecMixin:
|
|
|
78
154
|
|
|
79
155
|
def _vec_delete(self, table: str, rowid: int) -> None:
|
|
80
156
|
"""Delete a vec table row. No-op when sqlite-vec is unavailable."""
|
|
157
|
+
if not self._has_sqlite_vec:
|
|
158
|
+
return
|
|
159
|
+
if self._scope_depth > 0:
|
|
160
|
+
self._deferred_index_ops.append(("vec_delete", (table, rowid)))
|
|
161
|
+
return
|
|
162
|
+
self._vec_delete_now(table, rowid)
|
|
163
|
+
|
|
164
|
+
def _vec_delete_now(self, table: str, rowid: int) -> None:
|
|
81
165
|
if not self._has_sqlite_vec:
|
|
82
166
|
return
|
|
83
167
|
with self._lock:
|
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
|
|
@@ -609,7 +614,8 @@ class AgentPlaybookStoreMixin:
|
|
|
609
614
|
to_status="archived",
|
|
610
615
|
status_namespace="lifecycle_status",
|
|
611
616
|
)
|
|
612
|
-
self.
|
|
617
|
+
if self._own_transaction():
|
|
618
|
+
self.conn.commit()
|
|
613
619
|
|
|
614
620
|
@SQLiteStorageBase.handle_exceptions
|
|
615
621
|
def archive_agent_playbooks_by_ids(self, agent_playbook_ids: list[int]) -> None:
|
|
@@ -648,7 +654,8 @@ class AgentPlaybookStoreMixin:
|
|
|
648
654
|
to_status="archived",
|
|
649
655
|
status_namespace="lifecycle_status",
|
|
650
656
|
)
|
|
651
|
-
self.
|
|
657
|
+
if self._own_transaction():
|
|
658
|
+
self.conn.commit()
|
|
652
659
|
|
|
653
660
|
@SQLiteStorageBase.handle_exceptions
|
|
654
661
|
def supersede_agent_playbooks_by_ids(
|
|
@@ -712,7 +719,8 @@ class AgentPlaybookStoreMixin:
|
|
|
712
719
|
request_id=request_id,
|
|
713
720
|
)
|
|
714
721
|
updated += 1
|
|
715
|
-
self.
|
|
722
|
+
if self._own_transaction():
|
|
723
|
+
self.conn.commit()
|
|
716
724
|
return updated
|
|
717
725
|
|
|
718
726
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -773,7 +781,8 @@ class AgentPlaybookStoreMixin:
|
|
|
773
781
|
request_id=request_id,
|
|
774
782
|
)
|
|
775
783
|
updated += 1
|
|
776
|
-
self.
|
|
784
|
+
if self._own_transaction():
|
|
785
|
+
self.conn.commit()
|
|
777
786
|
return updated
|
|
778
787
|
|
|
779
788
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -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
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py
CHANGED
|
@@ -71,6 +71,7 @@ class UserPlaybookStoreMixin:
|
|
|
71
71
|
_delete_playbook_search_rows: Any
|
|
72
72
|
_subject_ref_for_user_id: Any
|
|
73
73
|
_assert_subject_writable_locked: Any
|
|
74
|
+
_own_transaction: Any
|
|
74
75
|
|
|
75
76
|
def _subject_ref_from_user_playbook_row(self, row: sqlite3.Row) -> str:
|
|
76
77
|
subject_ref = row["governance_subject_ref"]
|
|
@@ -119,8 +120,10 @@ class UserPlaybookStoreMixin:
|
|
|
119
120
|
|
|
120
121
|
created_at_iso = _epoch_to_iso(up.created_at)
|
|
121
122
|
with self._lock:
|
|
123
|
+
own_txn = self._own_transaction()
|
|
122
124
|
try:
|
|
123
|
-
|
|
125
|
+
if own_txn:
|
|
126
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
124
127
|
self._assert_subject_writable_locked(subject_ref)
|
|
125
128
|
cur = self.conn.execute(
|
|
126
129
|
"""INSERT INTO user_playbooks
|
|
@@ -159,9 +162,11 @@ class UserPlaybookStoreMixin:
|
|
|
159
162
|
)
|
|
160
163
|
upid = cur.lastrowid or 0
|
|
161
164
|
up.user_playbook_id = upid
|
|
162
|
-
|
|
165
|
+
if own_txn:
|
|
166
|
+
self.conn.commit()
|
|
163
167
|
except Exception:
|
|
164
|
-
|
|
168
|
+
if own_txn:
|
|
169
|
+
self.conn.rollback()
|
|
165
170
|
raise
|
|
166
171
|
|
|
167
172
|
fts_parts = [up.trigger or "", up.content or ""]
|
|
@@ -789,7 +794,8 @@ class UserPlaybookStoreMixin:
|
|
|
789
794
|
to_status=None,
|
|
790
795
|
status_namespace=None,
|
|
791
796
|
)
|
|
792
|
-
self.
|
|
797
|
+
if self._own_transaction():
|
|
798
|
+
self.conn.commit()
|
|
793
799
|
|
|
794
800
|
@SQLiteStorageBase.handle_exceptions
|
|
795
801
|
def supersede_user_playbooks_by_ids(
|
|
@@ -840,5 +846,6 @@ class UserPlaybookStoreMixin:
|
|
|
840
846
|
request_id=request_id,
|
|
841
847
|
)
|
|
842
848
|
updated += 1
|
|
843
|
-
self.
|
|
849
|
+
if self._own_transaction():
|
|
850
|
+
self.conn.commit()
|
|
844
851
|
return updated
|
|
@@ -45,6 +45,7 @@ class InteractionStoreMixin:
|
|
|
45
45
|
embedding_dimensions: int
|
|
46
46
|
_subject_ref_for_user_id: Any
|
|
47
47
|
_assert_subject_writable_locked: Any
|
|
48
|
+
_own_transaction: Any
|
|
48
49
|
|
|
49
50
|
# ------------------------------------------------------------------
|
|
50
51
|
# CRUD — Interactions
|
|
@@ -81,8 +82,10 @@ class InteractionStoreMixin:
|
|
|
81
82
|
created_at_iso = _epoch_to_iso(interaction.created_at)
|
|
82
83
|
subject_ref = self._subject_ref_for_user_id(interaction.user_id)
|
|
83
84
|
with self._lock:
|
|
85
|
+
own_txn = self._own_transaction()
|
|
84
86
|
try:
|
|
85
|
-
|
|
87
|
+
if own_txn:
|
|
88
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
86
89
|
self._assert_subject_writable_locked(subject_ref)
|
|
87
90
|
if interaction.interaction_id:
|
|
88
91
|
self.conn.execute(
|
|
@@ -148,9 +151,11 @@ class InteractionStoreMixin:
|
|
|
148
151
|
)
|
|
149
152
|
iid = cur.lastrowid or 0
|
|
150
153
|
interaction.interaction_id = iid
|
|
151
|
-
|
|
154
|
+
if own_txn:
|
|
155
|
+
self.conn.commit()
|
|
152
156
|
except Exception:
|
|
153
|
-
|
|
157
|
+
if own_txn:
|
|
158
|
+
self.conn.rollback()
|
|
154
159
|
raise
|
|
155
160
|
# Update FTS and vec
|
|
156
161
|
self._fts_upsert(
|
|
@@ -168,12 +173,53 @@ class InteractionStoreMixin:
|
|
|
168
173
|
self,
|
|
169
174
|
user_id: str, # noqa: ARG002
|
|
170
175
|
interactions: list[Interaction],
|
|
176
|
+
*,
|
|
177
|
+
embeddings_prepared: bool = False,
|
|
171
178
|
) -> None:
|
|
172
179
|
if not interactions:
|
|
173
180
|
return
|
|
181
|
+
if not embeddings_prepared:
|
|
182
|
+
# Only generate embeddings for interactions that do not already have them.
|
|
183
|
+
# This allows callers to pre-populate embeddings (e.g. via
|
|
184
|
+
# prepare_interaction_embeddings) before opening a commit_scope so that no
|
|
185
|
+
# network I/O occurs inside the transaction.
|
|
186
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
187
|
+
if to_embed:
|
|
188
|
+
texts = [
|
|
189
|
+
"\n".join([i.content or "", i.user_action_description or ""])
|
|
190
|
+
for i in to_embed
|
|
191
|
+
]
|
|
192
|
+
try:
|
|
193
|
+
embeddings = self.llm_client.get_embeddings(
|
|
194
|
+
texts, self.embedding_model_name, self.embedding_dimensions
|
|
195
|
+
)
|
|
196
|
+
except EmbeddingUnavailableError as exc:
|
|
197
|
+
logger.warning(
|
|
198
|
+
"Embedding unavailable for interaction bulk insert; "
|
|
199
|
+
"continuing without vectors: %s",
|
|
200
|
+
exc,
|
|
201
|
+
)
|
|
202
|
+
embeddings = [[] for _ in texts]
|
|
203
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
204
|
+
interaction.embedding = embedding
|
|
205
|
+
for interaction in interactions:
|
|
206
|
+
self._insert_interaction(interaction)
|
|
207
|
+
|
|
208
|
+
@SQLiteStorageBase.handle_exceptions
|
|
209
|
+
def prepare_interaction_embeddings(self, interactions: list[Interaction]) -> None:
|
|
210
|
+
"""Pre-populate interaction.embedding for each interaction (no DB write).
|
|
211
|
+
|
|
212
|
+
Generates embeddings in one batch call so the write path inside a
|
|
213
|
+
commit_scope can skip the network round-trip.
|
|
214
|
+
"""
|
|
215
|
+
if not interactions:
|
|
216
|
+
return
|
|
217
|
+
to_embed = [i for i in interactions if not i.embedding]
|
|
218
|
+
if not to_embed:
|
|
219
|
+
return
|
|
174
220
|
texts = [
|
|
175
221
|
"\n".join([i.content or "", i.user_action_description or ""])
|
|
176
|
-
for i in
|
|
222
|
+
for i in to_embed
|
|
177
223
|
]
|
|
178
224
|
try:
|
|
179
225
|
embeddings = self.llm_client.get_embeddings(
|
|
@@ -181,14 +227,13 @@ class InteractionStoreMixin:
|
|
|
181
227
|
)
|
|
182
228
|
except EmbeddingUnavailableError as exc:
|
|
183
229
|
logger.warning(
|
|
184
|
-
"Embedding unavailable
|
|
230
|
+
"Embedding unavailable during prepare_interaction_embeddings; "
|
|
185
231
|
"continuing without vectors: %s",
|
|
186
232
|
exc,
|
|
187
233
|
)
|
|
188
234
|
embeddings = [[] for _ in texts]
|
|
189
|
-
for interaction, embedding in zip(
|
|
235
|
+
for interaction, embedding in zip(to_embed, embeddings, strict=False):
|
|
190
236
|
interaction.embedding = embedding
|
|
191
|
-
self._insert_interaction(interaction)
|
|
192
237
|
|
|
193
238
|
@SQLiteStorageBase.handle_exceptions
|
|
194
239
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
@@ -77,6 +77,7 @@ class ProfileStoreMixin:
|
|
|
77
77
|
_has_sqlite_vec: bool
|
|
78
78
|
_subject_ref_for_user_id: Any
|
|
79
79
|
_assert_subject_writable_locked: Any
|
|
80
|
+
_own_transaction: Any
|
|
80
81
|
|
|
81
82
|
def _subject_ref_from_profile_row(self, row: sqlite3.Row) -> str:
|
|
82
83
|
subject_ref = row["governance_subject_ref"]
|
|
@@ -224,8 +225,10 @@ class ProfileStoreMixin:
|
|
|
224
225
|
profile.embedding = self._get_embedding(embedding_text)
|
|
225
226
|
embedding = profile.embedding
|
|
226
227
|
with self._lock:
|
|
228
|
+
own_txn = self._own_transaction()
|
|
227
229
|
try:
|
|
228
|
-
|
|
230
|
+
if own_txn:
|
|
231
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
229
232
|
self._assert_subject_writable_locked(subject_ref)
|
|
230
233
|
self.conn.execute(
|
|
231
234
|
"""INSERT OR REPLACE INTO profiles
|
|
@@ -261,9 +264,11 @@ class ProfileStoreMixin:
|
|
|
261
264
|
subject_ref,
|
|
262
265
|
),
|
|
263
266
|
)
|
|
264
|
-
|
|
267
|
+
if own_txn:
|
|
268
|
+
self.conn.commit()
|
|
265
269
|
except Exception:
|
|
266
|
-
|
|
270
|
+
if own_txn:
|
|
271
|
+
self.conn.rollback()
|
|
267
272
|
raise
|
|
268
273
|
fts_parts = [profile.content or ""]
|
|
269
274
|
if profile.custom_features:
|
|
@@ -473,6 +478,24 @@ class ProfileStoreMixin:
|
|
|
473
478
|
row = self._fetchone("SELECT COUNT(*) as cnt FROM profiles")
|
|
474
479
|
return row["cnt"] if row else 0
|
|
475
480
|
|
|
481
|
+
@SQLiteStorageBase.handle_exceptions
|
|
482
|
+
def count_user_profiles_by_status(
|
|
483
|
+
self, user_ids: list[str], status: Status | None
|
|
484
|
+
) -> int:
|
|
485
|
+
if not user_ids:
|
|
486
|
+
return 0
|
|
487
|
+
|
|
488
|
+
current_ts = _epoch_now()
|
|
489
|
+
status_frag, status_params = _build_status_sql([status])
|
|
490
|
+
ph = ",".join("?" for _ in user_ids)
|
|
491
|
+
sql = (
|
|
492
|
+
f"SELECT COUNT(*) as cnt FROM profiles "
|
|
493
|
+
f"WHERE user_id IN ({ph}) "
|
|
494
|
+
f"AND expiration_timestamp >= ? AND {status_frag}"
|
|
495
|
+
)
|
|
496
|
+
row = self._fetchone(sql, [*user_ids, current_ts, *status_params])
|
|
497
|
+
return row["cnt"] if row else 0
|
|
498
|
+
|
|
476
499
|
@SQLiteStorageBase.handle_exceptions
|
|
477
500
|
def update_all_profiles_status(
|
|
478
501
|
self,
|
|
@@ -809,7 +832,8 @@ class ProfileStoreMixin:
|
|
|
809
832
|
status_namespace="lifecycle_status",
|
|
810
833
|
)
|
|
811
834
|
committed_ids.append(pid)
|
|
812
|
-
self.
|
|
835
|
+
if self._own_transaction():
|
|
836
|
+
self.conn.commit()
|
|
813
837
|
return committed_ids
|
|
814
838
|
|
|
815
839
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from reflexio.models.api_schema.domain.enums import Status
|
|
2
2
|
|
|
3
|
+
from ._learning_jobs import LearningJob, LearningJobStatus, LearningJobStoreABC
|
|
3
4
|
from ._agent_run import (
|
|
4
5
|
NOT_APPLICABLE_ANSWER,
|
|
5
6
|
AgentBinding,
|
|
@@ -22,6 +23,7 @@ from ._agent_run import (
|
|
|
22
23
|
not_applicable_tool_result,
|
|
23
24
|
)
|
|
24
25
|
from ._base import BaseStorageCore, matches_status_filter
|
|
26
|
+
from ._commit_scope import CommitScopeMixin
|
|
25
27
|
from ._extras import ExtrasMixin
|
|
26
28
|
from ._lineage import EntityType, LineageEventMixin
|
|
27
29
|
from ._operations import OperationMixin
|
|
@@ -48,6 +50,8 @@ from .profiles import InteractionStoreMixin, ProfileSearchMixin, ProfileStoreMix
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class BaseStorage(
|
|
53
|
+
LearningJobStoreABC,
|
|
54
|
+
CommitScopeMixin,
|
|
51
55
|
AgentRunMixin,
|
|
52
56
|
ProfileStoreMixin,
|
|
53
57
|
InteractionStoreMixin,
|
|
@@ -260,8 +264,21 @@ class BaseStorage(
|
|
|
260
264
|
"purged_user_playbooks": len(purge_upb_ids),
|
|
261
265
|
}
|
|
262
266
|
|
|
267
|
+
def learning_jobs_columns(self) -> list[str]:
|
|
268
|
+
"""Return the column names of the learning_jobs table.
|
|
269
|
+
|
|
270
|
+
Each backend overrides this to query its own schema introspection
|
|
271
|
+
mechanism (SQLite: PRAGMA table_info; Postgres/Supabase:
|
|
272
|
+
information_schema.columns).
|
|
273
|
+
"""
|
|
274
|
+
raise NotImplementedError
|
|
275
|
+
|
|
263
276
|
|
|
264
277
|
__all__ = [
|
|
278
|
+
"LearningJob",
|
|
279
|
+
"LearningJobStatus",
|
|
280
|
+
"LearningJobStoreABC",
|
|
281
|
+
"CommitScopeMixin",
|
|
265
282
|
"AgentBinding",
|
|
266
283
|
"AgentRunMixin",
|
|
267
284
|
"EntityType",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import AbstractContextManager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommitScopeMixin(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def commit_scope(self) -> AbstractContextManager[None]:
|
|
8
|
+
"""Atomic multi-write transaction; see Workstream A plan Task 1."""
|
|
9
|
+
raise NotImplementedError
|