arkaos 4.13.2 → 4.14.0

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.
Files changed (130) hide show
  1. package/VERSION +1 -1
  2. package/arka/SKILL.md +16 -0
  3. package/bin/arka-doctor +20 -1
  4. package/config/cognition/schedules.yaml +16 -0
  5. package/config/hooks/_lib/fastpath/engine.cjs +337 -0
  6. package/config/hooks/gate-manifest.json +741 -0
  7. package/config/hooks/post-tool-use.cjs +113 -0
  8. package/config/hooks/pre-tool-use.cjs +109 -0
  9. package/config/hooks/session-end.ps1 +40 -0
  10. package/config/hooks/session-end.sh +55 -0
  11. package/config/hooks/session-start.sh +25 -189
  12. package/config/hooks/subagent-stop.ps1 +40 -0
  13. package/config/hooks/subagent-stop.sh +56 -0
  14. package/config/mcp-policy.yaml +8 -0
  15. package/config/skills-curated.yaml +39 -0
  16. package/config/statusline.sh +57 -1
  17. package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
  18. package/core/cognition/capture/store.py +7 -2
  19. package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
  21. package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
  22. package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
  23. package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
  24. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  25. package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
  26. package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
  27. package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
  28. package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
  29. package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
  30. package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
  31. package/core/governance/evidence_checks.py +42 -7
  32. package/core/governance/routing_feedback.py +229 -0
  33. package/core/governance/routing_feedback_cli.py +49 -0
  34. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  35. package/core/hooks/__pycache__/gate_manifest.cpython-313.pyc +0 -0
  36. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  37. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  38. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  39. package/core/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  40. package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  41. package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  42. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  43. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  44. package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
  45. package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
  46. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  47. package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
  48. package/core/hooks/gate_manifest.py +366 -0
  49. package/core/hooks/post_tool_use.py +5 -2
  50. package/core/hooks/session_end.py +127 -0
  51. package/core/hooks/session_start.py +322 -0
  52. package/core/hooks/stop.py +69 -0
  53. package/core/hooks/subagent_stop.py +186 -0
  54. package/core/hooks/user_prompt_submit.py +10 -6
  55. package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
  56. package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
  57. package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
  58. package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
  59. package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
  60. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  61. package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
  62. package/core/knowledge/embedding_backends.py +278 -0
  63. package/core/knowledge/pattern_cards.py +49 -1
  64. package/core/knowledge/vector_store.py +14 -2
  65. package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
  66. package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
  67. package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
  68. package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
  69. package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
  70. package/core/memory/semantic_store.py +301 -0
  71. package/core/memory/turn_capture.py +277 -0
  72. package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
  73. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  74. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  75. package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
  76. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  77. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
  78. package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
  79. package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
  80. package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
  81. package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
  82. package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
  83. package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
  84. package/core/shared/decay.py +94 -0
  85. package/core/shared/sqlite_recovery.py +266 -0
  86. package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
  87. package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
  88. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  89. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  90. package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
  91. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  92. package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
  93. package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
  94. package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
  95. package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
  96. package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
  97. package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
  98. package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
  99. package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
  100. package/core/synapse/agent_experiences_layer.py +42 -3
  101. package/core/synapse/engine.py +13 -0
  102. package/core/synapse/recipe_layer.py +27 -4
  103. package/core/synapse/routing_feedback_layer.py +132 -0
  104. package/core/synapse/session_memory_layer.py +152 -0
  105. package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
  107. package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
  108. package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
  109. package/installer/adapters/claude-code.js +37 -0
  110. package/installer/claude-plugins.js +6 -0
  111. package/installer/cli.js +23 -2
  112. package/installer/config-seed.js +25 -16
  113. package/installer/doctor.js +116 -1
  114. package/installer/hook-lib.js +28 -0
  115. package/installer/index.js +48 -153
  116. package/installer/mcp-runner.js +108 -0
  117. package/installer/skill-deploy.js +189 -0
  118. package/installer/skills-mode.js +64 -0
  119. package/installer/update.js +55 -79
  120. package/knowledge/commands-registry.json.bak +171 -4
  121. package/knowledge/skills-manifest.json +2337 -0
  122. package/package.json +1 -1
  123. package/pyproject.toml +9 -1
  124. package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
  125. package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
  126. package/scripts/marketplace_gen.py +310 -0
  127. package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
  128. package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
  129. package/scripts/tools/docs_stats.py +6 -4
  130. 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 Python lock below serialises our application-level
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