codrspot-processor-mcp 0.1.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 (100) hide show
  1. codepreproc_client/__init__.py +14 -0
  2. codepreproc_client/__main__.py +9 -0
  3. codepreproc_client/layer1_business/__init__.py +0 -0
  4. codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
  5. codepreproc_client/layer1_business/api_client/__init__.py +0 -0
  6. codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
  7. codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
  8. codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
  9. codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
  10. codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
  11. codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
  12. codepreproc_client/layer1_business/cli/__init__.py +0 -0
  13. codepreproc_client/layer1_business/cli/init.py +578 -0
  14. codepreproc_client/layer1_business/cli/main.py +78 -0
  15. codepreproc_client/layer1_business/config.py +104 -0
  16. codepreproc_client/layer1_business/git_utils.py +67 -0
  17. codepreproc_client/layer1_business/license/__init__.py +0 -0
  18. codepreproc_client/layer1_business/license/fingerprint.py +61 -0
  19. codepreproc_client/layer1_business/license/jwt_client.py +60 -0
  20. codepreproc_client/layer1_business/license/project_lock.py +44 -0
  21. codepreproc_client/layer1_business/local_registry.py +40 -0
  22. codepreproc_client/layer1_business/manifest/__init__.py +0 -0
  23. codepreproc_client/layer1_business/manifest/credentials.py +35 -0
  24. codepreproc_client/layer1_business/mcp/__init__.py +0 -0
  25. codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
  26. codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
  27. codepreproc_client/layer1_business/mcp/server.py +656 -0
  28. codepreproc_client/layer1_business/mcp/tools.py +1665 -0
  29. codepreproc_client/layer1_business/project_registry.py +99 -0
  30. codepreproc_client/layer2_tooling/__init__.py +0 -0
  31. codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
  32. codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
  33. codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
  34. codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
  35. codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
  36. codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
  37. codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
  38. codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
  39. codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
  40. codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
  41. codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
  42. codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
  43. codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
  44. codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
  45. codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
  46. codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
  47. codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
  48. codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
  49. codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
  50. codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
  51. codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
  52. codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
  53. codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
  54. codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
  55. codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
  56. codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
  57. codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
  58. codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
  59. codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
  60. codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
  61. codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
  62. codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
  63. codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
  64. codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
  65. codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
  66. codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
  67. codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
  68. codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
  69. codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
  70. codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
  71. codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
  72. codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
  73. codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
  74. codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
  75. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
  76. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
  77. codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
  78. codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
  79. codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
  80. codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
  81. codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
  82. codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
  83. codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
  84. codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
  85. codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
  86. codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
  87. codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
  88. codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
  89. codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
  90. codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
  91. codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
  92. codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
  93. codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
  94. codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
  95. codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
  96. codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
  97. codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
  98. contracts/__init__.py +0 -0
  99. contracts/dtos.py +1239 -0
  100. contracts/ir.py +102 -0
@@ -0,0 +1,47 @@
1
+ """Dependency expansion over the AST graph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from collections.abc import Callable
7
+
8
+ from codepreproc_client.layer2_tooling.condensation.ast_graph import AstGraph
9
+ from contracts.dtos import CodeChunk
10
+
11
+
12
+ class GraphWalker:
13
+ """Expand from seed chunks to nearby callers and callees."""
14
+
15
+ def __init__(self, max_chunks: int = 30) -> None:
16
+ self.max_chunks = max_chunks
17
+
18
+ def expand(
19
+ self,
20
+ seed_chunks: list[CodeChunk],
21
+ ast_graph: AstGraph,
22
+ chunk_loader: Callable[[list[str]], list[CodeChunk]],
23
+ depth: int,
24
+ ) -> list[CodeChunk]:
25
+ """Return extra chunks up to a fixed graph depth."""
26
+
27
+ symbol_map, callees_map, callers_map = ast_graph.load_for_walk_cached()
28
+
29
+ seen = {chunk.chunk_id for chunk in seed_chunks}
30
+ queue = deque((chunk.chunk_id, chunk.symbol, 0) for chunk in seed_chunks)
31
+ discovered: list[str] = []
32
+
33
+ while queue and len(discovered) < self.max_chunks:
34
+ chunk_id, symbol, level = queue.popleft()
35
+ if level >= depth:
36
+ continue
37
+ neighbors = callees_map.get(chunk_id, []) + callers_map.get(symbol, [])
38
+ for neighbor in neighbors:
39
+ if neighbor in seen:
40
+ continue
41
+ seen.add(neighbor)
42
+ discovered.append(neighbor)
43
+ nb_sym = symbol_map.get(neighbor) or symbol
44
+ queue.append((neighbor, nb_sym, level + 1))
45
+ if len(discovered) >= self.max_chunks:
46
+ break
47
+ return chunk_loader(discovered)
@@ -0,0 +1,95 @@
1
+ """Hybrid retrieval using dense plus BM25 fusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from codepreproc_client.layer2_tooling.condensation.bm25_store import BM25Store
10
+ from codepreproc_client.layer2_tooling.condensation.embeddings import Embedder
11
+ from codepreproc_client.layer2_tooling.condensation.qdrant_store import QdrantStore
12
+ from contracts.dtos import ProjectConfig, RetrievalConfig, ScoredChunk
13
+
14
+ if TYPE_CHECKING:
15
+ from codepreproc_client.layer2_tooling.condensation.ast_graph import AstGraph
16
+
17
+
18
+ class HybridRetriever:
19
+ """Fuse dense and lexical retrieval with RRF."""
20
+
21
+ def __init__(self, embedder: Embedder, qdrant_store: QdrantStore, bm25_store: BM25Store, config: RetrievalConfig) -> None:
22
+ self.embedder = embedder
23
+ self.qdrant_store = qdrant_store
24
+ self.bm25_store = bm25_store
25
+ self.config = config
26
+
27
+ async def retrieve(
28
+ self,
29
+ project: ProjectConfig,
30
+ query: str,
31
+ overlay_collection: str | None,
32
+ *,
33
+ collection_name: str | None = None,
34
+ user_collection_name: str | None = None,
35
+ monitor=None,
36
+ ast_graph: "AstGraph | None" = None,
37
+ md_harness_top_k: int = 3,
38
+ ) -> list[ScoredChunk]:
39
+ """Retrieve candidate chunks, optionally prepending directory-scoped MD harness."""
40
+
41
+ query_vec = (await self.embedder.encode_batch([query], monitor=monitor, stage_name="retrieve"))[0]
42
+ dense_results = []
43
+ active_collection = collection_name or project.qdrant_collection
44
+ if active_collection:
45
+ dense_results = self.qdrant_store.search(active_collection, query_vec, self.config.dense_top_k)
46
+ if user_collection_name and self.qdrant_store.collection_exists(user_collection_name):
47
+ dense_results.extend(self.qdrant_store.search(user_collection_name, query_vec, self.config.dense_top_k))
48
+ if overlay_collection:
49
+ dense_results.extend(self.qdrant_store.search(overlay_collection, query_vec, self.config.dense_top_k))
50
+ bm25_results = self.bm25_store.search(query, self.config.bm25_top_k)
51
+ results = self._rrf_fuse(dense_results, bm25_results)[: self.config.rerank_top_k]
52
+
53
+ if ast_graph is not None and md_harness_top_k > 0:
54
+ harness = self._get_md_harness(ast_graph, results, top_k=md_harness_top_k)
55
+ if harness:
56
+ existing_ids = {item.chunk.chunk_id for item in results}
57
+ new_harness = [h for h in harness if h.chunk.chunk_id not in existing_ids]
58
+ results = new_harness + results
59
+
60
+ return results
61
+
62
+ def _get_md_harness(self, ast_graph: "AstGraph", results: list[ScoredChunk], top_k: int = 3) -> list[ScoredChunk]:
63
+ """Fetch directory-scoped MD knowledge chunks for the retrieved results."""
64
+ file_paths = [item.chunk.file_path for item in results if item.chunk.language != 'markdown']
65
+ if not file_paths:
66
+ return []
67
+ dir_paths = self._dir_hierarchy(file_paths)
68
+ chunk_ids = ast_graph.get_md_chunk_ids_for_dirs(dir_paths)
69
+ if not chunk_ids:
70
+ return []
71
+ md_chunks = self.bm25_store.get_by_ids(chunk_ids[:top_k])
72
+ return [ScoredChunk(chunk=c, score=1.0, source="md_harness") for c in md_chunks]
73
+
74
+ @staticmethod
75
+ def _dir_hierarchy(file_paths: list[str]) -> list[str]:
76
+ """Return all unique ancestor directory paths for the given files."""
77
+ dirs: set[str] = set()
78
+ for fp in file_paths:
79
+ parts = Path(fp).parent.parts
80
+ dirs.add('.')
81
+ for i in range(1, len(parts) + 1):
82
+ dirs.add('/'.join(parts[:i]))
83
+ return sorted(dirs)
84
+
85
+ @staticmethod
86
+ def _rrf_fuse(*rank_lists: list[ScoredChunk], k: int = 60) -> list[ScoredChunk]:
87
+ scores: dict[str, float] = defaultdict(float)
88
+ chunks: dict[str, ScoredChunk] = {}
89
+ for rank_list in rank_lists:
90
+ for rank, item in enumerate(rank_list, start=1):
91
+ scores[item.chunk.chunk_id] += 1.0 / (k + rank)
92
+ chunks[item.chunk.chunk_id] = item
93
+ fused = [ScoredChunk(chunk=chunks[key].chunk, score=value, source="rrf") for key, value in scores.items()]
94
+ fused.sort(key=lambda item: item.score, reverse=True)
95
+ return fused
@@ -0,0 +1,86 @@
1
+ """Cross-encoder reranker subprocess worker.
2
+
3
+ Launched as ``python -u -m codepreproc_client.layer2_tooling.retrieval.rerank_worker``.
4
+ Communicates with the parent via stdin/stdout JSON lines.
5
+
6
+ Protocol
7
+ --------
8
+ Parent → worker (stdin):
9
+ First line: {"model_name": "...", "device": "..."}
10
+ Subsequent: {"type": "rerank", "request_id": "...", "pairs": [[q, t], ...], "batch_size": N}
11
+ or {"type": "shutdown"}
12
+
13
+ Worker → parent (stdout):
14
+ {"type": "worker_ready"}
15
+ {"type": "worker_failed", "error": "..."}
16
+ {"type": "rerank_done", "request_id": "...", "scores": [...]}
17
+ {"type": "failed", "request_id": "...", "error": "..."}
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import sys
24
+
25
+
26
+ def _send(event: dict) -> None:
27
+ sys.stdout.write(json.dumps(event) + "\n")
28
+ sys.stdout.flush()
29
+
30
+
31
+ def main() -> None:
32
+ config_line = sys.stdin.readline()
33
+ if not config_line:
34
+ return
35
+ try:
36
+ config = json.loads(config_line)
37
+ except json.JSONDecodeError as exc:
38
+ _send({"type": "worker_failed", "error": f"bad config line: {exc}"})
39
+ return
40
+
41
+ model_name = config["model_name"]
42
+ device = config["device"]
43
+
44
+ try:
45
+ from sentence_transformers import CrossEncoder
46
+
47
+ model = CrossEncoder(model_name, device=device)
48
+ _send({"type": "worker_ready"})
49
+ except Exception as exc:
50
+ _send({"type": "worker_failed", "error": str(exc)})
51
+ return
52
+
53
+ for raw_line in sys.stdin:
54
+ line = raw_line.strip()
55
+ if not line:
56
+ continue
57
+ try:
58
+ command = json.loads(line)
59
+ except json.JSONDecodeError:
60
+ continue
61
+
62
+ if command["type"] == "shutdown":
63
+ return
64
+ if command["type"] != "rerank":
65
+ continue
66
+
67
+ request_id = command["request_id"]
68
+ pairs = command["pairs"]
69
+ batch_size = max(1, int(command["batch_size"]))
70
+
71
+ try:
72
+ while True:
73
+ try:
74
+ scores = model.predict(pairs, batch_size=batch_size, show_progress_bar=False)
75
+ break
76
+ except RuntimeError as exc:
77
+ if "out of memory" not in str(exc).lower() or batch_size == 1:
78
+ raise
79
+ batch_size = max(1, batch_size // 2)
80
+ _send({"type": "rerank_done", "request_id": request_id, "scores": [float(s) for s in scores]})
81
+ except Exception as exc:
82
+ _send({"type": "failed", "request_id": request_id, "error": str(exc)})
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
@@ -0,0 +1,213 @@
1
+ """Cross-encoder reranker.
2
+
3
+ On non-Windows platforms the model is loaded in a thread pool via
4
+ ``asyncio.to_thread``. On Windows, loading PyTorch models inside a thread
5
+ pool deadlocks due to the Windows DLL Loader Lock, so we launch a dedicated
6
+ ``subprocess.Popen`` pointing at ``codepreproc_client.layer2_tooling.retrieval.rerank_worker`` which
7
+ loads the model in its own main thread.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import os
15
+ import queue
16
+ import subprocess
17
+ import sys
18
+ import threading
19
+ import uuid
20
+ from typing import Any, ClassVar
21
+
22
+ from codepreproc_client.layer1_business.mcp.action_monitor import ToolExecutionError
23
+ from contracts.dtos import DefaultsConfig, ScoredChunk
24
+
25
+
26
+ class Reranker:
27
+ """Lazy reranker singleton."""
28
+
29
+ _instance: ClassVar["Reranker" | None] = None
30
+
31
+ def __init__(self, defaults: DefaultsConfig) -> None:
32
+ self.defaults = defaults
33
+ self._use_subprocess_worker = os.name == "nt"
34
+ # --- in-process state (non-Windows) ---
35
+ self._model = None
36
+ # --- subprocess state (Windows) ---
37
+ self._proc: subprocess.Popen | None = None # type: ignore[type-arg]
38
+ self._proc_event_queue: queue.Queue | None = None
39
+ self._reader_thread: threading.Thread | None = None
40
+ self._worker_lock = asyncio.Lock()
41
+
42
+ @classmethod
43
+ def get(cls, defaults: DefaultsConfig) -> "Reranker":
44
+ """Return singleton instance."""
45
+
46
+ if cls._instance is None:
47
+ cls._instance = cls(defaults)
48
+ return cls._instance
49
+
50
+ # ------------------------------------------------------------------
51
+ # Public interface
52
+ # ------------------------------------------------------------------
53
+
54
+ async def rerank(self, query: str, chunks: list[ScoredChunk], top_k: int) -> list[ScoredChunk]:
55
+ """Rerank without blocking the event loop."""
56
+
57
+ if not chunks:
58
+ return []
59
+ async with self._worker_lock:
60
+ if self._use_subprocess_worker:
61
+ return await self._rerank_subprocess(query, chunks, top_k)
62
+ return await asyncio.to_thread(self.rerank_sync, query, chunks, top_k)
63
+
64
+ def rerank_sync(self, query: str, chunks: list[ScoredChunk], top_k: int) -> list[ScoredChunk]:
65
+ """Rerank synchronously (non-Windows / direct call)."""
66
+
67
+ if not chunks:
68
+ return []
69
+ model = self._ensure_model()
70
+ pairs = [(query, f"{item.chunk.file_path}::{item.chunk.symbol}\n{item.chunk.body[:1500]}") for item in chunks]
71
+ batch_size = min(16, max(1, len(pairs)))
72
+ while batch_size >= 1:
73
+ try:
74
+ scores = model.predict(pairs, batch_size=batch_size, show_progress_bar=False)
75
+ break
76
+ except RuntimeError as exc:
77
+ if "out of memory" not in str(exc).lower() or batch_size == 1:
78
+ raise
79
+ batch_size = max(1, batch_size // 2)
80
+ reranked = [ScoredChunk(chunk=item.chunk, score=float(score), source="rerank") for item, score in zip(chunks, scores, strict=True)]
81
+ reranked.sort(key=lambda item: item.score, reverse=True)
82
+ return reranked[:top_k]
83
+
84
+ # ------------------------------------------------------------------
85
+ # In-process model (non-Windows)
86
+ # ------------------------------------------------------------------
87
+
88
+ def _ensure_model(self):
89
+ if self._model is None:
90
+ from sentence_transformers import CrossEncoder
91
+
92
+ self._model = CrossEncoder(self.defaults.reranker_model, device=self.defaults.reranker_device)
93
+ return self._model
94
+
95
+ # ------------------------------------------------------------------
96
+ # Subprocess worker (Windows)
97
+ # ------------------------------------------------------------------
98
+
99
+ async def _rerank_subprocess(self, query: str, chunks: list[ScoredChunk], top_k: int) -> list[ScoredChunk]:
100
+ await self._ensure_subprocess_worker()
101
+ pairs = [[query, f"{item.chunk.file_path}::{item.chunk.symbol}\n{item.chunk.body[:1500]}"] for item in chunks]
102
+ batch_size = min(16, max(1, len(pairs)))
103
+ request_id = uuid.uuid4().hex
104
+ self._send_subprocess_command(
105
+ {"type": "rerank", "request_id": request_id, "pairs": pairs, "batch_size": batch_size}
106
+ )
107
+ while True:
108
+ event = await self._wait_subprocess_event()
109
+ event_type = event["type"]
110
+ if event.get("request_id") != request_id and event_type not in ("worker_failed", "worker_exited"):
111
+ continue
112
+ if event_type == "rerank_done":
113
+ scores = event["scores"]
114
+ reranked = [
115
+ ScoredChunk(chunk=item.chunk, score=float(score), source="rerank")
116
+ for item, score in zip(chunks, scores, strict=True)
117
+ ]
118
+ reranked.sort(key=lambda item: item.score, reverse=True)
119
+ return reranked[:top_k]
120
+ if event_type == "failed":
121
+ self._restart_subprocess_worker()
122
+ raise ToolExecutionError(
123
+ "rerank_failed",
124
+ event.get("error", "unknown rerank error")[:500],
125
+ details={"stage": "rerank/graph"},
126
+ )
127
+ if event_type in ("worker_failed", "worker_exited"):
128
+ self._restart_subprocess_worker()
129
+ raise ToolExecutionError(
130
+ "rerank_worker_failed",
131
+ event.get("error", "worker exited unexpectedly")[:500],
132
+ details={"stage": "rerank/graph"},
133
+ )
134
+
135
+ async def _ensure_subprocess_worker(self) -> None:
136
+ if self._proc is not None and self._proc.poll() is None:
137
+ return
138
+ self._restart_subprocess_worker()
139
+ self._proc_event_queue = queue.Queue()
140
+ self._proc = subprocess.Popen(
141
+ [sys.executable, "-u", "-m", "codepreproc_client.layer2_tooling.retrieval.rerank_worker"],
142
+ stdin=subprocess.PIPE,
143
+ stdout=subprocess.PIPE,
144
+ stderr=subprocess.DEVNULL,
145
+ )
146
+ config_line = (
147
+ json.dumps({"model_name": self.defaults.reranker_model, "device": self.defaults.reranker_device}) + "\n"
148
+ )
149
+ self._proc.stdin.write(config_line.encode())
150
+ self._proc.stdin.flush()
151
+ self._reader_thread = threading.Thread(target=self._read_subprocess_stdout, daemon=True)
152
+ self._reader_thread.start()
153
+ # Wait for worker_ready
154
+ while True:
155
+ event = await self._wait_subprocess_event()
156
+ event_type = event["type"]
157
+ if event_type == "worker_ready":
158
+ return
159
+ if event_type in ("worker_failed", "worker_exited"):
160
+ self._restart_subprocess_worker()
161
+ raise ToolExecutionError(
162
+ "rerank_worker_failed",
163
+ event.get("error", "worker exited unexpectedly")[:500],
164
+ details={"stage": "rerank/graph"},
165
+ )
166
+
167
+ def _read_subprocess_stdout(self) -> None:
168
+ try:
169
+ for raw_line in self._proc.stdout:
170
+ line = raw_line.decode("utf-8", errors="replace").strip()
171
+ if line:
172
+ try:
173
+ self._proc_event_queue.put(json.loads(line))
174
+ except json.JSONDecodeError:
175
+ pass
176
+ except Exception:
177
+ pass
178
+ finally:
179
+ if self._proc_event_queue is not None:
180
+ self._proc_event_queue.put({"type": "worker_exited"})
181
+
182
+ async def _wait_subprocess_event(self) -> dict[str, Any]:
183
+ while True:
184
+ try:
185
+ return await asyncio.to_thread(self._proc_event_queue.get, True, 1.0)
186
+ except queue.Empty:
187
+ if self._proc is None or self._proc.poll() is not None:
188
+ raise ToolExecutionError(
189
+ "rerank_worker_failed",
190
+ "reranker subprocess exited unexpectedly",
191
+ details={"stage": "rerank/graph"},
192
+ ) from None
193
+
194
+ def _send_subprocess_command(self, command: dict) -> None:
195
+ line = (json.dumps(command) + "\n").encode()
196
+ self._proc.stdin.write(line)
197
+ self._proc.stdin.flush()
198
+
199
+ def _restart_subprocess_worker(self) -> None:
200
+ if self._proc is not None:
201
+ try:
202
+ self._proc.stdin.close()
203
+ except Exception:
204
+ pass
205
+ if self._proc.poll() is None:
206
+ self._proc.terminate()
207
+ try:
208
+ self._proc.wait(timeout=2)
209
+ except subprocess.TimeoutExpired:
210
+ self._proc.kill()
211
+ self._proc = None
212
+ self._proc_event_queue = None
213
+ self._reader_thread = None
File without changes
@@ -0,0 +1,3 @@
1
+ """Filesystem watcher stub for phase 2."""
2
+
3
+ # TODO: implement in phase 2