python-delphi-lsp 3.3.0__py3-none-any.whl → 3.4.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 (35) hide show
  1. delphi_lsp/_version.py +1 -1
  2. delphi_lsp/agent_cache.py +110 -55
  3. delphi_lsp/agent_cli.py +117 -23
  4. delphi_lsp/agent_context.py +668 -72
  5. delphi_lsp/agent_layers.py +218 -29
  6. delphi_lsp/agent_metrics.py +227 -15
  7. delphi_lsp/agent_relations.py +475 -50
  8. delphi_lsp/agent_wiki.py +16 -7
  9. delphi_lsp/agent_workspace.py +18 -0
  10. delphi_lsp/delphiast_lexer.py +156 -39
  11. delphi_lsp/delphiast_parser.py +336 -48
  12. delphi_lsp/delphiast_tokens.py +4 -4
  13. delphi_lsp/incremental.py +1139 -0
  14. delphi_lsp/lark_builder.py +6 -3
  15. delphi_lsp/lsp_server.py +4045 -207
  16. delphi_lsp/metrics.py +654 -75
  17. delphi_lsp/navigation_cache.py +2 -2
  18. delphi_lsp/nodes.py +93 -2
  19. delphi_lsp/parallel_outline.py +349 -44
  20. delphi_lsp/parse_artifacts.py +969 -0
  21. delphi_lsp/parser.py +21 -2
  22. delphi_lsp/parser_backend.py +13 -2
  23. delphi_lsp/preprocessor.py +28 -14
  24. delphi_lsp/project_config.py +22 -4
  25. delphi_lsp/project_indexer.py +31 -7
  26. delphi_lsp/response_cache.py +465 -0
  27. delphi_lsp/semantic.py +4 -1
  28. delphi_lsp/semantic_builder.py +746 -21
  29. {python_delphi_lsp-3.3.0.dist-info → python_delphi_lsp-3.4.0.dist-info}/METADATA +17 -2
  30. python_delphi_lsp-3.4.0.dist-info/RECORD +51 -0
  31. python_delphi_lsp-3.3.0.dist-info/RECORD +0 -48
  32. {python_delphi_lsp-3.3.0.dist-info → python_delphi_lsp-3.4.0.dist-info}/WHEEL +0 -0
  33. {python_delphi_lsp-3.3.0.dist-info → python_delphi_lsp-3.4.0.dist-info}/entry_points.txt +0 -0
  34. {python_delphi_lsp-3.3.0.dist-info → python_delphi_lsp-3.4.0.dist-info}/licenses/LICENSE +0 -0
  35. {python_delphi_lsp-3.3.0.dist-info → python_delphi_lsp-3.4.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "3.3.0"
1
+ __version__ = "3.4.0"
delphi_lsp/agent_cache.py CHANGED
@@ -54,6 +54,7 @@ _WATCHER_READY_TIMEOUT_SECONDS = 1.0
54
54
  _WATCHER_READY_POLL_MILLISECONDS = 50
55
55
  _STOP_TIMEOUT_SECONDS = 15.0
56
56
  _STOP_SIGNAL_DELAY_SECONDS = 3.0
57
+ _STOP_FORCE_TIMEOUT_SECONDS = 2.0
57
58
  _WATCHED_SUFFIXES = frozenset(
58
59
  {".pas", ".pp", ".inc", ".dpr", ".dpk", ".dproj", ".cfg"}
59
60
  )
@@ -365,9 +366,12 @@ class _RequestGuard:
365
366
  self,
366
367
  service: _CacheService,
367
368
  connection: socket.socket,
369
+ *,
370
+ enforce_rss_budget: bool = True,
368
371
  ) -> None:
369
372
  self._service = service
370
373
  self._connection = connection
374
+ self._enforce_rss_budget = enforce_rss_budget
371
375
 
372
376
  def check(self) -> None:
373
377
  if self._service.shutdown.is_set():
@@ -394,14 +398,14 @@ class _RequestGuard:
394
398
  "client_disconnected",
395
399
  "Cache client disconnected.",
396
400
  ) from error
397
- rss_bytes = current_process_rss_bytes()
398
401
  if (
399
- rss_bytes > 0
400
- and rss_bytes > self._service.metadata.max_memory_bytes
402
+ self._enforce_rss_budget
403
+ and self._service._measure_retained_bytes()
404
+ > self._service.metadata.max_memory_bytes
401
405
  ):
402
406
  raise CacheClientError(
403
407
  "memory_budget_exceeded",
404
- "Cache request exceeded the configured RSS memory budget.",
408
+ "Cache request exceeded the configured retained-memory budget.",
405
409
  )
406
410
 
407
411
 
@@ -854,11 +858,15 @@ class _CacheService:
854
858
  context = self._initialize_context()
855
859
  try:
856
860
  self.last_revision = context.prewarm_navigation()
857
- self.cache_state = "warm"
858
861
  except AgentProtocolError as error:
859
862
  if error.code != "project_required":
860
863
  raise
861
- self.cache_state = "ready"
864
+ prewarm_rss = current_process_rss_bytes()
865
+ if prewarm_rss > 0:
866
+ # Parsing can leave a high allocator RSS watermark even though the
867
+ # retained navigation data is already covered by its own estimate.
868
+ # Track only subsequent, otherwise unaccounted-for RSS growth.
869
+ self._rss_baseline_bytes = prewarm_rss
862
870
  self._enforce_budget()
863
871
  self.prewarm_seconds = time.monotonic() - started
864
872
  self.stats.parallel_fallbacks = context.parallel_stats.fallbacks
@@ -921,58 +929,86 @@ class _CacheService:
921
929
  self.context.invalidate_revision_cache()
922
930
  with self._request_state_lock:
923
931
  self._active_requests += 1
932
+ lock_acquired = False
924
933
  try:
925
- with self.lock:
926
- self.last_activity = time.monotonic()
927
- before = self.last_revision
928
- was_warm = self.context.navigation_cache_is_warm
934
+ while not self.lock.acquire(timeout=0.05):
935
+ if self.shutdown.is_set():
936
+ raise CacheClientError(
937
+ "request_cancelled",
938
+ "Cache request was cancelled.",
939
+ )
940
+ if cancel_check is not None:
941
+ cancel_check()
942
+ lock_acquired = True
943
+ if self.shutdown.is_set():
944
+ raise CacheClientError(
945
+ "request_cancelled",
946
+ "Cache request was cancelled.",
947
+ )
948
+ if cancel_check is not None:
949
+ cancel_check()
950
+ self.last_activity = time.monotonic()
951
+ before = self.last_revision
952
+ was_warm = self.context.navigation_cache_is_warm
953
+ request_aborted = False
954
+ try:
929
955
  try:
930
- try:
931
- handled = (
932
- self.context.handle(request)
933
- if cancel_check is None
934
- else self.context.handle(
935
- request,
936
- cancel_check=cancel_check,
937
- )
956
+ handled = (
957
+ self.context.handle(request)
958
+ if cancel_check is None
959
+ else self.context.handle(
960
+ request,
961
+ cancel_check=cancel_check,
938
962
  )
939
- response = handled.to_mapping()
940
- except CacheClientError:
941
- raise
942
- except AgentProtocolError as error:
943
- raise CacheClientError(
944
- error.code,
945
- error.message,
946
- ) from None
947
- except (OSError, UnicodeError):
948
- raise CacheClientError(
949
- "source_unavailable",
950
- "Source is unavailable.",
951
- ) from None
952
- except Exception:
963
+ )
964
+ if self.shutdown.is_set():
953
965
  raise CacheClientError(
954
- "internal_error",
955
- "Internal cache error.",
956
- ) from None
957
- finally:
966
+ "request_cancelled",
967
+ "Cache request was cancelled.",
968
+ )
969
+ if cancel_check is not None:
970
+ cancel_check()
971
+ response = handled.to_mapping()
972
+ except CacheClientError:
973
+ request_aborted = True
974
+ raise
975
+ except AgentProtocolError as error:
976
+ raise CacheClientError(
977
+ error.code,
978
+ error.message,
979
+ ) from None
980
+ except (OSError, UnicodeError):
981
+ raise CacheClientError(
982
+ "source_unavailable",
983
+ "Source is unavailable.",
984
+ ) from None
985
+ except Exception:
986
+ raise CacheClientError(
987
+ "internal_error",
988
+ "Internal cache error.",
989
+ ) from None
990
+ finally:
991
+ if not request_aborted and not self.shutdown.is_set():
958
992
  self._enforce_budget()
959
- after = str(response["workspace_revision"])
960
- self.last_revision = after
961
- self.stats.requests += 1
962
- if was_warm:
963
- self.stats.warm_hits += 1
964
- else:
965
- self.stats.rebuilds += 1
966
- self.stats.parallel_fallbacks += (
967
- self.context.parallel_stats.fallbacks
968
- )
969
- if before != after:
970
- self.stats.invalidations += 1
971
- return CacheClientResponse(
972
- response,
973
- self._consume_warning(),
993
+ after = str(response["workspace_revision"])
994
+ self.last_revision = after
995
+ self.stats.requests += 1
996
+ if was_warm:
997
+ self.stats.warm_hits += 1
998
+ else:
999
+ self.stats.rebuilds += 1
1000
+ self.stats.parallel_fallbacks += (
1001
+ self.context.parallel_stats.fallbacks
974
1002
  )
1003
+ if before != after:
1004
+ self.stats.invalidations += 1
1005
+ return CacheClientResponse(
1006
+ response,
1007
+ self._consume_warning(),
1008
+ )
975
1009
  finally:
1010
+ if lock_acquired:
1011
+ self.lock.release()
976
1012
  with self._request_state_lock:
977
1013
  self._active_requests -= 1
978
1014
 
@@ -1142,10 +1178,11 @@ def _serve_connection(connection: socket.socket, service: _CacheService) -> None
1142
1178
  response: dict[str, object] = {"error": {"code": "authentication_failed", "message": "authentication failed"}}
1143
1179
  else:
1144
1180
  try:
1145
- guard = (
1146
- _RequestGuard(service, connection)
1147
- if request.get("action") in {"trace", "cpg"}
1148
- else None
1181
+ action = request.get("action")
1182
+ guard = None if action in {"status", "stop"} else _RequestGuard(
1183
+ service,
1184
+ connection,
1185
+ enforce_rss_budget=action in {"trace", "cpg"},
1149
1186
  )
1150
1187
  result = service.request(
1151
1188
  request,
@@ -1476,6 +1513,24 @@ def stop_cache(root: str | Path) -> None:
1476
1513
  os.kill(metadata.pid, signal.SIGTERM)
1477
1514
  signal_sent = True
1478
1515
  time.sleep(0.05)
1516
+ if not stopped and _pid_alive(metadata.pid):
1517
+ current = _read_metadata(root)
1518
+ if current == metadata:
1519
+ force_signal = (
1520
+ signal.SIGKILL
1521
+ if os.name != "nt" and hasattr(signal, "SIGKILL")
1522
+ else signal.SIGTERM
1523
+ )
1524
+ with contextlib.suppress(OSError):
1525
+ os.kill(metadata.pid, force_signal)
1526
+ force_deadline = time.monotonic() + _STOP_FORCE_TIMEOUT_SECONDS
1527
+ while time.monotonic() < force_deadline:
1528
+ if _reap_child_if_exited(metadata.pid) or not _pid_alive(
1529
+ metadata.pid
1530
+ ):
1531
+ stopped = True
1532
+ break
1533
+ time.sleep(0.05)
1479
1534
  if not stopped and _pid_alive(metadata.pid):
1480
1535
  raise CacheClientError("stop_failed", "Cache daemon did not stop.")
1481
1536
  _remove_metadata_if_owned(metadata)
delphi_lsp/agent_cli.py CHANGED
@@ -27,6 +27,7 @@ from .agent_cache import (
27
27
  from .agent_context import AgentContext
28
28
  from .agent_layers import (
29
29
  build_codebase_index,
30
+ build_metrics_payload,
30
31
  build_references_payload,
31
32
  layer_payload,
32
33
  render_layer,
@@ -45,6 +46,12 @@ from .agent_templates import install_opencode_support, install_skill
45
46
  from .agent_wiki import WikiExportError, WikiProgressEvent, export_okf_wiki
46
47
  from .parallel_outline import ParallelOutlineError, parse_worker_setting
47
48
  from .project_config import ProjectConfigError
49
+ from .response_cache import (
50
+ ResponseCacheKey,
51
+ ResponseCacheStore,
52
+ WorkspaceContentSnapshot,
53
+ workspace_content_snapshot,
54
+ )
48
55
 
49
56
  _MAX_WORKER_RECORD_BYTES = 1024 * 1024
50
57
  _INVALID_JSON_MESSAGE = "Invalid JSON request."
@@ -323,6 +330,17 @@ def _discard_broken_stdout() -> None:
323
330
  def _view(args: argparse.Namespace) -> None:
324
331
  args.root = _workspace_root(args.root)
325
332
  args.project_file = _project_file(args.root, args.project_file)
333
+ store, cache_key, snapshot = _batch_response_cache(
334
+ args,
335
+ command="view",
336
+ layer=args.layer,
337
+ query=args.query,
338
+ output_format=args.format,
339
+ )
340
+ cached = store.load(cache_key)
341
+ if cached is not None:
342
+ sys.stdout.write(cached.decode("utf-8"))
343
+ return
326
344
  if args.layer == "references":
327
345
  payload = build_references_payload(
328
346
  args.root,
@@ -330,40 +348,112 @@ def _view(args: argparse.Namespace) -> None:
330
348
  project_file=args.project_file,
331
349
  workers=args.workers,
332
350
  )
333
- sys.stdout.write(
334
- render_payload(payload, output_format=args.format)
351
+ rendered = render_payload(payload, output_format=args.format)
352
+ elif args.layer == "metrics":
353
+ payload = build_metrics_payload(
354
+ args.root,
355
+ query=args.query,
356
+ project_file=args.project_file,
357
+ workers=args.workers,
358
+ discovery=snapshot.discovery,
335
359
  )
336
- return
337
- index = build_codebase_index(
338
- args.root,
339
- project_file=args.project_file,
340
- index_projects=args.deep_projects,
341
- workers=args.workers,
342
- )
343
- sys.stdout.write(render_layer(index, args.layer, query=args.query, output_format=args.format))
360
+ if args.format == "json":
361
+ rendered = _json_text(payload)
362
+ else:
363
+ rendered = render_payload(payload, output_format=args.format)
364
+ else:
365
+ index = build_codebase_index(
366
+ args.root,
367
+ project_file=args.project_file,
368
+ index_projects=args.deep_projects,
369
+ workers=args.workers,
370
+ use_artifact_cache=False,
371
+ discovery=snapshot.discovery,
372
+ )
373
+ rendered = render_layer(
374
+ index,
375
+ args.layer,
376
+ query=args.query,
377
+ output_format=args.format,
378
+ )
379
+ encoded = rendered.encode("utf-8")
380
+ _store_batch_response(store, cache_key, encoded)
381
+ sys.stdout.write(rendered)
344
382
 
345
383
 
346
384
  def _index(args: argparse.Namespace) -> None:
347
385
  args.root = _workspace_root(args.root)
348
386
  args.project_file = _project_file(args.root, args.project_file)
349
387
  output = _resolve_output_path(args.root, args.out)
350
- index = build_codebase_index(
351
- args.root,
352
- project_file=args.project_file,
353
- index_projects=args.deep_projects,
354
- workers=args.workers,
388
+ store, cache_key, snapshot = _batch_response_cache(
389
+ args,
390
+ command="index",
391
+ layer="",
392
+ query="",
393
+ output_format="json",
355
394
  )
356
- payload = {
357
- "deep_projects": bool(args.deep_projects),
358
- "overview": layer_payload(index, "overview"),
359
- "projects": layer_payload(index, "projects"),
360
- "problems": layer_payload(index, "problems"),
361
- }
395
+ encoded = store.load(cache_key)
396
+ if encoded is None:
397
+ index = build_codebase_index(
398
+ args.root,
399
+ project_file=args.project_file,
400
+ index_projects=args.deep_projects,
401
+ workers=args.workers,
402
+ use_artifact_cache=False,
403
+ discovery=snapshot.discovery,
404
+ )
405
+ payload = {
406
+ "deep_projects": bool(args.deep_projects),
407
+ "overview": layer_payload(index, "overview"),
408
+ "projects": layer_payload(index, "projects"),
409
+ "problems": layer_payload(index, "problems"),
410
+ }
411
+ encoded = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode(
412
+ "utf-8"
413
+ )
414
+ _store_batch_response(store, cache_key, encoded)
362
415
  output.parent.mkdir(parents=True, exist_ok=True)
363
- output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
416
+ output.write_bytes(encoded)
364
417
  print(output)
365
418
 
366
419
 
420
+ def _batch_response_cache(
421
+ args: argparse.Namespace,
422
+ *,
423
+ command: str,
424
+ layer: str,
425
+ query: str,
426
+ output_format: str,
427
+ ) -> tuple[ResponseCacheStore, ResponseCacheKey, WorkspaceContentSnapshot]:
428
+ snapshot = workspace_content_snapshot(
429
+ args.root,
430
+ project_file=args.project_file,
431
+ )
432
+ return (
433
+ ResponseCacheStore(args.root / ".delphi-lsp" / "response-cache"),
434
+ ResponseCacheKey(
435
+ workspace_revision=snapshot.revision,
436
+ command=command,
437
+ layer=layer,
438
+ query=query,
439
+ output_format=output_format,
440
+ deep_projects=bool(args.deep_projects),
441
+ ),
442
+ snapshot,
443
+ )
444
+
445
+
446
+ def _store_batch_response(
447
+ store: ResponseCacheStore,
448
+ key: ResponseCacheKey,
449
+ payload: bytes,
450
+ ) -> None:
451
+ try:
452
+ store.store(key, payload)
453
+ except (OSError, ValueError):
454
+ pass
455
+
456
+
367
457
  def _wiki_export(args: argparse.Namespace) -> None:
368
458
  args.root = _workspace_root(args.root)
369
459
  args.project_file = _project_file(args.root, args.project_file)
@@ -518,7 +608,11 @@ def _open_worker_context(
518
608
 
519
609
 
520
610
  def _write_json(payload: object) -> None:
521
- sys.stdout.write(json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
611
+ sys.stdout.write(_json_text(payload))
612
+
613
+
614
+ def _json_text(payload: object) -> str:
615
+ return json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n"
522
616
 
523
617
 
524
618
  def _write_warning(warning: str) -> None: