arkaos 4.13.2 → 4.14.1
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/VERSION +1 -1
- package/arka/SKILL.md +16 -0
- package/bin/arka-doctor +20 -1
- package/config/cognition/schedules.yaml +16 -0
- package/config/hooks/_lib/fastpath/engine.cjs +337 -0
- package/config/hooks/gate-manifest.json +741 -0
- package/config/hooks/post-tool-use.cjs +113 -0
- package/config/hooks/pre-tool-use.cjs +109 -0
- package/config/hooks/session-end.ps1 +40 -0
- package/config/hooks/session-end.sh +55 -0
- package/config/hooks/session-start.sh +25 -189
- package/config/hooks/subagent-stop.ps1 +40 -0
- package/config/hooks/subagent-stop.sh +56 -0
- package/config/mcp-policy.yaml +8 -0
- package/config/skills-curated.yaml +39 -0
- package/config/statusline.sh +57 -1
- package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
- package/core/cognition/capture/store.py +7 -2
- package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
- package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
- package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
- package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
- package/core/governance/evidence_checks.py +42 -7
- package/core/governance/routing_feedback.py +229 -0
- package/core/governance/routing_feedback_cli.py +49 -0
- package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/gate_manifest.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
- package/core/hooks/gate_manifest.py +366 -0
- package/core/hooks/post_tool_use.py +5 -2
- package/core/hooks/session_end.py +127 -0
- package/core/hooks/session_start.py +322 -0
- package/core/hooks/stop.py +69 -0
- package/core/hooks/subagent_stop.py +186 -0
- package/core/hooks/user_prompt_submit.py +10 -6
- package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
- package/core/knowledge/embedding_backends.py +278 -0
- package/core/knowledge/pattern_cards.py +49 -1
- package/core/knowledge/vector_store.py +14 -2
- package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
- package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
- package/core/memory/semantic_store.py +301 -0
- package/core/memory/turn_capture.py +277 -0
- package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
- package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
- package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
- package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
- package/core/shared/decay.py +94 -0
- package/core/shared/sqlite_recovery.py +266 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
- package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
- package/core/synapse/agent_experiences_layer.py +42 -3
- package/core/synapse/engine.py +13 -0
- package/core/synapse/recipe_layer.py +27 -4
- package/core/synapse/routing_feedback_layer.py +132 -0
- package/core/synapse/session_memory_layer.py +152 -0
- package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
- package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
- package/installer/adapters/claude-code.js +37 -0
- package/installer/claude-plugins.js +6 -0
- package/installer/cli.js +23 -2
- package/installer/config-seed.js +25 -16
- package/installer/doctor.js +116 -1
- package/installer/hook-lib.js +28 -0
- package/installer/index.js +48 -153
- package/installer/mcp-runner.js +108 -0
- package/installer/skill-deploy.js +189 -0
- package/installer/skills-mode.js +64 -0
- package/installer/update.js +55 -79
- package/knowledge/commands-registry.json.bak +171 -4
- package/knowledge/skills-manifest.json +2337 -0
- package/mcps/arka-prompts/commands.py +451 -0
- package/mcps/arka-prompts/pyproject.toml +15 -0
- package/mcps/arka-prompts/server.py +156 -0
- package/mcps/arka-tools/__pycache__/server.cpython-313.pyc +0 -0
- package/mcps/arka-tools/pyproject.toml +10 -0
- package/mcps/arka-tools/server.py +319 -0
- package/mcps/profiles/base.json +19 -0
- package/mcps/profiles/brand.json +19 -0
- package/mcps/profiles/comms.json +13 -0
- package/mcps/profiles/content.json +20 -0
- package/mcps/profiles/ecommerce.json +13 -0
- package/mcps/profiles/full-stack.json +15 -0
- package/mcps/profiles/laravel.json +12 -0
- package/mcps/profiles/nextjs.json +13 -0
- package/mcps/profiles/nuxt.json +13 -0
- package/mcps/profiles/react.json +12 -0
- package/mcps/profiles/vue.json +12 -0
- package/mcps/registry.json +423 -0
- package/mcps/scripts/apply-mcps.sh +282 -0
- package/mcps/stacks/laravel-packages.json +49 -0
- package/mcps/stacks/react-packages.json +58 -0
- package/package.json +2 -1
- package/pyproject.toml +9 -1
- package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
- package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
- package/scripts/marketplace_gen.py +310 -0
- package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
- package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
- package/scripts/tools/docs_stats.py +6 -4
- package/scripts/tools/skill_budget.py +209 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Multi-backend embedding abstraction (F1-A1 — memory/learning reform).
|
|
2
|
+
|
|
3
|
+
Ordered auto-resolution, never a hardcoded single backend:
|
|
4
|
+
1. ``fastembed`` — local, delegates to the existing embedder.py
|
|
5
|
+
2. ``ollama`` — local ``/api/embed`` endpoint (stdlib urllib)
|
|
6
|
+
3. ``none`` — no embedding available; consumers must label
|
|
7
|
+
retrieval as ``keyword-degraded`` (the exact
|
|
8
|
+
honesty contract of vector_store.py)
|
|
9
|
+
|
|
10
|
+
Outside the auto chain: ``api`` (OpenAI-compatible endpoint) is
|
|
11
|
+
EXPLICIT OPT-IN ONLY (cost/privacy) — never chosen by ``auto``, and
|
|
12
|
+
refused over plaintext http except to loopback hosts.
|
|
13
|
+
|
|
14
|
+
Backend selection (env > config > default):
|
|
15
|
+
1. ``ARKA_EMBED_BACKEND`` env var
|
|
16
|
+
2. ``memory.embedBackend`` in ``~/.arkaos/config.json``
|
|
17
|
+
3. default ``auto``
|
|
18
|
+
|
|
19
|
+
Every result declares its ``backend`` — a vector is never presented
|
|
20
|
+
without saying where it came from.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import urllib.error
|
|
29
|
+
import urllib.request
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Literal, Optional
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel
|
|
34
|
+
|
|
35
|
+
from core.knowledge import embedder
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
BackendName = Literal["fastembed", "ollama", "api", "none"]
|
|
40
|
+
|
|
41
|
+
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
|
|
42
|
+
DEFAULT_OLLAMA_MODEL = "nomic-embed-text"
|
|
43
|
+
DEFAULT_API_BASE = "https://api.openai.com"
|
|
44
|
+
DEFAULT_API_MODEL = "text-embedding-3-small"
|
|
45
|
+
_OLLAMA_TIMEOUT_S = 2.0
|
|
46
|
+
_API_TIMEOUT_S = 10.0
|
|
47
|
+
|
|
48
|
+
# Probe results cached per process — resolution must stay cheap on hooks.
|
|
49
|
+
_resolved_backend: Optional[BackendName] = None
|
|
50
|
+
_ollama_available: Optional[bool] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EmbeddingResult(BaseModel):
|
|
54
|
+
"""An embedding that declares its own provenance."""
|
|
55
|
+
|
|
56
|
+
vector: Optional[list[float]] = None
|
|
57
|
+
backend: BackendName = "none"
|
|
58
|
+
model: str = ""
|
|
59
|
+
dims: int = 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _memory_config() -> dict:
|
|
63
|
+
config_path = Path.home() / ".arkaos" / "config.json"
|
|
64
|
+
try:
|
|
65
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
66
|
+
section = data.get("memory")
|
|
67
|
+
return section if isinstance(section, dict) else {}
|
|
68
|
+
except (OSError, json.JSONDecodeError, AttributeError):
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_backend_name() -> str:
|
|
73
|
+
"""Configured backend name (env > config > ``auto``) — unresolved."""
|
|
74
|
+
env_name = os.environ.get("ARKA_EMBED_BACKEND", "").strip().lower()
|
|
75
|
+
if env_name:
|
|
76
|
+
return env_name
|
|
77
|
+
configured = _memory_config().get("embedBackend", "")
|
|
78
|
+
if isinstance(configured, str) and configured.strip():
|
|
79
|
+
return configured.strip().lower()
|
|
80
|
+
return "auto"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ollama_host() -> str:
|
|
84
|
+
host = _memory_config().get("ollamaHost", "")
|
|
85
|
+
if isinstance(host, str) and host.strip():
|
|
86
|
+
return host.strip()
|
|
87
|
+
return os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _ollama_model() -> str:
|
|
91
|
+
model = _memory_config().get("embedModel", "")
|
|
92
|
+
if isinstance(model, str) and model.strip():
|
|
93
|
+
return model.strip()
|
|
94
|
+
return DEFAULT_OLLAMA_MODEL
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _probe_ollama(host: str) -> bool:
|
|
98
|
+
"""One reachability probe per process — hooks must never pay twice."""
|
|
99
|
+
global _ollama_available
|
|
100
|
+
if _ollama_available is not None:
|
|
101
|
+
return _ollama_available
|
|
102
|
+
url = f"{host.rstrip('/')}/api/tags"
|
|
103
|
+
try:
|
|
104
|
+
with urllib.request.urlopen(url, timeout=_OLLAMA_TIMEOUT_S) as resp:
|
|
105
|
+
_ollama_available = resp.status == 200
|
|
106
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
107
|
+
_ollama_available = False
|
|
108
|
+
return _ollama_available
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_backend() -> BackendName:
|
|
112
|
+
"""Resolve the configured name to a concrete, usable backend.
|
|
113
|
+
|
|
114
|
+
``auto`` probes fastembed → ollama → none. ``api`` is honored only
|
|
115
|
+
when explicitly configured AND a key exists; otherwise degrades to
|
|
116
|
+
``none`` (honestly, never silently to a paid default).
|
|
117
|
+
"""
|
|
118
|
+
global _resolved_backend
|
|
119
|
+
if _resolved_backend is not None:
|
|
120
|
+
return _resolved_backend
|
|
121
|
+
name = get_backend_name()
|
|
122
|
+
if name == "fastembed":
|
|
123
|
+
_resolved_backend = "fastembed" if embedder.is_available() else "none"
|
|
124
|
+
elif name == "ollama":
|
|
125
|
+
_resolved_backend = "ollama" if _probe_ollama(_ollama_host()) else "none"
|
|
126
|
+
elif name == "api":
|
|
127
|
+
_resolved_backend = "api" if _api_key() else "none"
|
|
128
|
+
elif name == "none":
|
|
129
|
+
_resolved_backend = "none"
|
|
130
|
+
else: # auto (and unknown names degrade to auto semantics)
|
|
131
|
+
if embedder.is_available():
|
|
132
|
+
_resolved_backend = "fastembed"
|
|
133
|
+
elif _probe_ollama(_ollama_host()):
|
|
134
|
+
_resolved_backend = "ollama"
|
|
135
|
+
else:
|
|
136
|
+
_resolved_backend = "none"
|
|
137
|
+
return _resolved_backend
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def reset_backend_cache() -> None:
|
|
141
|
+
"""Drop cached resolution so the next call re-probes (tests/config)."""
|
|
142
|
+
global _resolved_backend, _ollama_available
|
|
143
|
+
_resolved_backend = None
|
|
144
|
+
_ollama_available = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _api_key() -> Optional[str]:
|
|
148
|
+
try:
|
|
149
|
+
from core.keys import get_key
|
|
150
|
+
return get_key("openai")
|
|
151
|
+
except Exception: # noqa: BLE001 — degraded envs must never raise here
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _post_json(url: str, payload: dict, headers: dict, timeout: float) -> dict:
|
|
156
|
+
body = json.dumps(payload).encode("utf-8")
|
|
157
|
+
request = urllib.request.Request(
|
|
158
|
+
url, data=body, headers={"Content-Type": "application/json", **headers}
|
|
159
|
+
)
|
|
160
|
+
with urllib.request.urlopen(request, timeout=timeout) as resp:
|
|
161
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _embed_ollama(texts: list[str]) -> list[Optional[list[float]]]:
|
|
165
|
+
model = _ollama_model()
|
|
166
|
+
url = f"{_ollama_host().rstrip('/')}/api/embed"
|
|
167
|
+
try:
|
|
168
|
+
data = _post_json(url, {"model": model, "input": texts}, {}, _OLLAMA_TIMEOUT_S)
|
|
169
|
+
embeddings = data.get("embeddings") if isinstance(data, dict) else None
|
|
170
|
+
if not isinstance(embeddings, list) or len(embeddings) != len(texts):
|
|
171
|
+
# Positional alignment is unknowable on a count mismatch —
|
|
172
|
+
# degrading the whole batch beats misassociating vectors.
|
|
173
|
+
logger.warning("ollama embed returned %s embeddings for %s texts",
|
|
174
|
+
len(embeddings) if isinstance(embeddings, list) else "no",
|
|
175
|
+
len(texts))
|
|
176
|
+
return [None] * len(texts)
|
|
177
|
+
return [list(map(float, vec)) for vec in embeddings]
|
|
178
|
+
except (urllib.error.URLError, OSError, ValueError,
|
|
179
|
+
TypeError, AttributeError) as exc:
|
|
180
|
+
logger.warning("ollama embed failed (%s) — degrading to none", exc)
|
|
181
|
+
return [None] * len(texts)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _api_base_allowed(base: str) -> bool:
|
|
185
|
+
"""Bearer keys travel only over https, or http to loopback (local proxy)."""
|
|
186
|
+
if base.startswith("https://"):
|
|
187
|
+
return True
|
|
188
|
+
if base.startswith("http://"):
|
|
189
|
+
host = base[len("http://"):].split("/", 1)[0].split(":", 1)[0]
|
|
190
|
+
return host in ("localhost", "127.0.0.1", "::1")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _rows_to_vectors(rows: object, n: int) -> list[Optional[list[float]]]:
|
|
195
|
+
"""Map api rows by declared index; a malformed row degrades only itself."""
|
|
196
|
+
vectors: list[Optional[list[float]]] = [None] * n
|
|
197
|
+
for row in rows if isinstance(rows, list) else []:
|
|
198
|
+
try:
|
|
199
|
+
idx = row.get("index")
|
|
200
|
+
if isinstance(idx, int) and 0 <= idx < n:
|
|
201
|
+
vectors[idx] = list(map(float, row["embedding"]))
|
|
202
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
203
|
+
continue # one bad row must not discard valid siblings
|
|
204
|
+
return vectors
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _embed_api(texts: list[str]) -> list[Optional[list[float]]]:
|
|
208
|
+
key = _api_key()
|
|
209
|
+
if not key:
|
|
210
|
+
return [None] * len(texts)
|
|
211
|
+
cfg = _memory_config()
|
|
212
|
+
base = str(cfg.get("embedApiBase") or DEFAULT_API_BASE).rstrip("/")
|
|
213
|
+
model = str(cfg.get("embedApiModel") or DEFAULT_API_MODEL)
|
|
214
|
+
if not _api_base_allowed(base):
|
|
215
|
+
logger.warning("api embed refused: non-https base %r would leak the key", base)
|
|
216
|
+
return [None] * len(texts)
|
|
217
|
+
try:
|
|
218
|
+
data = _post_json(
|
|
219
|
+
f"{base}/v1/embeddings",
|
|
220
|
+
{"model": model, "input": texts},
|
|
221
|
+
{"Authorization": f"Bearer {key}"},
|
|
222
|
+
_API_TIMEOUT_S,
|
|
223
|
+
)
|
|
224
|
+
rows = data.get("data") if isinstance(data, dict) else None
|
|
225
|
+
return _rows_to_vectors(rows, len(texts))
|
|
226
|
+
except (urllib.error.URLError, OSError, ValueError,
|
|
227
|
+
TypeError, KeyError, AttributeError) as exc:
|
|
228
|
+
logger.warning("api embed failed (%s) — degrading to none", exc)
|
|
229
|
+
return [None] * len(texts)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _active_model_name(backend: BackendName) -> str:
|
|
233
|
+
if backend == "fastembed":
|
|
234
|
+
return embedder.get_model_name()
|
|
235
|
+
if backend == "ollama":
|
|
236
|
+
return _ollama_model()
|
|
237
|
+
if backend == "api":
|
|
238
|
+
cfg = _memory_config()
|
|
239
|
+
return str(cfg.get("embedApiModel") or DEFAULT_API_MODEL)
|
|
240
|
+
return ""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _aligned(vectors: object, n: int) -> list[Optional[list[float]]]:
|
|
244
|
+
"""Every backend obeys one contract: exactly ``n`` positional items."""
|
|
245
|
+
if not isinstance(vectors, list) or len(vectors) != n:
|
|
246
|
+
return [None] * n
|
|
247
|
+
return vectors
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def embed_batch(texts: list[str]) -> list[EmbeddingResult]:
|
|
251
|
+
"""Embed texts via the resolved backend, declaring provenance per item."""
|
|
252
|
+
if not texts:
|
|
253
|
+
return []
|
|
254
|
+
backend = resolve_backend()
|
|
255
|
+
model = _active_model_name(backend)
|
|
256
|
+
if backend == "fastembed":
|
|
257
|
+
vectors = _aligned(embedder.embed_batch(texts), len(texts))
|
|
258
|
+
elif backend == "ollama":
|
|
259
|
+
vectors = _aligned(_embed_ollama(texts), len(texts))
|
|
260
|
+
elif backend == "api":
|
|
261
|
+
vectors = _aligned(_embed_api(texts), len(texts))
|
|
262
|
+
else:
|
|
263
|
+
vectors = [None] * len(texts)
|
|
264
|
+
return [
|
|
265
|
+
EmbeddingResult(
|
|
266
|
+
vector=vec,
|
|
267
|
+
backend=backend if vec is not None else "none",
|
|
268
|
+
model=model if vec is not None else "",
|
|
269
|
+
dims=len(vec) if vec is not None else 0,
|
|
270
|
+
)
|
|
271
|
+
for vec in vectors
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def embed(text: str) -> EmbeddingResult:
|
|
276
|
+
"""Embed a single text (see ``embed_batch``)."""
|
|
277
|
+
results = embed_batch([text])
|
|
278
|
+
return results[0] if results else EmbeddingResult()
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import json
|
|
18
18
|
from contextlib import contextmanager
|
|
19
19
|
from dataclasses import asdict, dataclass, field
|
|
20
|
-
from datetime import datetime, timezone
|
|
20
|
+
from datetime import UTC, datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
@@ -48,6 +48,10 @@ class PatternCard:
|
|
|
48
48
|
projects_using: list[str] = field(default_factory=list)
|
|
49
49
|
created_at: str = ""
|
|
50
50
|
last_updated: str = ""
|
|
51
|
+
# F1-C1 decay fields — defaults keep every pre-existing JSONL line
|
|
52
|
+
# loading unchanged (no migration).
|
|
53
|
+
last_reinforced: str = ""
|
|
54
|
+
use_count: int = 0
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
def pattern_to_dict(card: PatternCard) -> dict:
|
|
@@ -132,6 +136,23 @@ def record_pattern(card: PatternCard) -> None:
|
|
|
132
136
|
_write_all(others)
|
|
133
137
|
|
|
134
138
|
|
|
139
|
+
def reinforce_pattern(pattern_id: str) -> bool:
|
|
140
|
+
"""Real usage evidence re-touches the card: refresh last_reinforced,
|
|
141
|
+
bump use_count. Returns True when the card existed. Decay fades
|
|
142
|
+
untouched cards; reinforcement is the only counter-force."""
|
|
143
|
+
safe = _safe_id(pattern_id)
|
|
144
|
+
if safe is None:
|
|
145
|
+
return False
|
|
146
|
+
cards = _load_all()
|
|
147
|
+
target = next((c for c in cards if c.id == safe), None)
|
|
148
|
+
if target is None:
|
|
149
|
+
return False
|
|
150
|
+
target.last_reinforced = datetime.now(UTC).isoformat()
|
|
151
|
+
target.use_count += 1
|
|
152
|
+
_write_all(cards)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
|
|
135
156
|
def _card_text(card: PatternCard) -> str:
|
|
136
157
|
"""Flatten searchable text from a card."""
|
|
137
158
|
parts = [card.name, card.description] + (card.feature_keywords or [])
|
|
@@ -171,5 +192,32 @@ def query_patterns(
|
|
|
171
192
|
c for c in cards
|
|
172
193
|
if _matches_keywords(c, kws) and _matches_tags(c, tgs)
|
|
173
194
|
]
|
|
195
|
+
# F1-C1 decay: stale cards fade from injection (never from disk).
|
|
196
|
+
# Weight decays from the freshest touch; below the floor the card
|
|
197
|
+
# is dropped from RESULTS only — the JSONL is untouched.
|
|
198
|
+
from core.shared.decay import (
|
|
199
|
+
INJECTION_FLOOR,
|
|
200
|
+
decay_enabled,
|
|
201
|
+
decayed_weight,
|
|
202
|
+
half_life_days,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if decay_enabled():
|
|
206
|
+
# Config resolved ONCE per query — this runs on the L7.5 prompt
|
|
207
|
+
# path; per-card config parsing would read the same file 2N times.
|
|
208
|
+
hl = half_life_days()
|
|
209
|
+
weighted = [
|
|
210
|
+
(decayed_weight(
|
|
211
|
+
c.last_reinforced or c.last_updated or c.created_at,
|
|
212
|
+
half_life=hl, enabled=True,
|
|
213
|
+
), c)
|
|
214
|
+
for c in matched
|
|
215
|
+
]
|
|
216
|
+
weighted = [(w, c) for w, c in weighted if w >= INJECTION_FLOOR]
|
|
217
|
+
weighted.sort(
|
|
218
|
+
key=lambda pair: (pair[0], pair[1].last_updated or pair[1].created_at or ""),
|
|
219
|
+
reverse=True,
|
|
220
|
+
)
|
|
221
|
+
return [c for _w, c in weighted[:limit]]
|
|
174
222
|
matched.sort(key=lambda c: c.last_updated or c.created_at, reverse=True)
|
|
175
223
|
return matched[:limit]
|
|
@@ -112,18 +112,30 @@ class VectorStore:
|
|
|
112
112
|
def __init__(self, db_path: str | Path = ":memory:") -> None:
|
|
113
113
|
import threading
|
|
114
114
|
self._db_path = str(db_path)
|
|
115
|
+
self._db: sqlite3.Connection | None = None
|
|
116
|
+
self._write_lock = threading.Lock()
|
|
117
|
+
# A corrupt file triggers ONE self-heal attempt (F1-D1) before
|
|
118
|
+
# the error propagates — the original is never destroyed.
|
|
119
|
+
from core.shared.sqlite_recovery import open_with_recovery
|
|
120
|
+
open_with_recovery(self._db_path, self._open)
|
|
121
|
+
|
|
122
|
+
def _open(self) -> None:
|
|
123
|
+
if self._db is not None:
|
|
124
|
+
try:
|
|
125
|
+
self._db.close()
|
|
126
|
+
except sqlite3.Error:
|
|
127
|
+
pass
|
|
115
128
|
# check_same_thread=False — see class docstring.
|
|
116
129
|
self._db = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
117
130
|
self._db.row_factory = sqlite3.Row
|
|
118
131
|
# WAL gives concurrent readers + a single writer at the engine
|
|
119
|
-
# level. The
|
|
132
|
+
# level. The instance write-lock serialises our application-level
|
|
120
133
|
# writes per VectorStore instance.
|
|
121
134
|
try:
|
|
122
135
|
self._db.execute("PRAGMA journal_mode=WAL")
|
|
123
136
|
except sqlite3.OperationalError:
|
|
124
137
|
# in-memory DBs don't support WAL — harmless.
|
|
125
138
|
pass
|
|
126
|
-
self._write_lock = threading.Lock()
|
|
127
139
|
self._vec_available = _load_vec(self._db)
|
|
128
140
|
self._init_schema()
|
|
129
141
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|