claude-smart 0.2.48 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +6 -2
- package/bin/claude-smart.js +289 -56
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/opencode/dist/server.mjs +60 -2
- package/plugin/opencode/server.mts +64 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +58 -6
- package/plugin/scripts/backend-service.sh +15 -10
- package/plugin/scripts/codex-hook.js +16 -4
- package/plugin/scripts/dashboard-service.sh +1 -1
- package/plugin/scripts/ensure-plugin-root.sh +106 -8
- package/plugin/scripts/opencode-claude-compat.js +8 -1
- package/plugin/scripts/smart-install.sh +101 -20
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +79 -29
- package/plugin/src/claude_smart/env_config.py +53 -11
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +15 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
- package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +150 -37
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +140 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +16 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +12 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +52 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +28 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +23 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +22 -0
package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""ABC, dataclass, and enum for the durable learning-job queue (Task 3)."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
# Upper bound of the done-row retention window.
|
|
9
|
+
# Shared by all backends so the value cannot drift between implementations.
|
|
10
|
+
# Err toward "not done" until we are sure a done row would have been GC'd.
|
|
11
|
+
_ABSENCE_DONE_AFTER_SECONDS = 72 * 3600
|
|
12
|
+
|
|
13
|
+
# Coverage-based status reported for a single request (§3.6). This is a
|
|
14
|
+
# collapsed view of the raw job statuses (e.g. claimed -> processing,
|
|
15
|
+
# reclaimable failed -> pending) and matches LearningStatusResponse.status.
|
|
16
|
+
LearningStatus = Literal["pending", "processing", "done", "failed"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LearningJobStatus(StrEnum):
|
|
20
|
+
PENDING = "pending"
|
|
21
|
+
CLAIMED = "claimed"
|
|
22
|
+
DONE = "done"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
DEAD = "dead"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class LearningJob:
|
|
29
|
+
job_id: str
|
|
30
|
+
org_id: str
|
|
31
|
+
user_id: str
|
|
32
|
+
job_type: str
|
|
33
|
+
latest_request_id: str | None
|
|
34
|
+
status: str
|
|
35
|
+
attempts: int
|
|
36
|
+
claim_token: str | None
|
|
37
|
+
covers_through: (
|
|
38
|
+
float | None
|
|
39
|
+
) # epoch seconds (converted from stored ISO/timestamptz)
|
|
40
|
+
force_extraction: bool = False
|
|
41
|
+
skip_aggregation: bool = False
|
|
42
|
+
max_attempts: int = 3
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LearningJobStoreABC(ABC):
|
|
46
|
+
"""Abstract interface for durable learning-job queue operations.
|
|
47
|
+
|
|
48
|
+
All methods are scoped to the storage instance's own org (``self.org_id``).
|
|
49
|
+
``enqueue_learning_job`` and ``complete_learning_job`` are safe to call
|
|
50
|
+
inside a ``commit_scope`` — they issue plain SQL on the scope's connection
|
|
51
|
+
without owning a separate BEGIN/COMMIT.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def enqueue_learning_job(
|
|
56
|
+
self,
|
|
57
|
+
*,
|
|
58
|
+
org_id: str,
|
|
59
|
+
user_id: str,
|
|
60
|
+
request_id: str,
|
|
61
|
+
covers_through: float,
|
|
62
|
+
job_type: str = "learning",
|
|
63
|
+
force_extraction: bool = False,
|
|
64
|
+
skip_aggregation: bool = False,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Coalescing upsert into the learning_jobs queue.
|
|
67
|
+
|
|
68
|
+
If a pending job for ``(org_id, user_id, job_type)`` already exists,
|
|
69
|
+
update its ``latest_request_id`` and keep the max ``covers_through``;
|
|
70
|
+
otherwise insert a new ``pending`` row. Returns the ``job_id`` of the
|
|
71
|
+
pending row (existing or newly inserted).
|
|
72
|
+
|
|
73
|
+
Safe to call inside a ``commit_scope`` — no own BEGIN/COMMIT issued.
|
|
74
|
+
"""
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def claim_learning_jobs(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
claimed_by: str,
|
|
82
|
+
limit: int,
|
|
83
|
+
lease_seconds: int,
|
|
84
|
+
) -> list[LearningJob]:
|
|
85
|
+
"""Atomically claim up to ``limit`` of this org's claimable jobs.
|
|
86
|
+
|
|
87
|
+
A job is claimable when it is ``pending``, ``failed`` (reclaimable —
|
|
88
|
+
attempts < max_attempts), OR (``claimed`` AND
|
|
89
|
+
``claim_expires_at < now``). Each claimed job receives a fresh
|
|
90
|
+
``claim_token``, ``claimed_by``, and ``claim_expires_at``.
|
|
91
|
+
|
|
92
|
+
Postgres/Supabase: uses ``FOR UPDATE SKIP LOCKED`` via the
|
|
93
|
+
``claim_learning_jobs`` SQL function. SQLite: ``BEGIN IMMEDIATE``.
|
|
94
|
+
The claim predicate uses the DB's ``now()`` to avoid app/DB clock skew.
|
|
95
|
+
"""
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def heartbeat_learning_job(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
job_id: str,
|
|
103
|
+
claim_token: str,
|
|
104
|
+
lease_seconds: int,
|
|
105
|
+
) -> bool:
|
|
106
|
+
"""Extend the lease on a claimed job.
|
|
107
|
+
|
|
108
|
+
Updates ``claim_expires_at = now + lease_seconds`` only when
|
|
109
|
+
``claim_token`` matches and status is still ``claimed``.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
True if the lease was extended (rowcount == 1), False if the token
|
|
113
|
+
was superseded or the job is no longer claimed.
|
|
114
|
+
"""
|
|
115
|
+
raise NotImplementedError
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def complete_learning_job(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
job_id: str,
|
|
122
|
+
claim_token: str,
|
|
123
|
+
) -> int:
|
|
124
|
+
"""Fenced transition to ``done``.
|
|
125
|
+
|
|
126
|
+
Executes::
|
|
127
|
+
|
|
128
|
+
UPDATE learning_jobs
|
|
129
|
+
SET status='done', updated_at=now()
|
|
130
|
+
WHERE job_id=:job_id AND claim_token=:claim_token AND status='claimed'
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
rowcount — 0 if the token was superseded or the job is already
|
|
134
|
+
done/dead/failed; 1 on success. Callers MUST raise on 0 to roll
|
|
135
|
+
back the enclosing ``commit_scope``.
|
|
136
|
+
|
|
137
|
+
Safe to call inside a ``commit_scope`` — no own BEGIN/COMMIT issued.
|
|
138
|
+
"""
|
|
139
|
+
raise NotImplementedError
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def fail_learning_job(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
job_id: str,
|
|
146
|
+
claim_token: str,
|
|
147
|
+
dead: bool,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Fenced fail/dead transition — sets status, does NOT increment attempts.
|
|
150
|
+
|
|
151
|
+
Fenced on ``claim_token`` and ``status='claimed'``. Sets
|
|
152
|
+
``status='dead'`` when ``dead=True``, else ``status='failed'`` (and
|
|
153
|
+
clears ``claim_token``/``claim_expires_at`` so the job is reclaimable).
|
|
154
|
+
|
|
155
|
+
``attempts`` is intentionally NOT incremented here. It is incremented
|
|
156
|
+
once per delivery attempt exclusively by ``claim_learning_jobs``; the
|
|
157
|
+
worker's dead-gate (``job.attempts >= max_attempts``) depends on that
|
|
158
|
+
single-increment-per-claim invariant. Incrementing here would
|
|
159
|
+
double-count and misfire the dead transition.
|
|
160
|
+
"""
|
|
161
|
+
raise NotImplementedError
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def list_org_ids_with_pending_learning_jobs(self) -> list[str]:
|
|
165
|
+
"""Return distinct org_ids that have actionable learning jobs.
|
|
166
|
+
|
|
167
|
+
Cross-org discovery query for the :class:`DurableLearningScheduler`:
|
|
168
|
+
surfaces every org with at least one job that is ``pending``, ``failed``
|
|
169
|
+
(reclaimable), or ``claimed`` with an expired lease (a stolen/abandoned
|
|
170
|
+
claim). Terminal ``done``/``dead`` rows and live claims are excluded.
|
|
171
|
+
|
|
172
|
+
Mirrors ``list_resumable_work_org_ids``: NOT scoped to ``self.org_id`` —
|
|
173
|
+
on the shared global table (Postgres ``public.learning_jobs``, or a
|
|
174
|
+
SQLite DB shared across orgs) it returns every org with work in one
|
|
175
|
+
query so the scheduler need not enumerate all orgs.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list[str]: Distinct org_ids with actionable work, ordered ascending.
|
|
179
|
+
"""
|
|
180
|
+
raise NotImplementedError
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def get_learning_status_for_request(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
user_id: str,
|
|
187
|
+
request_created_at: float,
|
|
188
|
+
) -> LearningStatus:
|
|
189
|
+
"""Coverage-based status for a single request (§3.6 rule).
|
|
190
|
+
|
|
191
|
+
Returns one of ``"done" | "processing" | "pending" | "failed"``.
|
|
192
|
+
|
|
193
|
+
Coverage rule (evaluated in priority order):
|
|
194
|
+
|
|
195
|
+
1. Any ``done`` job with ``covers_through >= request_created_at``
|
|
196
|
+
→ ``"done"`` (correct even for zero-yield windows).
|
|
197
|
+
2. Any ``claimed`` job (any ``covers_through``)
|
|
198
|
+
→ ``"processing"``.
|
|
199
|
+
3. Any ``pending`` job for this user
|
|
200
|
+
→ ``"pending"``.
|
|
201
|
+
4. Any ``failed`` (reclaimable) job
|
|
202
|
+
→ ``"pending"``.
|
|
203
|
+
5. Any ``dead`` job with ``covers_through >= request_created_at``
|
|
204
|
+
→ ``"failed"``.
|
|
205
|
+
6. No rows AND ``now - request_created_at >= 72 h`` (retention window)
|
|
206
|
+
→ ``"done"`` (done row would have been GC'd by now).
|
|
207
|
+
7. No rows AND request is recent
|
|
208
|
+
→ ``"pending"`` (err toward not-done until retention window passes).
|
|
209
|
+
"""
|
|
210
|
+
raise NotImplementedError
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"_ABSENCE_DONE_AFTER_SECONDS",
|
|
215
|
+
"LearningJob",
|
|
216
|
+
"LearningJobStatus",
|
|
217
|
+
"LearningStatus",
|
|
218
|
+
"LearningJobStoreABC",
|
|
219
|
+
]
|
|
@@ -28,16 +28,38 @@ class InteractionStoreMixin:
|
|
|
28
28
|
|
|
29
29
|
@abstractmethod
|
|
30
30
|
def add_user_interactions_bulk(
|
|
31
|
-
self,
|
|
31
|
+
self,
|
|
32
|
+
user_id: str,
|
|
33
|
+
interactions: list[Interaction],
|
|
34
|
+
*,
|
|
35
|
+
embeddings_prepared: bool = False,
|
|
32
36
|
) -> None:
|
|
33
37
|
"""Add multiple user interactions with batched embedding generation.
|
|
34
38
|
|
|
35
39
|
Args:
|
|
36
40
|
user_id: The user ID
|
|
37
41
|
interactions: List of interactions to add
|
|
42
|
+
embeddings_prepared: When True, skip all embedding generation and write
|
|
43
|
+
whatever embedding is already on each interaction (real or ``[]``).
|
|
44
|
+
Use on the durable path after ``prepare_interaction_embeddings`` to
|
|
45
|
+
ensure no network I/O occurs inside an open ``commit_scope``.
|
|
38
46
|
"""
|
|
39
47
|
raise NotImplementedError
|
|
40
48
|
|
|
49
|
+
def prepare_interaction_embeddings(self, interactions: list[Interaction]) -> None: # noqa: ARG002
|
|
50
|
+
"""Pre-populate interaction.embedding for each interaction without writing to storage.
|
|
51
|
+
|
|
52
|
+
Call this before opening a commit_scope to avoid a network round-trip inside
|
|
53
|
+
the transaction. Subclasses that generate embeddings client-side MUST override
|
|
54
|
+
this to call get_embeddings and populate interaction.embedding before the scope
|
|
55
|
+
is opened; backends where the embedding is generated server-side can leave this
|
|
56
|
+
as a no-op.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
interactions: Interactions whose embedding fields will be populated in-place.
|
|
60
|
+
"""
|
|
61
|
+
return
|
|
62
|
+
|
|
41
63
|
@abstractmethod
|
|
42
64
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
43
65
|
raise NotImplementedError
|
|
@@ -110,6 +110,28 @@ class ProfileStoreMixin:
|
|
|
110
110
|
"""
|
|
111
111
|
raise NotImplementedError
|
|
112
112
|
|
|
113
|
+
def count_user_profiles_by_status(
|
|
114
|
+
self, user_ids: list[str], status: Status | None
|
|
115
|
+
) -> int:
|
|
116
|
+
"""Count non-expired profiles for concrete users with the given status.
|
|
117
|
+
|
|
118
|
+
Storage backends may override this with a single query. The default keeps
|
|
119
|
+
the storage contract backward-compatible for out-of-tree backends.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
user_ids: Concrete user IDs to include. Empty list returns 0.
|
|
123
|
+
status: Profile status to match; None means CURRENT.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
int: Number of matching profiles
|
|
127
|
+
"""
|
|
128
|
+
if not user_ids:
|
|
129
|
+
return 0
|
|
130
|
+
return sum(
|
|
131
|
+
len(self.get_user_profile(user_id=user_id, status_filter=[status]))
|
|
132
|
+
for user_id in user_ids
|
|
133
|
+
)
|
|
134
|
+
|
|
113
135
|
@abstractmethod
|
|
114
136
|
def update_all_profiles_status(
|
|
115
137
|
self,
|