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
|
@@ -1,378 +1,72 @@
|
|
|
1
|
-
"""Shared storage types and helpers for resumable extraction agent runs.
|
|
1
|
+
"""Shared storage types and helpers for resumable extraction agent runs.
|
|
2
|
+
|
|
3
|
+
Residual holder for the ``_agent_run.py`` decomposition. The 18 shared
|
|
4
|
+
non-class symbols live in the leaf ``agent_run/_models.py`` and are re-exported
|
|
5
|
+
here so ``storage_base/__init__.py`` keeps importing them from ``._agent_run``
|
|
6
|
+
byte-identically. ``AgentRunMixin`` survives as a thin COMPOSITE ABC:
|
|
7
|
+
``AgentRunStoreABC`` supplies the agent-run lifecycle stubs,
|
|
8
|
+
``PendingToolCallStoreABC`` supplies the pending-tool-call stubs,
|
|
9
|
+
``RunToolDependencyStoreABC`` supplies the run-tool-dependency stubs, and only
|
|
10
|
+
the public helper staticmethods stay inline here. All twenty-three agent-run
|
|
11
|
+
methods now live in the three ``agent_run`` sub-mixins.
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
from __future__ import annotations
|
|
4
15
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class AgentRunRecord:
|
|
58
|
-
id: str
|
|
59
|
-
binding: AgentBinding
|
|
60
|
-
status: AgentRunStatus
|
|
61
|
-
generation_request_snapshot: dict[str, Any]
|
|
62
|
-
service_config_snapshot: dict[str, Any] | None = None
|
|
63
|
-
agent_context_snapshot: str | None = None
|
|
64
|
-
committed_output: dict[str, Any] | None = None
|
|
65
|
-
pending_tool_call_ids: list[str] = field(default_factory=list)
|
|
66
|
-
max_steps_remaining: int | None = None
|
|
67
|
-
resume_attempts: int = 0
|
|
68
|
-
finalization_attempts: int = 0
|
|
69
|
-
next_resume_at: datetime | None = None
|
|
70
|
-
claimed_by: str | None = None
|
|
71
|
-
claimed_at: datetime | None = None
|
|
72
|
-
agent_completed_at: datetime | None = None
|
|
73
|
-
finalized_at: datetime | None = None
|
|
74
|
-
created_at: datetime | None = None
|
|
75
|
-
updated_at: datetime | None = None
|
|
76
|
-
expires_at: datetime | None = None
|
|
77
|
-
last_error: str | None = None
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@dataclass(frozen=True)
|
|
81
|
-
class PendingToolCallRecord:
|
|
82
|
-
id: str
|
|
83
|
-
org_id: str
|
|
84
|
-
scope: dict[str, Any]
|
|
85
|
-
scope_hash: str
|
|
86
|
-
tool_name: str
|
|
87
|
-
dedup_key: str
|
|
88
|
-
status: PendingToolCallStatus
|
|
89
|
-
question_text: str
|
|
90
|
-
args: dict[str, Any] = field(default_factory=dict)
|
|
91
|
-
tags: list[str] = field(default_factory=list)
|
|
92
|
-
user_id: str | None = None
|
|
93
|
-
answer_format: str | None = None
|
|
94
|
-
result: dict[str, Any] | None = None
|
|
95
|
-
embedding: list[float] | None = None
|
|
96
|
-
superseded_by: str | None = None
|
|
97
|
-
created_at: datetime | None = None
|
|
98
|
-
resolved_at: datetime | None = None
|
|
99
|
-
expires_at: datetime | None = None
|
|
100
|
-
cache_until: datetime | None = None
|
|
101
|
-
valid_until: datetime | None = None
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@dataclass(frozen=True)
|
|
105
|
-
class RunToolDependencyRecord:
|
|
106
|
-
run_id: str
|
|
107
|
-
pending_tool_call_id: str
|
|
108
|
-
dependency_kind: RunToolDependencyKind = RunToolDependencyKind.FOLLOWUP
|
|
109
|
-
resolved_at: datetime | None = None
|
|
110
|
-
consumed_at: datetime | None = None
|
|
111
|
-
created_at: datetime | None = None
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@dataclass(frozen=True)
|
|
115
|
-
class PendingToolCallUpsertResult:
|
|
116
|
-
pending_tool_call: PendingToolCallRecord
|
|
117
|
-
created: bool
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@dataclass(frozen=True)
|
|
121
|
-
class PriorAnswerMatch:
|
|
122
|
-
pending_tool_call_id: str
|
|
123
|
-
status: PendingToolCallStatus
|
|
124
|
-
question_text: str
|
|
125
|
-
result: dict[str, Any] | None
|
|
126
|
-
valid_until: datetime | None
|
|
127
|
-
answer_format: str | None = None
|
|
128
|
-
created_at: datetime | None = None
|
|
129
|
-
resolved_at: datetime | None = None
|
|
130
|
-
expires_at: datetime | None = None
|
|
131
|
-
similarity: float | None = None
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def canonical_json(value: Any) -> str:
|
|
135
|
-
"""Return deterministic compact JSON for storage hashes."""
|
|
136
|
-
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def build_scope_hash(scope: dict[str, Any]) -> str:
|
|
140
|
-
"""Stable hash for a tool scope dictionary."""
|
|
141
|
-
return hashlib.sha256(canonical_json(scope).encode("utf-8")).hexdigest()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def human_feedback_scope(org_id: str) -> dict[str, str]:
|
|
145
|
-
"""Human feedback is always org-scoped, never user-scoped."""
|
|
146
|
-
return {"org_id": org_id, "scope_kind": "org"}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def normalize_dedup_text(value: str | None) -> str:
|
|
150
|
-
"""Normalize text before pending-tool-call dedup hashing."""
|
|
151
|
-
if value is None:
|
|
152
|
-
return ""
|
|
153
|
-
normalized = unicodedata.normalize("NFKC", value)
|
|
154
|
-
normalized = _WHITESPACE_RE.sub(" ", normalized.strip())
|
|
155
|
-
return normalized.casefold()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def build_pending_tool_call_dedup_key(
|
|
159
|
-
*,
|
|
160
|
-
tool_name: str,
|
|
161
|
-
question_text: str,
|
|
162
|
-
answer_format: str | None = None,
|
|
163
|
-
) -> str:
|
|
164
|
-
"""Stable dedup hash for a normalized tool question."""
|
|
165
|
-
parts = (
|
|
166
|
-
normalize_dedup_text(tool_name),
|
|
167
|
-
normalize_dedup_text(question_text),
|
|
168
|
-
normalize_dedup_text(answer_format),
|
|
169
|
-
)
|
|
170
|
-
return hashlib.sha256("\n".join(parts).encode("utf-8")).hexdigest()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def not_applicable_tool_result() -> dict[str, Any]:
|
|
174
|
-
return {"answer": NOT_APPLICABLE_ANSWER, "not_applicable": True}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def is_not_applicable_tool_result(result: dict[str, Any] | None) -> bool:
|
|
178
|
-
return isinstance(result, dict) and result.get("not_applicable") is True
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def embedding_similarity(a: list[float] | None, b: list[float] | None) -> float | None:
|
|
182
|
-
"""Cosine similarity for optional embedding vectors."""
|
|
183
|
-
if not a or not b or len(a) != len(b):
|
|
184
|
-
return None
|
|
185
|
-
dot = sum(x * y for x, y in zip(a, b, strict=True))
|
|
186
|
-
mag_a_sq = sum(x * x for x in a)
|
|
187
|
-
mag_b_sq = sum(y * y for y in b)
|
|
188
|
-
if mag_a_sq == 0.0 or mag_b_sq == 0.0:
|
|
189
|
-
return None
|
|
190
|
-
return dot / ((mag_a_sq**0.5) * (mag_b_sq**0.5))
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
class AgentRunMixin:
|
|
16
|
+
from .agent_run import (
|
|
17
|
+
AgentRunStoreABC,
|
|
18
|
+
PendingToolCallStoreABC,
|
|
19
|
+
RunToolDependencyStoreABC,
|
|
20
|
+
)
|
|
21
|
+
from .agent_run._models import (
|
|
22
|
+
NOT_APPLICABLE_ANSWER,
|
|
23
|
+
AgentBinding,
|
|
24
|
+
AgentRunRecord,
|
|
25
|
+
AgentRunStatus,
|
|
26
|
+
PendingToolCallRecord,
|
|
27
|
+
PendingToolCallStatus,
|
|
28
|
+
PendingToolCallUpsertResult,
|
|
29
|
+
PriorAnswerMatch,
|
|
30
|
+
RunToolDependencyKind,
|
|
31
|
+
RunToolDependencyRecord,
|
|
32
|
+
build_pending_tool_call_dedup_key,
|
|
33
|
+
build_scope_hash,
|
|
34
|
+
canonical_json,
|
|
35
|
+
embedding_similarity,
|
|
36
|
+
human_feedback_scope,
|
|
37
|
+
is_not_applicable_tool_result,
|
|
38
|
+
normalize_dedup_text,
|
|
39
|
+
not_applicable_tool_result,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"NOT_APPLICABLE_ANSWER",
|
|
44
|
+
"AgentBinding",
|
|
45
|
+
"AgentRunMixin",
|
|
46
|
+
"AgentRunRecord",
|
|
47
|
+
"AgentRunStatus",
|
|
48
|
+
"PendingToolCallRecord",
|
|
49
|
+
"PendingToolCallStatus",
|
|
50
|
+
"PendingToolCallUpsertResult",
|
|
51
|
+
"PriorAnswerMatch",
|
|
52
|
+
"RunToolDependencyKind",
|
|
53
|
+
"RunToolDependencyRecord",
|
|
54
|
+
"build_pending_tool_call_dedup_key",
|
|
55
|
+
"build_scope_hash",
|
|
56
|
+
"canonical_json",
|
|
57
|
+
"embedding_similarity",
|
|
58
|
+
"human_feedback_scope",
|
|
59
|
+
"is_not_applicable_tool_result",
|
|
60
|
+
"normalize_dedup_text",
|
|
61
|
+
"not_applicable_tool_result",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AgentRunMixin(
|
|
66
|
+
AgentRunStoreABC, PendingToolCallStoreABC, RunToolDependencyStoreABC
|
|
67
|
+
):
|
|
194
68
|
"""Backend-neutral helpers shared by resumable extraction storage backends."""
|
|
195
69
|
|
|
196
70
|
build_scope_hash = staticmethod(build_scope_hash)
|
|
197
71
|
human_feedback_scope = staticmethod(human_feedback_scope)
|
|
198
72
|
build_pending_tool_call_dedup_key = staticmethod(build_pending_tool_call_dedup_key)
|
|
199
|
-
|
|
200
|
-
def create_agent_run(self, record: AgentRunRecord) -> AgentRunRecord:
|
|
201
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
202
|
-
|
|
203
|
-
def get_agent_run(self, run_id: str) -> AgentRunRecord | None:
|
|
204
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
205
|
-
|
|
206
|
-
def update_agent_run_status(
|
|
207
|
-
self,
|
|
208
|
-
run_id: str,
|
|
209
|
-
status: AgentRunStatus,
|
|
210
|
-
*,
|
|
211
|
-
committed_output: dict[str, Any] | None = None,
|
|
212
|
-
pending_tool_call_ids: list[str] | None = None,
|
|
213
|
-
max_steps_remaining: int | None = None,
|
|
214
|
-
next_resume_at: datetime | None = None,
|
|
215
|
-
last_error: str | None = None,
|
|
216
|
-
increment_finalization_attempts: bool = False,
|
|
217
|
-
expected_statuses: tuple[AgentRunStatus, ...] | None = None,
|
|
218
|
-
) -> AgentRunRecord | None:
|
|
219
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
220
|
-
|
|
221
|
-
def fail_running_agent_runs_for_request(
|
|
222
|
-
self,
|
|
223
|
-
*,
|
|
224
|
-
org_id: str,
|
|
225
|
-
extractor_kind: str,
|
|
226
|
-
user_id: str | None,
|
|
227
|
-
request_id: str,
|
|
228
|
-
last_error: str,
|
|
229
|
-
) -> int:
|
|
230
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
231
|
-
|
|
232
|
-
def create_pending_tool_call(
|
|
233
|
-
self, record: PendingToolCallRecord
|
|
234
|
-
) -> PendingToolCallRecord:
|
|
235
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
236
|
-
|
|
237
|
-
def create_or_attach_pending_tool_call(
|
|
238
|
-
self,
|
|
239
|
-
*,
|
|
240
|
-
record: PendingToolCallRecord,
|
|
241
|
-
dependency: RunToolDependencyRecord,
|
|
242
|
-
now: datetime | None = None,
|
|
243
|
-
) -> PendingToolCallUpsertResult:
|
|
244
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
245
|
-
|
|
246
|
-
def get_pending_tool_call(self, call_id: str) -> PendingToolCallRecord | None:
|
|
247
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
248
|
-
|
|
249
|
-
def list_pending_tool_calls(
|
|
250
|
-
self,
|
|
251
|
-
*,
|
|
252
|
-
status: PendingToolCallStatus | None = None,
|
|
253
|
-
limit: int = 100,
|
|
254
|
-
) -> list[PendingToolCallRecord]:
|
|
255
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
256
|
-
|
|
257
|
-
def cancel_pending_tool_call(
|
|
258
|
-
self,
|
|
259
|
-
call_id: str,
|
|
260
|
-
*,
|
|
261
|
-
cancelled_at: datetime | None = None,
|
|
262
|
-
) -> PendingToolCallRecord | None:
|
|
263
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
264
|
-
|
|
265
|
-
def expire_pending_tool_calls(
|
|
266
|
-
self,
|
|
267
|
-
*,
|
|
268
|
-
now: datetime | None = None,
|
|
269
|
-
limit: int = 100,
|
|
270
|
-
) -> int:
|
|
271
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
272
|
-
|
|
273
|
-
def find_active_pending_tool_call(
|
|
274
|
-
self,
|
|
275
|
-
*,
|
|
276
|
-
org_id: str,
|
|
277
|
-
scope_hash: str,
|
|
278
|
-
tool_name: str,
|
|
279
|
-
dedup_key: str,
|
|
280
|
-
now: datetime | None = None,
|
|
281
|
-
) -> PendingToolCallRecord | None:
|
|
282
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
283
|
-
|
|
284
|
-
def search_prior_tool_calls(
|
|
285
|
-
self,
|
|
286
|
-
*,
|
|
287
|
-
org_id: str,
|
|
288
|
-
scope_hash: str,
|
|
289
|
-
tool_name: str,
|
|
290
|
-
query_embedding: list[float] | None = None,
|
|
291
|
-
now: datetime | None = None,
|
|
292
|
-
limit: int = 8,
|
|
293
|
-
) -> list[PriorAnswerMatch]:
|
|
294
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
295
|
-
|
|
296
|
-
def attach_run_tool_dependency(
|
|
297
|
-
self, record: RunToolDependencyRecord
|
|
298
|
-
) -> RunToolDependencyRecord:
|
|
299
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
300
|
-
|
|
301
|
-
def count_unresolved_followup_dependencies(
|
|
302
|
-
self,
|
|
303
|
-
*,
|
|
304
|
-
org_id: str,
|
|
305
|
-
extractor_kind: str,
|
|
306
|
-
tool_name: str,
|
|
307
|
-
) -> int:
|
|
308
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
309
|
-
|
|
310
|
-
def list_run_tool_dependencies(self, run_id: str) -> list[RunToolDependencyRecord]:
|
|
311
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
312
|
-
|
|
313
|
-
def resolve_pending_tool_call(
|
|
314
|
-
self,
|
|
315
|
-
call_id: str,
|
|
316
|
-
*,
|
|
317
|
-
result: dict[str, Any],
|
|
318
|
-
resolved_at: datetime | None = None,
|
|
319
|
-
valid_for_seconds: int,
|
|
320
|
-
) -> PendingToolCallRecord | None:
|
|
321
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
322
|
-
|
|
323
|
-
def update_resolved_pending_tool_call_result(
|
|
324
|
-
self,
|
|
325
|
-
call_id: str,
|
|
326
|
-
*,
|
|
327
|
-
result: dict[str, Any],
|
|
328
|
-
resolved_at: datetime | None = None,
|
|
329
|
-
valid_for_seconds: int,
|
|
330
|
-
) -> PendingToolCallRecord | None:
|
|
331
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
332
|
-
|
|
333
|
-
def mark_pending_tool_call_not_applicable(
|
|
334
|
-
self,
|
|
335
|
-
call_id: str,
|
|
336
|
-
*,
|
|
337
|
-
resolved_at: datetime | None = None,
|
|
338
|
-
valid_for_seconds: int,
|
|
339
|
-
) -> PendingToolCallRecord | None:
|
|
340
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
341
|
-
|
|
342
|
-
def claim_ready_agent_run(
|
|
343
|
-
self,
|
|
344
|
-
*,
|
|
345
|
-
org_id: str,
|
|
346
|
-
worker_id: str,
|
|
347
|
-
now: datetime | None = None,
|
|
348
|
-
claim_ttl_seconds: int = 600,
|
|
349
|
-
) -> AgentRunRecord | None:
|
|
350
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
351
|
-
|
|
352
|
-
def claim_finalization_failed_agent_run(
|
|
353
|
-
self,
|
|
354
|
-
*,
|
|
355
|
-
org_id: str,
|
|
356
|
-
worker_id: str,
|
|
357
|
-
now: datetime | None = None,
|
|
358
|
-
claim_ttl_seconds: int = 600,
|
|
359
|
-
) -> AgentRunRecord | None:
|
|
360
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
361
|
-
|
|
362
|
-
def list_resumable_work_org_ids(
|
|
363
|
-
self,
|
|
364
|
-
*,
|
|
365
|
-
now: datetime | None = None,
|
|
366
|
-
limit: int = 1000,
|
|
367
|
-
) -> list[str]:
|
|
368
|
-
"""Return distinct org_ids that have actionable resumable-extraction work.
|
|
369
|
-
|
|
370
|
-
Cross-org maintenance query (intentionally NOT scoped to ``self.org_id``):
|
|
371
|
-
the resume scheduler uses it to discover every org that has a run ready
|
|
372
|
-
to resume, a run awaiting finalization retry, or a pending tool call that
|
|
373
|
-
can be expired, so per-org workers can be driven for all of them.
|
|
374
|
-
"""
|
|
375
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
376
|
-
|
|
377
|
-
def consume_run_tool_dependencies(self, run_id: str) -> int:
|
|
378
|
-
raise NotImplementedError(f"{type(self).__name__} does not support agent runs")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import AbstractContextManager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CommitScopeMixin(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def commit_scope(self) -> AbstractContextManager[None]:
|
|
8
|
+
"""Atomic multi-write transaction; see Workstream A plan Task 1."""
|
|
9
|
+
raise NotImplementedError
|
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
|
+
]
|
package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py
CHANGED
|
@@ -91,3 +91,23 @@ class ShareLinkMixin:
|
|
|
91
91
|
int: Number of links deleted.
|
|
92
92
|
"""
|
|
93
93
|
raise NotImplementedError
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def delete_expired_share_links(
|
|
97
|
+
self, *, now: int, grace_seconds: int, limit: int = 1000
|
|
98
|
+
) -> int:
|
|
99
|
+
"""Physically delete share links whose expires_at < now - grace_seconds.
|
|
100
|
+
|
|
101
|
+
Rows where expires_at IS NULL (never expire) are always preserved.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
now (int): Current Unix epoch timestamp.
|
|
105
|
+
grace_seconds (int): Additional grace window; only rows with
|
|
106
|
+
expires_at < (now - grace_seconds) are deleted.
|
|
107
|
+
limit (int): Maximum number of rows to delete in one call.
|
|
108
|
+
Rows are processed in expires_at ASC order (oldest first).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
int: Number of rows physically deleted.
|
|
112
|
+
"""
|
|
113
|
+
raise NotImplementedError
|