python-delphi-lsp 2.3.0__tar.gz → 2.3.1__tar.gz

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 (154) hide show
  1. {python_delphi_lsp-2.3.0/python_delphi_lsp.egg-info → python_delphi_lsp-2.3.1}/PKG-INFO +2 -2
  2. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/README.md +1 -1
  3. python_delphi_lsp-2.3.1/delphi_lsp/_version.py +1 -0
  4. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cache.py +128 -9
  5. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cli.py +120 -13
  6. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_context.py +1 -1
  7. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_discovery.py +5 -1
  8. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/pyproject.toml +1 -1
  9. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1/python_delphi_lsp.egg-info}/PKG-INFO +2 -2
  10. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/SOURCES.txt +1 -0
  11. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_cache.py +126 -1
  12. python_delphi_lsp-2.3.1/tests/test_agent_cli_errors.py +222 -0
  13. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_context.py +37 -1
  14. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_worker.py +228 -6
  15. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_package_metadata.py +3 -3
  16. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_project_discovery.py +20 -0
  17. python_delphi_lsp-2.3.0/delphi_lsp/_version.py +0 -1
  18. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/.github/workflows/ci.yml +0 -0
  19. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/LICENSE +0 -0
  20. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/MANIFEST.in +0 -0
  21. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/__init__.py +0 -0
  22. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_layers.py +0 -0
  23. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_metrics.py +0 -0
  24. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_protocol.py +0 -0
  25. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_relations.py +0 -0
  26. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_templates.py +0 -0
  27. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_workspace.py +0 -0
  28. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/binary.py +0 -0
  29. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/comment_builder.py +0 -0
  30. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/consts.py +0 -0
  31. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_lexer.py +0 -0
  32. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_parser.py +0 -0
  33. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_tokens.py +0 -0
  34. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/grammar.py +0 -0
  35. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_builder.py +0 -0
  36. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_tokens.py +0 -0
  37. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lsp_server.py +0 -0
  38. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/metrics.py +0 -0
  39. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/navigation_cache.py +0 -0
  40. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/nodes.py +0 -0
  41. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parallel_outline.py +0 -0
  42. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser.py +0 -0
  43. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser_backend.py +0 -0
  44. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/preprocessor.py +0 -0
  45. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/progress.py +0 -0
  46. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_indexer.py +0 -0
  47. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic.py +0 -0
  48. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic_builder.py +0 -0
  49. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/source_reader.py +0 -0
  50. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/workspace.py +0 -0
  51. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/delphi_lsp/writer.py +0 -0
  52. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/opencode.json +0 -0
  53. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  54. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  55. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/requires.txt +0 -0
  56. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  57. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/audit_delphi_language_features.py +0 -0
  58. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_github_corpus.py +0 -0
  59. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_parallel_cache.py +0 -0
  60. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
  61. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  62. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  63. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  64. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/build_github_performance_corpus.py +0 -0
  65. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/check_ornith_cache.py +0 -0
  66. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/generate_progress_pdf.py +0 -0
  67. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/generate_release_evidence.py +0 -0
  68. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  69. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/prepare_ornith_cache.py +0 -0
  70. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/run_opencode_lsp_probe.py +0 -0
  71. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/run_openrouter_github_e2e.py +0 -0
  72. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/start_ornith_vllm.sh +0 -0
  73. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/scripts/summarize_opencode_request_payloads.py +0 -0
  74. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/setup.cfg +0 -0
  75. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/corpora.performance.lock.json +0 -0
  76. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  77. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  78. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  79. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  80. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  81. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  82. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  83. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  84. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  85. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  86. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  87. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  88. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  89. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  90. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  91. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  92. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  93. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  94. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  95. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  96. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  97. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  98. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  99. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  100. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  101. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  102. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  103. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  104. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  105. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/library_demo.dpr +0 -0
  106. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/package_demo.dpk +0 -0
  107. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/program_demo.dpr +0 -0
  108. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_advanced.pas +0 -0
  109. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_attributes.pas +0 -0
  110. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_basic.pas +0 -0
  111. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_consumer.pas +0 -0
  112. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_generics.pas +0 -0
  113. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_inheritance.pas +0 -0
  114. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_math.pas +0 -0
  115. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_properties.pas +0 -0
  116. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_sections.pas +0 -0
  117. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_statements.pas +0 -0
  118. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_types.pas +0 -0
  119. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_unresolved.pas +0 -0
  120. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_with.pas +0 -0
  121. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_codebase.py +0 -0
  122. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_metrics.py +0 -0
  123. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_protocol.py +0 -0
  124. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_relations.py +0 -0
  125. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_agent_workspace.py +0 -0
  126. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
  127. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_opencode.py +0 -0
  128. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_lexer.py +0 -0
  129. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_native_compatibility.py +0 -0
  130. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_parser.py +0 -0
  131. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_semantic_compatibility.py +0 -0
  132. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_diagnostics.py +0 -0
  133. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_github_performance_corpus.py +0 -0
  134. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_language_feature_matrix.py +0 -0
  135. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_legacy_snippets.py +0 -0
  136. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_features.py +0 -0
  137. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_support.py +0 -0
  138. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_metrics.py +0 -0
  139. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_navigation_cache.py +0 -0
  140. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_config.py +0 -0
  141. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_probe.py +0 -0
  142. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_request_payloads.py +0 -0
  143. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_openrouter_github_e2e.py +0 -0
  144. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_ornith_cache_prepare.py +0 -0
  145. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_parallel_outline.py +0 -0
  146. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_parser.py +0 -0
  147. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_parser_backends.py +0 -0
  148. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_preprocessor.py +0 -0
  149. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_progress.py +0 -0
  150. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_project_indexer.py +0 -0
  151. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_release_evidence.py +0 -0
  152. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_semantic.py +0 -0
  153. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_vllm_start_script.py +0 -0
  154. {python_delphi_lsp-2.3.0 → python_delphi_lsp-2.3.1}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-delphi-lsp
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Python Delphi/Object Pascal parser, semantic indexer, and language server.
5
5
  Author: Dark Light
6
6
  License-Expression: MPL-2.0
@@ -42,7 +42,7 @@ Dynamic: license-file
42
42
 
43
43
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
44
44
  indexes, serves LSP, and provides bounded codebase navigation for agents.
45
- Version 2.3.0 is authored by Dark Light and supports Windows, macOS, and Linux.
45
+ Version 2.3.1 is authored by Dark Light and supports Windows, macOS, and Linux.
46
46
 
47
47
  ## Install and quick start
48
48
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
4
4
  indexes, serves LSP, and provides bounded codebase navigation for agents.
5
- Version 2.3.0 is authored by Dark Light and supports Windows, macOS, and Linux.
5
+ Version 2.3.1 is authored by Dark Light and supports Windows, macOS, and Linux.
6
6
 
7
7
  ## Install and quick start
8
8
 
@@ -0,0 +1 @@
1
+ __version__ = "2.3.1"
@@ -45,6 +45,88 @@ _WATCHED_SUFFIXES = frozenset(
45
45
  )
46
46
 
47
47
 
48
+ def current_process_rss_bytes() -> int:
49
+ """Return the current resident set size, or zero when unavailable."""
50
+ try:
51
+ if sys.platform == "darwin":
52
+ return _darwin_process_rss_bytes()
53
+ if sys.platform.startswith("linux"):
54
+ statm = Path("/proc/self/statm").read_text(encoding="ascii").split()
55
+ return int(statm[1]) * int(os.sysconf("SC_PAGE_SIZE"))
56
+ if os.name == "nt":
57
+ return _windows_process_rss_bytes()
58
+ except Exception:
59
+ pass
60
+ return 0
61
+
62
+
63
+ def _darwin_process_rss_bytes() -> int:
64
+ import ctypes
65
+
66
+ class TimeValue(ctypes.Structure):
67
+ _fields_ = [("seconds", ctypes.c_int), ("microseconds", ctypes.c_int)]
68
+
69
+ class MachTaskBasicInfo(ctypes.Structure):
70
+ _fields_ = [
71
+ ("virtual_size", ctypes.c_uint64),
72
+ ("resident_size", ctypes.c_uint64),
73
+ ("resident_size_max", ctypes.c_uint64),
74
+ ("user_time", TimeValue),
75
+ ("system_time", TimeValue),
76
+ ("policy", ctypes.c_int),
77
+ ("suspend_count", ctypes.c_int),
78
+ ]
79
+
80
+ library = ctypes.CDLL(None)
81
+ library.mach_task_self.restype = ctypes.c_uint
82
+ library.task_info.argtypes = (
83
+ ctypes.c_uint,
84
+ ctypes.c_int,
85
+ ctypes.c_void_p,
86
+ ctypes.POINTER(ctypes.c_uint),
87
+ )
88
+ library.task_info.restype = ctypes.c_int
89
+ info = MachTaskBasicInfo()
90
+ count = ctypes.c_uint(ctypes.sizeof(info) // ctypes.sizeof(ctypes.c_int))
91
+ result = library.task_info(
92
+ library.mach_task_self(),
93
+ 20,
94
+ ctypes.byref(info),
95
+ ctypes.byref(count),
96
+ )
97
+ return int(info.resident_size) if result == 0 else 0
98
+
99
+
100
+ def _windows_process_rss_bytes() -> int:
101
+ import ctypes
102
+ from ctypes import wintypes
103
+
104
+ class ProcessMemoryCounters(ctypes.Structure):
105
+ _fields_ = [
106
+ ("cb", wintypes.DWORD),
107
+ ("PageFaultCount", wintypes.DWORD),
108
+ ("PeakWorkingSetSize", ctypes.c_size_t),
109
+ ("WorkingSetSize", ctypes.c_size_t),
110
+ ("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
111
+ ("QuotaPagedPoolUsage", ctypes.c_size_t),
112
+ ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
113
+ ("QuotaNonPagedPoolUsage", ctypes.c_size_t),
114
+ ("PagefileUsage", ctypes.c_size_t),
115
+ ("PeakPagefileUsage", ctypes.c_size_t),
116
+ ]
117
+
118
+ counters = ProcessMemoryCounters()
119
+ counters.cb = ctypes.sizeof(counters)
120
+ process = ctypes.windll.kernel32.GetCurrentProcess()
121
+ if not ctypes.windll.psapi.GetProcessMemoryInfo(
122
+ process,
123
+ ctypes.byref(counters),
124
+ counters.cb,
125
+ ):
126
+ return 0
127
+ return int(counters.WorkingSetSize)
128
+
129
+
48
130
  def estimate_deep_size(value: object) -> int:
49
131
  """Estimate the memory retained by an owned object graph.
50
132
 
@@ -292,6 +374,11 @@ def _safe_navigation_cache_path(root: str | Path, *, create: bool = False) -> Pa
292
374
  return result
293
375
 
294
376
 
377
+ def navigation_cache_path(root: str | Path, *, create: bool = False) -> Path:
378
+ """Return the validated persistent navigation-cache directory."""
379
+ return _safe_navigation_cache_path(root, create=create)
380
+
381
+
295
382
  def _metadata_mapping(metadata: CacheMetadata) -> dict[str, object]:
296
383
  return {field.name: getattr(metadata, field.name) for field in fields(metadata)}
297
384
 
@@ -555,13 +642,14 @@ def _read_startup_tail(diagnostics: object, *, max_bytes: int = _STARTUP_DIAGNOS
555
642
  class _CacheService:
556
643
  def __init__(self, metadata: CacheMetadata) -> None:
557
644
  self.metadata = metadata
645
+ self._rss_baseline_bytes = current_process_rss_bytes()
558
646
  self.context = AgentContext.open(
559
647
  metadata.root,
560
648
  metadata.project_file or None,
561
649
  workers=metadata.workers,
562
650
  worker_memory_budget_bytes=metadata.max_memory_bytes,
563
651
  revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
564
- navigation_cache_dir=_safe_navigation_cache_path(
652
+ navigation_cache_dir=navigation_cache_path(
565
653
  metadata.root,
566
654
  create=True,
567
655
  ),
@@ -577,6 +665,15 @@ class _CacheService:
577
665
  self.prewarm_seconds = 0.0
578
666
  self.shutdown = threading.Event()
579
667
 
668
+ def _measure_retained_bytes(self) -> int:
669
+ process_rss = current_process_rss_bytes()
670
+ process_growth = (
671
+ max(0, process_rss - self._rss_baseline_bytes)
672
+ if process_rss > 0 and self._rss_baseline_bytes > 0
673
+ else 0
674
+ )
675
+ return max(self.context.estimated_cache_bytes, process_growth)
676
+
580
677
  def prewarm(self) -> None:
581
678
  started = time.monotonic()
582
679
  try:
@@ -587,7 +684,7 @@ class _CacheService:
587
684
  raise
588
685
  self.cache_state = "ready"
589
686
  self.last_budget = self.budget.enforce(
590
- measure=lambda: self.context.estimated_cache_bytes,
687
+ measure=self._measure_retained_bytes,
591
688
  evict_auxiliary=self.context.evict_auxiliary_caches,
592
689
  evict_navigation=self.context.evict_navigation_caches,
593
690
  )
@@ -628,7 +725,7 @@ class _CacheService:
628
725
  if before != after:
629
726
  self.stats.invalidations += 1
630
727
  self.last_budget = self.budget.enforce(
631
- measure=lambda: self.context.estimated_cache_bytes,
728
+ measure=self._measure_retained_bytes,
632
729
  evict_auxiliary=self.context.evict_auxiliary_caches,
633
730
  evict_navigation=self.context.evict_navigation_caches,
634
731
  )
@@ -675,21 +772,34 @@ def _watch_filter(_change: object, path: str) -> bool:
675
772
  return Path(path).suffix.casefold() in _WATCHED_SUFFIXES
676
773
 
677
774
 
678
- def _watch_workspace(service: _CacheService) -> None:
775
+ def watch_workspace_changes(
776
+ root: str | Path,
777
+ *,
778
+ stop_event: threading.Event,
779
+ on_change: Callable[[], None],
780
+ ) -> None:
679
781
  try:
680
782
  for changes in watch(
681
- service.metadata.root,
783
+ root,
682
784
  watch_filter=_watch_filter,
683
- stop_event=service.shutdown,
785
+ stop_event=stop_event,
684
786
  debounce=50,
685
787
  step=20,
686
788
  recursive=True,
687
789
  raise_interrupt=False,
688
790
  ):
689
791
  if changes:
690
- service.context.invalidate_revision_cache()
792
+ on_change()
691
793
  except (OSError, RuntimeError):
692
- service.context.invalidate_revision_cache()
794
+ on_change()
795
+
796
+
797
+ def _watch_workspace(service: _CacheService) -> None:
798
+ watch_workspace_changes(
799
+ service.metadata.root,
800
+ stop_event=service.shutdown,
801
+ on_change=service.context.invalidate_revision_cache,
802
+ )
693
803
 
694
804
 
695
805
  def _serve_connection(connection: socket.socket, service: _CacheService) -> None:
@@ -867,6 +977,8 @@ def start_cache(
867
977
  ) -> CacheMetadata:
868
978
  if type(workers) is not int or not 0 <= workers <= 32:
869
979
  raise ValueError("workers must be auto or an integer from 1 through 32.")
980
+ if type(idle_timeout) is not int or idle_timeout <= 0:
981
+ raise ValueError("idle_timeout must be greater than zero.")
870
982
  if not math.isfinite(startup_timeout) or startup_timeout <= 0:
871
983
  raise ValueError("startup_timeout must be greater than zero.")
872
984
  with _start_lock(root, startup_timeout):
@@ -930,7 +1042,7 @@ def main(argv: list[str] | None = None) -> int:
930
1042
  serve.add_argument("--project-file", default="")
931
1043
  serve.add_argument("--max-memory", type=int, default=DEFAULT_MAX_MEMORY_BYTES)
932
1044
  serve.add_argument("--workers", type=int, default=0)
933
- serve.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
1045
+ serve.add_argument("--idle-timeout", type=_positive_integer, default=DEFAULT_IDLE_TIMEOUT)
934
1046
  args = parser.parse_args(argv)
935
1047
  if args.command == "serve":
936
1048
  run_cache_daemon(
@@ -943,5 +1055,12 @@ def main(argv: list[str] | None = None) -> int:
943
1055
  return 0
944
1056
 
945
1057
 
1058
+ def _positive_integer(value: str) -> int:
1059
+ parsed = int(value)
1060
+ if parsed <= 0:
1061
+ raise argparse.ArgumentTypeError("value must be greater than zero")
1062
+ return parsed
1063
+
1064
+
946
1065
  if __name__ == "__main__":
947
1066
  raise SystemExit(main())
@@ -5,6 +5,7 @@ import json
5
5
  import math
6
6
  import os
7
7
  import sys
8
+ import threading
8
9
  from pathlib import Path
9
10
  from typing import BinaryIO, TextIO
10
11
 
@@ -15,15 +16,17 @@ from .agent_cache import (
15
16
  DEFAULT_MAX_MEMORY_BYTES,
16
17
  DEFAULT_STARTUP_TIMEOUT,
17
18
  parse_memory_size,
19
+ navigation_cache_path,
18
20
  query_cache,
19
21
  run_cache_daemon,
20
22
  start_cache,
21
23
  stop_cache,
24
+ watch_workspace_changes,
22
25
  )
23
26
  from .agent_layers import build_codebase_index, layer_payload, render_layer
24
27
  from .agent_protocol import AgentProtocolError, SUPPORTED_ACTIONS, SUPPORTED_DETAILS, SUPPORTED_RELATIONS
25
28
  from .agent_templates import install_opencode_support, install_skill
26
- from .parallel_outline import parse_worker_setting
29
+ from .parallel_outline import ParallelOutlineError, parse_worker_setting
27
30
 
28
31
 
29
32
  _MAX_WORKER_RECORD_BYTES = 1024 * 1024
@@ -32,6 +35,14 @@ _INVALID_ENCODING_MESSAGE = "Invalid UTF-8 request."
32
35
  _REQUEST_TOO_LARGE_MESSAGE = "Request exceeds the 1 MiB limit."
33
36
  _INTERNAL_ERROR_MESSAGE = "Internal request error."
34
37
  _SOURCE_UNAVAILABLE_MESSAGE = "Selected source is unavailable."
38
+ _WORKER_REVISION_CHECK_INTERVAL_SECONDS = 30.0
39
+
40
+
41
+ class _CliError(RuntimeError):
42
+ def __init__(self, code: str, message: str) -> None:
43
+ self.code = code
44
+ self.message = message
45
+ super().__init__(message)
35
46
 
36
47
 
37
48
  def build_parser() -> argparse.ArgumentParser:
@@ -120,7 +131,7 @@ def build_parser() -> argparse.ArgumentParser:
120
131
  cache_serve.add_argument("--project-file", type=Path)
121
132
  cache_serve.add_argument("--max-memory", type=parse_memory_size, required=True)
122
133
  cache_serve.add_argument("--workers", type=parse_worker_setting, required=True)
123
- cache_serve.add_argument("--idle-timeout", type=int, required=True)
134
+ cache_serve.add_argument("--idle-timeout", type=_positive_integer, required=True)
124
135
  cache_serve.set_defaults(func=_cache_serve)
125
136
 
126
137
  query = subcommands.add_parser("query", help="Send an ergonomic request to a running cache daemon.")
@@ -131,8 +142,8 @@ def build_parser() -> argparse.ArgumentParser:
131
142
  query.add_argument("--detail", choices=SUPPORTED_DETAILS, default="summary")
132
143
  query.add_argument("--relation", choices=SUPPORTED_RELATIONS)
133
144
  query.add_argument("--cursor", default="")
134
- query.add_argument("--max-items", type=int, default=12)
135
- query.add_argument("--max-chars", type=int, default=12000)
145
+ query.add_argument("--max-items", type=_max_items, default=12)
146
+ query.add_argument("--max-chars", type=_max_chars, default=12000)
136
147
  query.set_defaults(func=_query)
137
148
 
138
149
  return parser
@@ -143,7 +154,7 @@ def _add_cache_start_arguments(parser: argparse.ArgumentParser) -> None:
143
154
  parser.add_argument("--project-file", type=Path)
144
155
  parser.add_argument("--max-memory", type=parse_memory_size, default=DEFAULT_MAX_MEMORY_BYTES)
145
156
  parser.add_argument("--workers", type=parse_worker_setting, default=0)
146
- parser.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
157
+ parser.add_argument("--idle-timeout", type=_positive_integer, default=DEFAULT_IDLE_TIMEOUT)
147
158
  parser.add_argument("--startup-timeout", type=_positive_float, default=DEFAULT_STARTUP_TIMEOUT)
148
159
 
149
160
 
@@ -154,6 +165,27 @@ def _positive_float(value: str) -> float:
154
165
  return parsed
155
166
 
156
167
 
168
+ def _positive_integer(value: str) -> int:
169
+ parsed = int(value)
170
+ if parsed <= 0:
171
+ raise argparse.ArgumentTypeError("value must be greater than zero")
172
+ return parsed
173
+
174
+
175
+ def _max_items(value: str) -> int:
176
+ parsed = int(value)
177
+ if not 1 <= parsed <= 50:
178
+ raise argparse.ArgumentTypeError("value must be between 1 and 50")
179
+ return parsed
180
+
181
+
182
+ def _max_chars(value: str) -> int:
183
+ parsed = int(value)
184
+ if not 256 <= parsed <= 40000:
185
+ raise argparse.ArgumentTypeError("value must be between 256 and 40000")
186
+ return parsed
187
+
188
+
157
189
  def main(argv: list[str] | None = None) -> int:
158
190
  parser = build_parser()
159
191
  args = parser.parse_args(argv)
@@ -161,9 +193,23 @@ def main(argv: list[str] | None = None) -> int:
161
193
  result = args.func(args)
162
194
  if not getattr(sys.stdout, "closed", False):
163
195
  sys.stdout.flush()
196
+ except _CliError as error:
197
+ sys.stderr.write(f"cli_error:{error.code}: {error.message}\n")
198
+ sys.stderr.flush()
199
+ return 1
200
+ except CacheClientError as error:
201
+ return _cache_error(error)
202
+ except ParallelOutlineError as error:
203
+ sys.stderr.write(f"cli_error:parallel_failed: {error}\n")
204
+ sys.stderr.flush()
205
+ return 1
164
206
  except BrokenPipeError:
165
207
  _discard_broken_stdout()
166
208
  os._exit(1)
209
+ except OSError as error:
210
+ sys.stderr.write(f"cli_error:io_error: {error}\n")
211
+ sys.stderr.flush()
212
+ return 1
167
213
  return result if isinstance(result, int) else 0
168
214
 
169
215
 
@@ -189,6 +235,7 @@ def _discard_broken_stdout() -> None:
189
235
 
190
236
 
191
237
  def _view(args: argparse.Namespace) -> None:
238
+ args.root = _workspace_root(args.root)
192
239
  index = build_codebase_index(
193
240
  args.root,
194
241
  project_file=args.project_file,
@@ -199,6 +246,7 @@ def _view(args: argparse.Namespace) -> None:
199
246
 
200
247
 
201
248
  def _index(args: argparse.Namespace) -> None:
249
+ args.root = _workspace_root(args.root)
202
250
  index = build_codebase_index(
203
251
  args.root,
204
252
  project_file=args.project_file,
@@ -216,33 +264,72 @@ def _index(args: argparse.Namespace) -> None:
216
264
 
217
265
 
218
266
  def _skill_install(args: argparse.Namespace) -> None:
219
- skill_path = install_skill(args.target, force=args.force)
267
+ try:
268
+ skill_path = install_skill(args.target, force=args.force)
269
+ except FileExistsError as error:
270
+ raise _CliError("install_conflict", str(error)) from None
220
271
  print(skill_path)
221
272
 
222
273
 
223
274
  def _opencode_install(args: argparse.Namespace) -> None:
224
- skill_path, plugin_path, agent_path = install_opencode_support(
225
- args.target,
226
- python_executable=args.python,
227
- force=args.force,
228
- write_config=args.write_config,
229
- )
275
+ try:
276
+ skill_path, plugin_path, agent_path = install_opencode_support(
277
+ args.target,
278
+ python_executable=args.python,
279
+ force=args.force,
280
+ write_config=args.write_config,
281
+ )
282
+ except FileExistsError as error:
283
+ raise _CliError("install_conflict", str(error)) from None
230
284
  print(skill_path)
231
285
  print(plugin_path)
232
286
  print(agent_path)
233
287
 
234
288
 
235
289
  def _worker(args: argparse.Namespace) -> None:
236
- context = AgentContext.open(args.root, args.project_file, workers=args.workers)
290
+ args.root = _workspace_root(args.root)
291
+ context = _open_worker_context(args.root, args.project_file, args.workers)
292
+ watcher_stop = threading.Event()
293
+ watcher = threading.Thread(
294
+ target=watch_workspace_changes,
295
+ kwargs={
296
+ "root": args.root,
297
+ "stop_event": watcher_stop,
298
+ "on_change": context.invalidate_revision_cache,
299
+ },
300
+ name="delphi-worker-watcher",
301
+ daemon=True,
302
+ )
303
+ watcher.start()
237
304
  try:
238
305
  _serve_worker(context, sys.stdin.buffer, sys.stdout.buffer, sys.stderr)
239
306
  finally:
307
+ watcher_stop.set()
308
+ watcher.join(timeout=2.0)
240
309
  try:
241
310
  sys.stdout.close()
242
311
  except (BrokenPipeError, OSError) as error:
243
312
  raise BrokenPipeError from error
244
313
 
245
314
 
315
+ def _open_worker_context(
316
+ root: Path,
317
+ project_file: Path | None,
318
+ workers: int,
319
+ ) -> AgentContext:
320
+ try:
321
+ cache_dir: Path | None = navigation_cache_path(root, create=True)
322
+ except (CacheClientError, OSError):
323
+ cache_dir = None
324
+ return AgentContext.open(
325
+ root,
326
+ project_file,
327
+ workers=workers,
328
+ revision_check_interval_seconds=_WORKER_REVISION_CHECK_INTERVAL_SECONDS,
329
+ navigation_cache_dir=cache_dir,
330
+ )
331
+
332
+
246
333
  def _write_json(payload: object) -> None:
247
334
  sys.stdout.write(json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
248
335
 
@@ -260,6 +347,7 @@ def _cache_error(error: CacheClientError) -> int:
260
347
 
261
348
 
262
349
  def _cache_start(args: argparse.Namespace) -> int:
350
+ args.root = _workspace_root(args.root)
263
351
  try:
264
352
  start_cache(
265
353
  args.root,
@@ -278,6 +366,7 @@ def _cache_start(args: argparse.Namespace) -> int:
278
366
 
279
367
 
280
368
  def _cache_status(args: argparse.Namespace) -> int:
369
+ args.root = _workspace_root(args.root)
281
370
  try:
282
371
  response = query_cache(args.root, {"action": "status"})
283
372
  except CacheClientError as error:
@@ -295,6 +384,7 @@ def _cache_status(args: argparse.Namespace) -> int:
295
384
 
296
385
 
297
386
  def _cache_stop(args: argparse.Namespace) -> int:
387
+ args.root = _workspace_root(args.root)
298
388
  try:
299
389
  response = query_cache(args.root, {"action": "status"})
300
390
  except CacheClientError as error:
@@ -316,6 +406,7 @@ def _cache_stop(args: argparse.Namespace) -> int:
316
406
 
317
407
 
318
408
  def _cache_serve(args: argparse.Namespace) -> int:
409
+ args.root = _workspace_root(args.root)
319
410
  run_cache_daemon(
320
411
  args.root,
321
412
  project_file=str(args.project_file or ""),
@@ -327,6 +418,7 @@ def _cache_serve(args: argparse.Namespace) -> int:
327
418
 
328
419
 
329
420
  def _query(args: argparse.Namespace) -> int:
421
+ args.root = _workspace_root(args.root)
330
422
  request: dict[str, object] = {"action": args.action}
331
423
  if args.value:
332
424
  if args.action in {"find", "metrics"}:
@@ -350,6 +442,21 @@ def _query(args: argparse.Namespace) -> int:
350
442
  return 0
351
443
 
352
444
 
445
+ def _workspace_root(value: str | Path) -> Path:
446
+ root = Path(value).expanduser()
447
+ if not root.exists():
448
+ raise _CliError(
449
+ "workspace_not_found",
450
+ f"Workspace root does not exist: {root}",
451
+ )
452
+ if not root.is_dir():
453
+ raise _CliError(
454
+ "workspace_not_directory",
455
+ f"Workspace root is not a directory: {root}",
456
+ )
457
+ return root.resolve()
458
+
459
+
353
460
  def _serve_worker(context: AgentContext, input_stream: BinaryIO, output_stream: BinaryIO, error_stream: TextIO) -> None:
354
461
  discarding_oversize_record = False
355
462
  while True:
@@ -658,7 +658,7 @@ class AgentContext:
658
658
  return revision
659
659
 
660
660
  def invalidate_revision_cache(self) -> None:
661
- self._last_revision_check_at = 0.0
661
+ self._last_revision_check_at = float("-inf")
662
662
 
663
663
  def handle(self, request: AgentRequest | Mapping[str, object]) -> AgentResponse:
664
664
  parsed = _validated_request(request)
@@ -75,7 +75,11 @@ def discover_delphi_project(
75
75
  on_progress: ProgressCallback | None = None,
76
76
  ) -> DelphiProjectDiscovery:
77
77
  root_path = Path(root).expanduser().resolve()
78
- project_path = Path(project_file).expanduser().resolve() if project_file is not None else None
78
+ project_path = Path(project_file).expanduser() if project_file is not None else None
79
+ if project_path is not None:
80
+ if not project_path.is_absolute():
81
+ project_path = root_path / project_path
82
+ project_path = project_path.resolve()
79
83
  discovery = DelphiProjectDiscovery(root=str(root_path))
80
84
  _emit_progress(on_progress, "discovery", str(root_path), 0, 0, None, "project discovery started")
81
85
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-delphi-lsp"
7
- version = "2.3.0"
7
+ version = "2.3.1"
8
8
  description = "Python Delphi/Object Pascal parser, semantic indexer, and language server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-delphi-lsp
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Python Delphi/Object Pascal parser, semantic indexer, and language server.
5
5
  Author: Dark Light
6
6
  License-Expression: MPL-2.0
@@ -42,7 +42,7 @@ Dynamic: license-file
42
42
 
43
43
  `python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
44
44
  indexes, serves LSP, and provides bounded codebase navigation for agents.
45
- Version 2.3.0 is authored by Dark Light and supports Windows, macOS, and Linux.
45
+ Version 2.3.1 is authored by Dark Light and supports Windows, macOS, and Linux.
46
46
 
47
47
  ## Install and quick start
48
48
 
@@ -65,6 +65,7 @@ scripts/summarize_opencode_request_payloads.py
65
65
  scripts/ollama/ornith-lspctx.Modelfile
66
66
  tests/corpora.performance.lock.json
67
67
  tests/test_agent_cache.py
68
+ tests/test_agent_cli_errors.py
68
69
  tests/test_agent_codebase.py
69
70
  tests/test_agent_context.py
70
71
  tests/test_agent_metrics.py