codegraph-engine 2.1.1__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 (62) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/architecture.py +328 -0
  4. codegraph/audit.py +106 -0
  5. codegraph/cache.py +95 -0
  6. codegraph/cli.py +854 -0
  7. codegraph/config.py +43 -0
  8. codegraph/constraints.py +238 -0
  9. codegraph/context.py +1228 -0
  10. codegraph/epistemic.py +90 -0
  11. codegraph/errors.py +275 -0
  12. codegraph/evidence/__init__.py +15 -0
  13. codegraph/evidence/citations.py +397 -0
  14. codegraph/frameworks.py +434 -0
  15. codegraph/freshness.py +295 -0
  16. codegraph/git.py +278 -0
  17. codegraph/graph/__init__.py +46 -0
  18. codegraph/graph/models.py +41 -0
  19. codegraph/graph/traversal.py +1291 -0
  20. codegraph/indexing/__init__.py +4 -0
  21. codegraph/indexing/classifier.py +274 -0
  22. codegraph/indexing/indexer.py +943 -0
  23. codegraph/indexing/models.py +338 -0
  24. codegraph/indexing/parser.py +1240 -0
  25. codegraph/indexing/scanner.py +200 -0
  26. codegraph/indexing/test_framework.py +116 -0
  27. codegraph/interrogation.py +1582 -0
  28. codegraph/llm/__init__.py +3 -0
  29. codegraph/llm/base.py +15 -0
  30. codegraph/llm/context.py +20 -0
  31. codegraph/mcp/__init__.py +3 -0
  32. codegraph/mcp/server.py +736 -0
  33. codegraph/memory/__init__.py +3 -0
  34. codegraph/memory/store.py +46 -0
  35. codegraph/models.py +289 -0
  36. codegraph/observability.py +151 -0
  37. codegraph/optimizer.py +372 -0
  38. codegraph/planner.py +417 -0
  39. codegraph/py.typed +1 -0
  40. codegraph/query_expansion.py +199 -0
  41. codegraph/ranking.py +363 -0
  42. codegraph/resolver.py +843 -0
  43. codegraph/resources/__init__.py +45 -0
  44. codegraph/resources/cache.py +117 -0
  45. codegraph/resources/coalescer.py +83 -0
  46. codegraph/resources/debouncer.py +98 -0
  47. codegraph/resources/governor.py +232 -0
  48. codegraph/resources/policy.py +123 -0
  49. codegraph/retrieval_policy.py +220 -0
  50. codegraph/search/__init__.py +23 -0
  51. codegraph/search/hybrid.py +301 -0
  52. codegraph/search/semantic.py +28 -0
  53. codegraph/security/__init__.py +3 -0
  54. codegraph/security/paths.py +35 -0
  55. codegraph/target_resolver.py +348 -0
  56. codegraph/task.py +637 -0
  57. codegraph_engine-2.1.1.dist-info/METADATA +334 -0
  58. codegraph_engine-2.1.1.dist-info/RECORD +62 -0
  59. codegraph_engine-2.1.1.dist-info/WHEEL +5 -0
  60. codegraph_engine-2.1.1.dist-info/entry_points.txt +2 -0
  61. codegraph_engine-2.1.1.dist-info/licenses/LICENSE +21 -0
  62. codegraph_engine-2.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,45 @@
1
+ """Resource Management, Concurrency Throttling, and Laptop Protection.
2
+
3
+ Provides:
4
+ - ResourceGovernor: Priority scheduling, cooperative yielding, process memory monitoring
5
+ - ResourcePolicy: Profiles (LIGHT, BALANCED, PERFORMANCE), bounds, and thresholds
6
+ - RequestCoalescer: De-duplicating identical concurrent flights
7
+ - FileChangeDebouncer: Coalescing editor save bursts
8
+ - BoundedMemoryCache: Memory-bounded LRU caches for parse results and graphs
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from .cache import (
13
+ BoundedMemoryCache,
14
+ get_graph_cache,
15
+ get_parse_cache,
16
+ )
17
+ from .coalescer import RequestCoalescer, get_global_coalescer
18
+ from .debouncer import FileChangeDebouncer
19
+ from .governor import ResourceGovernor, get_global_governor, get_process_memory_mb
20
+ from .policy import (
21
+ ActivityMode,
22
+ PressureLevel,
23
+ ResourcePolicy,
24
+ ResourceProfile,
25
+ ResourceState,
26
+ TaskPriority,
27
+ )
28
+
29
+ __all__ = [
30
+ "ActivityMode",
31
+ "BoundedMemoryCache",
32
+ "FileChangeDebouncer",
33
+ "PressureLevel",
34
+ "RequestCoalescer",
35
+ "ResourceGovernor",
36
+ "ResourcePolicy",
37
+ "ResourceProfile",
38
+ "ResourceState",
39
+ "TaskPriority",
40
+ "get_global_coalescer",
41
+ "get_global_governor",
42
+ "get_graph_cache",
43
+ "get_parse_cache",
44
+ "get_process_memory_mb",
45
+ ]
@@ -0,0 +1,117 @@
1
+ """Bounded LRU Memory Caches with Size Tracking and Eviction.
2
+
3
+ Prevents unbounded memory growth from ParseResults, graph neighborhoods,
4
+ and retrieval artifacts during sustained coding sessions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ import threading
10
+ from collections import OrderedDict
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+
15
+ def _approx_size(obj: Any) -> int:
16
+ """Approximate memory footprint of an object."""
17
+ try:
18
+ if isinstance(obj, (str, bytes)):
19
+ return len(obj)
20
+ elif hasattr(obj, "__sizeof__"):
21
+ return int(obj.__sizeof__())
22
+ return sys.getsizeof(obj, 128)
23
+ except Exception:
24
+ return 128
25
+
26
+
27
+ class BoundedMemoryCache[K, V]:
28
+ """Thread-safe LRU cache with both item count and memory byte limits."""
29
+
30
+ def __init__(self, max_items: int = 500, max_bytes: int = 50 * 1024 * 1024) -> None:
31
+ self.max_items = max_items
32
+ self.max_bytes = max_bytes
33
+ self._lock = threading.Lock()
34
+ self._cache: OrderedDict[K, tuple[V, int]] = OrderedDict()
35
+ self._current_bytes: int = 0
36
+ self._hits: int = 0
37
+ self._misses: int = 0
38
+
39
+ def get(self, key: K) -> V | None:
40
+ with self._lock:
41
+ if key not in self._cache:
42
+ self._misses += 1
43
+ return None
44
+ val, size = self._cache.pop(key)
45
+ self._cache[key] = (val, size) # move to end (MRU)
46
+ self._hits += 1
47
+ return val
48
+
49
+ def put(self, key: K, value: V, size_bytes: int | None = None) -> None:
50
+ sz = size_bytes if size_bytes is not None else _approx_size(value)
51
+ with self._lock:
52
+ if key in self._cache:
53
+ _, old_sz = self._cache.pop(key)
54
+ self._current_bytes -= old_sz
55
+
56
+ self._cache[key] = (value, sz)
57
+ self._current_bytes += sz
58
+
59
+ # Evict until limits are satisfied
60
+ while len(self._cache) > self.max_items or (self._current_bytes > self.max_bytes and len(self._cache) > 1):
61
+ _, (_, evicted_sz) = self._cache.popitem(last=False) # pop LRU
62
+ self._current_bytes -= evicted_sz
63
+
64
+ def set(self, key: K, value: V, size_bytes: int | None = None) -> None:
65
+ """Alias for put."""
66
+ self.put(key, value, size_bytes)
67
+
68
+ def invalidate(self, predicate: Callable[[K], bool]) -> int:
69
+ removed = 0
70
+ with self._lock:
71
+ keys_to_del = [k for k in self._cache if predicate(k)]
72
+ for k in keys_to_del:
73
+ _, sz = self._cache.pop(k)
74
+ self._current_bytes -= sz
75
+ removed += 1
76
+ return removed
77
+
78
+ def clear(self) -> None:
79
+ with self._lock:
80
+ self._cache.clear()
81
+ self._current_bytes = 0
82
+
83
+ def stats(self) -> dict[str, object]:
84
+ with self._lock:
85
+ return {
86
+ "items": len(self._cache),
87
+ "max_items": self.max_items,
88
+ "current_mb": round(self._current_bytes / (1024 * 1024), 2),
89
+ "max_mb": round(self.max_bytes / (1024 * 1024), 2),
90
+ "hits": self._hits,
91
+ "misses": self._misses,
92
+ }
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Global Specialized Caches
97
+ # ---------------------------------------------------------------------------
98
+
99
+ # ParseResult cache: (source_hash, parser_version, language) -> ParseResult
100
+ _PARSE_CACHE: BoundedMemoryCache[tuple[str, str, str], Any] = BoundedMemoryCache(
101
+ max_items=300,
102
+ max_bytes=30 * 1024 * 1024, # 30 MB
103
+ )
104
+
105
+ # Graph neighborhood cache: (repo_generation, symbol_id, relationship, depth) -> list[dict]
106
+ _GRAPH_CACHE: BoundedMemoryCache[tuple[int, str, str, int], Any] = BoundedMemoryCache(
107
+ max_items=500,
108
+ max_bytes=20 * 1024 * 1024, # 20 MB
109
+ )
110
+
111
+
112
+ def get_parse_cache() -> BoundedMemoryCache[tuple[str, str, str], Any]:
113
+ return _PARSE_CACHE
114
+
115
+
116
+ def get_graph_cache() -> BoundedMemoryCache[tuple[int, str, str, int], Any]:
117
+ return _GRAPH_CACHE
@@ -0,0 +1,83 @@
1
+ """Thread-safe Request Coalescing and In-Flight Flight Sharing.
2
+
3
+ Prevents duplicate concurrent executions of identical read or compilation requests
4
+ by allowing concurrent callers to share a single in-flight computation.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import threading
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+
13
+ class _InFlight[T]:
14
+ def __init__(self) -> None:
15
+ self.event = threading.Event()
16
+ self.result: T | None = None
17
+ self.error: BaseException | None = None
18
+
19
+
20
+ class RequestCoalescer:
21
+ """Coalesces identical concurrent tasks into a single execution."""
22
+
23
+ def __init__(self) -> None:
24
+ self._lock = threading.Lock()
25
+ self._flights: dict[str, _InFlight[Any]] = {}
26
+ self._coalesced_count: int = 0
27
+ self._total_requests: int = 0
28
+
29
+ def coalesce[T](self, key: str, fn: Callable[[], T]) -> T:
30
+ """Execute fn() or wait for an identical in-flight execution under the given key."""
31
+ with self._lock:
32
+ self._total_requests += 1
33
+ flight: _InFlight[T] | None = self._flights.get(key)
34
+ if flight is not None:
35
+ is_leader = False
36
+ self._coalesced_count += 1
37
+ else:
38
+ is_leader = True
39
+ flight = _InFlight[T]()
40
+ self._flights[key] = flight
41
+
42
+ if not is_leader:
43
+ # Wait for leader to complete computation
44
+ flight.event.wait()
45
+ if flight.error is not None:
46
+ raise flight.error
47
+ return flight.result # type: ignore[return-value]
48
+
49
+ # Leader executes fn()
50
+ try:
51
+ res = fn()
52
+ flight.result = res
53
+ return res
54
+ except BaseException as exc:
55
+ flight.error = exc
56
+ raise
57
+ finally:
58
+ with self._lock:
59
+ self._flights.pop(key, None)
60
+ flight.event.set()
61
+
62
+ def in_flight_count(self) -> int:
63
+ with self._lock:
64
+ return len(self._flights)
65
+
66
+ def clear(self) -> None:
67
+ with self._lock:
68
+ self._flights.clear()
69
+
70
+ def stats(self) -> dict[str, object]:
71
+ with self._lock:
72
+ return {
73
+ "in_flight": len(self._flights),
74
+ "total_requests": self._total_requests,
75
+ "coalesced_requests": self._coalesced_count,
76
+ }
77
+
78
+
79
+ _GLOBAL_COALESCER = RequestCoalescer()
80
+
81
+
82
+ def get_global_coalescer() -> RequestCoalescer:
83
+ return _GLOBAL_COALESCER
@@ -0,0 +1,98 @@
1
+ """File Change Debouncer and Burst Coalescer.
2
+
3
+ Coalesces rapid file system save events from code editors into bounded batches
4
+ to prevent repeated, wasteful index cycles.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import threading
9
+ import time
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+
14
+ class FileChangeDebouncer:
15
+ """Thread-safe debounce accumulator for editor save bursts."""
16
+
17
+ def __init__(
18
+ self,
19
+ debounce_ms: int = 500,
20
+ max_batch_size: int = 100,
21
+ on_batch: Callable[[set[str]], None] | None = None,
22
+ ) -> None:
23
+ self.debounce_ms = debounce_ms
24
+ self.max_batch_size = max_batch_size
25
+ self.on_batch = on_batch
26
+
27
+ self._lock = threading.Lock()
28
+ self._pending_paths: set[str] = set()
29
+ self._timer: threading.Timer | None = None
30
+ self._last_event_time: float = 0.0
31
+
32
+ def add_change(self, path: str | Path) -> None:
33
+ """Record a file modification or save event."""
34
+ p_str = path.as_posix() if isinstance(path, Path) else str(path)
35
+ with self._lock:
36
+ self._pending_paths.add(p_str)
37
+ self._last_event_time = time.time()
38
+
39
+ if len(self._pending_paths) >= self.max_batch_size:
40
+ self._dispatch_locked()
41
+ return
42
+
43
+ if self._timer is not None:
44
+ self._timer.cancel()
45
+
46
+ delay_sec = self.debounce_ms / 1000.0
47
+ self._timer = threading.Timer(delay_sec, self._on_timer_fired)
48
+ self._timer.daemon = True
49
+ self._timer.start()
50
+
51
+ def _on_timer_fired(self) -> None:
52
+ with self._lock:
53
+ self._dispatch_locked()
54
+
55
+ def _dispatch_locked(self) -> None:
56
+ if self._timer is not None:
57
+ self._timer.cancel()
58
+ self._timer = None
59
+
60
+ if not self._pending_paths:
61
+ return
62
+
63
+ batch = set(self._pending_paths)
64
+ self._pending_paths.clear()
65
+
66
+ if self.on_batch:
67
+ # Dispatch outside lock or in thread to avoid deadlocks
68
+ threading.Thread(target=self.on_batch, args=(batch,), daemon=True).start()
69
+
70
+ def flush(self) -> set[str]:
71
+ """Synchronously drain all pending coalesced paths."""
72
+ with self._lock:
73
+ if self._timer is not None:
74
+ self._timer.cancel()
75
+ self._timer = None
76
+ batch = set(self._pending_paths)
77
+ self._pending_paths.clear()
78
+ return batch
79
+
80
+ def pending_count(self) -> int:
81
+ with self._lock:
82
+ return len(self._pending_paths)
83
+
84
+ def record_change(self, path: str | Path) -> None:
85
+ """Alias for add_change."""
86
+ self.add_change(path)
87
+
88
+ def start(self, on_batch: Callable[[set[str]], None]) -> None:
89
+ """Configure or update batch callback."""
90
+ with self._lock:
91
+ self.on_batch = on_batch
92
+
93
+ def stop(self) -> None:
94
+ """Cancel any pending timer."""
95
+ with self._lock:
96
+ if self._timer is not None:
97
+ self._timer.cancel()
98
+ self._timer = None
@@ -0,0 +1,232 @@
1
+ """Central Resource Governor for CodeGraph MCP.
2
+
3
+ Protects developer laptop responsiveness during active coding by bounding concurrency,
4
+ throttling background indexing, managing process memory, and cooperatively yielding.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import resource
9
+ import sys
10
+ import threading
11
+ import time
12
+ from collections.abc import Iterator
13
+ from contextlib import contextmanager
14
+
15
+ from .policy import (
16
+ ActivityMode,
17
+ PressureLevel,
18
+ ResourcePolicy,
19
+ ResourceProfile,
20
+ ResourceState,
21
+ TaskPriority,
22
+ )
23
+
24
+
25
+ def get_process_memory_mb() -> float:
26
+ """Return estimated resident process memory in megabytes portably."""
27
+ try:
28
+ usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
29
+ if sys.platform == "darwin":
30
+ return round(usage / (1024 * 1024), 2)
31
+ else:
32
+ return round(usage / 1024, 2)
33
+ except Exception:
34
+ return 0.0
35
+
36
+
37
+ class ResourceGovernor:
38
+ """Thread-safe central resource governor and priority scheduler."""
39
+
40
+ def __init__(self, policy: ResourcePolicy | None = None) -> None:
41
+ self.policy = policy or ResourcePolicy.for_profile(ResourceProfile.BALANCED)
42
+ self._lock = threading.RLock()
43
+ self._condition = threading.Condition(self._lock)
44
+
45
+ self._active_interactive: int = 0
46
+ self._active_background: int = 0
47
+ self._queued_tasks: int = 0
48
+ self._indexing_active: bool = False
49
+ self._watcher_active: bool = False
50
+
51
+ self._last_interactive_time: float = 0.0
52
+ self._last_file_change_time: float = 0.0
53
+
54
+ self._recent_cpu_time_ms: float = 0.0
55
+ self._recent_wall_time_ms: float = 0.0
56
+
57
+ self._interactive_sem = threading.Semaphore(self.policy.max_interactive_workers)
58
+ self._background_sem = threading.Semaphore(self.policy.max_background_workers)
59
+
60
+ def set_policy(self, policy: ResourcePolicy) -> None:
61
+ """Update runtime policy dynamically."""
62
+ with self._lock:
63
+ self.policy = policy
64
+ self._interactive_sem = threading.Semaphore(policy.max_interactive_workers)
65
+ self._background_sem = threading.Semaphore(policy.max_background_workers)
66
+ self._condition.notify_all()
67
+
68
+ def record_activity(self, kind: str = "interactive") -> None:
69
+ """Record non-invasive activity to maintain active coding state."""
70
+ now = time.time()
71
+ with self._lock:
72
+ if kind == "file_change":
73
+ self._last_file_change_time = now
74
+ else:
75
+ self._last_interactive_time = now
76
+
77
+ def get_activity_mode(self) -> ActivityMode:
78
+ """Infer activity mode from recent MCP and file change events."""
79
+ with self._lock:
80
+ now = time.time()
81
+ recent_interactive = (now - self._last_interactive_time) < 45.0
82
+ recent_files = (now - self._last_file_change_time) < 45.0
83
+ if recent_interactive or recent_files:
84
+ return ActivityMode.ACTIVE_CODING
85
+ if self._active_background > 0 or self._indexing_active:
86
+ return ActivityMode.BACKGROUND
87
+ return ActivityMode.IDLE
88
+
89
+ def is_active_coding(self) -> bool:
90
+ return self.get_activity_mode() == ActivityMode.ACTIVE_CODING
91
+
92
+ def get_pressure(self) -> PressureLevel:
93
+ """Compute system resource pressure with conservative hysteresis."""
94
+ mem_mb = get_process_memory_mb()
95
+ limit_mb = self.policy.max_memory_mb
96
+
97
+ with self._lock:
98
+ active_inter = self._active_interactive
99
+
100
+ if limit_mb and mem_mb >= limit_mb * 0.90:
101
+ return PressureLevel.CRITICAL
102
+ if active_inter >= self.policy.max_interactive_workers * 2:
103
+ return PressureLevel.CRITICAL
104
+
105
+ if limit_mb and mem_mb >= limit_mb * 0.75:
106
+ return PressureLevel.HIGH
107
+ if active_inter > self.policy.max_interactive_workers:
108
+ return PressureLevel.HIGH
109
+
110
+ if limit_mb and mem_mb >= limit_mb * 0.60:
111
+ return PressureLevel.ELEVATED
112
+ if active_inter > 0:
113
+ return PressureLevel.ELEVATED
114
+
115
+ return PressureLevel.NORMAL
116
+
117
+ def should_throttle(self, priority: TaskPriority = TaskPriority.BACKGROUND) -> bool:
118
+ """Check whether a background or maintenance task should throttle or yield."""
119
+ if priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL):
120
+ return False
121
+ pressure = self.get_pressure()
122
+ if pressure in (PressureLevel.CRITICAL, PressureLevel.HIGH):
123
+ return True
124
+ return self.is_active_coding()
125
+
126
+ def yield_if_needed(self, priority: TaskPriority = TaskPriority.BACKGROUND) -> None:
127
+ """Cooperative yield point for long-running iterative tasks."""
128
+ if priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL):
129
+ return
130
+
131
+ pressure = self.get_pressure()
132
+ if pressure == PressureLevel.CRITICAL:
133
+ # Under critical pressure, background work yields for 100ms
134
+ time.sleep(0.10)
135
+ elif pressure == PressureLevel.HIGH or self.is_active_coding():
136
+ # Under high pressure or active coding, yield 20ms to free up CPU
137
+ time.sleep(0.02)
138
+ else:
139
+ # Minimal cooperative yield to let OS reschedule other threads
140
+ time.sleep(0.001)
141
+
142
+ def get_state(self) -> ResourceState:
143
+ """Snapshot current resource state."""
144
+ mem = get_process_memory_mb()
145
+ pressure = self.get_pressure()
146
+ mode = self.get_activity_mode()
147
+ with self._lock:
148
+ return ResourceState(
149
+ policy_profile=self.policy.profile.value,
150
+ active_interactive_requests=self._active_interactive,
151
+ active_background_tasks=self._active_background,
152
+ queued_tasks=self._queued_tasks,
153
+ recent_cpu_time_ms=round(self._recent_cpu_time_ms, 2),
154
+ recent_wall_time_ms=round(self._recent_wall_time_ms, 2),
155
+ estimated_memory_mb=mem,
156
+ pressure_level=pressure.value,
157
+ activity_mode=mode.value,
158
+ indexing_active=self._indexing_active,
159
+ watcher_active=self._watcher_active,
160
+ )
161
+
162
+ @contextmanager
163
+ def task_scope(
164
+ self,
165
+ priority: TaskPriority = TaskPriority.INTERACTIVE_HIGH,
166
+ task_id: str | None = None,
167
+ ) -> Iterator[None]:
168
+ """Scoped execution guard that handles priority scheduling, tracking, and yielding."""
169
+ del task_id
170
+ is_interactive = priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL)
171
+ sem = self._interactive_sem if is_interactive else self._background_sem
172
+
173
+ with self._lock:
174
+ self._queued_tasks += 1
175
+ if is_interactive:
176
+ self.record_activity("interactive")
177
+
178
+ sem.acquire()
179
+ try:
180
+ throttle = False
181
+ with self._lock:
182
+ self._queued_tasks = max(0, self._queued_tasks - 1)
183
+ if is_interactive:
184
+ self._active_interactive += 1
185
+ else:
186
+ self._active_background += 1
187
+ if self.is_active_coding() and self.policy.aggressive_throttling:
188
+ throttle = True
189
+ if throttle:
190
+ time.sleep(0.01)
191
+
192
+ t0_cpu = time.process_time()
193
+ t0_wall = time.perf_counter()
194
+ try:
195
+ yield
196
+ finally:
197
+ elapsed_cpu = (time.process_time() - t0_cpu) * 1000
198
+ elapsed_wall = (time.perf_counter() - t0_wall) * 1000
199
+ with self._lock:
200
+ if is_interactive:
201
+ self._active_interactive = max(0, self._active_interactive - 1)
202
+ self._last_interactive_time = time.time()
203
+ else:
204
+ self._active_background = max(0, self._active_background - 1)
205
+ self._recent_cpu_time_ms = elapsed_cpu
206
+ self._recent_wall_time_ms = elapsed_wall
207
+ self._condition.notify_all()
208
+ finally:
209
+ sem.release()
210
+
211
+ def set_indexing_active(self, active: bool) -> None:
212
+ with self._lock:
213
+ self._indexing_active = active
214
+
215
+ def set_watcher_active(self, active: bool) -> None:
216
+ with self._lock:
217
+ self._watcher_active = active
218
+
219
+
220
+ _GLOBAL_GOVERNOR: ResourceGovernor | None = None
221
+ _GOVERNOR_LOCK = threading.Lock()
222
+
223
+
224
+ def get_global_governor(policy: ResourcePolicy | None = None) -> ResourceGovernor:
225
+ """Access the singleton resource governor for the current process."""
226
+ global _GLOBAL_GOVERNOR
227
+ with _GOVERNOR_LOCK:
228
+ if _GLOBAL_GOVERNOR is None:
229
+ _GLOBAL_GOVERNOR = ResourceGovernor(policy)
230
+ elif policy is not None:
231
+ _GLOBAL_GOVERNOR.set_policy(policy)
232
+ return _GLOBAL_GOVERNOR
@@ -0,0 +1,123 @@
1
+ """Resource Policies, Profiles, Task Priorities, and State Models.
2
+
3
+ Defines bounded execution budgets, memory limits, concurrency profiles,
4
+ and cooperative pressure levels without platform-specific hardware assumptions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import asdict, dataclass
9
+ from enum import StrEnum
10
+
11
+
12
+ class ResourceProfile(StrEnum):
13
+ LIGHT = "LIGHT"
14
+ BALANCED = "BALANCED"
15
+ PERFORMANCE = "PERFORMANCE"
16
+
17
+
18
+ class PressureLevel(StrEnum):
19
+ NORMAL = "NORMAL"
20
+ ELEVATED = "ELEVATED"
21
+ HIGH = "HIGH"
22
+ CRITICAL = "CRITICAL"
23
+
24
+
25
+ class TaskPriority(StrEnum):
26
+ INTERACTIVE_HIGH = "INTERACTIVE_HIGH"
27
+ INTERACTIVE_NORMAL = "INTERACTIVE_NORMAL"
28
+ BACKGROUND = "BACKGROUND"
29
+ MAINTENANCE = "MAINTENANCE"
30
+
31
+
32
+ class ActivityMode(StrEnum):
33
+ ACTIVE_CODING = "ACTIVE_CODING"
34
+ IDLE = "IDLE"
35
+ BACKGROUND = "BACKGROUND"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ResourcePolicy:
40
+ profile: ResourceProfile = ResourceProfile.BALANCED
41
+ max_interactive_workers: int = 2
42
+ max_background_workers: int = 1
43
+ max_files_per_incremental_batch: int = 25
44
+ max_graph_nodes_per_query: int = 300
45
+ max_graph_edges_per_query: int = 1200
46
+ max_git_commits: int = 15
47
+ max_memory_mb: int = 1024
48
+ max_cpu_time_ms_per_request: int = 5000
49
+ background_enabled: bool = True
50
+ aggressive_throttling: bool = True
51
+ debounce_ms: int = 500
52
+
53
+ @classmethod
54
+ def for_profile(cls, profile: str | ResourceProfile) -> ResourcePolicy:
55
+ p = ResourceProfile(str(profile).upper()) if isinstance(profile, str) else profile
56
+ if p == ResourceProfile.LIGHT:
57
+ return cls(
58
+ profile=ResourceProfile.LIGHT,
59
+ max_interactive_workers=1,
60
+ max_background_workers=1,
61
+ max_files_per_incremental_batch=10,
62
+ max_graph_nodes_per_query=100,
63
+ max_graph_edges_per_query=400,
64
+ max_git_commits=5,
65
+ max_memory_mb=512,
66
+ max_cpu_time_ms_per_request=2000,
67
+ background_enabled=True,
68
+ aggressive_throttling=True,
69
+ debounce_ms=800,
70
+ )
71
+ elif p == ResourceProfile.PERFORMANCE:
72
+ return cls(
73
+ profile=ResourceProfile.PERFORMANCE,
74
+ max_interactive_workers=4,
75
+ max_background_workers=2,
76
+ max_files_per_incremental_batch=100,
77
+ max_graph_nodes_per_query=1000,
78
+ max_graph_edges_per_query=5000,
79
+ max_git_commits=50,
80
+ max_memory_mb=4096,
81
+ max_cpu_time_ms_per_request=15000,
82
+ background_enabled=True,
83
+ aggressive_throttling=False,
84
+ debounce_ms=250,
85
+ )
86
+ else:
87
+ return cls(
88
+ profile=ResourceProfile.BALANCED,
89
+ max_interactive_workers=2,
90
+ max_background_workers=1,
91
+ max_files_per_incremental_batch=25,
92
+ max_graph_nodes_per_query=300,
93
+ max_graph_edges_per_query=1200,
94
+ max_git_commits=15,
95
+ max_memory_mb=1024,
96
+ max_cpu_time_ms_per_request=5000,
97
+ background_enabled=True,
98
+ aggressive_throttling=True,
99
+ debounce_ms=500,
100
+ )
101
+
102
+ def as_dict(self) -> dict[str, object]:
103
+ d = asdict(self)
104
+ d["profile"] = self.profile.value
105
+ return d
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class ResourceState:
110
+ policy_profile: str = ResourceProfile.BALANCED.value
111
+ active_interactive_requests: int = 0
112
+ active_background_tasks: int = 0
113
+ queued_tasks: int = 0
114
+ recent_cpu_time_ms: float = 0.0
115
+ recent_wall_time_ms: float = 0.0
116
+ estimated_memory_mb: float = 0.0
117
+ pressure_level: str = PressureLevel.NORMAL.value
118
+ activity_mode: str = ActivityMode.IDLE.value
119
+ indexing_active: bool = False
120
+ watcher_active: bool = False
121
+
122
+ def as_dict(self) -> dict[str, object]:
123
+ return asdict(self)