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.
Files changed (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,25 @@
1
+ """Memory-related errors."""
2
+
3
+
4
+ class VeluneMemoryError(Exception):
5
+ """Base exception for memory errors."""
6
+
7
+ pass
8
+
9
+
10
+ class VeluneMemoryStoreError(VeluneMemoryError):
11
+ """Raised when memory store operation fails."""
12
+
13
+ pass
14
+
15
+
16
+ class VeluneMemoryRetrievalError(VeluneMemoryError):
17
+ """Raised when memory retrieval fails."""
18
+
19
+ pass
20
+
21
+
22
+ class VeluneMemoryConsolidationError(VeluneMemoryError):
23
+ """Raised when memory consolidation fails."""
24
+
25
+ pass
@@ -0,0 +1,31 @@
1
+ """Orchestration-related errors."""
2
+
3
+
4
+ class OrchestrationError(Exception):
5
+ """Base exception for orchestration errors."""
6
+
7
+ pass
8
+
9
+
10
+ class AgentExecutionError(OrchestrationError):
11
+ """Raised when agent execution fails."""
12
+
13
+ pass
14
+
15
+
16
+ class PipelineExecutionError(OrchestrationError):
17
+ """Raised when pipeline execution fails."""
18
+
19
+ pass
20
+
21
+
22
+ class StateTransitionError(OrchestrationError):
23
+ """Raised when state transition fails."""
24
+
25
+ pass
26
+
27
+
28
+ class CheckpointError(OrchestrationError):
29
+ """Raised when checkpoint operation fails."""
30
+
31
+ pass
@@ -0,0 +1,37 @@
1
+ """Provider-related errors."""
2
+
3
+
4
+ class ProviderError(Exception):
5
+ """Base exception for provider errors."""
6
+
7
+ pass
8
+
9
+
10
+ class ProviderNotFoundError(ProviderError):
11
+ """Raised when a provider is not found."""
12
+
13
+ pass
14
+
15
+
16
+ class ProviderConnectionError(ProviderError):
17
+ """Raised when connection to provider fails."""
18
+
19
+ pass
20
+
21
+
22
+ class ProviderAuthenticationError(ProviderError):
23
+ """Raised when provider authentication fails."""
24
+
25
+ pass
26
+
27
+
28
+ class ModelNotFoundError(ProviderError):
29
+ """Raised when a model is not found."""
30
+
31
+ pass
32
+
33
+
34
+ class InferenceError(ProviderError):
35
+ """Raised when inference fails."""
36
+
37
+ pass
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Coroutine
5
+ from typing import Any, TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ def get_loop() -> asyncio.AbstractEventLoop:
11
+ """Return the running event loop, or a freshly created one."""
12
+ try:
13
+ return asyncio.get_running_loop()
14
+ except RuntimeError:
15
+ return asyncio.new_event_loop()
16
+
17
+
18
+ def submit(coro: Coroutine[Any, Any, T]) -> T:
19
+ """Run *coro* to completion from a synchronous call site.
20
+
21
+ Raises RuntimeError if called from within a running event loop — callers
22
+ inside an async context should await the coroutine directly.
23
+
24
+ Delegates to ``velune.kernel.entrypoint.run_async`` so that
25
+ ``asyncio.run`` is called from exactly one place in the codebase.
26
+ """
27
+ try:
28
+ asyncio.get_running_loop()
29
+ except RuntimeError:
30
+ from velune.kernel.entrypoint import run_async
31
+
32
+ return run_async(coro)
33
+ raise RuntimeError(
34
+ "submit() called from a running event loop — await the coroutine directly instead."
35
+ )
velune/core/logging.py ADDED
@@ -0,0 +1,83 @@
1
+ """Logging configuration for Velune."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import warnings
9
+ from dataclasses import dataclass
10
+ from datetime import UTC, datetime
11
+
12
+ from rich.logging import RichHandler
13
+
14
+ from velune.core.trace import TracedLogger, _agent_id, _run_id, _step_id
15
+
16
+
17
+ class JsonFormatter(logging.Formatter):
18
+ """Production JSON log formatter that extracts context tracing attributes."""
19
+
20
+ def format(self, record: logging.LogRecord) -> str:
21
+ exc_info = None
22
+ if record.exc_info:
23
+ exc_info = self.formatException(record.exc_info)
24
+
25
+ ts = datetime.now(UTC).isoformat().replace("+00:00", "")
26
+
27
+ log_data = {
28
+ "ts": ts,
29
+ "level": record.levelname,
30
+ "logger": record.name,
31
+ "msg": record.getMessage(),
32
+ "run_id": _run_id.get(),
33
+ "agent_id": _agent_id.get(),
34
+ "step_id": _step_id.get(),
35
+ }
36
+ if exc_info:
37
+ log_data["exception"] = exc_info
38
+
39
+ return json.dumps(log_data)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class LoggingConfig:
44
+ """Logging configuration used during CLI bootstrap."""
45
+
46
+ level: str = "INFO"
47
+ show_path: bool = False
48
+ rich_tracebacks: bool = True
49
+
50
+
51
+ def configure_logging(config: LoggingConfig) -> None:
52
+ """Configure process-wide logging once at startup."""
53
+
54
+ log_format = os.environ.get("VELUNE_LOG_FORMAT", "").lower()
55
+
56
+ if log_format == "json":
57
+ handler = logging.StreamHandler()
58
+ handler.setFormatter(JsonFormatter())
59
+ handlers: list[logging.Handler] = [handler]
60
+ else:
61
+ handlers = [
62
+ RichHandler(
63
+ rich_tracebacks=config.rich_tracebacks,
64
+ show_path=config.show_path,
65
+ omit_repeated_times=False,
66
+ )
67
+ ]
68
+
69
+ logging.basicConfig(
70
+ level=getattr(logging, config.level.upper(), logging.INFO),
71
+ format="%(message)s" if log_format != "json" else None,
72
+ datefmt="[%X]" if log_format != "json" else None,
73
+ handlers=handlers,
74
+ force=True,
75
+ )
76
+ logging.captureWarnings(True)
77
+ warnings.simplefilter("default")
78
+
79
+
80
+ def get_logger(name: str) -> TracedLogger:
81
+ """Get a namespaced Velune logger."""
82
+
83
+ return TracedLogger(name)
velune/core/paths.py ADDED
@@ -0,0 +1,165 @@
1
+ """Platform-compliant storage paths for Velune.
2
+
3
+ Heavy, frequently-written state (the SQLite cognitive core and the Qdrant
4
+ vector store) must NOT live inside the workspace tree. On Windows the
5
+ workspace is often under a cloud-synced folder (OneDrive, Dropbox, Google
6
+ Drive). Every file touch then triggers a sync-engine reparse, multiplying
7
+ I/O latency by 3-10x and serializing startup behind metadata syncs. This was
8
+ the dominant contributor to Velune's ~78s cold start.
9
+
10
+ This module centralizes storage resolution so all subsystems agree on one
11
+ location, and relocates heavy state to the platform-native, *non-synced*
12
+ application-data directory:
13
+
14
+ Windows -> %LOCALAPPDATA%\\Velune
15
+ macOS -> ~/Library/Application Support/Velune
16
+ Linux -> $XDG_DATA_HOME/velune (default ~/.local/share/velune)
17
+
18
+ State is still isolated *per workspace* (so two projects never share a
19
+ cognitive core) by hashing the workspace's resolved absolute path into a
20
+ stable, human-readable slug under ``<data_root>/workspaces/``.
21
+
22
+ Lightweight, human-facing files (``velune.toml``, ``.veluneignore``,
23
+ snapshots, sessions) intentionally stay in the workspace ``.velune/`` dir —
24
+ they're tiny, rarely written, and users expect them project-local.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import hashlib
30
+ import logging
31
+ import os
32
+ import platform
33
+ import re
34
+ import shutil
35
+ from pathlib import Path
36
+
37
+ logger = logging.getLogger("velune.core.paths")
38
+
39
+ _APP_NAME = "Velune"
40
+
41
+ # Heavy state filenames relocated off the workspace tree.
42
+ COGNITIVE_DB_NAME = "velune_cognitive_core.db"
43
+ QDRANT_STORE_NAME = "qdrant_local_store"
44
+
45
+ # Marker written into a relocated workspace dir once legacy data has been
46
+ # migrated, so migration runs at most once per workspace.
47
+ _MIGRATION_MARKER = ".migrated_from_workspace"
48
+
49
+
50
+ def app_data_root() -> Path:
51
+ """Return the platform-native, non-synced application data root.
52
+
53
+ Honors ``VELUNE_DATA_HOME`` as an explicit override (useful for tests,
54
+ CI, and power users who want all state in one place).
55
+ """
56
+ override = os.environ.get("VELUNE_DATA_HOME")
57
+ if override:
58
+ return Path(override).expanduser()
59
+
60
+ system = platform.system()
61
+ if system == "Windows":
62
+ base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
63
+ return Path(base) / _APP_NAME
64
+ if system == "Darwin":
65
+ return Path.home() / "Library" / "Application Support" / _APP_NAME
66
+ # Linux / other POSIX
67
+ xdg = os.environ.get("XDG_DATA_HOME")
68
+ base = Path(xdg) if xdg else (Path.home() / ".local" / "share")
69
+ return base / "velune"
70
+
71
+
72
+ def _workspace_slug(workspace: Path) -> str:
73
+ """Build a stable, collision-resistant, readable slug for *workspace*.
74
+
75
+ Combines a sanitized directory name with a short hash of the resolved
76
+ absolute path so distinct projects with the same folder name never
77
+ collide.
78
+ """
79
+ try:
80
+ resolved = workspace.resolve()
81
+ except Exception:
82
+ resolved = workspace.absolute()
83
+ digest = hashlib.sha1(str(resolved).encode("utf-8")).hexdigest()[:10]
84
+ name = re.sub(r"[^A-Za-z0-9._-]", "_", resolved.name) or "workspace"
85
+ return f"{name}-{digest}"
86
+
87
+
88
+ def workspace_storage_dir(workspace: Path) -> Path:
89
+ """Return (creating) the non-synced storage dir for *workspace*."""
90
+ target = app_data_root() / "workspaces" / _workspace_slug(workspace)
91
+ target.mkdir(parents=True, exist_ok=True)
92
+ return target
93
+
94
+
95
+ def cognitive_db_path(workspace: Path) -> Path:
96
+ """Absolute path to the workspace's SQLite cognitive core (non-synced)."""
97
+ return workspace_storage_dir(workspace) / COGNITIVE_DB_NAME
98
+
99
+
100
+ def qdrant_store_path(workspace: Path) -> Path:
101
+ """Absolute path to the workspace's Qdrant local store (non-synced)."""
102
+ return workspace_storage_dir(workspace) / QDRANT_STORE_NAME
103
+
104
+
105
+ LANCEDB_STORE_NAME = "lancedb_semantic_store"
106
+
107
+
108
+ def lancedb_store_path(workspace: Path) -> Path:
109
+ """Absolute path to the workspace's LanceDB semantic store (non-synced)."""
110
+ return workspace_storage_dir(workspace) / LANCEDB_STORE_NAME
111
+
112
+
113
+ def legacy_workspace_dir(workspace: Path) -> Path:
114
+ """The old in-workspace ``.velune`` directory (may be cloud-synced)."""
115
+ return workspace / ".velune"
116
+
117
+
118
+ def migrate_legacy_storage(workspace: Path) -> bool:
119
+ """One-time, best-effort relocation of heavy state out of the workspace.
120
+
121
+ Copies (does not move) any pre-existing ``.velune/velune_cognitive_core.db``
122
+ and ``.velune/qdrant_local_store`` into the non-synced storage dir, leaving
123
+ the originals untouched as a safety backup. Writes a marker so this runs at
124
+ most once per workspace. Returns True if anything was migrated.
125
+
126
+ Copy-then-mark (rather than move) is deliberate: an interrupted move could
127
+ corrupt a live cognitive core, whereas a partial copy is simply retried.
128
+ """
129
+ target = workspace_storage_dir(workspace)
130
+ marker = target / _MIGRATION_MARKER
131
+ if marker.exists():
132
+ return False
133
+
134
+ legacy = legacy_workspace_dir(workspace)
135
+ migrated = False
136
+
137
+ legacy_db = legacy / COGNITIVE_DB_NAME
138
+ new_db = target / COGNITIVE_DB_NAME
139
+ if legacy_db.exists() and not new_db.exists():
140
+ try:
141
+ # Bring along WAL/SHM sidecars if present for a consistent copy.
142
+ for suffix in ("", "-wal", "-shm"):
143
+ src = legacy / f"{COGNITIVE_DB_NAME}{suffix}"
144
+ if src.exists():
145
+ shutil.copy2(src, target / src.name)
146
+ migrated = True
147
+ logger.info("Migrated cognitive core from %s -> %s", legacy_db, new_db)
148
+ except Exception as exc:
149
+ logger.warning("Cognitive core migration skipped: %s", exc)
150
+
151
+ legacy_qdrant = legacy / QDRANT_STORE_NAME
152
+ new_qdrant = target / QDRANT_STORE_NAME
153
+ if legacy_qdrant.is_dir() and not new_qdrant.exists():
154
+ try:
155
+ shutil.copytree(legacy_qdrant, new_qdrant)
156
+ migrated = True
157
+ logger.info("Migrated vector store from %s -> %s", legacy_qdrant, new_qdrant)
158
+ except Exception as exc:
159
+ logger.warning("Vector store migration skipped: %s", exc)
160
+
161
+ try:
162
+ marker.write_text("ok\n", encoding="utf-8")
163
+ except Exception:
164
+ pass
165
+ return migrated
velune/core/runtime.py ADDED
@@ -0,0 +1,113 @@
1
+ """Process runtime bootstrap for Velune Cognitive OS CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+
11
+ from velune.kernel.config import ConfigService, VeluneConfig, get_default_config
12
+ from velune.kernel.lifecycle import LifecycleCoordinator
13
+ from velune.kernel.registry import ServiceContainer
14
+
15
+ # Stateful Orchestration & Memory Tiers
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class RuntimeContext:
20
+ """Runtime resources shared across CLI commands."""
21
+
22
+ workspace: Path
23
+ config_path: Path | None
24
+ config: VeluneConfig
25
+ console: Console
26
+ logger_name: str
27
+ container: ServiceContainer
28
+
29
+
30
+ def build_runtime(
31
+ workspace: Path,
32
+ config_path: Path | None = None,
33
+ verbose: bool = False,
34
+ ) -> RuntimeContext:
35
+ """Create and register the shared runtime services using the declarative bootstrapper."""
36
+ from velune.core.startup_profiler import mark
37
+
38
+ mark("build_runtime: enter")
39
+ # 1. Setup Logging and Console
40
+ console = Console(highlight=False)
41
+ logger_name = "velune"
42
+ logger = logging.getLogger(logger_name)
43
+
44
+ # Configure root logger levels. Non-verbose hides internal INFO/DEBUG
45
+ # logs so users only ever see Rich-formatted output, not raw Python logs.
46
+ logging.basicConfig(level=logging.DEBUG if verbose else logging.WARNING)
47
+ logger.setLevel(logging.DEBUG if verbose else logging.WARNING)
48
+
49
+ # 2. Configuration Service
50
+ config_service = ConfigService(workspace=workspace, config_path=config_path)
51
+ try:
52
+ config = config_service.load()
53
+ except Exception as e:
54
+ logger.warning("Failed to load configuration. Falling back to system defaults: %s", e)
55
+ config = get_default_config()
56
+ mark("config loaded")
57
+
58
+ container = ServiceContainer()
59
+ lifecycle = LifecycleCoordinator()
60
+ lifecycle.container = container
61
+
62
+ # Pre-register basic primitive context dependencies
63
+ container.register_instance("runtime.config", config)
64
+ container.register_instance("runtime.config_service", config_service)
65
+ container.register_instance("runtime.console", console)
66
+ container.register_instance("runtime.logger", logger)
67
+ container.register_instance("runtime.workspace", workspace)
68
+ container.register_instance("runtime.config_path", config_path)
69
+ container.register_instance("runtime.lifecycle", lifecycle)
70
+
71
+ # Detect and persist GPU info
72
+ try:
73
+ from velune.providers.discovery.gpu import GPUDetector
74
+
75
+ gpu_info = GPUDetector().detect()
76
+ except Exception as e:
77
+ logger.warning("GPU detection failed, using safe defaults: %s", e)
78
+ gpu_info = {
79
+ "has_gpu": False,
80
+ "gpu_type": None,
81
+ "vram_total_gb": None,
82
+ "vram_free_gb": None,
83
+ "cuda_available": False,
84
+ }
85
+ container.register_instance("runtime.gpu_info", gpu_info)
86
+ mark("gpu detected")
87
+
88
+ # 3. Initialize and bootstrap declarative subsystems in dependency order
89
+ from velune.kernel.bootstrap import RuntimeBootstrapper, RuntimeEnvironment
90
+ from velune.kernel.modules import ALL_MODULES
91
+
92
+ env = RuntimeEnvironment(
93
+ workspace=workspace,
94
+ config=config,
95
+ container=container,
96
+ lifecycle=lifecycle,
97
+ verbose=verbose,
98
+ )
99
+
100
+ bootstrapper = RuntimeBootstrapper()
101
+ for module in ALL_MODULES:
102
+ bootstrapper.register_module(module)
103
+ bootstrapper.bootstrap(env)
104
+ mark("subsystems bootstrapped")
105
+
106
+ return RuntimeContext(
107
+ workspace=workspace,
108
+ config_path=config_path,
109
+ config=config,
110
+ console=console,
111
+ logger_name=logger_name,
112
+ container=container,
113
+ )
@@ -0,0 +1,56 @@
1
+ """Lightweight startup phase profiler.
2
+
3
+ Enable with ``VELUNE_PROFILE_STARTUP=1`` to print a per-phase timing breakdown
4
+ of the boot path to stderr. Each ``mark()`` prints immediately (with the delta
5
+ since the previous mark), so the breakdown survives even if startup crashes
6
+ mid-phase — invaluable for diagnosing where the time goes on a given machine.
7
+
8
+ Zero overhead when disabled: ``mark()`` short-circuits on the first line.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import atexit
14
+ import os
15
+ import sys
16
+ import time
17
+
18
+ _ENABLED = os.environ.get("VELUNE_PROFILE_STARTUP", "").lower() in ("1", "true", "yes")
19
+ _PROCESS_START = time.perf_counter()
20
+ _marks: list[tuple[str, float]] = []
21
+ _reported = False
22
+
23
+
24
+ def enabled() -> bool:
25
+ return _ENABLED
26
+
27
+
28
+ def mark(label: str) -> None:
29
+ """Record and print a startup phase boundary (no-op unless enabled)."""
30
+ if not _ENABLED:
31
+ return
32
+ now = time.perf_counter()
33
+ prev = _marks[-1][1] if _marks else _PROCESS_START
34
+ _marks.append((label, now))
35
+ delta = now - prev
36
+ elapsed = now - _PROCESS_START
37
+ print(
38
+ f"[velune startup] +{elapsed:6.3f}s Δ{delta:6.3f}s {label}",
39
+ file=sys.stderr,
40
+ flush=True,
41
+ )
42
+
43
+
44
+ def report() -> None:
45
+ """Print a final total. Registered via atexit; safe to call directly."""
46
+ global _reported
47
+ if not _ENABLED or _reported:
48
+ return
49
+ _reported = True
50
+ total = time.perf_counter() - _PROCESS_START
51
+ print(f"[velune startup] ── total {total:.3f}s ──", file=sys.stderr, flush=True)
52
+
53
+
54
+ if _ENABLED:
55
+ atexit.register(report)
56
+ mark("interpreter -> profiler import")
@@ -0,0 +1,117 @@
1
+ """Structured concurrency manager for tracking, timing out, and cancelling background tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import threading
8
+ from collections.abc import Awaitable, Callable
9
+ from typing import Any, TypeVar
10
+
11
+ T = TypeVar("T")
12
+ logger = logging.getLogger("velune.core.task_registry")
13
+
14
+
15
+ class BackgroundTaskRegistry:
16
+ """Tracks in-flight asyncio tasks with timeout and cancellation support."""
17
+
18
+ def __init__(self) -> None:
19
+ self._tasks: dict[str, asyncio.Task] = {}
20
+ self._lock = threading.Lock()
21
+ self._submitted_count = 0
22
+ self._dropped_count = 0
23
+
24
+ def submit(
25
+ self,
26
+ name: str,
27
+ coro: Awaitable[T],
28
+ timeout_seconds: float = 60.0,
29
+ on_error: Callable[[Exception], None] | None = None,
30
+ ) -> asyncio.Task | None:
31
+ """Submit a named background task from an async context only."""
32
+ try:
33
+ running_loop = asyncio.get_running_loop()
34
+ except RuntimeError:
35
+ logger.error(
36
+ "BackgroundTaskRegistry.submit('%s') called outside an async context. "
37
+ "Background tasks must be submitted from within a running event loop.",
38
+ name,
39
+ )
40
+ with self._lock:
41
+ self._dropped_count += 1
42
+ return None
43
+
44
+ with self._lock:
45
+ self._submitted_count += 1
46
+
47
+ async def _wrapped() -> Any:
48
+ try:
49
+ result = await asyncio.wait_for(coro, timeout=timeout_seconds)
50
+ logger.debug("Background task '%s' completed.", name)
51
+ return result
52
+ except TimeoutError:
53
+ logger.warning("Background task '%s' timed out after %.1fs", name, timeout_seconds)
54
+ except asyncio.CancelledError:
55
+ raise
56
+ except Exception as exc:
57
+ logger.error("Background task '%s' failed: %s", name, exc)
58
+ if on_error:
59
+ try:
60
+ on_error(exc)
61
+ except Exception as inner:
62
+ logger.error("Error handler for task '%s' failed: %s", name, inner)
63
+ finally:
64
+ with self._lock:
65
+ self._tasks.pop(name, None)
66
+
67
+ task = running_loop.create_task(_wrapped(), name=name)
68
+ with self._lock:
69
+ self._tasks[name] = task
70
+ return task
71
+
72
+ async def cancel_all(self, timeout: float = 5.0) -> None:
73
+ """Cancel all pending tasks and wait for completion."""
74
+ with self._lock:
75
+ tasks = [t for t in self._tasks.values() if not t.done()]
76
+
77
+ if not tasks:
78
+ return
79
+
80
+ for task in tasks:
81
+ task.cancel()
82
+
83
+ # Give tasks a chance to handle CancelledError
84
+ done, pending = await asyncio.wait(tasks, timeout=timeout)
85
+
86
+ if pending:
87
+ logger.warning("%d background tasks did not cancel within %.1fs", len(pending), timeout)
88
+
89
+ # Shutdown stats reporting of dropped tasks
90
+ stats = self.stats()
91
+ if stats["dropped"] > 0:
92
+ logger.info("Task registry shutdown: %d dropped tasks reported.", stats["dropped"])
93
+
94
+ with self._lock:
95
+ self._tasks.clear()
96
+
97
+ def pending_count(self) -> int:
98
+ """Expose pending tasks count for health checks."""
99
+ with self._lock:
100
+ return len([t for t in self._tasks.values() if not t.done()])
101
+
102
+ def is_healthy(self) -> bool:
103
+ """Check if task registry is healthy."""
104
+ try:
105
+ loop = asyncio.get_running_loop()
106
+ loop_alive = loop.is_running()
107
+ except Exception:
108
+ loop_alive = False
109
+ return loop_alive and self.pending_count() < 50
110
+
111
+ def stats(self) -> dict[str, int]:
112
+ """Return task submission telemetry stats."""
113
+ with self._lock:
114
+ return {
115
+ "submitted": self._submitted_count,
116
+ "dropped": self._dropped_count,
117
+ }