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.
- {python_delphi_lsp-2.2.0/python_delphi_lsp.egg-info → python_delphi_lsp-2.3.1}/PKG-INFO +11 -3
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/README.md +10 -2
- python_delphi_lsp-2.3.1/delphi_lsp/_version.py +1 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cache.py +145 -8
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_cli.py +120 -13
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_context.py +454 -184
- python_delphi_lsp-2.3.1/delphi_lsp/agent_metrics.py +179 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_protocol.py +4 -3
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_relations.py +272 -26
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_workspace.py +16 -3
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/metrics.py +45 -7
- python_delphi_lsp-2.3.1/delphi_lsp/navigation_cache.py +134 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/preprocessor.py +56 -20
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_discovery.py +5 -1
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/pyproject.toml +1 -1
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1/python_delphi_lsp.egg-info}/PKG-INFO +11 -3
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/SOURCES.txt +3 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_cache.py +131 -1
- python_delphi_lsp-2.3.1/tests/test_agent_cli_errors.py +222 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_context.py +295 -16
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_metrics.py +36 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_protocol.py +31 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_relations.py +55 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_worker.py +228 -6
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_workspace.py +35 -0
- python_delphi_lsp-2.3.1/tests/test_navigation_cache.py +43 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_package_metadata.py +3 -3
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_preprocessor.py +57 -1
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_project_discovery.py +20 -0
- python_delphi_lsp-2.2.0/delphi_lsp/_version.py +0 -1
- python_delphi_lsp-2.2.0/delphi_lsp/agent_metrics.py +0 -82
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/.github/workflows/ci.yml +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/LICENSE +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/MANIFEST.in +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/__init__.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_layers.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/agent_templates.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/binary.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/comment_builder.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/consts.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_lexer.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_parser.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/delphiast_tokens.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/grammar.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_builder.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lark_tokens.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/lsp_server.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/nodes.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parallel_outline.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/parser_backend.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/progress.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/project_indexer.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/semantic_builder.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/source_reader.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/workspace.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/delphi_lsp/writer.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/opencode.json +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/requires.txt +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/python_delphi_lsp.egg-info/top_level.txt +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/audit_delphi_language_features.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_github_corpus.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/benchmark_parallel_cache.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/build_github_performance_corpus.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/check_ornith_cache.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/generate_progress_pdf.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/generate_release_evidence.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/prepare_ornith_cache.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/run_opencode_lsp_probe.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/run_openrouter_github_e2e.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/start_ornith_vllm.sh +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/scripts/summarize_opencode_request_payloads.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/setup.cfg +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/corpora.performance.lock.json +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/constset.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/properties.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/library_demo.dpr +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/package_demo.dpk +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/program_demo.dpr +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_advanced.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_attributes.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_basic.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_consumer.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_generics.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_inheritance.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_math.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_properties.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_sections.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_statements.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_types.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_unresolved.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/fixtures/unit_with.pas +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_agent_codebase.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_bootstrap_vllm_opencode.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_lexer.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_native_compatibility.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_parser.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_delphiast_semantic_compatibility.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_diagnostics.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_github_performance_corpus.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_language_feature_matrix.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_legacy_snippets.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_features.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_lsp_support.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_metrics.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_config.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_probe.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_opencode_request_payloads.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_openrouter_github_e2e.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_ornith_cache_prepare.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parallel_outline.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parser.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_parser_backends.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_progress.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_project_indexer.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_release_evidence.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_semantic.py +0 -0
- {python_delphi_lsp-2.2.0 → python_delphi_lsp-2.3.1}/tests/test_vllm_start_script.py +0 -0
- {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.
|
|
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.
|
|
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`,
|
|
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.
|
|
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`,
|
|
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=
|
|
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=
|
|
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
|
|
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
|
-
|
|
783
|
+
root,
|
|
664
784
|
watch_filter=_watch_filter,
|
|
665
|
-
stop_event=
|
|
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
|
-
|
|
792
|
+
on_change()
|
|
673
793
|
except (OSError, RuntimeError):
|
|
674
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
135
|
-
query.add_argument("--max-chars", type=
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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:
|