Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENT_DIARY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Key Historical Decisions

- **PyPI-packaging + user-data isolation (2026-08-28):** wheel теперь содержит `tools.stale_detector`, `adapters`, `locales` (норм. данные в site-packages); бинарники/модели в pip-режиме — `get_data_root()` (`%LOCALAPPDATA%\mscodebase`), гейт-маркер `__mscodebase_ext__.marker` отличает расширение от установленного пакета; CLI `--project-path/--project-dir` → env `MSCODEBASE_PROJECT_PATH` (приоритет НАД CWD, trust_self_index); `PROJECT_PATH` остался после CWD (multi-window сохранён). Live-Smoke из чистого venv: tools-ok, `en (78 ключей)`, корень резолвится. Файлы: `pyproject.toml`, `project_resolution.py`, `main.py`, `llama_install.py`. INC-C4CD.

- **Server freeze during full reindex (2026-08-25):** root cause — `begin_write()` держит `_write_lock` (RLock) весь reindex (~7.5 мин embedding), а `IndexStatusReporter.get_status()` синхронно на event-loop-потоке ждал тот же lock (intel_get_runtime_status/require_ready_project/ProjectContext) → заморозка ВСЕХ MCP-вызовов. Фикс: reindex fast-fail в get_status (кэш + status="reindexing") + asyncio.to_thread в 3 loop-точках + guard в _get_stale_warning.
- **Embedder:** multilingual-e5-small-int8 + batch=32 (100 ch/s sustained) — 2026-07-17
- **Concurrency:** AsyncInferQueue → лок (тихая гонка подмены векторов) — 2026-07-18
Expand Down
8 changes: 8 additions & 0 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@

---

## 2026-08-28 — PyPI-упаковка: `tools`/`locales`/`adapters` вне wheel, модели/бинарники в site-packages, нет `--project-path` (CLOSED)

**Что:** При упаковке в PyPI-пакет (wheel) сервер не стартовал из установленного пакета по трём причинам: (1) `ModuleNotFoundError: No module named 'tools'` — `tools.stale_detector` (single source of truth для stale-проверки, инцидент 2026-08-14) не был в `include`; (2) `locales/` и `adapters/` не пакетизировались — i18n-предупреждения при старте и `--install` не работал из пакета; (3) в pip-режиме `_get_ext_dir()` мог писать бинарники/модели в site-packages; не было CLI-выбора целевого проекта.
**Fix:** `pyproject.toml` `include += ["tools.stale_detector*", "adapters*", "locales*"]`, `package-data "locales" = ["*.json"]`; добавить `locales/__init__.py`; `_get_ext_dir()` гейтится маркером `__mscodebase_ext__.marker` → без маркера (pip/uvx) бинарники/модели уходят в `get_data_root()` (`%LOCALAPPDATA%\mscodebase`); CLI `--project-path/--project-dir` → env `MSCODEBASE_PROJECT_PATH` (приоритет над CWD, trust_self_index) в `resolve_project_root()`; `PROJECT_PATH` остался после CWD (обратная совместимость/multi-window).
**Guard:** пустые `tools/__init__.py`, `tools/stale_detector/__init__.py`, `locales/__init__.py`; Live-Smoke из чистого venv с `--project-path` (tools-ok, locales `en (78 ключей)`, корень проекта резолвится); полный прогон `1562 passed / 0 failed`; +4 теста на `MSCODEBASE_PROJECT_PATH` (multiwindow). Удалён side-effect-мусор `write_text(r"C:\temp\mscb_MAIN.txt")` в `write_tools.py` (из исходников и wheel).
**Статус:** ✅ CLOSED (live-верифицирован из wheel, чистая установка) | **Deadline:** — | **Владелец:** misha.
**Note:** `tools/__init__.py` не попадает в wheel (namespace package PEP-420) — import `doc_tools.py:14-15` работает; безвредно.

## 2026-08-28 — Full reindex зависает в фазе «Finalizing» (PropertyGraph.optimize/create_index) (OPEN / WATCHING)

**Что:** При live-верификации фиксов A/B (полный реиндекс job 1ff77294) embedding-фаза прошла БЕЗ заморозки сервера (подтверждает фикс A — QueueHandler), chunks записались корректно (1-based; см. AGENT_DIARY post-mortem off-by-one: save_symbol_index=341). После embed job завис в фазе «Finalizing» (отладка через intel_get_job_status + лог + netstat/py-spy: оба процесса 0% CPU — заблокированы, не считают) на `PropertyGraph.optimize()`/`create_index()` (`src/core/intelligence/layer.py:741` и `:1786`). Флаг `set_reindexing(True)` не снимается → блокирует concurrent search. Это PRE-EXISTING баг индексатора/графа, НЕ вызван фиксами A/B (фиксы касаются только логирования и display-строки; они live-верифицированы на этапе embed).
Expand Down
Empty file added locales/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,12 @@ mscodebase-mcp = "src.main:main"

[tool.setuptools.packages.find]
where = ["."]
include = ["src*", "scripts*"]
include = ["src*", "scripts*", "tools.stale_detector*", "adapters*", "locales*"]

[tool.setuptools.package-data]
"*" = ["*.json", "*.md", "*.bat", "*.cfg", "*.scm"]
"src" = ["*.json", "*.txt", "*.cfg", "*.scm"]
"locales" = ["*.json"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
106 changes: 68 additions & 38 deletions src/core/project_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,49 @@ def _reject_self_index_target(p: Path, *, source: str) -> bool:
return False


def _try_resolve_path_env(raw: str, source: str, *, trust_self_index: bool = False) -> Optional[Path]:
"""Резолвит путь из env-raw (литерал $ZED_WORKTREE_ROOT или прямой путь).

Общая логика для PROJECT_PATH и MSCODEBASE_PROJECT_PATH. Возвращает Path
или None (невалид / self-indexing / пусто).

trust_self_index=True — доверяем явному override даже если это ext_root
(аналог ACTIVE_WORKSPACE). Используется ТОЛЬКО для MSCODEBASE_PROJECT_PATH
(клиент осознанно указал проект через --project-path); PROJECT_PATH
сохраняет блокировку self-indexing для обратной совместимости.
"""
if not raw:
return None
# Случай 1: Zed literal "$ZED_WORKTREE_ROOT" без подстановки.
if raw.startswith("$"):
zed_root = os.environ.get("ZED_WORKTREE_ROOT")
if zed_root:
p = Path(zed_root).resolve()
if p.exists() and not _reject_self_index_target(p, source="ZED_WORKTREE_ROOT"):
return p
return None
# Случай 2: прямой путь.
try:
resolved = Path(raw).resolve()
except (OSError, ValueError):
return None
if not resolved.exists() or not resolved.is_dir():
return None
# Self-indexing guard: для автоматических fallback'ов блокируем ext_root;
# для явного CLI override (trust_self_index=True) — доверяем.
if (
not trust_self_index
and _reject_self_index_target(resolved, source=source)
and os.environ.get("MSCODEBASE_ALLOW_SELF_INDEX", "").strip().lower() not in ("1", "true", "yes")
):
logger.warning(
f"{source} указывает на self-indexing target ({resolved}). "
f"Игнорирую — установите MSCODEBASE_ALLOW_SELF_INDEX=1 или используйте ACTIVE_WORKSPACE."
)
return None
return resolved


def _resolve_env_project_root() -> Optional[Path]:
"""Резолвит PROJECT_PATH из окружения лениво + один раз кэширует результат.

Expand All @@ -216,38 +259,9 @@ def _resolve_env_project_root() -> Optional[Path]:
if _env_project_root_cache is not None:
return _env_project_root_cache
raw = os.environ.get("PROJECT_PATH", "").strip()
if not raw:
return None
# Случай 1: Zed literal "$ZED_WORKTREE_ROOT" без подстановки.
if raw.startswith("$"):
zed_root = os.environ.get("ZED_WORKTREE_ROOT")
if zed_root:
p = Path(zed_root).resolve()
if p.exists() and not _reject_self_index_target(
p, source="ZED_WORKTREE_ROOT"
):
_env_project_root_cache = p
return _env_project_root_cache
return None
# Случай 2: прямой путь.
try:
resolved = Path(raw).resolve()
except (OSError, ValueError):
return None
if not resolved.exists() or not resolved.is_dir():
return None
# Self-indexing guard (см. INC-53EC / REFC-02): если PROJECT_PATH
# Если пользователь ЯВНО задал PROJECT_PATH — доверяем ему,
# не блокируем self-indexing guard. Он знает, что делает.
# Только автоматический fallback (CWD/ext_root) блокируем.
if _reject_self_index_target(resolved, source="PROJECT_PATH") and os.environ.get("MSCODEBASE_ALLOW_SELF_INDEX", "").strip() not in ("1", "true", "yes"):
logger.warning(
f"PROJECT_PATH указывает на self-indexing target ({resolved}). "
f"Игнорирую — установите PROJECT_PATH=$ZED_WORKTREE_ROOT или MSCODEBASE_ALLOW_SELF_INDEX=1."
)
return None
_env_project_root_cache = resolved
return _env_project_root_cache
env_root = _try_resolve_path_env(raw, "PROJECT_PATH")
_env_project_root_cache = env_root
return env_root


def reset_project_root_cache() -> None:
Expand All @@ -262,23 +276,39 @@ def resolve_project_root(provided: str = "") -> Path:

Приоритет (каждый вызов резолвит заново — см. INC-53EC / REFC-02):
0. Явно переданный provided
1. CWD (корень окна, для которого запущен MCP-процесс) — per-window
1. MSCODEBASE_PROJECT_PATH env (явный override CLI/владельца, НАД CWD).
Приоритет над CWD — только для этого явного override; обычный Zed-режим
CWD-first сохраняет per-window изоляцию (INC-MULTI-WINDOW).
2. CWD (корень окна, для которого запущен MCP-процесс) — per-window
изоляция (INC-MULTI-WINDOW): Zed запускает отдельный MCP на окно
и ставит CWD = корень окна. SQLite active_workspace_id глобальный
на весь Zed — два окна резолвят один проект → PID-lock конфликт.
Self-indexing guard: CWD == ext_root отклоняется (dev/test-режим
добирается через SQLite active_workspace с доверием ACTIVE_WORKSPACE).
2. PROJECT_PATH из окружения (lazy, с self-indexing guard) — явный
3. PROJECT_PATH из окружения (lazy, с self-indexing guard) — явный
override пользователя; Zed-литерал "$ZED_WORKTREE_ROOT" → None.
3. SQLite multi_workspace_state.active_workspace_id (fallback для
4. SQLite multi_workspace_state.active_workspace_id (fallback для
single-window / когда CWD отклонён self-indexing guard'ом)
4. Zed SQLite DB (workspaces table — fallback, если нет active)
5. ZED_WORKTREE_ROOT env
6. ext_root как fallback
5. Zed SQLite DB (workspaces table — fallback, если нет active)
6. ZED_WORKTREE_ROOT env
7. ext_root как fallback
"""
if provided and provided.strip():
return Path(provided).resolve()

# ─── 0.5. MSCODEBASE_PROJECT_PATH: явный override НАД CWD ───
# Отличается от PROJECT_PATH (см. ниже, позиция 3): имеет приоритет над
# CWD, т.к. это намеренный override (CLI --project-path устанавливает
# именно его). В обычном Zed-режиме env не задан → CWD-first сохраняется.
prio_raw = os.environ.get("MSCODEBASE_PROJECT_PATH", "").strip()
if prio_raw:
prio_root = _try_resolve_path_env(
prio_raw, "MSCODEBASE_PROJECT_PATH", trust_self_index=True
)
if prio_root is not None:
logger.debug(f"resolve_project_root: MSCODEBASE_PROJECT_PATH={prio_root}")
return prio_root

# ─── 1. CWD-FIRST: per-window изоляция (INC-MULTI-WINDOW) ───
# Раньше CWD был предпоследним в цепочке, а SQLite active_workspace_id
# (один на весь Zed) — первым: оба окна резолвили один проект →
Expand Down
35 changes: 35 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,44 @@ def _gc_worker() -> None:
pass

return original_stdout


def _cli_project_path() -> str:
"""Извлекает --project-path/--project-dir из sys.argv (значение или после =).

Возвращает пустую строку, если аргумент не передан. Значение — путь
к целевому проекту (корень, для которого работают MCP-инструменты).
"""
for flag in ("--project-path", "--project-dir"):
for i, arg in enumerate(sys.argv):
if arg == flag and i + 1 < len(sys.argv):
return sys.argv[i + 1].strip()
if arg.startswith(flag + "="):
return arg[len(flag) + 1:].strip()
return ""


def main():
"""Главная функция запуска MCP-сервера."""
original_stdout = setup_logging()
logger = logging.getLogger("MSCodebase")
logger.info("MSCodebase Intelligence MCP Server запускается...")
logger.info(f"PROJECT_ROOT: {PROJECT_ROOT}")

# ─── CLI --project-path: явный выбор целевого проекта ───
# Устанавливаем MSCODEBASE_PROJECT_PATH (приоритет над CWD, см.
# resolve_project_root). PROJECT_ROOT (корень установки/расширения) НЕ
# трогаем — он не является корнем целевого проекта. Env задаётся ДО
# любого вызова resolve_project_root (в т.ч. при --install резолв корня).
_cli_proj = _cli_project_path()
if _cli_proj and not os.environ.get("MSCODEBASE_PROJECT_PATH"):
try:
_resolved_cli = str(Path(_cli_proj).resolve())
except (OSError, ValueError):
_resolved_cli = _cli_proj
os.environ["MSCODEBASE_PROJECT_PATH"] = _resolved_cli
logger.info(f"CLI --project-path: {_resolved_cli}")

try:
# Обработка аргументов командной строки
if "--help" in sys.argv or "-h" in sys.argv:
Expand All @@ -197,6 +228,10 @@ def main():
" python -m src.main # Запуск MCP сервера",
file=sys.stderr,
)
print(
" python -m src.main --project-path <DIR> # Запуск на указанном проекте",
file=sys.stderr,
)
print(
" python -m src.main --install # Установка в проект (.zed/settings.json)",
file=sys.stderr,
Expand Down
14 changes: 9 additions & 5 deletions src/providers/reranker/llama_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,19 @@ def _get_ext_dir() -> Path:
# Frozen (PyInstaller) — sys.executable это сам бинарник
if getattr(sys, "frozen", False):
return Path(sys.executable).parent.parent.parent
# Расширение: .../venv/Scripts/python.exe → …/
# Расширение: .../venv/Scripts/python.exe → …/ (маркер расширения обязателен).
# Маркер __mscodebase_ext__.marker отличает исходники/расширение от
# установленного pip/wheel-пакета: в site-packages маркера НЕТ, поэтому
# бинарники/модели НЕ пишутся в site-packages, а уходят в data_root.
p = Path(sys.executable).resolve().parent.parent.parent
if (p / "src" / "main.py").exists():
if (p / "src" / "main.py").exists() and (p / "__mscodebase_ext__.marker").exists():
return p
# Режим разработки
# Режим разработки (исходники с маркером расширения в корне)
p = Path(__file__).resolve().parent.parent.parent
if (p / "src" / "main.py").exists():
if (p / "src" / "main.py").exists() and (p / "__mscodebase_ext__.marker").exists():
return p
# Последний fallback: единый data root (исторически ~/.cache/mscodebase).
# Установленный пакет (pip/uvx) или неизвестный контекст: единый data root
# (%LOCALAPPDATA%/mscodebase | ~/.cache/mscodebase) — НЕ site-packages.
from src.core.artifact_paths import get_data_root

return get_data_root()
Expand Down
Loading
Loading