python-delphi-lsp 2.2.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 (155) hide show
  1. {python_delphi_lsp-2.2.0/python_delphi_lsp.egg-info → python_delphi_lsp-2.3.1}/PKG-INFO +11 -3
  2. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/README.md +10 -2
  3. python_delphi_lsp-2.3.1/delphi_lsp/_version.py +1 -0
  4. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cache.py +145 -8
  5. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cli.py +120 -13
  6. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_context.py +454 -184
  7. python_delphi_lsp-2.3.1/delphi_lsp/agent_metrics.py +179 -0
  8. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_protocol.py +4 -3
  9. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_relations.py +272 -26
  10. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_workspace.py +16 -3
  11. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/metrics.py +45 -7
  12. python_delphi_lsp-2.3.1/delphi_lsp/navigation_cache.py +134 -0
  13. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/preprocessor.py +56 -20
  14. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_discovery.py +5 -1
  15. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/pyproject.toml +1 -1
  16. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1/python_delphi_lsp.egg-info}/PKG-INFO +11 -3
  17. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/SOURCES.txt +3 -0
  18. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_cache.py +131 -1
  19. python_delphi_lsp-2.3.1/tests/test_agent_cli_errors.py +222 -0
  20. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_context.py +295 -16
  21. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_metrics.py +36 -0
  22. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_protocol.py +31 -0
  23. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_relations.py +55 -0
  24. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_worker.py +228 -6
  25. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_workspace.py +35 -0
  26. python_delphi_lsp-2.3.1/tests/test_navigation_cache.py +43 -0
  27. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_package_metadata.py +3 -3
  28. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_preprocessor.py +57 -1
  29. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_project_discovery.py +20 -0
  30. python_delphi_lsp-2.2.0/delphi_lsp/_version.py +0 -1
  31. python_delphi_lsp-2.2.0/delphi_lsp/agent_metrics.py +0 -82
  32. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/.github/workflows/ci.yml +0 -0
  33. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/LICENSE +0 -0
  34. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/MANIFEST.in +0 -0
  35. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/__init__.py +0 -0
  36. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_layers.py +0 -0
  37. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_templates.py +0 -0
  38. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/binary.py +0 -0
  39. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/comment_builder.py +0 -0
  40. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/consts.py +0 -0
  41. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_lexer.py +0 -0
  42. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_parser.py +0 -0
  43. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_tokens.py +0 -0
  44. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/grammar.py +0 -0
  45. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_builder.py +0 -0
  46. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_tokens.py +0 -0
  47. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lsp_server.py +0 -0
  48. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/nodes.py +0 -0
  49. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parallel_outline.py +0 -0
  50. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser.py +0 -0
  51. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser_backend.py +0 -0
  52. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/progress.py +0 -0
  53. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_indexer.py +0 -0
  54. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic.py +0 -0
  55. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic_builder.py +0 -0
  56. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/source_reader.py +0 -0
  57. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/workspace.py +0 -0
  58. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/writer.py +0 -0
  59. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/opencode.json +0 -0
  60. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
  61. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
  62. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/requires.txt +0 -0
  63. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/top_level.txt +0 -0
  64. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/audit_delphi_language_features.py +0 -0
  65. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_github_corpus.py +0 -0
  66. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_parallel_cache.py +0 -0
  67. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
  68. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
  69. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.py +0 -0
  70. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
  71. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/build_github_performance_corpus.py +0 -0
  72. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/check_ornith_cache.py +0 -0
  73. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/generate_progress_pdf.py +0 -0
  74. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/generate_release_evidence.py +0 -0
  75. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
  76. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/prepare_ornith_cache.py +0 -0
  77. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/run_opencode_lsp_probe.py +0 -0
  78. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/run_openrouter_github_e2e.py +0 -0
  79. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/start_ornith_vllm.sh +0 -0
  80. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/summarize_opencode_request_payloads.py +0 -0
  81. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/setup.cfg +0 -0
  82. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/corpora.performance.lock.json +0 -0
  83. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
  84. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
  85. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
  86. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/constset.pas +0 -0
  87. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
  88. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
  89. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
  90. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
  91. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
  92. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
  93. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
  94. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
  95. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
  96. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
  97. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
  98. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
  99. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
  100. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
  101. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
  102. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
  103. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
  104. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
  105. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
  106. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
  107. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/properties.pas +0 -0
  108. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
  109. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
  110. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
  111. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
  112. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/library_demo.dpr +0 -0
  113. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/package_demo.dpk +0 -0
  114. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/program_demo.dpr +0 -0
  115. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_advanced.pas +0 -0
  116. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_attributes.pas +0 -0
  117. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_basic.pas +0 -0
  118. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_consumer.pas +0 -0
  119. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_generics.pas +0 -0
  120. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_inheritance.pas +0 -0
  121. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_math.pas +0 -0
  122. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_properties.pas +0 -0
  123. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_sections.pas +0 -0
  124. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_statements.pas +0 -0
  125. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_types.pas +0 -0
  126. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_unresolved.pas +0 -0
  127. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_with.pas +0 -0
  128. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_codebase.py +0 -0
  129. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
  130. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_opencode.py +0 -0
  131. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_lexer.py +0 -0
  132. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_native_compatibility.py +0 -0
  133. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_parser.py +0 -0
  134. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_semantic_compatibility.py +0 -0
  135. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_diagnostics.py +0 -0
  136. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_github_performance_corpus.py +0 -0
  137. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_language_feature_matrix.py +0 -0
  138. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_legacy_snippets.py +0 -0
  139. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_features.py +0 -0
  140. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_support.py +0 -0
  141. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_metrics.py +0 -0
  142. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_config.py +0 -0
  143. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_probe.py +0 -0
  144. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_request_payloads.py +0 -0
  145. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_openrouter_github_e2e.py +0 -0
  146. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_ornith_cache_prepare.py +0 -0
  147. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parallel_outline.py +0 -0
  148. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parser.py +0 -0
  149. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parser_backends.py +0 -0
  150. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_progress.py +0 -0
  151. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_project_indexer.py +0 -0
  152. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_release_evidence.py +0 -0
  153. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_semantic.py +0 -0
  154. {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_vllm_start_script.py +0 -0
  155. {python_delphi_lsp-2.2.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.2.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.2.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
 
@@ -257,6 +257,13 @@ to measure it. If an automatic pool fails before accepting a result, one
257
257
  automatic serial fallback is attempted; explicit worker counts fail instead of
258
258
  silently changing the requested configuration.
259
259
 
260
+ Detached navigation shards are also stored as versioned, content-addressed JSON
261
+ under `.delphi-lsp/agent-cache/navigation-v1`. A restarted CLI or OpenCode cache
262
+ daemon reuses unchanged units without parsing them again. Source content,
263
+ conditional defines, or a shard-schema change produces a cache miss; malformed
264
+ or incompatible JSON is ignored and rebuilt. The disk cache contains no pickle
265
+ payloads and does not count against the retained-RAM budget.
266
+
260
267
  Cache prewarming builds the navigation registry directly without constructing
261
268
  an empty-query result, symbol cards, pagination, or JSON payloads. Up to sixteen
262
269
  recent ranked queries are retained in a small LRU so alternating CLI and
@@ -276,7 +283,8 @@ The daemon tracks a 30-minute idle timeout; idle state shows in JSON status (`ca
276
283
  Workspace state appears in status as `requests`, `warm_hits`, `rebuilds`, `invalidations`,
277
284
  `evictions`, and `cache_state`. Parallel prewarm status adds
278
285
  `workers_configured`, `workers_effective`, `parallel_files_completed`,
279
- `prewarm_seconds`, `parallel_seconds`, and `parallel_fallbacks`.
286
+ `prewarm_seconds`, `parallel_seconds`, `parallel_fallbacks`,
287
+ `navigation_disk_hits`, and `navigation_disk_misses`.
280
288
 
281
289
  Metadata is stored in `.delphi-lsp/agent-cache/daemon.json` with owner-only token and
282
290
  permissions (`daemon.json` mode 600 and parent 700). Do not copy or share this token
@@ -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.2.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
 
@@ -217,6 +217,13 @@ to measure it. If an automatic pool fails before accepting a result, one
217
217
  automatic serial fallback is attempted; explicit worker counts fail instead of
218
218
  silently changing the requested configuration.
219
219
 
220
+ Detached navigation shards are also stored as versioned, content-addressed JSON
221
+ under `.delphi-lsp/agent-cache/navigation-v1`. A restarted CLI or OpenCode cache
222
+ daemon reuses unchanged units without parsing them again. Source content,
223
+ conditional defines, or a shard-schema change produces a cache miss; malformed
224
+ or incompatible JSON is ignored and rebuilt. The disk cache contains no pickle
225
+ payloads and does not count against the retained-RAM budget.
226
+
220
227
  Cache prewarming builds the navigation registry directly without constructing
221
228
  an empty-query result, symbol cards, pagination, or JSON payloads. Up to sixteen
222
229
  recent ranked queries are retained in a small LRU so alternating CLI and
@@ -236,7 +243,8 @@ The daemon tracks a 30-minute idle timeout; idle state shows in JSON status (`ca
236
243
  Workspace state appears in status as `requests`, `warm_hits`, `rebuilds`, `invalidations`,
237
244
  `evictions`, and `cache_state`. Parallel prewarm status adds
238
245
  `workers_configured`, `workers_effective`, `parallel_files_completed`,
239
- `prewarm_seconds`, `parallel_seconds`, and `parallel_fallbacks`.
246
+ `prewarm_seconds`, `parallel_seconds`, `parallel_fallbacks`,
247
+ `navigation_disk_hits`, and `navigation_disk_misses`.
240
248
 
241
249
  Metadata is stored in `.delphi-lsp/agent-cache/daemon.json` with owner-only token and
242
250
  permissions (`daemon.json` mode 600 and parent 700). Do not copy or share this token
@@ -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
 
@@ -280,6 +362,23 @@ def _safe_metadata_path(root: str | Path, *, create: bool = False) -> Path:
280
362
  return result
281
363
 
282
364
 
365
+ def _safe_navigation_cache_path(root: str | Path, *, create: bool = False) -> Path:
366
+ parent = _safe_metadata_path(root, create=create).parent
367
+ result = parent / "navigation-v1"
368
+ if result.exists() and result.is_symlink():
369
+ raise CacheClientError("unsafe_metadata", "Navigation cache path is unsafe.")
370
+ if create:
371
+ result.mkdir(mode=0o700, exist_ok=True)
372
+ if os.name != "nt":
373
+ os.chmod(result, 0o700)
374
+ return result
375
+
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
+
283
382
  def _metadata_mapping(metadata: CacheMetadata) -> dict[str, object]:
284
383
  return {field.name: getattr(metadata, field.name) for field in fields(metadata)}
285
384
 
@@ -543,12 +642,17 @@ def _read_startup_tail(diagnostics: object, *, max_bytes: int = _STARTUP_DIAGNOS
543
642
  class _CacheService:
544
643
  def __init__(self, metadata: CacheMetadata) -> None:
545
644
  self.metadata = metadata
645
+ self._rss_baseline_bytes = current_process_rss_bytes()
546
646
  self.context = AgentContext.open(
547
647
  metadata.root,
548
648
  metadata.project_file or None,
549
649
  workers=metadata.workers,
550
650
  worker_memory_budget_bytes=metadata.max_memory_bytes,
551
651
  revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
652
+ navigation_cache_dir=navigation_cache_path(
653
+ metadata.root,
654
+ create=True,
655
+ ),
552
656
  )
553
657
  self.budget = CacheBudget(metadata.max_memory_bytes)
554
658
  self.stats = CacheStats()
@@ -561,6 +665,15 @@ class _CacheService:
561
665
  self.prewarm_seconds = 0.0
562
666
  self.shutdown = threading.Event()
563
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
+
564
677
  def prewarm(self) -> None:
565
678
  started = time.monotonic()
566
679
  try:
@@ -571,7 +684,7 @@ class _CacheService:
571
684
  raise
572
685
  self.cache_state = "ready"
573
686
  self.last_budget = self.budget.enforce(
574
- measure=lambda: self.context.estimated_cache_bytes,
687
+ measure=self._measure_retained_bytes,
575
688
  evict_auxiliary=self.context.evict_auxiliary_caches,
576
689
  evict_navigation=self.context.evict_navigation_caches,
577
690
  )
@@ -612,7 +725,7 @@ class _CacheService:
612
725
  if before != after:
613
726
  self.stats.invalidations += 1
614
727
  self.last_budget = self.budget.enforce(
615
- measure=lambda: self.context.estimated_cache_bytes,
728
+ measure=self._measure_retained_bytes,
616
729
  evict_auxiliary=self.context.evict_auxiliary_caches,
617
730
  evict_navigation=self.context.evict_navigation_caches,
618
731
  )
@@ -648,6 +761,8 @@ class _CacheService:
648
761
  "prewarm_seconds": self.prewarm_seconds,
649
762
  "parallel_seconds": self.context.parallel_stats.elapsed_seconds,
650
763
  "parallel_fallbacks": self.stats.parallel_fallbacks,
764
+ "navigation_disk_hits": self.context.navigation_disk_hits,
765
+ "navigation_disk_misses": self.context.navigation_disk_misses,
651
766
  "idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
652
767
  "workspace_revision": self.last_revision,
653
768
  }
@@ -657,21 +772,34 @@ def _watch_filter(_change: object, path: str) -> bool:
657
772
  return Path(path).suffix.casefold() in _WATCHED_SUFFIXES
658
773
 
659
774
 
660
- 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:
661
781
  try:
662
782
  for changes in watch(
663
- service.metadata.root,
783
+ root,
664
784
  watch_filter=_watch_filter,
665
- stop_event=service.shutdown,
785
+ stop_event=stop_event,
666
786
  debounce=50,
667
787
  step=20,
668
788
  recursive=True,
669
789
  raise_interrupt=False,
670
790
  ):
671
791
  if changes:
672
- service.context.invalidate_revision_cache()
792
+ on_change()
673
793
  except (OSError, RuntimeError):
674
- 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
+ )
675
803
 
676
804
 
677
805
  def _serve_connection(connection: socket.socket, service: _CacheService) -> None:
@@ -849,6 +977,8 @@ def start_cache(
849
977
  ) -> CacheMetadata:
850
978
  if type(workers) is not int or not 0 <= workers <= 32:
851
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.")
852
982
  if not math.isfinite(startup_timeout) or startup_timeout <= 0:
853
983
  raise ValueError("startup_timeout must be greater than zero.")
854
984
  with _start_lock(root, startup_timeout):
@@ -912,7 +1042,7 @@ def main(argv: list[str] | None = None) -> int:
912
1042
  serve.add_argument("--project-file", default="")
913
1043
  serve.add_argument("--max-memory", type=int, default=DEFAULT_MAX_MEMORY_BYTES)
914
1044
  serve.add_argument("--workers", type=int, default=0)
915
- serve.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
1045
+ serve.add_argument("--idle-timeout", type=_positive_integer, default=DEFAULT_IDLE_TIMEOUT)
916
1046
  args = parser.parse_args(argv)
917
1047
  if args.command == "serve":
918
1048
  run_cache_daemon(
@@ -925,5 +1055,12 @@ def main(argv: list[str] | None = None) -> int:
925
1055
  return 0
926
1056
 
927
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
+
928
1065
  if __name__ == "__main__":
929
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: