claude-smart 0.2.47 → 0.2.48

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