codegraph-engine 3.0.0__2-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 (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +868 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,46 @@
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
20
+ from .monitor import get_process_memory_mb
21
+ from .policy import (
22
+ ActivityMode,
23
+ PressureLevel,
24
+ ResourcePolicy,
25
+ ResourceProfile,
26
+ ResourceState,
27
+ TaskPriority,
28
+ )
29
+
30
+ __all__ = [
31
+ "ActivityMode",
32
+ "BoundedMemoryCache",
33
+ "FileChangeDebouncer",
34
+ "PressureLevel",
35
+ "RequestCoalescer",
36
+ "ResourceGovernor",
37
+ "ResourcePolicy",
38
+ "ResourceProfile",
39
+ "ResourceState",
40
+ "TaskPriority",
41
+ "get_global_coalescer",
42
+ "get_global_governor",
43
+ "get_graph_cache",
44
+ "get_parse_cache",
45
+ "get_process_memory_mb",
46
+ ]
@@ -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,219 @@
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 threading
9
+ import time
10
+ from collections.abc import Iterator
11
+ from contextlib import contextmanager
12
+
13
+ from .monitor import get_process_memory_mb
14
+ from .policy import (
15
+ ActivityMode,
16
+ PressureLevel,
17
+ ResourcePolicy,
18
+ ResourceProfile,
19
+ ResourceState,
20
+ TaskPriority,
21
+ )
22
+
23
+
24
+ class ResourceGovernor:
25
+ """Thread-safe central resource governor and priority scheduler."""
26
+
27
+ def __init__(self, policy: ResourcePolicy | None = None) -> None:
28
+ self.policy = policy or ResourcePolicy.for_profile(ResourceProfile.BALANCED)
29
+ self._lock = threading.RLock()
30
+ self._condition = threading.Condition(self._lock)
31
+
32
+ self._active_interactive: int = 0
33
+ self._active_background: int = 0
34
+ self._queued_tasks: int = 0
35
+ self._indexing_active: bool = False
36
+ self._watcher_active: bool = False
37
+
38
+ self._last_interactive_time: float = 0.0
39
+ self._last_file_change_time: float = 0.0
40
+
41
+ self._recent_cpu_time_ms: float = 0.0
42
+ self._recent_wall_time_ms: float = 0.0
43
+
44
+ self._interactive_sem = threading.Semaphore(self.policy.max_interactive_workers)
45
+ self._background_sem = threading.Semaphore(self.policy.max_background_workers)
46
+
47
+ def set_policy(self, policy: ResourcePolicy) -> None:
48
+ """Update runtime policy dynamically."""
49
+ with self._lock:
50
+ self.policy = policy
51
+ self._interactive_sem = threading.Semaphore(policy.max_interactive_workers)
52
+ self._background_sem = threading.Semaphore(policy.max_background_workers)
53
+ self._condition.notify_all()
54
+
55
+ def record_activity(self, kind: str = "interactive") -> None:
56
+ """Record non-invasive activity to maintain active coding state."""
57
+ now = time.time()
58
+ with self._lock:
59
+ if kind == "file_change":
60
+ self._last_file_change_time = now
61
+ else:
62
+ self._last_interactive_time = now
63
+
64
+ def get_activity_mode(self) -> ActivityMode:
65
+ """Infer activity mode from recent MCP and file change events."""
66
+ with self._lock:
67
+ now = time.time()
68
+ recent_interactive = (now - self._last_interactive_time) < 45.0
69
+ recent_files = (now - self._last_file_change_time) < 45.0
70
+ if recent_interactive or recent_files:
71
+ return ActivityMode.ACTIVE_CODING
72
+ if self._active_background > 0 or self._indexing_active:
73
+ return ActivityMode.BACKGROUND
74
+ return ActivityMode.IDLE
75
+
76
+ def is_active_coding(self) -> bool:
77
+ return self.get_activity_mode() == ActivityMode.ACTIVE_CODING
78
+
79
+ def get_pressure(self) -> PressureLevel:
80
+ """Compute system resource pressure with conservative hysteresis."""
81
+ mem_mb = get_process_memory_mb()
82
+ limit_mb = self.policy.max_memory_mb
83
+
84
+ with self._lock:
85
+ active_inter = self._active_interactive
86
+
87
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.90:
88
+ return PressureLevel.CRITICAL
89
+ if active_inter >= self.policy.max_interactive_workers * 2:
90
+ return PressureLevel.CRITICAL
91
+
92
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.75:
93
+ return PressureLevel.HIGH
94
+ if active_inter > self.policy.max_interactive_workers:
95
+ return PressureLevel.HIGH
96
+
97
+ if mem_mb is not None and limit_mb and mem_mb >= limit_mb * 0.60:
98
+ return PressureLevel.ELEVATED
99
+ if active_inter > 0:
100
+ return PressureLevel.ELEVATED
101
+
102
+ return PressureLevel.NORMAL
103
+
104
+ def should_throttle(self, priority: TaskPriority = TaskPriority.BACKGROUND) -> bool:
105
+ """Check whether a background or maintenance task should throttle or yield."""
106
+ if priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL):
107
+ return False
108
+ pressure = self.get_pressure()
109
+ if pressure in (PressureLevel.CRITICAL, PressureLevel.HIGH):
110
+ return True
111
+ return self.is_active_coding()
112
+
113
+ def yield_if_needed(self, priority: TaskPriority = TaskPriority.BACKGROUND) -> None:
114
+ """Cooperative yield point for long-running iterative tasks."""
115
+ if priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL):
116
+ return
117
+
118
+ pressure = self.get_pressure()
119
+ if pressure == PressureLevel.CRITICAL:
120
+ # Under critical pressure, background work yields for 100ms
121
+ time.sleep(0.10)
122
+ elif pressure == PressureLevel.HIGH or self.is_active_coding():
123
+ # Under high pressure or active coding, yield 20ms to free up CPU
124
+ time.sleep(0.02)
125
+ else:
126
+ # Minimal cooperative yield to let OS reschedule other threads
127
+ time.sleep(0.001)
128
+
129
+ def get_state(self) -> ResourceState:
130
+ """Snapshot current resource state."""
131
+ mem = get_process_memory_mb()
132
+ pressure = self.get_pressure()
133
+ mode = self.get_activity_mode()
134
+ with self._lock:
135
+ return ResourceState(
136
+ policy_profile=self.policy.profile.value,
137
+ active_interactive_requests=self._active_interactive,
138
+ active_background_tasks=self._active_background,
139
+ queued_tasks=self._queued_tasks,
140
+ recent_cpu_time_ms=round(self._recent_cpu_time_ms, 2),
141
+ recent_wall_time_ms=round(self._recent_wall_time_ms, 2),
142
+ estimated_memory_mb=mem,
143
+ pressure_level=pressure.value,
144
+ activity_mode=mode.value,
145
+ indexing_active=self._indexing_active,
146
+ watcher_active=self._watcher_active,
147
+ )
148
+
149
+ @contextmanager
150
+ def task_scope(
151
+ self,
152
+ priority: TaskPriority = TaskPriority.INTERACTIVE_HIGH,
153
+ task_id: str | None = None,
154
+ ) -> Iterator[None]:
155
+ """Scoped execution guard that handles priority scheduling, tracking, and yielding."""
156
+ del task_id
157
+ is_interactive = priority in (TaskPriority.INTERACTIVE_HIGH, TaskPriority.INTERACTIVE_NORMAL)
158
+ sem = self._interactive_sem if is_interactive else self._background_sem
159
+
160
+ with self._lock:
161
+ self._queued_tasks += 1
162
+ if is_interactive:
163
+ self.record_activity("interactive")
164
+
165
+ sem.acquire()
166
+ try:
167
+ throttle = False
168
+ with self._lock:
169
+ self._queued_tasks = max(0, self._queued_tasks - 1)
170
+ if is_interactive:
171
+ self._active_interactive += 1
172
+ else:
173
+ self._active_background += 1
174
+ if self.is_active_coding() and self.policy.aggressive_throttling:
175
+ throttle = True
176
+ if throttle:
177
+ time.sleep(0.01)
178
+
179
+ t0_cpu = time.process_time()
180
+ t0_wall = time.perf_counter()
181
+ try:
182
+ yield
183
+ finally:
184
+ elapsed_cpu = (time.process_time() - t0_cpu) * 1000
185
+ elapsed_wall = (time.perf_counter() - t0_wall) * 1000
186
+ with self._lock:
187
+ if is_interactive:
188
+ self._active_interactive = max(0, self._active_interactive - 1)
189
+ self._last_interactive_time = time.time()
190
+ else:
191
+ self._active_background = max(0, self._active_background - 1)
192
+ self._recent_cpu_time_ms = elapsed_cpu
193
+ self._recent_wall_time_ms = elapsed_wall
194
+ self._condition.notify_all()
195
+ finally:
196
+ sem.release()
197
+
198
+ def set_indexing_active(self, active: bool) -> None:
199
+ with self._lock:
200
+ self._indexing_active = active
201
+
202
+ def set_watcher_active(self, active: bool) -> None:
203
+ with self._lock:
204
+ self._watcher_active = active
205
+
206
+
207
+ _GLOBAL_GOVERNOR: ResourceGovernor | None = None
208
+ _GOVERNOR_LOCK = threading.Lock()
209
+
210
+
211
+ def get_global_governor(policy: ResourcePolicy | None = None) -> ResourceGovernor:
212
+ """Access the singleton resource governor for the current process."""
213
+ global _GLOBAL_GOVERNOR
214
+ with _GOVERNOR_LOCK:
215
+ if _GLOBAL_GOVERNOR is None:
216
+ _GLOBAL_GOVERNOR = ResourceGovernor(policy)
217
+ elif policy is not None:
218
+ _GLOBAL_GOVERNOR.set_policy(policy)
219
+ return _GLOBAL_GOVERNOR
@@ -0,0 +1,102 @@
1
+ """Cross-platform process resource monitoring for CodeGraph.
2
+
3
+ Provides portable resident memory (RSS) measurements across Windows, macOS,
4
+ and Linux without requiring Unix-only APIs at import or runtime.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from typing import Any
10
+
11
+
12
+ def _get_unix_memory_mb() -> float | None:
13
+ """Read process RSS using standard library resource module on Unix."""
14
+ try:
15
+ import resource # Unix-only standard library module
16
+
17
+ usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
18
+ if sys.platform == "darwin":
19
+ # macOS ru_maxrss is returned in bytes
20
+ return round(usage / (1024 * 1024), 2)
21
+ else:
22
+ # Linux and BSD ru_maxrss is returned in kilobytes
23
+ return round(usage / 1024, 2)
24
+ except Exception:
25
+ return None
26
+
27
+
28
+ def _get_psutil_memory_mb() -> float | None:
29
+ """Read process RSS using psutil if installed."""
30
+ try:
31
+ import psutil
32
+
33
+ rss_bytes = float(psutil.Process().memory_info().rss)
34
+ return round(rss_bytes / (1024 * 1024), 2)
35
+ except Exception:
36
+ return None
37
+
38
+
39
+ def _get_windows_memory_mb_ctypes() -> float | None:
40
+ """Read process RSS on Windows using standard library ctypes (zero dependencies)."""
41
+ try:
42
+ import ctypes
43
+ from ctypes import wintypes
44
+
45
+ class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
46
+ _fields_ = [
47
+ ("cb", wintypes.DWORD),
48
+ ("PageFaultCount", wintypes.DWORD),
49
+ ("PeakWorkingSetSize", ctypes.c_size_t),
50
+ ("WorkingSetSize", ctypes.c_size_t),
51
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
52
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
53
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
54
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
55
+ ("PagefileUsage", ctypes.c_size_t),
56
+ ("PeakPagefileUsage", ctypes.c_size_t),
57
+ ]
58
+
59
+ windll: Any = getattr(ctypes, "windll", None)
60
+ if windll is None:
61
+ return None
62
+
63
+ handle = windll.kernel32.GetCurrentProcess()
64
+ counters = PROCESS_MEMORY_COUNTERS()
65
+ counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
66
+
67
+ # On Windows 7+, K32GetProcessMemoryInfo is in kernel32; otherwise in psapi
68
+ get_info = getattr(windll.kernel32, "K32GetProcessMemoryInfo", None)
69
+ if get_info is None:
70
+ psapi = getattr(windll, "psapi", None)
71
+ if psapi is not None:
72
+ get_info = getattr(psapi, "GetProcessMemoryInfo", None)
73
+
74
+ if get_info is not None and bool(get_info(handle, ctypes.byref(counters), counters.cb)):
75
+ # WorkingSetSize is the resident set size in bytes
76
+ return round(float(counters.WorkingSetSize) / (1024 * 1024), 2)
77
+ except Exception:
78
+ return None
79
+ return None
80
+
81
+
82
+ def get_process_memory_mb() -> float | None:
83
+ """Return estimated resident process memory in megabytes cross-platform.
84
+
85
+ Returns:
86
+ float: Estimated memory in MB if measurement is supported and succeeds.
87
+ None: If measurement is unsupported or fails (degrades gracefully to UNKNOWN).
88
+ """
89
+ if sys.platform == "win32":
90
+ # 1. On Windows: try psutil first, then ctypes Windows API
91
+ mem = _get_psutil_memory_mb()
92
+ if mem is not None:
93
+ return mem
94
+ return _get_windows_memory_mb_ctypes()
95
+
96
+ # 2. On Unix-like platforms (macOS, Linux): try standard resource module
97
+ mem = _get_unix_memory_mb()
98
+ if mem is not None:
99
+ return mem
100
+
101
+ # 3. Fallback to psutil on any platform if resource module failed or is unavailable
102
+ return _get_psutil_memory_mb()