velune-cli 0.9.0__py3-none-any.whl
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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Embedding pipeline: content preparation → Ollama nomic-embed-text → LanceDB upsert.
|
|
2
|
+
|
|
3
|
+
Architecture
|
|
4
|
+
------------
|
|
5
|
+
* ``embed_text / embed_batch`` — synchronous embedding (used for search queries).
|
|
6
|
+
* ``enqueue`` — non-blocking: drops a turn onto a bounded async queue and returns
|
|
7
|
+
immediately. The REPL never waits for an embedding call to complete.
|
|
8
|
+
* Background worker — drains the queue, calls Ollama, upserts to LanceDB.
|
|
9
|
+
Applies exponential back-off (1 → 2 → 4 … 60 s) when Ollama is unavailable,
|
|
10
|
+
then re-queues the item for a later retry.
|
|
11
|
+
|
|
12
|
+
Content preparation before embedding
|
|
13
|
+
-------------------------------------
|
|
14
|
+
1. Code blocks longer than 200 tokens are replaced with a ``[lang block, N tokens]``
|
|
15
|
+
placeholder — raw code degrades embedding quality without adding recall value.
|
|
16
|
+
2. A context prefix is prepended: ``"Session YYYY-MM-DD, {role}: "``.
|
|
17
|
+
3. The combined text is truncated to 512 tokens (embedding quality falls beyond
|
|
18
|
+
this length for nomic-embed-text).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from velune.context.window import estimate_tokens
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("velune.memory.embedding_pipeline")
|
|
32
|
+
|
|
33
|
+
_TOKEN_LIMIT = 512
|
|
34
|
+
_CODE_TOKEN_LIMIT = 200
|
|
35
|
+
_BATCH_CONCURRENCY = 3 # max simultaneous Ollama embed calls
|
|
36
|
+
_BACKOFF_BASE = 1.0
|
|
37
|
+
_BACKOFF_MAX = 60.0
|
|
38
|
+
_QUEUE_MAXSIZE = 1_000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Queue item ────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class EmbedQueueItem:
|
|
46
|
+
"""One unit of embedding work enqueued for background processing."""
|
|
47
|
+
|
|
48
|
+
record_id: str
|
|
49
|
+
turn_id: str
|
|
50
|
+
session_id: str
|
|
51
|
+
role: str
|
|
52
|
+
content: str
|
|
53
|
+
source_type: str
|
|
54
|
+
workspace_root: str
|
|
55
|
+
created_at: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Content preparation ───────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _strip_long_code_blocks(text: str) -> str:
|
|
62
|
+
"""Replace ```lang ... ``` blocks that exceed ``_CODE_TOKEN_LIMIT`` tokens."""
|
|
63
|
+
|
|
64
|
+
def _replace(match: re.Match) -> str:
|
|
65
|
+
block = match.group(0)
|
|
66
|
+
toks = estimate_tokens(block)
|
|
67
|
+
if toks > _CODE_TOKEN_LIMIT:
|
|
68
|
+
lang = (match.group(1) or "code").strip() or "code"
|
|
69
|
+
return f"[{lang} block, {toks} tokens]"
|
|
70
|
+
return block
|
|
71
|
+
|
|
72
|
+
return re.sub(r"```(\w*)\n.*?```", _replace, text, flags=re.DOTALL)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _prepare_content(content: str, role: str, created_at: float) -> str:
|
|
76
|
+
"""Return a semantics-friendly, token-capped version of *content*.
|
|
77
|
+
|
|
78
|
+
1. Strip long code blocks.
|
|
79
|
+
2. Prepend ``"Session {date}, {role}: "``.
|
|
80
|
+
3. Truncate so the total fits inside ``_TOKEN_LIMIT`` tokens.
|
|
81
|
+
"""
|
|
82
|
+
from datetime import datetime
|
|
83
|
+
|
|
84
|
+
date_str = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
|
|
85
|
+
prefix = f"Session {date_str}, {role}: "
|
|
86
|
+
|
|
87
|
+
cleaned = _strip_long_code_blocks(content)
|
|
88
|
+
|
|
89
|
+
combined = prefix + cleaned
|
|
90
|
+
if estimate_tokens(combined) <= _TOKEN_LIMIT:
|
|
91
|
+
return combined
|
|
92
|
+
|
|
93
|
+
# Trim the content (never the prefix) to fit the token budget.
|
|
94
|
+
# Use a char-per-token heuristic (4 chars ≈ 1 token) for truncation.
|
|
95
|
+
budget_chars = max(0, (_TOKEN_LIMIT - estimate_tokens(prefix)) * 4)
|
|
96
|
+
return prefix + cleaned[:budget_chars]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Pipeline ──────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class EmbeddingPipeline:
|
|
103
|
+
"""Async embedding pipeline backed by a provider that supports ``embed()``.
|
|
104
|
+
|
|
105
|
+
Typical provider: ``OllamaProvider`` with ``nomic-embed-text``.
|
|
106
|
+
If *provider* is ``None`` (Ollama unavailable at startup), the pipeline
|
|
107
|
+
degrades gracefully: queries return ``RuntimeError``, queue items accumulate
|
|
108
|
+
until the worker succeeds or the process exits.
|
|
109
|
+
|
|
110
|
+
Lifecycle
|
|
111
|
+
---------
|
|
112
|
+
``await pipeline.initialize()`` — start the background worker task.
|
|
113
|
+
``await pipeline.shutdown()`` — cancel the worker, drain in-flight items.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
provider: Any, # OllamaProvider | None
|
|
119
|
+
store: Any, # LanceDBStore
|
|
120
|
+
model_id: str = "nomic-embed-text",
|
|
121
|
+
) -> None:
|
|
122
|
+
self._provider = provider
|
|
123
|
+
self._store = store
|
|
124
|
+
self._model_id = model_id
|
|
125
|
+
self._queue: asyncio.Queue[EmbedQueueItem] = asyncio.Queue(maxsize=_QUEUE_MAXSIZE)
|
|
126
|
+
self._worker_task: asyncio.Task | None = None
|
|
127
|
+
self._running = False
|
|
128
|
+
self._backoff = _BACKOFF_BASE
|
|
129
|
+
|
|
130
|
+
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
async def initialize(self) -> None:
|
|
133
|
+
self._running = True
|
|
134
|
+
self._worker_task = asyncio.create_task(
|
|
135
|
+
self._background_worker(), name="velune.embedding_worker"
|
|
136
|
+
)
|
|
137
|
+
logger.debug("EmbeddingPipeline background worker started (model=%s)", self._model_id)
|
|
138
|
+
|
|
139
|
+
async def shutdown(self) -> None:
|
|
140
|
+
self._running = False
|
|
141
|
+
if self._worker_task and not self._worker_task.done():
|
|
142
|
+
self._worker_task.cancel()
|
|
143
|
+
try:
|
|
144
|
+
await self._worker_task
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
pass
|
|
147
|
+
logger.debug("EmbeddingPipeline background worker stopped")
|
|
148
|
+
|
|
149
|
+
# ── Direct embedding (synchronous / on-demand) ────────────────────────────
|
|
150
|
+
|
|
151
|
+
async def embed_text(self, text: str) -> list[float]:
|
|
152
|
+
"""Embed a single string. Raises ``RuntimeError`` if provider unavailable."""
|
|
153
|
+
if not self._provider:
|
|
154
|
+
raise RuntimeError("No embedding provider configured")
|
|
155
|
+
results = await self._provider.embed([text], self._model_id)
|
|
156
|
+
return results[0]
|
|
157
|
+
|
|
158
|
+
async def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
159
|
+
"""Embed multiple strings with bounded concurrency."""
|
|
160
|
+
if not self._provider:
|
|
161
|
+
raise RuntimeError("No embedding provider configured")
|
|
162
|
+
sem = asyncio.Semaphore(_BATCH_CONCURRENCY)
|
|
163
|
+
|
|
164
|
+
async def _one(t: str) -> list[float]:
|
|
165
|
+
async with sem:
|
|
166
|
+
res = await self._provider.embed([t], self._model_id)
|
|
167
|
+
return res[0]
|
|
168
|
+
|
|
169
|
+
return list(await asyncio.gather(*[_one(t) for t in texts]))
|
|
170
|
+
|
|
171
|
+
# ── Background-queue interface ─────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def enqueue(self, item: EmbedQueueItem) -> None:
|
|
174
|
+
"""Non-blocking enqueue. Silently drops the item when the queue is full."""
|
|
175
|
+
try:
|
|
176
|
+
self._queue.put_nowait(item)
|
|
177
|
+
except asyncio.QueueFull:
|
|
178
|
+
logger.warning(
|
|
179
|
+
"Embedding queue full (%d items) — dropping turn %s",
|
|
180
|
+
_QUEUE_MAXSIZE,
|
|
181
|
+
item.turn_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def embed_turn(self, item: EmbedQueueItem) -> None:
|
|
185
|
+
"""Prepare, embed, and upsert a single turn. Called by the background worker."""
|
|
186
|
+
from velune.memory.storage.lancedb_store import MemoryRecord
|
|
187
|
+
|
|
188
|
+
prepared = _prepare_content(item.content, item.role, item.created_at)
|
|
189
|
+
embedding = await self.embed_text(prepared)
|
|
190
|
+
|
|
191
|
+
record = MemoryRecord(
|
|
192
|
+
id=item.record_id,
|
|
193
|
+
embedding=embedding,
|
|
194
|
+
content=item.content,
|
|
195
|
+
source_type=item.source_type,
|
|
196
|
+
session_id=item.session_id,
|
|
197
|
+
turn_id=item.turn_id,
|
|
198
|
+
workspace_root=item.workspace_root,
|
|
199
|
+
created_at=item.created_at,
|
|
200
|
+
trust_score=1.0,
|
|
201
|
+
)
|
|
202
|
+
await self._store.upsert([record])
|
|
203
|
+
logger.debug(
|
|
204
|
+
"Indexed %s (session=%s, role=%s)",
|
|
205
|
+
item.record_id,
|
|
206
|
+
item.session_id,
|
|
207
|
+
item.role,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# ── Background worker ─────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async def _background_worker(self) -> None:
|
|
213
|
+
"""Drain the queue, retrying with exponential back-off on errors."""
|
|
214
|
+
while self._running:
|
|
215
|
+
# Wait up to 1 second for an item; loop to check _running flag.
|
|
216
|
+
try:
|
|
217
|
+
item = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
|
218
|
+
except TimeoutError:
|
|
219
|
+
continue
|
|
220
|
+
except asyncio.CancelledError:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
await self.embed_turn(item)
|
|
225
|
+
self._backoff = _BACKOFF_BASE # reset on success
|
|
226
|
+
self._queue.task_done()
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
logger.warning(
|
|
229
|
+
"Embedding failed for turn %s (%s) — retry in %.1fs",
|
|
230
|
+
item.turn_id,
|
|
231
|
+
type(exc).__name__,
|
|
232
|
+
self._backoff,
|
|
233
|
+
)
|
|
234
|
+
# Re-enqueue for retry; drop silently if queue is full.
|
|
235
|
+
try:
|
|
236
|
+
self._queue.put_nowait(item)
|
|
237
|
+
except asyncio.QueueFull:
|
|
238
|
+
logger.error("Queue full on retry — permanently dropping turn %s", item.turn_id)
|
|
239
|
+
|
|
240
|
+
await asyncio.sleep(self._backoff)
|
|
241
|
+
self._backoff = min(self._backoff * 2, _BACKOFF_MAX)
|