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
|
@@ -7,6 +7,7 @@ are available.
|
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import functools
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
@@ -14,7 +15,7 @@ import math
|
|
|
14
15
|
import re
|
|
15
16
|
import sqlite3
|
|
16
17
|
import threading
|
|
17
|
-
from collections.abc import Callable, Sequence
|
|
18
|
+
from collections.abc import Callable, Generator, Sequence
|
|
18
19
|
from datetime import UTC, datetime
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from typing import Any, ClassVar, Literal
|
|
@@ -53,12 +54,7 @@ from reflexio.server.services.storage.error import (
|
|
|
53
54
|
StorageError,
|
|
54
55
|
require_non_empty_session_id,
|
|
55
56
|
)
|
|
56
|
-
from reflexio.server.services.storage.
|
|
57
|
-
from reflexio.server.services.storage.retention_mixin import (
|
|
58
|
-
RETENTION_DELETE_CHUNK,
|
|
59
|
-
RetentionMixin,
|
|
60
|
-
chunked,
|
|
61
|
-
)
|
|
57
|
+
from reflexio.server.services.storage.retention_mixin import RetentionMixin
|
|
62
58
|
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
63
59
|
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
64
60
|
|
|
@@ -288,6 +284,42 @@ def _true_rrf_merge(
|
|
|
288
284
|
# explicit status_filter on list/count methods.
|
|
289
285
|
_TOMBSTONE_STATUS_VALUES = (Status.MERGED.value, Status.SUPERSEDED.value)
|
|
290
286
|
|
|
287
|
+
# Profile reads also treat EXPIRED (TTL-expired tombstone) as non-current. Kept
|
|
288
|
+
# separate from _TOMBSTONE_STATUS_VALUES because that tuple is shared with playbook
|
|
289
|
+
# queries (which never carry EXPIRED) via hardcoded placeholders.
|
|
290
|
+
_PROFILE_TOMBSTONE_STATUS_VALUES = (
|
|
291
|
+
Status.MERGED.value,
|
|
292
|
+
Status.SUPERSEDED.value,
|
|
293
|
+
Status.EXPIRED.value,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def parse_status(value: str | None) -> Status | None:
|
|
298
|
+
"""Parse a stored status string into a Status, tolerating unknown values.
|
|
299
|
+
|
|
300
|
+
Unknown non-null values (e.g. a status written by a newer build after a
|
|
301
|
+
rollback) map to a non-current tombstone sentinel and log an anomaly, rather
|
|
302
|
+
than raising ValueError. Never maps to None (which models CURRENT/live).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
value: Raw status string from the database, or None/empty for CURRENT.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
The matching Status enum member, Status.SUPERSEDED for unknown values,
|
|
309
|
+
or None for falsy input (representing the CURRENT/live state).
|
|
310
|
+
"""
|
|
311
|
+
if not value:
|
|
312
|
+
return None
|
|
313
|
+
try:
|
|
314
|
+
return Status(value)
|
|
315
|
+
except ValueError:
|
|
316
|
+
from reflexio.server.tracing import capture_anomaly
|
|
317
|
+
|
|
318
|
+
capture_anomaly(
|
|
319
|
+
"storage.status.unknown_value", level="warning", status_value=value
|
|
320
|
+
)
|
|
321
|
+
return Status.SUPERSEDED
|
|
322
|
+
|
|
291
323
|
|
|
292
324
|
def _status_value(status: Status | None) -> str | None:
|
|
293
325
|
"""Convert a Status enum (or None) to its DB string value."""
|
|
@@ -363,14 +395,19 @@ def _iso_to_epoch(iso_str: str | None) -> int:
|
|
|
363
395
|
# stored row) with a valid value.
|
|
364
396
|
_MAX_SAFE_EPOCH_TS = 253_402_300_799 # 9999-12-31T23:59:59Z
|
|
365
397
|
_MIN_SAFE_EPOCH_TS = 0 # 1970-01-01T00:00:00Z
|
|
398
|
+
_MIN_CONTEMPORARY_MILLISECOND_EPOCH_TS = 1_500_000_000_000
|
|
366
399
|
|
|
367
400
|
|
|
368
401
|
def _epoch_to_iso(ts: int) -> str:
|
|
369
402
|
"""Convert a Unix timestamp (seconds) to an ISO 8601 string.
|
|
370
403
|
|
|
371
|
-
|
|
372
|
-
|
|
404
|
+
Plausible contemporary millisecond timestamps are normalized to seconds.
|
|
405
|
+
Lower values stay in seconds-space so documented sentinel bounds such as
|
|
406
|
+
``to_ts=10**12`` still clamp to the representable range instead of being
|
|
407
|
+
treated as a 2001 millisecond timestamp.
|
|
373
408
|
"""
|
|
409
|
+
if _MIN_CONTEMPORARY_MILLISECOND_EPOCH_TS <= ts <= _MAX_SAFE_EPOCH_TS * 1000:
|
|
410
|
+
ts = ts // 1000
|
|
374
411
|
clamped = max(_MIN_SAFE_EPOCH_TS, min(ts, _MAX_SAFE_EPOCH_TS))
|
|
375
412
|
return datetime.fromtimestamp(clamped, tz=UTC).isoformat()
|
|
376
413
|
|
|
@@ -392,7 +429,7 @@ def _row_to_profile(row: sqlite3.Row) -> UserProfile:
|
|
|
392
429
|
expiration_timestamp=d["expiration_timestamp"],
|
|
393
430
|
custom_features=_json_loads(d.get("custom_features")),
|
|
394
431
|
source=d.get("source") or "",
|
|
395
|
-
status=
|
|
432
|
+
status=parse_status(d.get("status")),
|
|
396
433
|
extractor_names=_json_loads(d.get("extractor_names")),
|
|
397
434
|
expanded_terms=d.get("expanded_terms"),
|
|
398
435
|
source_span=d.get("source_span"),
|
|
@@ -472,7 +509,7 @@ def _row_to_user_playbook(
|
|
|
472
509
|
blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
|
|
473
510
|
if d.get("blocking_issue")
|
|
474
511
|
else None,
|
|
475
|
-
status=
|
|
512
|
+
status=parse_status(d.get("status")),
|
|
476
513
|
source=d.get("source"),
|
|
477
514
|
source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
|
|
478
515
|
tags=_json_loads(d.get("tags")),
|
|
@@ -505,7 +542,7 @@ def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
|
|
|
505
542
|
playbook_metadata=d.get("playbook_metadata") or "",
|
|
506
543
|
tags=_json_loads(d.get("tags")),
|
|
507
544
|
embedding=[],
|
|
508
|
-
status=
|
|
545
|
+
status=parse_status(d.get("status")),
|
|
509
546
|
expanded_terms=d.get("expanded_terms"),
|
|
510
547
|
merged_into=d.get("merged_into"),
|
|
511
548
|
superseded_by=d.get("superseded_by"),
|
|
@@ -546,6 +583,16 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
546
583
|
|
|
547
584
|
supports_embedding: ClassVar[bool] = True
|
|
548
585
|
|
|
586
|
+
# Chunked bulk-delete helpers provided by SQLiteDeletionMixin via the
|
|
587
|
+
# composed SQLiteStorage MRO; declared here for clear_user_data's benefit.
|
|
588
|
+
_delete_in_chunks: Any
|
|
589
|
+
_delete_source_windows_for_user_playbook_ids: Any
|
|
590
|
+
|
|
591
|
+
# FTS/vec index helpers provided by SQLiteFtsVecMixin via the composed
|
|
592
|
+
# SQLiteStorage MRO; declared here for _migrate_vec_tables's benefit.
|
|
593
|
+
_vec_upsert: Any
|
|
594
|
+
_flush_index_op: Any
|
|
595
|
+
|
|
549
596
|
@staticmethod
|
|
550
597
|
def handle_exceptions(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
551
598
|
@functools.wraps(func)
|
|
@@ -588,6 +635,10 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
588
635
|
|
|
589
636
|
self.db_path = db_path
|
|
590
637
|
self._lock = threading.RLock()
|
|
638
|
+
self._scope_depth = 0
|
|
639
|
+
self._deferred_index_ops: list[
|
|
640
|
+
tuple[str, Any]
|
|
641
|
+
] = [] # (kind, args) flushed post-commit
|
|
591
642
|
|
|
592
643
|
logger.info("SQLite Storage for org %s using db_path: %s", org_id, db_path)
|
|
593
644
|
|
|
@@ -625,6 +676,45 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
625
676
|
# Create tables
|
|
626
677
|
self.migrate()
|
|
627
678
|
|
|
679
|
+
# ------------------------------------------------------------------
|
|
680
|
+
# Transaction scope
|
|
681
|
+
# ------------------------------------------------------------------
|
|
682
|
+
|
|
683
|
+
def _own_transaction(self) -> bool:
|
|
684
|
+
"""True when the caller is NOT inside a commit_scope (owns its BEGIN/commit)."""
|
|
685
|
+
return self._scope_depth == 0
|
|
686
|
+
|
|
687
|
+
@contextlib.contextmanager
|
|
688
|
+
def commit_scope(self) -> Generator[None, None, None]:
|
|
689
|
+
"""Group writes into one atomic commit; defer FTS/vec index ops.
|
|
690
|
+
|
|
691
|
+
Nested scopes join the outermost: only the outer scope commits. On
|
|
692
|
+
exception the whole transaction rolls back and deferred index ops
|
|
693
|
+
are discarded.
|
|
694
|
+
"""
|
|
695
|
+
with self._lock:
|
|
696
|
+
if self._scope_depth > 0:
|
|
697
|
+
self._scope_depth += 1
|
|
698
|
+
try:
|
|
699
|
+
yield
|
|
700
|
+
finally:
|
|
701
|
+
self._scope_depth -= 1
|
|
702
|
+
return
|
|
703
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
704
|
+
self._scope_depth = 1
|
|
705
|
+
try:
|
|
706
|
+
yield
|
|
707
|
+
self.conn.commit()
|
|
708
|
+
ops, self._deferred_index_ops = self._deferred_index_ops, []
|
|
709
|
+
for kind, args in ops:
|
|
710
|
+
self._flush_index_op(kind, args) # self-commits — fine post-commit
|
|
711
|
+
except Exception:
|
|
712
|
+
self.conn.rollback()
|
|
713
|
+
self._deferred_index_ops = []
|
|
714
|
+
raise
|
|
715
|
+
finally:
|
|
716
|
+
self._scope_depth = 0
|
|
717
|
+
|
|
628
718
|
# ------------------------------------------------------------------
|
|
629
719
|
# DDL / migration
|
|
630
720
|
# ------------------------------------------------------------------
|
|
@@ -669,241 +759,9 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
669
759
|
self._migrate_retire_profile_change_logs()
|
|
670
760
|
self._migrate_retire_playbook_aggregation_change_logs()
|
|
671
761
|
init_stall_state_table(self.conn)
|
|
762
|
+
self._migrate_learning_jobs()
|
|
672
763
|
return True
|
|
673
764
|
|
|
674
|
-
# -- Retention hooks (see RetentionMixin) --
|
|
675
|
-
|
|
676
|
-
@handle_exceptions
|
|
677
|
-
def _retention_table_exists(self, table_name: str) -> bool:
|
|
678
|
-
row = self._fetchone(
|
|
679
|
-
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
680
|
-
(table_name,),
|
|
681
|
-
)
|
|
682
|
-
return row is not None
|
|
683
|
-
|
|
684
|
-
@handle_exceptions
|
|
685
|
-
def _retention_count_rows(self, target: RetentionTarget) -> int:
|
|
686
|
-
row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {target.table_name}") # noqa: S608
|
|
687
|
-
return int(row["cnt"]) if row else 0
|
|
688
|
-
|
|
689
|
-
@handle_exceptions
|
|
690
|
-
def _retention_select_oldest_keys(
|
|
691
|
-
self, target: RetentionTarget, count: int
|
|
692
|
-
) -> list[tuple[Any, ...]]:
|
|
693
|
-
id_sql = ", ".join(target.id_columns)
|
|
694
|
-
tiebreak_sql = id_sql
|
|
695
|
-
rows = self._fetchall(
|
|
696
|
-
f"SELECT {id_sql} FROM {target.table_name} " # noqa: S608
|
|
697
|
-
f"ORDER BY {target.order_column} ASC, {tiebreak_sql} ASC LIMIT ?",
|
|
698
|
-
(count,),
|
|
699
|
-
)
|
|
700
|
-
return [tuple(row[col] for col in target.id_columns) for row in rows]
|
|
701
|
-
|
|
702
|
-
@handle_exceptions
|
|
703
|
-
def _retention_perform_delete(
|
|
704
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
705
|
-
) -> None:
|
|
706
|
-
# Wrap dependency + target deletes in a single critical section so
|
|
707
|
-
# concurrent writers see either both or neither.
|
|
708
|
-
with self._lock:
|
|
709
|
-
try:
|
|
710
|
-
self._retention_delete_dependencies(target, keys)
|
|
711
|
-
self._retention_delete_target_rows(target, keys)
|
|
712
|
-
self.conn.commit()
|
|
713
|
-
except Exception:
|
|
714
|
-
self.conn.rollback()
|
|
715
|
-
raise
|
|
716
|
-
|
|
717
|
-
def _retention_delete_dependencies(
|
|
718
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
719
|
-
) -> None:
|
|
720
|
-
ids = [key[0] for key in keys]
|
|
721
|
-
target_name = target.name
|
|
722
|
-
if target_name == "requests":
|
|
723
|
-
self._delete_interactions_for_request_ids([str(v) for v in ids])
|
|
724
|
-
elif target_name == "interactions":
|
|
725
|
-
self._delete_interaction_search_rows([int(v) for v in ids])
|
|
726
|
-
elif target_name == "profiles":
|
|
727
|
-
self._delete_profile_search_rows([str(v) for v in ids])
|
|
728
|
-
elif target_name == "user_playbooks":
|
|
729
|
-
self._delete_source_windows_for_user_playbook_ids([int(v) for v in ids])
|
|
730
|
-
self._delete_playbook_search_rows(
|
|
731
|
-
"user", [int(v) for v in ids], commit=False
|
|
732
|
-
)
|
|
733
|
-
elif target_name == "agent_playbooks":
|
|
734
|
-
self._delete_source_windows_for_agent_playbook_ids([int(v) for v in ids])
|
|
735
|
-
self._delete_playbook_search_rows(
|
|
736
|
-
"agent", [int(v) for v in ids], commit=False
|
|
737
|
-
)
|
|
738
|
-
elif target_name == "playbook_optimization_jobs":
|
|
739
|
-
self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
|
|
740
|
-
elif target_name == "playbook_optimization_candidates":
|
|
741
|
-
self._delete_optimizer_evaluations_for_candidate_ids([int(v) for v in ids])
|
|
742
|
-
|
|
743
|
-
def _retention_delete_target_rows(
|
|
744
|
-
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
745
|
-
) -> None:
|
|
746
|
-
if len(target.id_columns) == 1:
|
|
747
|
-
self._delete_in_chunks(
|
|
748
|
-
target.table_name,
|
|
749
|
-
target.id_columns[0],
|
|
750
|
-
[key[0] for key in keys],
|
|
751
|
-
)
|
|
752
|
-
return
|
|
753
|
-
# Composite-key delete: chunk by row to bound parameter count.
|
|
754
|
-
params_per_key = len(target.id_columns)
|
|
755
|
-
rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
|
|
756
|
-
for chunk in chunked(keys, rows_per_chunk):
|
|
757
|
-
where = " OR ".join(
|
|
758
|
-
"("
|
|
759
|
-
+ " AND ".join(f"{column} = ?" for column in target.id_columns)
|
|
760
|
-
+ ")"
|
|
761
|
-
for _ in chunk
|
|
762
|
-
)
|
|
763
|
-
params = [value for key in chunk for value in key]
|
|
764
|
-
self.conn.execute(
|
|
765
|
-
f"DELETE FROM {target.table_name} WHERE {where}", # noqa: S608
|
|
766
|
-
params,
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
# -- Chunked-delete primitives shared by the cascade helpers --
|
|
770
|
-
|
|
771
|
-
def _delete_in_chunks(
|
|
772
|
-
self, table_name: str, column_name: str, values: list[Any]
|
|
773
|
-
) -> None:
|
|
774
|
-
"""Chunked ``DELETE FROM table WHERE col IN (...)``.
|
|
775
|
-
|
|
776
|
-
Chunking keeps parameter count under ``SQLITE_MAX_VARIABLE_NUMBER``
|
|
777
|
-
on older sqlite builds (default 999) and avoids degenerate plans
|
|
778
|
-
on very large IN lists.
|
|
779
|
-
"""
|
|
780
|
-
if not values:
|
|
781
|
-
return
|
|
782
|
-
for chunk in chunked(values):
|
|
783
|
-
placeholders = ",".join("?" for _ in chunk)
|
|
784
|
-
self.conn.execute(
|
|
785
|
-
f"DELETE FROM {table_name} WHERE {column_name} IN ({placeholders})", # noqa: S608
|
|
786
|
-
chunk,
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
def _select_in_chunks(self, sql_template: str, values: list[Any]) -> list[Any]:
|
|
790
|
-
"""Run ``sql_template`` (containing ``{placeholders}``) over chunks of
|
|
791
|
-
``values`` and aggregate the result rows."""
|
|
792
|
-
results: list[Any] = []
|
|
793
|
-
for chunk in chunked(values):
|
|
794
|
-
placeholders = ",".join("?" for _ in chunk)
|
|
795
|
-
stmt = sql_template.format(placeholders=placeholders)
|
|
796
|
-
results.extend(self.conn.execute(stmt, chunk).fetchall())
|
|
797
|
-
return results
|
|
798
|
-
|
|
799
|
-
def _delete_interactions_for_request_ids(self, request_ids: list[str]) -> None:
|
|
800
|
-
if not request_ids:
|
|
801
|
-
return
|
|
802
|
-
rows = self._select_in_chunks(
|
|
803
|
-
"SELECT interaction_id FROM interactions WHERE request_id IN ({placeholders})",
|
|
804
|
-
request_ids,
|
|
805
|
-
)
|
|
806
|
-
self._delete_interaction_search_rows(
|
|
807
|
-
[int(row["interaction_id"]) for row in rows]
|
|
808
|
-
)
|
|
809
|
-
self._delete_in_chunks("interactions", "request_id", request_ids)
|
|
810
|
-
|
|
811
|
-
def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
|
|
812
|
-
"""Remove fts + vec index rows for the given interaction IDs.
|
|
813
|
-
|
|
814
|
-
Non-committing: participates in the caller's transaction. Only called
|
|
815
|
-
from inside the retention atomic block (_retention_perform_delete).
|
|
816
|
-
"""
|
|
817
|
-
if not interaction_ids:
|
|
818
|
-
return
|
|
819
|
-
self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
|
|
820
|
-
if self._has_sqlite_vec:
|
|
821
|
-
self._delete_in_chunks("interactions_vec", "rowid", interaction_ids)
|
|
822
|
-
|
|
823
|
-
def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
|
|
824
|
-
"""Remove fts + vec index rows for the given profile IDs.
|
|
825
|
-
|
|
826
|
-
Non-committing: participates in the caller's transaction. Only called
|
|
827
|
-
from inside the retention atomic block (_retention_perform_delete).
|
|
828
|
-
profiles_fts is keyed by profile_id (TEXT); profiles_vec by rowid (INT).
|
|
829
|
-
"""
|
|
830
|
-
if not profile_ids:
|
|
831
|
-
return
|
|
832
|
-
self._delete_in_chunks("profiles_fts", "profile_id", profile_ids)
|
|
833
|
-
if self._has_sqlite_vec:
|
|
834
|
-
rows = self._select_in_chunks(
|
|
835
|
-
"SELECT rowid FROM profiles WHERE profile_id IN ({placeholders})",
|
|
836
|
-
profile_ids,
|
|
837
|
-
)
|
|
838
|
-
rowids = [row["rowid"] for row in rows]
|
|
839
|
-
if rowids:
|
|
840
|
-
self._delete_in_chunks("profiles_vec", "rowid", rowids)
|
|
841
|
-
|
|
842
|
-
def _delete_playbook_search_rows(
|
|
843
|
-
self, kind: str, ids: list[int], *, commit: bool = True
|
|
844
|
-
) -> None:
|
|
845
|
-
"""Remove fts + vec index rows for the given playbook IDs.
|
|
846
|
-
|
|
847
|
-
Args:
|
|
848
|
-
kind: ``"user"`` or ``"agent"``.
|
|
849
|
-
ids: Playbook row IDs to remove from the search indexes.
|
|
850
|
-
commit: When ``True`` (default) commits after the deletes so the
|
|
851
|
-
after-commit callers in ``_playbook.py`` get a clean, durable
|
|
852
|
-
cleanup. Pass ``commit=False`` from inside the retention atomic
|
|
853
|
-
block so the deletes participate in the single block-level commit
|
|
854
|
-
(``_retention_perform_delete``).
|
|
855
|
-
|
|
856
|
-
Note: callers may already hold ``self._lock`` when calling this (the
|
|
857
|
-
``commit=False`` retention/atomic-delete call sites do). The internal
|
|
858
|
-
``with self._lock:`` re-acquire is safe ONLY because ``self._lock`` is a
|
|
859
|
-
reentrant ``threading.RLock``; a non-reentrant lock would deadlock here.
|
|
860
|
-
"""
|
|
861
|
-
if not ids:
|
|
862
|
-
return
|
|
863
|
-
with self._lock:
|
|
864
|
-
self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
|
|
865
|
-
if self._has_sqlite_vec:
|
|
866
|
-
self._delete_in_chunks(f"{kind}_playbooks_vec", "rowid", ids)
|
|
867
|
-
if commit:
|
|
868
|
-
self.conn.commit()
|
|
869
|
-
|
|
870
|
-
def _delete_source_windows_for_agent_playbook_ids(
|
|
871
|
-
self, agent_playbook_ids: list[int]
|
|
872
|
-
) -> None:
|
|
873
|
-
self._delete_in_chunks(
|
|
874
|
-
"agent_playbook_source_user_playbooks",
|
|
875
|
-
"agent_playbook_id",
|
|
876
|
-
agent_playbook_ids,
|
|
877
|
-
)
|
|
878
|
-
|
|
879
|
-
def _delete_source_windows_for_user_playbook_ids(
|
|
880
|
-
self, user_playbook_ids: list[int]
|
|
881
|
-
) -> None:
|
|
882
|
-
self._delete_in_chunks(
|
|
883
|
-
"agent_playbook_source_user_playbooks",
|
|
884
|
-
"user_playbook_id",
|
|
885
|
-
user_playbook_ids,
|
|
886
|
-
)
|
|
887
|
-
|
|
888
|
-
def _delete_optimizer_rows_for_job_ids(self, job_ids: list[int]) -> None:
|
|
889
|
-
if not job_ids:
|
|
890
|
-
return
|
|
891
|
-
for table in (
|
|
892
|
-
"playbook_optimization_evaluations",
|
|
893
|
-
"playbook_optimization_events",
|
|
894
|
-
"playbook_optimization_candidates",
|
|
895
|
-
):
|
|
896
|
-
self._delete_in_chunks(table, "job_id", job_ids)
|
|
897
|
-
|
|
898
|
-
def _delete_optimizer_evaluations_for_candidate_ids(
|
|
899
|
-
self, candidate_ids: list[int]
|
|
900
|
-
) -> None:
|
|
901
|
-
self._delete_in_chunks(
|
|
902
|
-
"playbook_optimization_evaluations",
|
|
903
|
-
"candidate_id",
|
|
904
|
-
candidate_ids,
|
|
905
|
-
)
|
|
906
|
-
|
|
907
765
|
def _try_load_sqlite_vec(self) -> bool:
|
|
908
766
|
"""Attempt to load the sqlite-vec extension for native KNN search.
|
|
909
767
|
|
|
@@ -1449,6 +1307,70 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
1449
1307
|
)
|
|
1450
1308
|
self.conn.commit()
|
|
1451
1309
|
|
|
1310
|
+
def _migrate_learning_jobs(self) -> None:
|
|
1311
|
+
"""Create the learning_jobs table + partial indexes if missing (idempotent).
|
|
1312
|
+
|
|
1313
|
+
Runs ``CREATE TABLE IF NOT EXISTS`` and ``CREATE INDEX IF NOT EXISTS`` only —
|
|
1314
|
+
both are no-ops when the table / indexes already exist. New columns added in
|
|
1315
|
+
subsequent tasks are backfilled via ``PRAGMA table_info`` + ``ALTER TABLE … ADD
|
|
1316
|
+
COLUMN`` (mirroring ``_migrate_lineage_event_table``), because
|
|
1317
|
+
``CREATE TABLE IF NOT EXISTS`` silently skips the DDL on existing databases.
|
|
1318
|
+
|
|
1319
|
+
Called at the end of migrate() so the table is always present on startup.
|
|
1320
|
+
"""
|
|
1321
|
+
with self._lock:
|
|
1322
|
+
self.conn.executescript("""
|
|
1323
|
+
CREATE TABLE IF NOT EXISTS learning_jobs (
|
|
1324
|
+
job_id TEXT PRIMARY KEY,
|
|
1325
|
+
org_id TEXT NOT NULL,
|
|
1326
|
+
user_id TEXT NOT NULL,
|
|
1327
|
+
job_type TEXT NOT NULL DEFAULT 'learning',
|
|
1328
|
+
latest_request_id TEXT,
|
|
1329
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1330
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1331
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
1332
|
+
claimed_by TEXT,
|
|
1333
|
+
claim_token TEXT,
|
|
1334
|
+
claim_expires_at TEXT,
|
|
1335
|
+
covers_through TEXT,
|
|
1336
|
+
force_extraction INTEGER NOT NULL DEFAULT 0,
|
|
1337
|
+
skip_aggregation INTEGER NOT NULL DEFAULT 0,
|
|
1338
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
1339
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
1340
|
+
);
|
|
1341
|
+
CREATE UNIQUE INDEX IF NOT EXISTS learning_jobs_coalesce
|
|
1342
|
+
ON learning_jobs (org_id, user_id, job_type) WHERE status = 'pending';
|
|
1343
|
+
-- Recreate the poll index with 'failed' in the predicate. CREATE
|
|
1344
|
+
-- INDEX IF NOT EXISTS is a no-op on an existing index with the old
|
|
1345
|
+
-- ('pending','claimed') predicate, so DROP first to migrate it.
|
|
1346
|
+
DROP INDEX IF EXISTS learning_jobs_poll;
|
|
1347
|
+
CREATE INDEX IF NOT EXISTS learning_jobs_poll
|
|
1348
|
+
ON learning_jobs (created_at) WHERE status IN ('pending','failed','claimed');
|
|
1349
|
+
""")
|
|
1350
|
+
# Backfill columns added after the initial release (existing DBs skip
|
|
1351
|
+
# CREATE TABLE IF NOT EXISTS so these must be applied separately).
|
|
1352
|
+
existing_cols = {
|
|
1353
|
+
row["name"]
|
|
1354
|
+
for row in self.conn.execute(
|
|
1355
|
+
"PRAGMA table_info(learning_jobs)"
|
|
1356
|
+
).fetchall()
|
|
1357
|
+
}
|
|
1358
|
+
if "force_extraction" not in existing_cols:
|
|
1359
|
+
self.conn.execute(
|
|
1360
|
+
"ALTER TABLE learning_jobs ADD COLUMN force_extraction INTEGER NOT NULL DEFAULT 0"
|
|
1361
|
+
)
|
|
1362
|
+
if "skip_aggregation" not in existing_cols:
|
|
1363
|
+
self.conn.execute(
|
|
1364
|
+
"ALTER TABLE learning_jobs ADD COLUMN skip_aggregation INTEGER NOT NULL DEFAULT 0"
|
|
1365
|
+
)
|
|
1366
|
+
self.conn.commit()
|
|
1367
|
+
|
|
1368
|
+
def learning_jobs_columns(self) -> list[str]:
|
|
1369
|
+
"""Return the column names of the learning_jobs table."""
|
|
1370
|
+
with self._lock:
|
|
1371
|
+
rows = self.conn.execute("PRAGMA table_info(learning_jobs)").fetchall()
|
|
1372
|
+
return [row["name"] for row in rows]
|
|
1373
|
+
|
|
1452
1374
|
def _migrate_agent_playbook_source_windows(self) -> None:
|
|
1453
1375
|
"""Add source window snapshots to existing agent source mappings."""
|
|
1454
1376
|
cols = {
|
|
@@ -1639,7 +1561,8 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
1639
1561
|
) -> sqlite3.Cursor:
|
|
1640
1562
|
with self._lock:
|
|
1641
1563
|
cur = self.conn.execute(sql, params)
|
|
1642
|
-
self.
|
|
1564
|
+
if self._own_transaction():
|
|
1565
|
+
self.conn.commit()
|
|
1643
1566
|
return cur
|
|
1644
1567
|
|
|
1645
1568
|
def _fetchone(
|
|
@@ -1717,113 +1640,6 @@ class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
|
1717
1640
|
def _current_timestamp(self) -> str:
|
|
1718
1641
|
return datetime.now(UTC).isoformat()
|
|
1719
1642
|
|
|
1720
|
-
# FTS helpers
|
|
1721
|
-
def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
|
|
1722
|
-
"""Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
|
|
1723
|
-
with self._lock:
|
|
1724
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1725
|
-
cols = list(text_fields.keys())
|
|
1726
|
-
vals = [text_fields[c] or "" for c in cols]
|
|
1727
|
-
placeholders = ",".join("?" for _ in cols)
|
|
1728
|
-
col_str = ",".join(cols)
|
|
1729
|
-
self.conn.execute(
|
|
1730
|
-
f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})",
|
|
1731
|
-
[rowid, *vals],
|
|
1732
|
-
)
|
|
1733
|
-
self.conn.commit()
|
|
1734
|
-
|
|
1735
|
-
def _fts_delete(self, table: str, rowid: int) -> None:
|
|
1736
|
-
with self._lock:
|
|
1737
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1738
|
-
self.conn.commit()
|
|
1739
|
-
|
|
1740
|
-
def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
|
|
1741
|
-
"""FTS for profiles uses profile_id TEXT as key column."""
|
|
1742
|
-
with self._lock:
|
|
1743
|
-
self.conn.execute(
|
|
1744
|
-
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1745
|
-
)
|
|
1746
|
-
self.conn.execute(
|
|
1747
|
-
"INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
|
|
1748
|
-
(profile_id, content),
|
|
1749
|
-
)
|
|
1750
|
-
self.conn.commit()
|
|
1751
|
-
|
|
1752
|
-
def _fts_delete_profile(self, profile_id: str) -> None:
|
|
1753
|
-
with self._lock:
|
|
1754
|
-
self.conn.execute(
|
|
1755
|
-
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1756
|
-
)
|
|
1757
|
-
self.conn.commit()
|
|
1758
|
-
|
|
1759
|
-
# Vec helpers (sqlite-vec)
|
|
1760
|
-
def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
1761
|
-
"""Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1762
|
-
if not self._has_sqlite_vec:
|
|
1763
|
-
return
|
|
1764
|
-
with self._lock:
|
|
1765
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1766
|
-
self.conn.execute(
|
|
1767
|
-
f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)",
|
|
1768
|
-
(rowid, json.dumps(embedding)),
|
|
1769
|
-
)
|
|
1770
|
-
self.conn.commit()
|
|
1771
|
-
|
|
1772
|
-
def _vec_delete(self, table: str, rowid: int) -> None:
|
|
1773
|
-
"""Delete a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1774
|
-
if not self._has_sqlite_vec:
|
|
1775
|
-
return
|
|
1776
|
-
with self._lock:
|
|
1777
|
-
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1778
|
-
self.conn.commit()
|
|
1779
|
-
|
|
1780
|
-
def _vec_knn_search(
|
|
1781
|
-
self,
|
|
1782
|
-
vec_table: str,
|
|
1783
|
-
main_table: str,
|
|
1784
|
-
query_embedding: list[float],
|
|
1785
|
-
match_count: int,
|
|
1786
|
-
conditions: list[str] | None = None,
|
|
1787
|
-
params: list[Any] | None = None,
|
|
1788
|
-
) -> list[sqlite3.Row]:
|
|
1789
|
-
"""Run a native KNN search via sqlite-vec and join back to the main table.
|
|
1790
|
-
|
|
1791
|
-
Over-fetches from the KNN index (5x ``match_count``) so that post-filter
|
|
1792
|
-
WHERE conditions (org, user, status, etc.) don't silently reduce the
|
|
1793
|
-
result set below the requested count.
|
|
1794
|
-
|
|
1795
|
-
Args:
|
|
1796
|
-
vec_table: Name of the vec0 virtual table.
|
|
1797
|
-
main_table: Name of the main data table.
|
|
1798
|
-
query_embedding: Query embedding vector.
|
|
1799
|
-
match_count: Number of results to return.
|
|
1800
|
-
conditions: Optional WHERE conditions for the main table.
|
|
1801
|
-
params: Parameters for the conditions.
|
|
1802
|
-
|
|
1803
|
-
Returns:
|
|
1804
|
-
Up to ``match_count`` rows from the main table, ordered by vector
|
|
1805
|
-
distance (ascending).
|
|
1806
|
-
"""
|
|
1807
|
-
knn_overfetch = match_count * 5
|
|
1808
|
-
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
1809
|
-
sql = f"""SELECT m.* FROM {main_table} m
|
|
1810
|
-
JOIN (
|
|
1811
|
-
SELECT rowid, distance FROM {vec_table}
|
|
1812
|
-
WHERE embedding MATCH ?
|
|
1813
|
-
ORDER BY distance
|
|
1814
|
-
LIMIT ?
|
|
1815
|
-
) v ON m.rowid = v.rowid
|
|
1816
|
-
WHERE {where_clause}
|
|
1817
|
-
ORDER BY v.distance
|
|
1818
|
-
LIMIT ?"""
|
|
1819
|
-
all_params = [
|
|
1820
|
-
json.dumps(query_embedding),
|
|
1821
|
-
knn_overfetch,
|
|
1822
|
-
*(params or []),
|
|
1823
|
-
match_count,
|
|
1824
|
-
]
|
|
1825
|
-
return self._fetchall(sql, all_params)
|
|
1826
|
-
|
|
1827
1643
|
# ------------------------------------------------------------------
|
|
1828
1644
|
# Per-user data clear
|
|
1829
1645
|
# ------------------------------------------------------------------
|
|
@@ -2389,4 +2205,31 @@ CREATE TABLE IF NOT EXISTS lineage_event (
|
|
|
2389
2205
|
);
|
|
2390
2206
|
CREATE INDEX IF NOT EXISTS idx_lineage_entity ON lineage_event (entity_type, entity_id);
|
|
2391
2207
|
|
|
2208
|
+
-- ============================================================================
|
|
2209
|
+
-- Durable learning pipeline — cross-org job queue
|
|
2210
|
+
-- ============================================================================
|
|
2211
|
+
|
|
2212
|
+
CREATE TABLE IF NOT EXISTS learning_jobs (
|
|
2213
|
+
job_id TEXT PRIMARY KEY,
|
|
2214
|
+
org_id TEXT NOT NULL,
|
|
2215
|
+
user_id TEXT NOT NULL,
|
|
2216
|
+
job_type TEXT NOT NULL DEFAULT 'learning',
|
|
2217
|
+
latest_request_id TEXT,
|
|
2218
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
2219
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
2220
|
+
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
2221
|
+
claimed_by TEXT,
|
|
2222
|
+
claim_token TEXT,
|
|
2223
|
+
claim_expires_at TEXT,
|
|
2224
|
+
covers_through TEXT,
|
|
2225
|
+
force_extraction INTEGER NOT NULL DEFAULT 0,
|
|
2226
|
+
skip_aggregation INTEGER NOT NULL DEFAULT 0,
|
|
2227
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
2228
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
2229
|
+
);
|
|
2230
|
+
CREATE UNIQUE INDEX IF NOT EXISTS learning_jobs_coalesce
|
|
2231
|
+
ON learning_jobs (org_id, user_id, job_type) WHERE status = 'pending';
|
|
2232
|
+
CREATE INDEX IF NOT EXISTS learning_jobs_poll
|
|
2233
|
+
ON learning_jobs (created_at) WHERE status IN ('pending','failed','claimed');
|
|
2234
|
+
|
|
2392
2235
|
"""
|