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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +19 -11
- package/bin/claude-smart.js +290 -68
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +11 -10
- package/plugin/dashboard/app/layout.tsx +20 -0
- 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 +76 -2
- package/plugin/opencode/server.mts +79 -2
- package/plugin/pyproject.toml +6 -2
- package/plugin/scripts/smart-install.sh +7 -1
- package/plugin/src/claude_smart/cli.py +210 -22
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/README.md +3 -3
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +11 -6
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +27 -5
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +37 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +38 -9
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +274 -3267
- 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/api_endpoints/request_context.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
- 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/extensions.py +213 -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 +73 -1819
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +57 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -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/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
- 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 +259 -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 +5 -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/extraction/resume_scheduler.py +26 -41
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +232 -123
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +385 -78
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +121 -525
- 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/playbook/service.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +43 -6
- 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 +58 -351
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +452 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1126
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
- 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 +132 -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/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +955 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +844 -0
- 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 +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +896 -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 -9
- 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/_extras.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -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/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -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 +73 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +57 -86
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
|
@@ -30,6 +30,8 @@ class RequestMixin:
|
|
|
30
30
|
_execute: Any
|
|
31
31
|
_fetchone: Any
|
|
32
32
|
_fetchall: Any
|
|
33
|
+
_subject_ref_for_user_id: Any
|
|
34
|
+
_assert_subject_writable_locked: Any
|
|
33
35
|
|
|
34
36
|
# ------------------------------------------------------------------
|
|
35
37
|
# Request methods
|
|
@@ -38,20 +40,31 @@ class RequestMixin:
|
|
|
38
40
|
@SQLiteStorageBase.handle_exceptions
|
|
39
41
|
def add_request(self, request: Request) -> None:
|
|
40
42
|
created_at_iso = _epoch_to_iso(request.created_at)
|
|
41
|
-
self.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
subject_ref = self._subject_ref_for_user_id(request.user_id)
|
|
44
|
+
with self._lock:
|
|
45
|
+
try:
|
|
46
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
47
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
48
|
+
self.conn.execute(
|
|
49
|
+
"""INSERT OR REPLACE INTO requests
|
|
50
|
+
(request_id, user_id, created_at, source, agent_version, session_id,
|
|
51
|
+
evaluation_only, governance_subject_ref)
|
|
52
|
+
VALUES (?,?,?,?,?,?,?,?)""",
|
|
53
|
+
(
|
|
54
|
+
request.request_id,
|
|
55
|
+
request.user_id,
|
|
56
|
+
created_at_iso,
|
|
57
|
+
request.source,
|
|
58
|
+
request.agent_version,
|
|
59
|
+
request.session_id,
|
|
60
|
+
1 if request.evaluation_only else 0,
|
|
61
|
+
subject_ref,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
self.conn.commit()
|
|
65
|
+
except Exception:
|
|
66
|
+
self.conn.rollback()
|
|
67
|
+
raise
|
|
55
68
|
|
|
56
69
|
@SQLiteStorageBase.handle_exceptions
|
|
57
70
|
def get_request(self, request_id: str) -> Request | None:
|
|
@@ -155,35 +168,62 @@ class RequestMixin:
|
|
|
155
168
|
end_time: int | None = None,
|
|
156
169
|
top_k: int | None = 30,
|
|
157
170
|
offset: int = 0,
|
|
171
|
+
source: str | None = None,
|
|
158
172
|
) -> dict[str, list[RequestInteractionDataModel]]:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
# Request-level filters shared by both the session-page query and the
|
|
174
|
+
# request-fetch query. Pagination is applied at the SESSION level
|
|
175
|
+
# (top_k/offset count sessions, not request rows) so a session with many
|
|
176
|
+
# requests is never truncated to a subset of its rows.
|
|
177
|
+
filter_sql = ""
|
|
178
|
+
filter_params: list[Any] = []
|
|
162
179
|
if user_id:
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
filter_sql += " AND user_id = ?"
|
|
181
|
+
filter_params.append(user_id)
|
|
165
182
|
if request_id:
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
filter_sql += " AND request_id = ?"
|
|
184
|
+
filter_params.append(request_id)
|
|
168
185
|
if session_id:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
filter_sql += " AND session_id = ?"
|
|
187
|
+
filter_params.append(session_id)
|
|
188
|
+
if source is not None:
|
|
189
|
+
filter_sql += " AND source = ?"
|
|
190
|
+
filter_params.append(source)
|
|
191
|
+
if start_time is not None:
|
|
192
|
+
filter_sql += " AND created_at >= ?"
|
|
193
|
+
filter_params.append(_epoch_to_iso(start_time))
|
|
194
|
+
if end_time is not None:
|
|
195
|
+
filter_sql += " AND created_at <= ?"
|
|
196
|
+
filter_params.append(_epoch_to_iso(end_time))
|
|
177
197
|
|
|
198
|
+
# Step 1: select the page of session_ids, ordered by each session's most
|
|
199
|
+
# recent matching request (latest-first), with session_id as a stable
|
|
200
|
+
# tiebreak so pages don't overlap or skip.
|
|
178
201
|
effective_limit = top_k or 100
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
page_rows = self._fetchall(
|
|
203
|
+
f"SELECT session_id, MAX(created_at) AS latest FROM requests "
|
|
204
|
+
f"WHERE 1=1{filter_sql} "
|
|
205
|
+
f"GROUP BY session_id ORDER BY latest DESC, session_id DESC "
|
|
206
|
+
f"LIMIT ? OFFSET ?",
|
|
207
|
+
[*filter_params, effective_limit, offset],
|
|
208
|
+
)
|
|
209
|
+
if not page_rows:
|
|
210
|
+
return {}
|
|
211
|
+
page_session_ids = [r["session_id"] for r in page_rows]
|
|
181
212
|
|
|
182
|
-
|
|
213
|
+
# Step 2: fetch ALL matching requests for the sessions in this page.
|
|
214
|
+
placeholders = ",".join("?" for _ in page_session_ids)
|
|
215
|
+
req_rows = self._fetchall(
|
|
216
|
+
f"SELECT * FROM requests WHERE 1=1{filter_sql} "
|
|
217
|
+
f"AND session_id IN ({placeholders}) ORDER BY created_at DESC",
|
|
218
|
+
[*filter_params, *page_session_ids],
|
|
219
|
+
)
|
|
183
220
|
if not req_rows:
|
|
184
221
|
return {}
|
|
185
222
|
|
|
186
|
-
|
|
223
|
+
# Preserve the latest-first session ordering from step 1.
|
|
224
|
+
grouped: dict[str, list[RequestInteractionDataModel]] = {
|
|
225
|
+
(sid or ""): [] for sid in page_session_ids
|
|
226
|
+
}
|
|
187
227
|
for rr in req_rows:
|
|
188
228
|
req = _row_to_request(rr)
|
|
189
229
|
group_name = req.session_id or ""
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py
CHANGED
|
@@ -164,3 +164,33 @@ class SQLiteShareLinkMixin:
|
|
|
164
164
|
(self.org_id,),
|
|
165
165
|
)
|
|
166
166
|
return cur.rowcount
|
|
167
|
+
|
|
168
|
+
@SQLiteStorageBase.handle_exceptions
|
|
169
|
+
def delete_expired_share_links(
|
|
170
|
+
self, *, now: int, grace_seconds: int, limit: int = 1000
|
|
171
|
+
) -> int:
|
|
172
|
+
"""Physically delete share links whose expires_at < now - grace_seconds.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
now (int): Current Unix epoch timestamp.
|
|
176
|
+
grace_seconds (int): Additional grace window; only rows with
|
|
177
|
+
expires_at < (now - grace_seconds) are deleted.
|
|
178
|
+
limit (int): Maximum number of rows to delete in one call.
|
|
179
|
+
Rows are processed in expires_at ASC order (oldest first).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
int: Number of rows physically deleted.
|
|
183
|
+
"""
|
|
184
|
+
cutoff = now - grace_seconds
|
|
185
|
+
# Re-assert the expiry condition inside the DELETE via a subquery so
|
|
186
|
+
# the predicate cannot race between the SELECT and the DELETE (TOCTOU).
|
|
187
|
+
# Matches the Supabase RPC sibling which re-asserts expires_at in a single
|
|
188
|
+
# atomic statement.
|
|
189
|
+
cur = self._execute(
|
|
190
|
+
"DELETE FROM share_links WHERE id IN ("
|
|
191
|
+
" SELECT id FROM share_links WHERE org_id = ? AND expires_at IS NOT NULL"
|
|
192
|
+
" AND expires_at < ? ORDER BY expires_at ASC LIMIT ?"
|
|
193
|
+
")",
|
|
194
|
+
(self.org_id, cutoff, limit),
|
|
195
|
+
)
|
|
196
|
+
return cur.rowcount
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ._agent_run_store import SQLiteAgentRunStoreMixin
|
|
2
|
+
from ._pending_tool_call_store import SQLitePendingToolCallStoreMixin
|
|
3
|
+
from ._run_tool_dependency_store import SQLiteRunToolDependencyStoreMixin
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"SQLiteAgentRunStoreMixin",
|
|
7
|
+
"SQLitePendingToolCallStoreMixin",
|
|
8
|
+
"SQLiteRunToolDependencyStoreMixin",
|
|
9
|
+
]
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""SQLite AgentRunStore methods (the AgentRunStore bucket).
|
|
2
|
+
|
|
3
|
+
Extracted verbatim from ``_agent_run.py`` (the AgentRunStore bucket): the seven
|
|
4
|
+
agent-run lifecycle methods plus the three ``_..._unlocked`` run-cascade
|
|
5
|
+
privates. The privates are consumed cross-bucket by the pending-tool-call
|
|
6
|
+
methods (``cancel``/``expire``/``update_resolved``/``mark_not_applicable``) in
|
|
7
|
+
``SQLitePendingToolCallStoreMixin``, which reach them via MRO co-composition —
|
|
8
|
+
kept SINGLE here, never duplicated.
|
|
9
|
+
|
|
10
|
+
The residual ``_agent_run.py`` module (helpers-only, no mixin class)
|
|
11
|
+
permanently holds the shared row/datetime helpers (``_dt``, ``_dt_str``,
|
|
12
|
+
``_row_to_agent_run`` …); ``_json_dumps`` comes from ``.._base``. Both are
|
|
13
|
+
imported here rather than duplicated.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sqlite3
|
|
19
|
+
from datetime import UTC, datetime, timedelta
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from reflexio.server.services.storage.storage_base import (
|
|
23
|
+
AgentRunRecord,
|
|
24
|
+
AgentRunStatus,
|
|
25
|
+
PendingToolCallStatus,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .._agent_run import _dt_str, _row_to_agent_run
|
|
29
|
+
from .._base import SQLiteStorageBase, _json_dumps
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SQLiteAgentRunStoreMixin:
|
|
33
|
+
"""SQLite-backed resumable extraction run store primitives."""
|
|
34
|
+
|
|
35
|
+
_lock: Any
|
|
36
|
+
conn: sqlite3.Connection
|
|
37
|
+
_fetchone: Any
|
|
38
|
+
_fetchall: Any
|
|
39
|
+
_current_timestamp: Any
|
|
40
|
+
org_id: str
|
|
41
|
+
|
|
42
|
+
def _finalize_runs_without_pending_dependencies_unlocked(self, now_s: str) -> None:
|
|
43
|
+
self.conn.execute(
|
|
44
|
+
"""
|
|
45
|
+
UPDATE _agent_runs
|
|
46
|
+
SET status = ?,
|
|
47
|
+
finalized_at = COALESCE(finalized_at, ?),
|
|
48
|
+
updated_at = ?
|
|
49
|
+
WHERE status = ?
|
|
50
|
+
AND NOT EXISTS (
|
|
51
|
+
SELECT 1
|
|
52
|
+
FROM _run_tool_dependencies d
|
|
53
|
+
JOIN _pending_tool_calls p
|
|
54
|
+
ON p.id = d.pending_tool_call_id
|
|
55
|
+
WHERE d.run_id = _agent_runs.id
|
|
56
|
+
AND d.resolved_at IS NULL
|
|
57
|
+
AND d.consumed_at IS NULL
|
|
58
|
+
AND p.status = ?
|
|
59
|
+
)
|
|
60
|
+
""",
|
|
61
|
+
(
|
|
62
|
+
AgentRunStatus.FINALIZED.value,
|
|
63
|
+
now_s,
|
|
64
|
+
now_s,
|
|
65
|
+
AgentRunStatus.FINALIZED_PENDING_TOOL.value,
|
|
66
|
+
PendingToolCallStatus.PENDING.value,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _mark_runs_ready_with_actionable_dependencies_unlocked(
|
|
71
|
+
self, now_s: str, *, pending_tool_call_id: str
|
|
72
|
+
) -> None:
|
|
73
|
+
self.conn.execute(
|
|
74
|
+
"""
|
|
75
|
+
UPDATE _agent_runs
|
|
76
|
+
SET status = ?,
|
|
77
|
+
updated_at = ?
|
|
78
|
+
WHERE status IN (?, ?)
|
|
79
|
+
AND EXISTS (
|
|
80
|
+
SELECT 1
|
|
81
|
+
FROM _run_tool_dependencies changed
|
|
82
|
+
WHERE changed.run_id = _agent_runs.id
|
|
83
|
+
AND changed.pending_tool_call_id = ?
|
|
84
|
+
)
|
|
85
|
+
AND EXISTS (
|
|
86
|
+
SELECT 1
|
|
87
|
+
FROM _run_tool_dependencies d
|
|
88
|
+
JOIN _pending_tool_calls p
|
|
89
|
+
ON p.id = d.pending_tool_call_id
|
|
90
|
+
WHERE d.run_id = _agent_runs.id
|
|
91
|
+
AND d.resolved_at IS NOT NULL
|
|
92
|
+
AND d.consumed_at IS NULL
|
|
93
|
+
AND p.status = ?
|
|
94
|
+
AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
|
|
95
|
+
)
|
|
96
|
+
""",
|
|
97
|
+
(
|
|
98
|
+
AgentRunStatus.RESUME_READY.value,
|
|
99
|
+
now_s,
|
|
100
|
+
AgentRunStatus.FINALIZED.value,
|
|
101
|
+
AgentRunStatus.FINALIZED_PENDING_TOOL.value,
|
|
102
|
+
pending_tool_call_id,
|
|
103
|
+
PendingToolCallStatus.RESOLVED.value,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _finalize_runs_without_actionable_dependencies_unlocked(
|
|
108
|
+
self, now_s: str, *, pending_tool_call_id: str
|
|
109
|
+
) -> None:
|
|
110
|
+
self.conn.execute(
|
|
111
|
+
"""
|
|
112
|
+
UPDATE _agent_runs
|
|
113
|
+
SET status = ?,
|
|
114
|
+
finalized_at = COALESCE(finalized_at, ?),
|
|
115
|
+
updated_at = ?
|
|
116
|
+
WHERE status IN (?, ?)
|
|
117
|
+
AND EXISTS (
|
|
118
|
+
SELECT 1
|
|
119
|
+
FROM _run_tool_dependencies changed
|
|
120
|
+
WHERE changed.run_id = _agent_runs.id
|
|
121
|
+
AND changed.pending_tool_call_id = ?
|
|
122
|
+
)
|
|
123
|
+
AND NOT EXISTS (
|
|
124
|
+
SELECT 1
|
|
125
|
+
FROM _run_tool_dependencies d
|
|
126
|
+
JOIN _pending_tool_calls p
|
|
127
|
+
ON p.id = d.pending_tool_call_id
|
|
128
|
+
WHERE d.run_id = _agent_runs.id
|
|
129
|
+
AND d.resolved_at IS NULL
|
|
130
|
+
AND d.consumed_at IS NULL
|
|
131
|
+
AND p.status = ?
|
|
132
|
+
)
|
|
133
|
+
AND NOT EXISTS (
|
|
134
|
+
SELECT 1
|
|
135
|
+
FROM _run_tool_dependencies d
|
|
136
|
+
JOIN _pending_tool_calls p
|
|
137
|
+
ON p.id = d.pending_tool_call_id
|
|
138
|
+
WHERE d.run_id = _agent_runs.id
|
|
139
|
+
AND d.resolved_at IS NOT NULL
|
|
140
|
+
AND d.consumed_at IS NULL
|
|
141
|
+
AND p.status = ?
|
|
142
|
+
AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
|
|
143
|
+
)
|
|
144
|
+
""",
|
|
145
|
+
(
|
|
146
|
+
AgentRunStatus.FINALIZED.value,
|
|
147
|
+
now_s,
|
|
148
|
+
now_s,
|
|
149
|
+
AgentRunStatus.FINALIZED_PENDING_TOOL.value,
|
|
150
|
+
AgentRunStatus.RESUME_READY.value,
|
|
151
|
+
pending_tool_call_id,
|
|
152
|
+
PendingToolCallStatus.PENDING.value,
|
|
153
|
+
PendingToolCallStatus.RESOLVED.value,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@SQLiteStorageBase.handle_exceptions
|
|
158
|
+
def create_agent_run(self, record: AgentRunRecord) -> AgentRunRecord:
|
|
159
|
+
binding = record.binding
|
|
160
|
+
with self._lock:
|
|
161
|
+
self.conn.execute(
|
|
162
|
+
"""
|
|
163
|
+
INSERT INTO _agent_runs (
|
|
164
|
+
id, org_id, extractor_kind, user_id,
|
|
165
|
+
request_id, agent_version, source, source_interaction_ids,
|
|
166
|
+
window_start_interaction_id, window_end_interaction_id,
|
|
167
|
+
extractor_config_hash, status, generation_request_snapshot,
|
|
168
|
+
service_config_snapshot, agent_context_snapshot,
|
|
169
|
+
committed_output, pending_tool_call_ids, max_steps_remaining,
|
|
170
|
+
resume_attempts, finalization_attempts, next_resume_at,
|
|
171
|
+
claimed_by, claimed_at, agent_completed_at, finalized_at,
|
|
172
|
+
expires_at, last_error
|
|
173
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
174
|
+
""",
|
|
175
|
+
(
|
|
176
|
+
record.id,
|
|
177
|
+
binding.org_id,
|
|
178
|
+
binding.extractor_kind,
|
|
179
|
+
binding.user_id,
|
|
180
|
+
binding.request_id,
|
|
181
|
+
binding.agent_version,
|
|
182
|
+
binding.source,
|
|
183
|
+
_json_dumps(binding.source_interaction_ids),
|
|
184
|
+
binding.window_start_interaction_id,
|
|
185
|
+
binding.window_end_interaction_id,
|
|
186
|
+
binding.extractor_config_hash,
|
|
187
|
+
record.status.value,
|
|
188
|
+
_json_dumps(record.generation_request_snapshot),
|
|
189
|
+
_json_dumps(record.service_config_snapshot),
|
|
190
|
+
record.agent_context_snapshot,
|
|
191
|
+
_json_dumps(record.committed_output),
|
|
192
|
+
_json_dumps(record.pending_tool_call_ids),
|
|
193
|
+
record.max_steps_remaining,
|
|
194
|
+
record.resume_attempts,
|
|
195
|
+
record.finalization_attempts,
|
|
196
|
+
_dt_str(record.next_resume_at),
|
|
197
|
+
record.claimed_by,
|
|
198
|
+
_dt_str(record.claimed_at),
|
|
199
|
+
_dt_str(record.agent_completed_at),
|
|
200
|
+
_dt_str(record.finalized_at),
|
|
201
|
+
_dt_str(record.expires_at),
|
|
202
|
+
record.last_error,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
self.conn.commit()
|
|
206
|
+
stored = self.get_agent_run(record.id)
|
|
207
|
+
if stored is None: # pragma: no cover
|
|
208
|
+
raise RuntimeError(f"Failed to create agent run {record.id}")
|
|
209
|
+
return stored
|
|
210
|
+
|
|
211
|
+
@SQLiteStorageBase.handle_exceptions
|
|
212
|
+
def get_agent_run(self, run_id: str) -> AgentRunRecord | None:
|
|
213
|
+
row = self._fetchone("SELECT * FROM _agent_runs WHERE id = ?", (run_id,))
|
|
214
|
+
return _row_to_agent_run(row) if row else None
|
|
215
|
+
|
|
216
|
+
@SQLiteStorageBase.handle_exceptions
|
|
217
|
+
def update_agent_run_status(
|
|
218
|
+
self,
|
|
219
|
+
run_id: str,
|
|
220
|
+
status: AgentRunStatus,
|
|
221
|
+
*,
|
|
222
|
+
committed_output: dict[str, Any] | None = None,
|
|
223
|
+
pending_tool_call_ids: list[str] | None = None,
|
|
224
|
+
max_steps_remaining: int | None = None,
|
|
225
|
+
next_resume_at: datetime | None = None,
|
|
226
|
+
last_error: str | None = None,
|
|
227
|
+
increment_finalization_attempts: bool = False,
|
|
228
|
+
expected_statuses: tuple[AgentRunStatus, ...] | None = None,
|
|
229
|
+
) -> AgentRunRecord | None:
|
|
230
|
+
current_timestamp = self._current_timestamp()
|
|
231
|
+
assignments = ["status = ?", "updated_at = ?"]
|
|
232
|
+
params: list[Any] = [status.value, current_timestamp]
|
|
233
|
+
if committed_output is not None:
|
|
234
|
+
assignments.append("committed_output = ?")
|
|
235
|
+
params.append(_json_dumps(committed_output))
|
|
236
|
+
if pending_tool_call_ids is not None:
|
|
237
|
+
assignments.append("pending_tool_call_ids = ?")
|
|
238
|
+
params.append(_json_dumps(pending_tool_call_ids))
|
|
239
|
+
if max_steps_remaining is not None:
|
|
240
|
+
assignments.append("max_steps_remaining = ?")
|
|
241
|
+
params.append(max(0, max_steps_remaining))
|
|
242
|
+
if next_resume_at is not None:
|
|
243
|
+
assignments.append("next_resume_at = ?")
|
|
244
|
+
params.append(_dt_str(next_resume_at))
|
|
245
|
+
if last_error is not None:
|
|
246
|
+
assignments.append("last_error = ?")
|
|
247
|
+
params.append(last_error)
|
|
248
|
+
if increment_finalization_attempts:
|
|
249
|
+
assignments.append("finalization_attempts = finalization_attempts + 1")
|
|
250
|
+
if status == AgentRunStatus.AGENT_COMPLETED:
|
|
251
|
+
assignments.append("agent_completed_at = ?")
|
|
252
|
+
params.append(current_timestamp)
|
|
253
|
+
if status in (AgentRunStatus.FINALIZED, AgentRunStatus.FINALIZED_PENDING_TOOL):
|
|
254
|
+
assignments.append("finalized_at = ?")
|
|
255
|
+
params.append(current_timestamp)
|
|
256
|
+
params.append(run_id)
|
|
257
|
+
status_filter = ""
|
|
258
|
+
if expected_statuses:
|
|
259
|
+
placeholders = ",".join("?" for _ in expected_statuses)
|
|
260
|
+
status_filter = f" AND status IN ({placeholders})"
|
|
261
|
+
params.extend(expected.value for expected in expected_statuses)
|
|
262
|
+
with self._lock:
|
|
263
|
+
self.conn.execute(
|
|
264
|
+
f"UPDATE _agent_runs SET {', '.join(assignments)} WHERE id = ?{status_filter}",
|
|
265
|
+
params,
|
|
266
|
+
)
|
|
267
|
+
self.conn.commit()
|
|
268
|
+
return self.get_agent_run(run_id)
|
|
269
|
+
|
|
270
|
+
@SQLiteStorageBase.handle_exceptions
|
|
271
|
+
def fail_running_agent_runs_for_request(
|
|
272
|
+
self,
|
|
273
|
+
*,
|
|
274
|
+
org_id: str,
|
|
275
|
+
extractor_kind: str,
|
|
276
|
+
user_id: str | None,
|
|
277
|
+
request_id: str,
|
|
278
|
+
last_error: str,
|
|
279
|
+
) -> int:
|
|
280
|
+
current_timestamp = self._current_timestamp()
|
|
281
|
+
with self._lock:
|
|
282
|
+
cursor = self.conn.execute(
|
|
283
|
+
"""
|
|
284
|
+
UPDATE _agent_runs
|
|
285
|
+
SET status = ?,
|
|
286
|
+
updated_at = ?,
|
|
287
|
+
last_error = ?
|
|
288
|
+
WHERE org_id = ?
|
|
289
|
+
AND extractor_kind = ?
|
|
290
|
+
AND user_id IS ?
|
|
291
|
+
AND request_id = ?
|
|
292
|
+
AND status IN (?, ?)
|
|
293
|
+
""",
|
|
294
|
+
(
|
|
295
|
+
AgentRunStatus.FAILED.value,
|
|
296
|
+
current_timestamp,
|
|
297
|
+
last_error,
|
|
298
|
+
org_id,
|
|
299
|
+
extractor_kind,
|
|
300
|
+
user_id,
|
|
301
|
+
request_id,
|
|
302
|
+
AgentRunStatus.RUNNING.value,
|
|
303
|
+
AgentRunStatus.RESUMING.value,
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
self.conn.commit()
|
|
307
|
+
return cursor.rowcount
|
|
308
|
+
|
|
309
|
+
@SQLiteStorageBase.handle_exceptions
|
|
310
|
+
def claim_ready_agent_run(
|
|
311
|
+
self,
|
|
312
|
+
*,
|
|
313
|
+
org_id: str,
|
|
314
|
+
worker_id: str,
|
|
315
|
+
now: datetime | None = None,
|
|
316
|
+
claim_ttl_seconds: int = 600,
|
|
317
|
+
) -> AgentRunRecord | None:
|
|
318
|
+
current = now or datetime.now(UTC)
|
|
319
|
+
stale_before = current - timedelta(seconds=claim_ttl_seconds)
|
|
320
|
+
with self._lock:
|
|
321
|
+
row = self.conn.execute(
|
|
322
|
+
"""
|
|
323
|
+
SELECT r.*
|
|
324
|
+
FROM _agent_runs r
|
|
325
|
+
WHERE r.org_id = ?
|
|
326
|
+
AND (
|
|
327
|
+
r.status = ?
|
|
328
|
+
OR (r.status = ? AND r.claimed_at < ?)
|
|
329
|
+
)
|
|
330
|
+
AND (r.next_resume_at IS NULL OR r.next_resume_at <= ?)
|
|
331
|
+
AND EXISTS (
|
|
332
|
+
SELECT 1
|
|
333
|
+
FROM _run_tool_dependencies d
|
|
334
|
+
JOIN _pending_tool_calls p
|
|
335
|
+
ON p.id = d.pending_tool_call_id
|
|
336
|
+
WHERE d.run_id = r.id
|
|
337
|
+
AND d.resolved_at IS NOT NULL
|
|
338
|
+
AND d.consumed_at IS NULL
|
|
339
|
+
AND p.status = ?
|
|
340
|
+
AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
|
|
341
|
+
)
|
|
342
|
+
ORDER BY
|
|
343
|
+
r.org_id ASC,
|
|
344
|
+
r.extractor_kind ASC,
|
|
345
|
+
COALESCE(r.user_id, '') ASC,
|
|
346
|
+
COALESCE(r.window_start_interaction_id, 0) ASC,
|
|
347
|
+
r.updated_at ASC
|
|
348
|
+
LIMIT 1
|
|
349
|
+
""",
|
|
350
|
+
(
|
|
351
|
+
org_id,
|
|
352
|
+
AgentRunStatus.RESUME_READY.value,
|
|
353
|
+
AgentRunStatus.RESUMING.value,
|
|
354
|
+
_dt_str(stale_before),
|
|
355
|
+
_dt_str(current),
|
|
356
|
+
PendingToolCallStatus.RESOLVED.value,
|
|
357
|
+
),
|
|
358
|
+
).fetchone()
|
|
359
|
+
if row is None:
|
|
360
|
+
return None
|
|
361
|
+
self.conn.execute(
|
|
362
|
+
"""
|
|
363
|
+
UPDATE _agent_runs
|
|
364
|
+
SET status = ?,
|
|
365
|
+
claimed_by = ?,
|
|
366
|
+
claimed_at = ?,
|
|
367
|
+
resume_attempts = resume_attempts + 1,
|
|
368
|
+
updated_at = ?
|
|
369
|
+
WHERE id = ?
|
|
370
|
+
""",
|
|
371
|
+
(
|
|
372
|
+
AgentRunStatus.RESUMING.value,
|
|
373
|
+
worker_id,
|
|
374
|
+
_dt_str(current),
|
|
375
|
+
self._current_timestamp(),
|
|
376
|
+
row["id"],
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
self.conn.commit()
|
|
380
|
+
return self.get_agent_run(row["id"])
|
|
381
|
+
|
|
382
|
+
@SQLiteStorageBase.handle_exceptions
|
|
383
|
+
def claim_finalization_failed_agent_run(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
org_id: str,
|
|
387
|
+
worker_id: str,
|
|
388
|
+
now: datetime | None = None,
|
|
389
|
+
claim_ttl_seconds: int = 600,
|
|
390
|
+
) -> AgentRunRecord | None:
|
|
391
|
+
current = now or datetime.now(UTC)
|
|
392
|
+
stale_before = current - timedelta(seconds=claim_ttl_seconds)
|
|
393
|
+
with self._lock:
|
|
394
|
+
# Claim runs that still need their committed output finalized:
|
|
395
|
+
# - FINALIZATION_FAILED: an explicit retry is due.
|
|
396
|
+
# - stale FINALIZING: a worker crashed mid-finalize.
|
|
397
|
+
# - stale AGENT_COMPLETED: publish-time finalization never ran or
|
|
398
|
+
# crashed before flipping the status (the run row carries
|
|
399
|
+
# committed_output but was orphaned); the staleness guard ensures
|
|
400
|
+
# we never race an in-flight publish-time finalize.
|
|
401
|
+
row = self.conn.execute(
|
|
402
|
+
"""
|
|
403
|
+
SELECT *
|
|
404
|
+
FROM _agent_runs
|
|
405
|
+
WHERE org_id = ?
|
|
406
|
+
AND (
|
|
407
|
+
status = ?
|
|
408
|
+
OR (status = ? AND claimed_at < ?)
|
|
409
|
+
OR (status = ? AND updated_at < ?)
|
|
410
|
+
)
|
|
411
|
+
AND committed_output IS NOT NULL
|
|
412
|
+
AND (next_resume_at IS NULL OR next_resume_at <= ?)
|
|
413
|
+
ORDER BY
|
|
414
|
+
org_id ASC,
|
|
415
|
+
extractor_kind ASC,
|
|
416
|
+
COALESCE(user_id, '') ASC,
|
|
417
|
+
COALESCE(window_start_interaction_id, 0) ASC,
|
|
418
|
+
updated_at ASC
|
|
419
|
+
LIMIT 1
|
|
420
|
+
""",
|
|
421
|
+
(
|
|
422
|
+
org_id,
|
|
423
|
+
AgentRunStatus.FINALIZATION_FAILED.value,
|
|
424
|
+
AgentRunStatus.FINALIZING.value,
|
|
425
|
+
_dt_str(stale_before),
|
|
426
|
+
AgentRunStatus.AGENT_COMPLETED.value,
|
|
427
|
+
_dt_str(stale_before),
|
|
428
|
+
_dt_str(current),
|
|
429
|
+
),
|
|
430
|
+
).fetchone()
|
|
431
|
+
if row is None:
|
|
432
|
+
return None
|
|
433
|
+
self.conn.execute(
|
|
434
|
+
"""
|
|
435
|
+
UPDATE _agent_runs
|
|
436
|
+
SET status = ?,
|
|
437
|
+
claimed_by = ?,
|
|
438
|
+
claimed_at = ?,
|
|
439
|
+
updated_at = ?
|
|
440
|
+
WHERE id = ?
|
|
441
|
+
""",
|
|
442
|
+
(
|
|
443
|
+
AgentRunStatus.FINALIZING.value,
|
|
444
|
+
worker_id,
|
|
445
|
+
_dt_str(current),
|
|
446
|
+
self._current_timestamp(),
|
|
447
|
+
row["id"],
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
self.conn.commit()
|
|
451
|
+
return self.get_agent_run(row["id"])
|
|
452
|
+
|
|
453
|
+
@SQLiteStorageBase.handle_exceptions
|
|
454
|
+
def list_resumable_work_org_ids(
|
|
455
|
+
self,
|
|
456
|
+
*,
|
|
457
|
+
now: datetime | None = None,
|
|
458
|
+
limit: int = 1000,
|
|
459
|
+
) -> list[str]:
|
|
460
|
+
current = now or datetime.now(UTC)
|
|
461
|
+
now_s = _dt_str(current)
|
|
462
|
+
bounded_limit = max(1, min(limit, 10_000))
|
|
463
|
+
# Cross-org maintenance query. Surfaces any org that has a run ready to
|
|
464
|
+
# resume / awaiting finalization, or a pending tool call due to expire.
|
|
465
|
+
rows = self._fetchall(
|
|
466
|
+
"""
|
|
467
|
+
SELECT DISTINCT org_id FROM (
|
|
468
|
+
SELECT r.org_id
|
|
469
|
+
FROM _agent_runs r
|
|
470
|
+
WHERE r.status IN (?, ?)
|
|
471
|
+
AND EXISTS (
|
|
472
|
+
SELECT 1
|
|
473
|
+
FROM _run_tool_dependencies d
|
|
474
|
+
JOIN _pending_tool_calls p
|
|
475
|
+
ON p.id = d.pending_tool_call_id
|
|
476
|
+
WHERE d.run_id = r.id
|
|
477
|
+
AND d.resolved_at IS NOT NULL
|
|
478
|
+
AND d.consumed_at IS NULL
|
|
479
|
+
AND p.status = ?
|
|
480
|
+
AND COALESCE(json_extract(p.result, '$.not_applicable'), 0) != 1
|
|
481
|
+
)
|
|
482
|
+
UNION
|
|
483
|
+
SELECT org_id FROM _agent_runs
|
|
484
|
+
WHERE status IN (?, ?, ?)
|
|
485
|
+
UNION
|
|
486
|
+
SELECT org_id FROM _pending_tool_calls
|
|
487
|
+
WHERE status = ?
|
|
488
|
+
AND expires_at IS NOT NULL
|
|
489
|
+
AND expires_at <= ?
|
|
490
|
+
)
|
|
491
|
+
ORDER BY org_id ASC
|
|
492
|
+
LIMIT ?
|
|
493
|
+
""",
|
|
494
|
+
(
|
|
495
|
+
AgentRunStatus.RESUME_READY.value,
|
|
496
|
+
AgentRunStatus.RESUMING.value,
|
|
497
|
+
PendingToolCallStatus.RESOLVED.value,
|
|
498
|
+
AgentRunStatus.FINALIZATION_FAILED.value,
|
|
499
|
+
AgentRunStatus.FINALIZING.value,
|
|
500
|
+
AgentRunStatus.AGENT_COMPLETED.value,
|
|
501
|
+
PendingToolCallStatus.PENDING.value,
|
|
502
|
+
now_s,
|
|
503
|
+
bounded_limit,
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
return [row["org_id"] for row in rows]
|