claude-smart 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +36 -2
- package/plugin/dashboard/package-lock.json +61 -390
- package/plugin/dashboard/package.json +2 -2
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +7 -0
- package/plugin/vendor/reflexio/pyproject.toml +2 -1
- package/plugin/vendor/reflexio/reflexio/README.md +8 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +23 -18
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +16 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +15 -11
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +24 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +25 -8
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +21 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +148 -3277
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +4 -3
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +16 -1
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +62 -36
- package/plugin/vendor/reflexio/reflexio/server/deployment_profile.py +69 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +424 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_json_extraction.py +249 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_structured_output.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +980 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_types.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +72 -1817
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +56 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/middleware.py +244 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/rate_limit.py +79 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_common.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/_metering.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/braintrust.py +129 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +549 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +320 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/playbooks.py +578 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/profiles.py +423 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/provenance.py +345 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/search.py +349 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +261 -0
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/__init__.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_batch_progress.py +298 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_config_filter.py +152 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +243 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +299 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_status_change.py +273 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +258 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +17 -1183
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +24 -41
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +335 -113
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +362 -81
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -490
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_clustering.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_postprocessing.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator_prompt_formatting.py +212 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +258 -69
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/publish_learning_worker.py +288 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +31 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +10 -1167
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +196 -353
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +0 -1513
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +17 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +7 -1281
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_agent_run_store.py +506 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_pending_tool_call_store.py +704 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/agent_run/_run_tool_dependency_store.py +123 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +216 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_audit.py +122 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +465 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_purge.py +387 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_rebuild_hide.py +332 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +511 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +22 -10
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +25 -12
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +308 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +920 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_search.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +50 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +64 -370
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_agent_run_store.py +86 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +195 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_pending_tool_call_store.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_run_tool_dependency_store.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_audit.py +29 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_erase_execution.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_purge.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_rebuild_hide.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/governance/_subject_barrier.py +49 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/__init__.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +95 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/{_profiles.py → profiles/_profile_store.py} +60 -80
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_search.py +32 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +0 -148
package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""SQLite implementation of the durable learning-job queue (Task 3)."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reflexio.server.services.storage.storage_base._learning_jobs import (
|
|
9
|
+
_ABSENCE_DONE_AFTER_SECONDS,
|
|
10
|
+
LearningJob,
|
|
11
|
+
LearningJobStoreABC,
|
|
12
|
+
LearningStatus,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ._base import SQLiteStorageBase, _epoch_to_iso, _iso_to_epoch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _row_to_learning_job(row: sqlite3.Row) -> LearningJob:
|
|
19
|
+
"""Convert a sqlite3.Row from learning_jobs to a LearningJob dataclass."""
|
|
20
|
+
d = dict(row)
|
|
21
|
+
ct = d.get("covers_through")
|
|
22
|
+
return LearningJob(
|
|
23
|
+
job_id=d["job_id"],
|
|
24
|
+
org_id=d["org_id"],
|
|
25
|
+
user_id=d["user_id"],
|
|
26
|
+
job_type=d["job_type"],
|
|
27
|
+
latest_request_id=d.get("latest_request_id"),
|
|
28
|
+
status=d["status"],
|
|
29
|
+
attempts=d["attempts"],
|
|
30
|
+
claim_token=d.get("claim_token"),
|
|
31
|
+
covers_through=float(_iso_to_epoch(ct)) if ct else None,
|
|
32
|
+
force_extraction=bool(d.get("force_extraction", 0)),
|
|
33
|
+
skip_aggregation=bool(d.get("skip_aggregation", 0)),
|
|
34
|
+
max_attempts=int(d.get("max_attempts", 3)),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SQLiteLearningJobStoreMixin(LearningJobStoreABC):
|
|
39
|
+
"""SQLite implementation of the learning-job queue.
|
|
40
|
+
|
|
41
|
+
Relies on instance attributes provided by SQLiteStorageBase via MRO:
|
|
42
|
+
``conn``, ``_lock``, ``_own_transaction``, ``org_id``.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Type annotations for attributes/methods supplied by SQLiteStorageBase via MRO
|
|
46
|
+
_lock: Any
|
|
47
|
+
conn: sqlite3.Connection
|
|
48
|
+
org_id: str
|
|
49
|
+
_own_transaction: Any
|
|
50
|
+
_fetchall: Any
|
|
51
|
+
|
|
52
|
+
@SQLiteStorageBase.handle_exceptions
|
|
53
|
+
def enqueue_learning_job(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
org_id: str,
|
|
57
|
+
user_id: str,
|
|
58
|
+
request_id: str,
|
|
59
|
+
covers_through: float,
|
|
60
|
+
job_type: str = "learning",
|
|
61
|
+
force_extraction: bool = False,
|
|
62
|
+
skip_aggregation: bool = False,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""Coalescing upsert — safe to call inside a commit_scope."""
|
|
65
|
+
job_id = str(uuid.uuid4())
|
|
66
|
+
# int() truncates sub-second precision — intentional (second-precision epochs).
|
|
67
|
+
iso_covers = _epoch_to_iso(int(covers_through))
|
|
68
|
+
fe_int = int(force_extraction)
|
|
69
|
+
sa_int = int(skip_aggregation)
|
|
70
|
+
with self._lock:
|
|
71
|
+
own_txn = self._own_transaction()
|
|
72
|
+
try:
|
|
73
|
+
if own_txn:
|
|
74
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
75
|
+
row = self.conn.execute(
|
|
76
|
+
"""
|
|
77
|
+
INSERT INTO learning_jobs
|
|
78
|
+
(job_id, org_id, user_id, job_type, latest_request_id,
|
|
79
|
+
covers_through, status, force_extraction, skip_aggregation,
|
|
80
|
+
created_at, updated_at)
|
|
81
|
+
VALUES
|
|
82
|
+
(?, ?, ?, ?, ?,
|
|
83
|
+
?, 'pending', ?, ?,
|
|
84
|
+
strftime('%Y-%m-%dT%H:%M:%fZ','now'),
|
|
85
|
+
strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
86
|
+
ON CONFLICT (org_id, user_id, job_type) WHERE status = 'pending'
|
|
87
|
+
DO UPDATE SET
|
|
88
|
+
latest_request_id = excluded.latest_request_id,
|
|
89
|
+
covers_through = CASE
|
|
90
|
+
WHEN learning_jobs.covers_through > excluded.covers_through
|
|
91
|
+
THEN learning_jobs.covers_through
|
|
92
|
+
ELSE excluded.covers_through
|
|
93
|
+
END,
|
|
94
|
+
force_extraction = excluded.force_extraction,
|
|
95
|
+
skip_aggregation = excluded.skip_aggregation,
|
|
96
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
97
|
+
RETURNING job_id
|
|
98
|
+
""",
|
|
99
|
+
(
|
|
100
|
+
job_id,
|
|
101
|
+
org_id,
|
|
102
|
+
user_id,
|
|
103
|
+
job_type,
|
|
104
|
+
request_id,
|
|
105
|
+
iso_covers,
|
|
106
|
+
fe_int,
|
|
107
|
+
sa_int,
|
|
108
|
+
),
|
|
109
|
+
).fetchone()
|
|
110
|
+
if own_txn:
|
|
111
|
+
self.conn.commit()
|
|
112
|
+
except Exception:
|
|
113
|
+
if own_txn:
|
|
114
|
+
self.conn.rollback()
|
|
115
|
+
raise
|
|
116
|
+
if row is None:
|
|
117
|
+
raise RuntimeError("enqueue_learning_job RETURNING job_id returned no row")
|
|
118
|
+
return str(row["job_id"])
|
|
119
|
+
|
|
120
|
+
@SQLiteStorageBase.handle_exceptions
|
|
121
|
+
def claim_learning_jobs(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
claimed_by: str,
|
|
125
|
+
limit: int,
|
|
126
|
+
lease_seconds: int,
|
|
127
|
+
) -> list[LearningJob]:
|
|
128
|
+
"""BEGIN IMMEDIATE + SELECT + UPDATE to atomically claim jobs."""
|
|
129
|
+
with self._lock:
|
|
130
|
+
own_txn = self._own_transaction()
|
|
131
|
+
try:
|
|
132
|
+
if own_txn:
|
|
133
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
134
|
+
|
|
135
|
+
# Find candidate job_ids using DB's now() to avoid clock skew.
|
|
136
|
+
# Include 'failed' so a failed-but-not-dead job is naturally
|
|
137
|
+
# reclaimable without a manual status reset.
|
|
138
|
+
candidate_rows = self.conn.execute(
|
|
139
|
+
"""
|
|
140
|
+
SELECT job_id FROM learning_jobs
|
|
141
|
+
WHERE org_id = ?
|
|
142
|
+
AND (
|
|
143
|
+
status = 'pending'
|
|
144
|
+
OR status = 'failed'
|
|
145
|
+
OR (status = 'claimed'
|
|
146
|
+
AND claim_expires_at < strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
147
|
+
)
|
|
148
|
+
ORDER BY created_at
|
|
149
|
+
LIMIT ?
|
|
150
|
+
""",
|
|
151
|
+
(self.org_id, limit),
|
|
152
|
+
).fetchall()
|
|
153
|
+
|
|
154
|
+
claimed: list[LearningJob] = []
|
|
155
|
+
for cand in candidate_rows:
|
|
156
|
+
job_id = cand["job_id"]
|
|
157
|
+
claim_token = str(uuid.uuid4())
|
|
158
|
+
updated = self.conn.execute(
|
|
159
|
+
"""
|
|
160
|
+
UPDATE learning_jobs SET
|
|
161
|
+
status = 'claimed',
|
|
162
|
+
claimed_by = ?,
|
|
163
|
+
claim_token = ?,
|
|
164
|
+
claim_expires_at = strftime(
|
|
165
|
+
'%Y-%m-%dT%H:%M:%fZ', 'now',
|
|
166
|
+
? || ' seconds'
|
|
167
|
+
),
|
|
168
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
|
|
169
|
+
attempts = attempts + 1
|
|
170
|
+
WHERE job_id = ?
|
|
171
|
+
RETURNING *
|
|
172
|
+
""",
|
|
173
|
+
(claimed_by, claim_token, str(lease_seconds), job_id),
|
|
174
|
+
).fetchone()
|
|
175
|
+
if updated is not None:
|
|
176
|
+
claimed.append(_row_to_learning_job(updated))
|
|
177
|
+
|
|
178
|
+
if own_txn:
|
|
179
|
+
self.conn.commit()
|
|
180
|
+
except Exception:
|
|
181
|
+
if own_txn:
|
|
182
|
+
self.conn.rollback()
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
return claimed
|
|
186
|
+
|
|
187
|
+
@SQLiteStorageBase.handle_exceptions
|
|
188
|
+
def heartbeat_learning_job(
|
|
189
|
+
self,
|
|
190
|
+
*,
|
|
191
|
+
job_id: str,
|
|
192
|
+
claim_token: str,
|
|
193
|
+
lease_seconds: int,
|
|
194
|
+
) -> bool:
|
|
195
|
+
"""Extend the lease; return True if the token is still live."""
|
|
196
|
+
with self._lock:
|
|
197
|
+
own_txn = self._own_transaction()
|
|
198
|
+
try:
|
|
199
|
+
if own_txn:
|
|
200
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
201
|
+
cur = self.conn.execute(
|
|
202
|
+
"""
|
|
203
|
+
UPDATE learning_jobs SET
|
|
204
|
+
claim_expires_at = strftime(
|
|
205
|
+
'%Y-%m-%dT%H:%M:%fZ', 'now',
|
|
206
|
+
? || ' seconds'
|
|
207
|
+
),
|
|
208
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
209
|
+
WHERE job_id = ? AND claim_token = ? AND status = 'claimed'
|
|
210
|
+
""",
|
|
211
|
+
(str(lease_seconds), job_id, claim_token),
|
|
212
|
+
)
|
|
213
|
+
updated = cur.rowcount == 1
|
|
214
|
+
if own_txn:
|
|
215
|
+
self.conn.commit()
|
|
216
|
+
except Exception:
|
|
217
|
+
if own_txn:
|
|
218
|
+
self.conn.rollback()
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
return updated
|
|
222
|
+
|
|
223
|
+
@SQLiteStorageBase.handle_exceptions
|
|
224
|
+
def complete_learning_job(
|
|
225
|
+
self,
|
|
226
|
+
*,
|
|
227
|
+
job_id: str,
|
|
228
|
+
claim_token: str,
|
|
229
|
+
) -> int:
|
|
230
|
+
"""Fenced completion — returns rowcount (0=superseded, 1=success).
|
|
231
|
+
|
|
232
|
+
Safe to call inside a commit_scope — no own BEGIN/COMMIT issued.
|
|
233
|
+
"""
|
|
234
|
+
with self._lock:
|
|
235
|
+
own_txn = self._own_transaction()
|
|
236
|
+
try:
|
|
237
|
+
if own_txn:
|
|
238
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
239
|
+
cur = self.conn.execute(
|
|
240
|
+
"""
|
|
241
|
+
UPDATE learning_jobs SET
|
|
242
|
+
status = 'done',
|
|
243
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
244
|
+
WHERE job_id = ? AND claim_token = ? AND status = 'claimed'
|
|
245
|
+
""",
|
|
246
|
+
(job_id, claim_token),
|
|
247
|
+
)
|
|
248
|
+
rowcount = cur.rowcount
|
|
249
|
+
if own_txn:
|
|
250
|
+
self.conn.commit()
|
|
251
|
+
except Exception:
|
|
252
|
+
if own_txn:
|
|
253
|
+
self.conn.rollback()
|
|
254
|
+
raise
|
|
255
|
+
|
|
256
|
+
return rowcount
|
|
257
|
+
|
|
258
|
+
@SQLiteStorageBase.handle_exceptions
|
|
259
|
+
def fail_learning_job(
|
|
260
|
+
self,
|
|
261
|
+
*,
|
|
262
|
+
job_id: str,
|
|
263
|
+
claim_token: str,
|
|
264
|
+
dead: bool,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Fenced fail/dead transition — sets status, clears token for retry.
|
|
267
|
+
|
|
268
|
+
Does NOT increment attempts: claim_learning_jobs already incremented on
|
|
269
|
+
delivery. attempts tracks delivery count; fail only transitions status.
|
|
270
|
+
"""
|
|
271
|
+
new_status = "dead" if dead else "failed"
|
|
272
|
+
# Clear claim_token and claim_expires_at only for 'failed' so it's reclaimable.
|
|
273
|
+
# For 'dead', we keep claim_token set for auditability (won't be reclaimed anyway).
|
|
274
|
+
with self._lock:
|
|
275
|
+
own_txn = self._own_transaction()
|
|
276
|
+
try:
|
|
277
|
+
if own_txn:
|
|
278
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
279
|
+
self.conn.execute(
|
|
280
|
+
"""
|
|
281
|
+
UPDATE learning_jobs SET
|
|
282
|
+
status = ?,
|
|
283
|
+
claim_token = CASE WHEN ? THEN claim_token ELSE NULL END,
|
|
284
|
+
claim_expires_at = CASE WHEN ? THEN claim_expires_at ELSE NULL END,
|
|
285
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
286
|
+
WHERE job_id = ? AND claim_token = ? AND status = 'claimed'
|
|
287
|
+
""",
|
|
288
|
+
(new_status, dead, dead, job_id, claim_token),
|
|
289
|
+
)
|
|
290
|
+
if own_txn:
|
|
291
|
+
self.conn.commit()
|
|
292
|
+
except Exception:
|
|
293
|
+
if own_txn:
|
|
294
|
+
self.conn.rollback()
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
@SQLiteStorageBase.handle_exceptions
|
|
298
|
+
def list_org_ids_with_pending_learning_jobs(self) -> list[str]:
|
|
299
|
+
"""Distinct org_ids with actionable jobs (cross-org, not org-scoped).
|
|
300
|
+
|
|
301
|
+
Uses the DB's now() for the expired-lease comparison to avoid clock skew,
|
|
302
|
+
mirroring ``claim_learning_jobs``. Index-aided by ``learning_jobs_poll``
|
|
303
|
+
(partial index narrows the scan to non-terminal rows; org_id still requires
|
|
304
|
+
a heap fetch).
|
|
305
|
+
"""
|
|
306
|
+
rows = self._fetchall(
|
|
307
|
+
"""
|
|
308
|
+
SELECT DISTINCT org_id FROM learning_jobs
|
|
309
|
+
WHERE status = 'pending'
|
|
310
|
+
OR status = 'failed'
|
|
311
|
+
OR (status = 'claimed'
|
|
312
|
+
AND claim_expires_at < strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
313
|
+
ORDER BY org_id ASC
|
|
314
|
+
""",
|
|
315
|
+
(),
|
|
316
|
+
)
|
|
317
|
+
return [str(row["org_id"]) for row in rows]
|
|
318
|
+
|
|
319
|
+
@SQLiteStorageBase.handle_exceptions
|
|
320
|
+
def get_learning_status_for_request(
|
|
321
|
+
self,
|
|
322
|
+
*,
|
|
323
|
+
user_id: str,
|
|
324
|
+
request_created_at: float,
|
|
325
|
+
) -> LearningStatus:
|
|
326
|
+
"""Coverage-based status lookup (§3.6 rule).
|
|
327
|
+
|
|
328
|
+
Converts request_created_at epoch to ISO for lexicographic comparison
|
|
329
|
+
with stored covers_through ISO strings (same format, both UTC).
|
|
330
|
+
"""
|
|
331
|
+
req_iso = _epoch_to_iso(int(request_created_at))
|
|
332
|
+
rows = self._fetchall(
|
|
333
|
+
"SELECT status, covers_through FROM learning_jobs "
|
|
334
|
+
"WHERE org_id = ? AND user_id = ?",
|
|
335
|
+
(self.org_id, user_id),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
has_pending = False
|
|
339
|
+
has_claimed_covering = False
|
|
340
|
+
has_dead_covering = False
|
|
341
|
+
has_failed = False
|
|
342
|
+
|
|
343
|
+
for row in rows:
|
|
344
|
+
status = row["status"]
|
|
345
|
+
ct: str | None = row["covers_through"]
|
|
346
|
+
covers = ct is not None and ct >= req_iso
|
|
347
|
+
|
|
348
|
+
if covers and status == "done":
|
|
349
|
+
return "done"
|
|
350
|
+
if covers and status == "claimed":
|
|
351
|
+
has_claimed_covering = True
|
|
352
|
+
if covers and status == "dead":
|
|
353
|
+
has_dead_covering = True
|
|
354
|
+
if status == "failed":
|
|
355
|
+
# 'failed' is reclaimable (attempts < max_attempts); treat as pending.
|
|
356
|
+
# Accumulate as a flag so a covering done row (encountered later in the
|
|
357
|
+
# iteration) is not shadowed by a failed row yielded earlier.
|
|
358
|
+
has_failed = True
|
|
359
|
+
if status == "pending":
|
|
360
|
+
has_pending = True
|
|
361
|
+
if status == "claimed":
|
|
362
|
+
# Deliberately treats any claimed job as covering, regardless of its
|
|
363
|
+
# covers_through value — it will extend the window once it completes.
|
|
364
|
+
has_claimed_covering = True
|
|
365
|
+
|
|
366
|
+
if has_claimed_covering:
|
|
367
|
+
return "processing"
|
|
368
|
+
if has_pending:
|
|
369
|
+
return "pending"
|
|
370
|
+
if has_failed:
|
|
371
|
+
return "pending"
|
|
372
|
+
if has_dead_covering:
|
|
373
|
+
return "failed"
|
|
374
|
+
# Absence semantics: terminal rows (done/dead) are GC'd after 24-72 h.
|
|
375
|
+
# Only treat absence as "done" once the request is old enough that a done
|
|
376
|
+
# row would have been reaped; a recent request with no rows is still pending.
|
|
377
|
+
if time.time() - request_created_at >= _ABSENCE_DONE_AFTER_SECONDS:
|
|
378
|
+
return "done"
|
|
379
|
+
return "pending"
|
|
@@ -17,7 +17,12 @@ EntityType = Literal["user_playbook", "agent_playbook", "profile"]
|
|
|
17
17
|
# Also used as the merge guard: a source that already carries any of these
|
|
18
18
|
# statuses is skipped (no re-tombstone, no clock reset).
|
|
19
19
|
_GC_ELIGIBLE_STATUSES: frozenset[str] = frozenset(
|
|
20
|
-
{
|
|
20
|
+
{
|
|
21
|
+
Status.MERGED.value,
|
|
22
|
+
Status.SUPERSEDED.value,
|
|
23
|
+
Status.ARCHIVED.value,
|
|
24
|
+
Status.EXPIRED.value,
|
|
25
|
+
}
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
# Mapping from entity_type string to (table_name, primary_key_column).
|
|
@@ -122,6 +127,7 @@ class SQLiteLineageMixin:
|
|
|
122
127
|
conn: sqlite3.Connection
|
|
123
128
|
_lock: threading.RLock
|
|
124
129
|
org_id: str
|
|
130
|
+
_own_transaction: Any
|
|
125
131
|
|
|
126
132
|
def append_lineage_event(self, event: LineageEvent) -> int:
|
|
127
133
|
"""Append an event; idempotent on (org_id, entity_type, entity_id, op, request_id).
|
|
@@ -168,10 +174,12 @@ class SQLiteLineageMixin:
|
|
|
168
174
|
),
|
|
169
175
|
).fetchone()
|
|
170
176
|
eid = row[0] if row else None
|
|
171
|
-
self.
|
|
177
|
+
if self._own_transaction():
|
|
178
|
+
self.conn.commit()
|
|
172
179
|
return int(eid) if eid is not None else 0
|
|
173
180
|
last = cur.lastrowid
|
|
174
|
-
self.
|
|
181
|
+
if self._own_transaction():
|
|
182
|
+
self.conn.commit()
|
|
175
183
|
return int(last) if last is not None else 0
|
|
176
184
|
|
|
177
185
|
def get_lineage_events(
|
|
@@ -296,7 +304,8 @@ class SQLiteLineageMixin:
|
|
|
296
304
|
request_id=context.request_id,
|
|
297
305
|
reason=context.reason,
|
|
298
306
|
)
|
|
299
|
-
self.
|
|
307
|
+
if self._own_transaction():
|
|
308
|
+
self.conn.commit()
|
|
300
309
|
|
|
301
310
|
def supersede_record(
|
|
302
311
|
self,
|
|
@@ -338,7 +347,8 @@ class SQLiteLineageMixin:
|
|
|
338
347
|
(Status.SUPERSEDED.value, successor_id, _epoch_now(), incumbent_id),
|
|
339
348
|
)
|
|
340
349
|
if cur.rowcount == 0:
|
|
341
|
-
self.
|
|
350
|
+
if self._own_transaction():
|
|
351
|
+
self.conn.commit()
|
|
342
352
|
return False
|
|
343
353
|
_append_event_stmt(
|
|
344
354
|
self.conn,
|
|
@@ -352,7 +362,8 @@ class SQLiteLineageMixin:
|
|
|
352
362
|
request_id=context.request_id,
|
|
353
363
|
reason=context.reason,
|
|
354
364
|
)
|
|
355
|
-
self.
|
|
365
|
+
if self._own_transaction():
|
|
366
|
+
self.conn.commit()
|
|
356
367
|
return True
|
|
357
368
|
|
|
358
369
|
def purge_content(self, *, entity_type: EntityType, entity_id: str) -> bool:
|