python-delphi-lsp 2.0.5__tar.gz → 2.1.0__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.0.5 → python_delphi_lsp-2.1.0}/.github/workflows/ci.yml +1 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/MANIFEST.in +1 -0
- {python_delphi_lsp-2.0.5/python_delphi_lsp.egg-info → python_delphi_lsp-2.1.0}/PKG-INFO +30 -4
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/README.md +29 -3
- python_delphi_lsp-2.1.0/delphi_lsp/_version.py +1 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_cache.py +211 -32
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_cli.py +44 -5
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_context.py +74 -29
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_layers.py +32 -21
- python_delphi_lsp-2.1.0/delphi_lsp/parallel_outline.py +278 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/pyproject.toml +1 -1
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0/python_delphi_lsp.egg-info}/PKG-INFO +30 -4
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/python_delphi_lsp.egg-info/SOURCES.txt +3 -0
- python_delphi_lsp-2.1.0/scripts/benchmark_parallel_cache.py +71 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_cache.py +183 -7
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_codebase.py +27 -1
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_context.py +47 -24
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_worker.py +48 -3
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_lsp_support.py +15 -10
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_package_metadata.py +4 -3
- python_delphi_lsp-2.1.0/tests/test_parallel_outline.py +422 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_progress.py +48 -3
- python_delphi_lsp-2.0.5/delphi_lsp/_version.py +0 -1
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/LICENSE +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/__init__.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_metrics.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_protocol.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_relations.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_templates.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/agent_workspace.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/binary.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/comment_builder.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/consts.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/grammar.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/lark_builder.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/lark_tokens.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/lsp_server.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/metrics.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/nodes.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/parser.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/preprocessor.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/progress.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/project_discovery.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/project_indexer.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/semantic.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/semantic_builder.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/source_reader.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/workspace.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/delphi_lsp/writer.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/opencode.json +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/python_delphi_lsp.egg-info/dependency_links.txt +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/python_delphi_lsp.egg-info/entry_points.txt +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/python_delphi_lsp.egg-info/requires.txt +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/python_delphi_lsp.egg-info/top_level.txt +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/audit_delphi_language_features.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/benchmark_github_corpus.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/bootstrap_vllm_codebase_skill_test.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/bootstrap_vllm_opencode_test.ps1 +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/bootstrap_vllm_opencode_test.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/bootstrap_vllm_opencode_test.sh +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/build_github_performance_corpus.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/check_ornith_cache.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/generate_progress_pdf.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/generate_release_evidence.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/ollama/ornith-lspctx.Modelfile +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/prepare_ornith_cache.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/run_opencode_lsp_probe.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/run_openrouter_github_e2e.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/start_ornith_vllm.sh +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/scripts/summarize_opencode_request_payloads.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/setup.cfg +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/corpora.performance.lock.json +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/DeprecatedOnConst.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/VariantRecordFieldAttributes.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/alignedrecords.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/constset.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/deprecatedtype.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/dottedtypes.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/endtoken.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/experimentals.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/externalfunction.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/finalizationinitializationexports.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/forwardoverloaded.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/forwardwithoutsemicolon.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/genericconstraints.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/genericinterfacemethoddelegation.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/implementsgenerictype.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/include file2.inc +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/includefile.inc +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/includefile.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/managedrecords.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/messagemethod.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/multiline.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/nonalignedrecords.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/numbers.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/pointerchars.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/properties.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/strictvisibility.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/tryexcept.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/umlauts.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/legacy_snippets/whitespacearoundifdefcondition.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/library_demo.dpr +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/package_demo.dpk +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/program_demo.dpr +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_advanced.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_attributes.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_basic.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_consumer.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_generics.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_inheritance.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_math.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_properties.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_sections.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_statements.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_types.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_unresolved.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/fixtures/unit_with.pas +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_metrics.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_protocol.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_relations.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_agent_workspace.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_bootstrap_vllm_codebase_skill.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_bootstrap_vllm_opencode.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_diagnostics.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_github_performance_corpus.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_language_feature_matrix.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_legacy_snippets.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_lsp_features.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_metrics.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_opencode_config.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_opencode_probe.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_opencode_request_payloads.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_openrouter_github_e2e.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_ornith_cache_prepare.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_parser.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_preprocessor.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_project_discovery.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_project_indexer.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_release_evidence.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_semantic.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_vllm_start_script.py +0 -0
- {python_delphi_lsp-2.0.5 → python_delphi_lsp-2.1.0}/tests/test_workspace.py +0 -0
|
@@ -6,6 +6,7 @@ include scripts/check_ornith_cache.py
|
|
|
6
6
|
include scripts/audit_delphi_language_features.py
|
|
7
7
|
include scripts/build_github_performance_corpus.py
|
|
8
8
|
include scripts/benchmark_github_corpus.py
|
|
9
|
+
include scripts/benchmark_parallel_cache.py
|
|
9
10
|
include scripts/bootstrap_vllm_opencode_test.py
|
|
10
11
|
include scripts/bootstrap_vllm_codebase_skill_test.py
|
|
11
12
|
include scripts/bootstrap_vllm_opencode_test.ps1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-delphi-lsp
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
`python-delphi-lsp` parses Delphi/Object Pascal, builds semantic and project
|
|
43
43
|
indexes, serves LSP, and provides bounded codebase navigation for agents.
|
|
44
|
-
Version 2.0
|
|
44
|
+
Version 2.1.0 is authored by Dark Light and supports Windows, macOS, and Linux.
|
|
45
45
|
|
|
46
46
|
## Install and quick start
|
|
47
47
|
|
|
@@ -186,12 +186,15 @@ problems; paths are not guessed.
|
|
|
186
186
|
|
|
187
187
|
```text
|
|
188
188
|
delphi-lsp-agent cache start --root PATH [--project-file FILE] [--max-memory 512M]
|
|
189
|
+
[--workers auto|N] [--startup-timeout 120]
|
|
189
190
|
[--idle-timeout 1800]
|
|
190
191
|
delphi-lsp-agent cache status --root PATH [--format text|json]
|
|
191
192
|
delphi-lsp-agent cache stop --root PATH
|
|
192
193
|
delphi-lsp-agent view --root PATH [--project-file FILE] --layer LAYER
|
|
193
194
|
[--query TEXT] [--format markdown|json] [--deep-projects]
|
|
195
|
+
[--workers auto|N]
|
|
194
196
|
delphi-lsp-agent index --root PATH [--project-file FILE] [--out FILE]
|
|
197
|
+
[--workers auto|N]
|
|
195
198
|
delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
196
199
|
[--project-id FILE] [--detail summary|declaration|members|context|body|implementations]
|
|
197
200
|
[--relation references|callers|callees|uses|used_by|inherits|implements]
|
|
@@ -199,7 +202,7 @@ delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
|
199
202
|
delphi-lsp-agent skill install [--target PATH] [--force]
|
|
200
203
|
delphi-lsp-agent opencode install [--target PATH] [--python PYTHON]
|
|
201
204
|
[--force] [--write-agent|--write-config]
|
|
202
|
-
delphi-lsp-agent worker --root PATH [--project-file FILE]
|
|
205
|
+
delphi-lsp-agent worker --root PATH [--project-file FILE] [--workers auto|N]
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
The `cache` commands manage one daemon per canonical root. Use these:
|
|
@@ -233,6 +236,27 @@ fast. The cache retained-cache budget is `512 MiB` by default and tracks retaine
|
|
|
233
236
|
cache usage only, not a hard RSS/parse peak. Warnings are emitted on stderr at or
|
|
234
237
|
above 80 percent.
|
|
235
238
|
|
|
239
|
+
Cold builds parse independent source units in short-lived processes created with
|
|
240
|
+
the cross-platform `spawn` method. `--workers auto|N` defaults to `auto`.
|
|
241
|
+
Automatic selection uses at most four worker processes, leaves one detected CPU
|
|
242
|
+
free, never exceeds the source task count, and—for the cache daemon—allows one
|
|
243
|
+
worker per `128 MiB` of retained-cache budget. `view` and `index` use the same
|
|
244
|
+
task, CPU, and four-worker caps without the cache-budget term. An explicit value
|
|
245
|
+
from 1 through 32 overrides the automatic CPU and memory caps but is still
|
|
246
|
+
limited by the number of tasks.
|
|
247
|
+
|
|
248
|
+
Worker processes exit before retained-cache accounting. Their models and source
|
|
249
|
+
text are streamed into the parent, so transient worker memory is separate from
|
|
250
|
+
the retained navigation structures and the existing 80-percent warning. If an
|
|
251
|
+
automatic pool fails before accepting a result, one automatic serial fallback
|
|
252
|
+
is attempted; explicit worker counts fail instead of silently changing the
|
|
253
|
+
requested configuration.
|
|
254
|
+
|
|
255
|
+
`cache start` waits up to `--startup-timeout 120` seconds by default for a large
|
|
256
|
+
workspace to prewarm. The timeout belongs to the starting client and does not
|
|
257
|
+
change daemon compatibility or idle shutdown. Starting a live root with a
|
|
258
|
+
different worker configuration reports a configuration conflict.
|
|
259
|
+
|
|
236
260
|
Eviction is ordered: auxiliary caches are evicted first, navigation caches second.
|
|
237
261
|
If compaction removes navigable data, the daemon rebuilds the navigation state on demand
|
|
238
262
|
while preserving focus state for the next request.
|
|
@@ -240,7 +264,9 @@ while preserving focus state for the next request.
|
|
|
240
264
|
The daemon tracks a 30-minute idle timeout; idle state shows in JSON status (`cache status`).
|
|
241
265
|
`source revision` changes on source edits and invalidate reused request caches.
|
|
242
266
|
Workspace state appears in status as `requests`, `warm_hits`, `rebuilds`, `invalidations`,
|
|
243
|
-
`evictions`, and `cache_state`.
|
|
267
|
+
`evictions`, and `cache_state`. Parallel prewarm status adds
|
|
268
|
+
`workers_configured`, `workers_effective`, `parallel_files_completed`,
|
|
269
|
+
`prewarm_seconds`, `parallel_seconds`, and `parallel_fallbacks`.
|
|
244
270
|
|
|
245
271
|
Metadata is stored in `.delphi-lsp/agent-cache/daemon.json` with owner-only token and
|
|
246
272
|
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.0
|
|
5
|
+
Version 2.1.0 is authored by Dark Light and supports Windows, macOS, and Linux.
|
|
6
6
|
|
|
7
7
|
## Install and quick start
|
|
8
8
|
|
|
@@ -147,12 +147,15 @@ problems; paths are not guessed.
|
|
|
147
147
|
|
|
148
148
|
```text
|
|
149
149
|
delphi-lsp-agent cache start --root PATH [--project-file FILE] [--max-memory 512M]
|
|
150
|
+
[--workers auto|N] [--startup-timeout 120]
|
|
150
151
|
[--idle-timeout 1800]
|
|
151
152
|
delphi-lsp-agent cache status --root PATH [--format text|json]
|
|
152
153
|
delphi-lsp-agent cache stop --root PATH
|
|
153
154
|
delphi-lsp-agent view --root PATH [--project-file FILE] --layer LAYER
|
|
154
155
|
[--query TEXT] [--format markdown|json] [--deep-projects]
|
|
156
|
+
[--workers auto|N]
|
|
155
157
|
delphi-lsp-agent index --root PATH [--project-file FILE] [--out FILE]
|
|
158
|
+
[--workers auto|N]
|
|
156
159
|
delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
157
160
|
[--project-id FILE] [--detail summary|declaration|members|context|body|implementations]
|
|
158
161
|
[--relation references|callers|callees|uses|used_by|inherits|implements]
|
|
@@ -160,7 +163,7 @@ delphi-lsp-agent query --root PATH ACTION [VALUE]
|
|
|
160
163
|
delphi-lsp-agent skill install [--target PATH] [--force]
|
|
161
164
|
delphi-lsp-agent opencode install [--target PATH] [--python PYTHON]
|
|
162
165
|
[--force] [--write-agent|--write-config]
|
|
163
|
-
delphi-lsp-agent worker --root PATH [--project-file FILE]
|
|
166
|
+
delphi-lsp-agent worker --root PATH [--project-file FILE] [--workers auto|N]
|
|
164
167
|
```
|
|
165
168
|
|
|
166
169
|
The `cache` commands manage one daemon per canonical root. Use these:
|
|
@@ -194,6 +197,27 @@ fast. The cache retained-cache budget is `512 MiB` by default and tracks retaine
|
|
|
194
197
|
cache usage only, not a hard RSS/parse peak. Warnings are emitted on stderr at or
|
|
195
198
|
above 80 percent.
|
|
196
199
|
|
|
200
|
+
Cold builds parse independent source units in short-lived processes created with
|
|
201
|
+
the cross-platform `spawn` method. `--workers auto|N` defaults to `auto`.
|
|
202
|
+
Automatic selection uses at most four worker processes, leaves one detected CPU
|
|
203
|
+
free, never exceeds the source task count, and—for the cache daemon—allows one
|
|
204
|
+
worker per `128 MiB` of retained-cache budget. `view` and `index` use the same
|
|
205
|
+
task, CPU, and four-worker caps without the cache-budget term. An explicit value
|
|
206
|
+
from 1 through 32 overrides the automatic CPU and memory caps but is still
|
|
207
|
+
limited by the number of tasks.
|
|
208
|
+
|
|
209
|
+
Worker processes exit before retained-cache accounting. Their models and source
|
|
210
|
+
text are streamed into the parent, so transient worker memory is separate from
|
|
211
|
+
the retained navigation structures and the existing 80-percent warning. If an
|
|
212
|
+
automatic pool fails before accepting a result, one automatic serial fallback
|
|
213
|
+
is attempted; explicit worker counts fail instead of silently changing the
|
|
214
|
+
requested configuration.
|
|
215
|
+
|
|
216
|
+
`cache start` waits up to `--startup-timeout 120` seconds by default for a large
|
|
217
|
+
workspace to prewarm. The timeout belongs to the starting client and does not
|
|
218
|
+
change daemon compatibility or idle shutdown. Starting a live root with a
|
|
219
|
+
different worker configuration reports a configuration conflict.
|
|
220
|
+
|
|
197
221
|
Eviction is ordered: auxiliary caches are evicted first, navigation caches second.
|
|
198
222
|
If compaction removes navigable data, the daemon rebuilds the navigation state on demand
|
|
199
223
|
while preserving focus state for the next request.
|
|
@@ -201,7 +225,9 @@ while preserving focus state for the next request.
|
|
|
201
225
|
The daemon tracks a 30-minute idle timeout; idle state shows in JSON status (`cache status`).
|
|
202
226
|
`source revision` changes on source edits and invalidate reused request caches.
|
|
203
227
|
Workspace state appears in status as `requests`, `warm_hits`, `rebuilds`, `invalidations`,
|
|
204
|
-
`evictions`, and `cache_state`.
|
|
228
|
+
`evictions`, and `cache_state`. Parallel prewarm status adds
|
|
229
|
+
`workers_configured`, `workers_effective`, `parallel_files_completed`,
|
|
230
|
+
`prewarm_seconds`, `parallel_seconds`, and `parallel_fallbacks`.
|
|
205
231
|
|
|
206
232
|
Metadata is stored in `.delphi-lsp/agent-cache/daemon.json` with owner-only token and
|
|
207
233
|
permissions (`daemon.json` mode 600 and parent 700). Do not copy or share this token
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.0"
|
|
@@ -6,6 +6,7 @@ import argparse
|
|
|
6
6
|
import contextlib
|
|
7
7
|
import hmac
|
|
8
8
|
import json
|
|
9
|
+
import math
|
|
9
10
|
import os
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
import secrets
|
|
@@ -26,13 +27,15 @@ from .agent_protocol import AgentProtocolError
|
|
|
26
27
|
|
|
27
28
|
DEFAULT_MAX_MEMORY_BYTES = 512 * 1024**2
|
|
28
29
|
WARNING_THRESHOLD_PERCENT = 80
|
|
29
|
-
DAEMON_SCHEMA =
|
|
30
|
+
DAEMON_SCHEMA = 2
|
|
30
31
|
DEFAULT_IDLE_TIMEOUT = 1800
|
|
32
|
+
DEFAULT_STARTUP_TIMEOUT = 120.0
|
|
31
33
|
_MAX_MESSAGE_BYTES = 1024 * 1024
|
|
32
34
|
_CONNECTION_TIMEOUT = 2.0
|
|
33
35
|
_MEMORY_SIZE = re.compile(r"^(?P<count>[1-9][0-9]*)(?P<suffix>[KMG]?)$", re.IGNORECASE)
|
|
34
36
|
_STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
|
|
35
37
|
_STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
|
|
38
|
+
_START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
def estimate_deep_size(value: object) -> int:
|
|
@@ -72,6 +75,7 @@ class CacheStats:
|
|
|
72
75
|
rebuilds: int = 0
|
|
73
76
|
invalidations: int = 0
|
|
74
77
|
evictions: int = 0
|
|
78
|
+
parallel_fallbacks: int = 0
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
@dataclass(frozen=True)
|
|
@@ -232,6 +236,7 @@ class CacheMetadata:
|
|
|
232
236
|
version: str
|
|
233
237
|
project_file: str
|
|
234
238
|
max_memory_bytes: int
|
|
239
|
+
workers: int
|
|
235
240
|
idle_timeout: int
|
|
236
241
|
started_at: float
|
|
237
242
|
|
|
@@ -291,7 +296,7 @@ def _write_metadata(metadata: CacheMetadata) -> None:
|
|
|
291
296
|
os.unlink(temporary)
|
|
292
297
|
|
|
293
298
|
|
|
294
|
-
def
|
|
299
|
+
def _read_metadata_record(root: str | Path) -> object | None:
|
|
295
300
|
path = _safe_metadata_path(root)
|
|
296
301
|
if not path.exists():
|
|
297
302
|
return None
|
|
@@ -309,15 +314,36 @@ def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
|
309
314
|
raise
|
|
310
315
|
except (OSError, ValueError, UnicodeError):
|
|
311
316
|
return None
|
|
317
|
+
return raw
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
321
|
+
raw = _read_metadata_record(root)
|
|
312
322
|
required = {field.name for field in fields(CacheMetadata)}
|
|
313
323
|
if not isinstance(raw, dict) or set(raw) != required:
|
|
314
324
|
return None
|
|
315
|
-
values = tuple(
|
|
325
|
+
values = tuple(
|
|
326
|
+
raw[name]
|
|
327
|
+
for name in (
|
|
328
|
+
"schema",
|
|
329
|
+
"root",
|
|
330
|
+
"pid",
|
|
331
|
+
"port",
|
|
332
|
+
"token",
|
|
333
|
+
"version",
|
|
334
|
+
"project_file",
|
|
335
|
+
"max_memory_bytes",
|
|
336
|
+
"workers",
|
|
337
|
+
"idle_timeout",
|
|
338
|
+
"started_at",
|
|
339
|
+
)
|
|
340
|
+
)
|
|
316
341
|
if (type(values[0]) is not int or values[0] != DAEMON_SCHEMA or not isinstance(values[1], str)
|
|
317
342
|
or type(values[2]) is not int or values[2] <= 0 or type(values[3]) is not int or not 0 < values[3] < 65536
|
|
318
343
|
or not isinstance(values[4], str) or len(values[4]) < 32 or not isinstance(values[5], str)
|
|
319
344
|
or not isinstance(values[6], str) or type(values[7]) is not int or values[7] <= 0
|
|
320
|
-
or type(values[8]) is not int or
|
|
345
|
+
or type(values[8]) is not int or not 0 <= values[8] <= 32
|
|
346
|
+
or type(values[9]) is not int or values[9] <= 0 or type(values[10]) not in (int, float)):
|
|
321
347
|
return None
|
|
322
348
|
canonical = str(Path(root).resolve())
|
|
323
349
|
if values[1] != canonical:
|
|
@@ -325,15 +351,56 @@ def _read_metadata(root: str | Path) -> CacheMetadata | None:
|
|
|
325
351
|
return CacheMetadata(*values)
|
|
326
352
|
|
|
327
353
|
|
|
354
|
+
def _remove_incompatible_metadata_if_stale(root: str | Path) -> None:
|
|
355
|
+
path = _safe_metadata_path(root)
|
|
356
|
+
if not path.exists():
|
|
357
|
+
return
|
|
358
|
+
raw = _read_metadata_record(root)
|
|
359
|
+
canonical = str(Path(root).resolve())
|
|
360
|
+
pid = raw.get("pid") if isinstance(raw, dict) and raw.get("root") == canonical else None
|
|
361
|
+
if type(pid) is int and pid > 0 and _pid_alive(pid):
|
|
362
|
+
raise CacheClientError("unavailable", "Live cache daemon is unavailable.")
|
|
363
|
+
with contextlib.suppress(FileNotFoundError):
|
|
364
|
+
path.unlink()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _metadata_is_owned(metadata: CacheMetadata) -> bool:
|
|
368
|
+
current = _read_metadata(metadata.root)
|
|
369
|
+
return current is not None and current.pid == metadata.pid and hmac.compare_digest(current.token, metadata.token)
|
|
370
|
+
|
|
371
|
+
|
|
328
372
|
def _remove_metadata_if_owned(metadata: CacheMetadata) -> None:
|
|
329
373
|
path = _safe_metadata_path(metadata.root)
|
|
330
|
-
|
|
331
|
-
if current is not None and current.pid == metadata.pid and hmac.compare_digest(current.token, metadata.token):
|
|
374
|
+
if _metadata_is_owned(metadata):
|
|
332
375
|
with contextlib.suppress(FileNotFoundError):
|
|
333
376
|
path.unlink()
|
|
334
377
|
|
|
335
378
|
|
|
379
|
+
def _windows_pid_alive(pid: int) -> bool:
|
|
380
|
+
import ctypes
|
|
381
|
+
from ctypes import wintypes
|
|
382
|
+
|
|
383
|
+
synchronize = 0x00100000
|
|
384
|
+
wait_timeout = 0x00000102
|
|
385
|
+
kernel32 = ctypes.windll.kernel32
|
|
386
|
+
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
387
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
388
|
+
kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD)
|
|
389
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
390
|
+
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
391
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
392
|
+
handle = kernel32.OpenProcess(synchronize, False, pid)
|
|
393
|
+
if not handle:
|
|
394
|
+
return False
|
|
395
|
+
try:
|
|
396
|
+
return kernel32.WaitForSingleObject(handle, 0) == wait_timeout
|
|
397
|
+
finally:
|
|
398
|
+
kernel32.CloseHandle(handle)
|
|
399
|
+
|
|
400
|
+
|
|
336
401
|
def _pid_alive(pid: int) -> bool:
|
|
402
|
+
if os.name == "nt":
|
|
403
|
+
return _windows_pid_alive(pid)
|
|
337
404
|
try:
|
|
338
405
|
os.kill(pid, 0)
|
|
339
406
|
except OSError:
|
|
@@ -341,15 +408,41 @@ def _pid_alive(pid: int) -> bool:
|
|
|
341
408
|
return True
|
|
342
409
|
|
|
343
410
|
|
|
411
|
+
def _reap_child_if_exited(pid: int) -> bool:
|
|
412
|
+
waitpid = getattr(os, "waitpid", None)
|
|
413
|
+
nohang = getattr(os, "WNOHANG", None)
|
|
414
|
+
if not callable(waitpid) or nohang is None:
|
|
415
|
+
return False
|
|
416
|
+
try:
|
|
417
|
+
return waitpid(pid, nohang)[0] == pid
|
|
418
|
+
except (ChildProcessError, OSError):
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
|
|
344
422
|
def _start_lock_path(root: str | Path) -> Path:
|
|
345
423
|
return _safe_metadata_path(root, create=True).with_name("start.lock")
|
|
346
424
|
|
|
347
425
|
|
|
426
|
+
def _start_lock_is_stale(path: Path) -> bool:
|
|
427
|
+
try:
|
|
428
|
+
age = max(0.0, time.time() - path.stat().st_mtime)
|
|
429
|
+
except OSError:
|
|
430
|
+
return True
|
|
431
|
+
try:
|
|
432
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
433
|
+
except (OSError, ValueError, UnicodeError):
|
|
434
|
+
return age >= _START_LOCK_INCOMPLETE_GRACE_SECONDS
|
|
435
|
+
owner = record.get("pid") if isinstance(record, dict) else None
|
|
436
|
+
if type(owner) is int and owner > 0:
|
|
437
|
+
return not _pid_alive(owner)
|
|
438
|
+
return age >= _START_LOCK_INCOMPLETE_GRACE_SECONDS
|
|
439
|
+
|
|
440
|
+
|
|
348
441
|
@contextlib.contextmanager
|
|
349
|
-
def _start_lock(root: str | Path):
|
|
442
|
+
def _start_lock(root: str | Path, timeout: float):
|
|
350
443
|
path = _start_lock_path(root)
|
|
351
444
|
token = secrets.token_urlsafe(24)
|
|
352
|
-
deadline = time.monotonic() +
|
|
445
|
+
deadline = time.monotonic() + timeout
|
|
353
446
|
while True:
|
|
354
447
|
try:
|
|
355
448
|
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
@@ -361,14 +454,7 @@ def _start_lock(root: str | Path):
|
|
|
361
454
|
os.close(descriptor)
|
|
362
455
|
break
|
|
363
456
|
except FileExistsError:
|
|
364
|
-
|
|
365
|
-
record = json.loads(path.read_text(encoding="utf-8"))
|
|
366
|
-
owner = record.get("pid") if isinstance(record, dict) else None
|
|
367
|
-
started = record.get("started_at") if isinstance(record, dict) else None
|
|
368
|
-
stale = type(owner) is not int or not _pid_alive(owner) or type(started) not in (int, float) or time.time() - started > 30
|
|
369
|
-
except (OSError, ValueError, UnicodeError):
|
|
370
|
-
stale = True
|
|
371
|
-
if stale:
|
|
457
|
+
if _start_lock_is_stale(path):
|
|
372
458
|
with contextlib.suppress(FileNotFoundError):
|
|
373
459
|
path.unlink()
|
|
374
460
|
continue
|
|
@@ -450,7 +536,12 @@ def _read_startup_tail(diagnostics: object, *, max_bytes: int = _STARTUP_DIAGNOS
|
|
|
450
536
|
class _CacheService:
|
|
451
537
|
def __init__(self, metadata: CacheMetadata) -> None:
|
|
452
538
|
self.metadata = metadata
|
|
453
|
-
self.context = AgentContext.open(
|
|
539
|
+
self.context = AgentContext.open(
|
|
540
|
+
metadata.root,
|
|
541
|
+
metadata.project_file or None,
|
|
542
|
+
workers=metadata.workers,
|
|
543
|
+
worker_memory_budget_bytes=metadata.max_memory_bytes,
|
|
544
|
+
)
|
|
454
545
|
self.budget = CacheBudget(metadata.max_memory_bytes)
|
|
455
546
|
self.stats = CacheStats()
|
|
456
547
|
self.lock = threading.Lock()
|
|
@@ -459,9 +550,11 @@ class _CacheService:
|
|
|
459
550
|
self.cache_state = "warming"
|
|
460
551
|
self.last_revision = self.context.workspace.workspace_revision
|
|
461
552
|
self.last_budget = BudgetResult(0, 0.0, 0.0, False, False, False)
|
|
553
|
+
self.prewarm_seconds = 0.0
|
|
462
554
|
self.shutdown = threading.Event()
|
|
463
555
|
|
|
464
556
|
def prewarm(self) -> None:
|
|
557
|
+
started = time.monotonic()
|
|
465
558
|
try:
|
|
466
559
|
self.context.handle({"action": "find", "query": "", "max_items": 1, "max_chars": 256})
|
|
467
560
|
self.last_revision = self.context.workspace.workspace_revision
|
|
@@ -478,6 +571,8 @@ class _CacheService:
|
|
|
478
571
|
if self.last_budget.compacted:
|
|
479
572
|
self.stats.evictions += 1
|
|
480
573
|
self.cache_state = "compact"
|
|
574
|
+
self.prewarm_seconds = time.monotonic() - started
|
|
575
|
+
self.stats.parallel_fallbacks = self.context.parallel_stats.fallbacks
|
|
481
576
|
|
|
482
577
|
def request(self, request: dict[str, object]) -> CacheClientResponse:
|
|
483
578
|
with self.lock:
|
|
@@ -506,6 +601,7 @@ class _CacheService:
|
|
|
506
601
|
self.stats.warm_hits += 1
|
|
507
602
|
else:
|
|
508
603
|
self.stats.rebuilds += 1
|
|
604
|
+
self.stats.parallel_fallbacks += self.context.parallel_stats.fallbacks
|
|
509
605
|
if before != after:
|
|
510
606
|
self.stats.invalidations += 1
|
|
511
607
|
self.last_budget = self.budget.enforce(
|
|
@@ -539,6 +635,12 @@ class _CacheService:
|
|
|
539
635
|
"warning_active": self.last_budget.warning_active, "warning_threshold_percent": WARNING_THRESHOLD_PERCENT,
|
|
540
636
|
"cache_state": self.cache_state, "requests": self.stats.requests, "warm_hits": self.stats.warm_hits,
|
|
541
637
|
"rebuilds": self.stats.rebuilds, "invalidations": self.stats.invalidations, "evictions": self.stats.evictions,
|
|
638
|
+
"workers_configured": "auto" if self.metadata.workers == 0 else self.metadata.workers,
|
|
639
|
+
"workers_effective": self.context.parallel_stats.effective_workers,
|
|
640
|
+
"parallel_files_completed": self.context.parallel_stats.files_completed,
|
|
641
|
+
"prewarm_seconds": self.prewarm_seconds,
|
|
642
|
+
"parallel_seconds": self.context.parallel_stats.elapsed_seconds,
|
|
643
|
+
"parallel_fallbacks": self.stats.parallel_fallbacks,
|
|
542
644
|
"idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
|
|
543
645
|
"workspace_revision": self.context.workspace.workspace_revision,
|
|
544
646
|
}
|
|
@@ -562,14 +664,33 @@ def _serve_connection(connection: socket.socket, service: _CacheService) -> None
|
|
|
562
664
|
connection.sendall(json.dumps(response, separators=(",", ":")).encode("utf-8") + b"\n")
|
|
563
665
|
|
|
564
666
|
|
|
565
|
-
def run_cache_daemon(
|
|
667
|
+
def run_cache_daemon(
|
|
668
|
+
root: str | Path,
|
|
669
|
+
*,
|
|
670
|
+
project_file: str = "",
|
|
671
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
672
|
+
workers: int = 0,
|
|
673
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
674
|
+
) -> None:
|
|
566
675
|
canonical = str(Path(root).resolve())
|
|
567
676
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
568
677
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
569
678
|
listener.bind(("127.0.0.1", 0))
|
|
570
679
|
listener.listen(16)
|
|
571
680
|
listener.settimeout(0.25)
|
|
572
|
-
metadata = CacheMetadata(
|
|
681
|
+
metadata = CacheMetadata(
|
|
682
|
+
DAEMON_SCHEMA,
|
|
683
|
+
canonical,
|
|
684
|
+
os.getpid(),
|
|
685
|
+
listener.getsockname()[1],
|
|
686
|
+
secrets.token_urlsafe(32),
|
|
687
|
+
__version__,
|
|
688
|
+
project_file,
|
|
689
|
+
max_memory_bytes,
|
|
690
|
+
workers,
|
|
691
|
+
idle_timeout,
|
|
692
|
+
time.time(),
|
|
693
|
+
)
|
|
573
694
|
try:
|
|
574
695
|
service = _CacheService(metadata)
|
|
575
696
|
service.prewarm()
|
|
@@ -587,7 +708,15 @@ def run_cache_daemon(root: str | Path, *, project_file: str = "", max_memory_byt
|
|
|
587
708
|
_remove_metadata_if_owned(metadata)
|
|
588
709
|
|
|
589
710
|
|
|
590
|
-
def _start_cache_unlocked(
|
|
711
|
+
def _start_cache_unlocked(
|
|
712
|
+
root: str | Path,
|
|
713
|
+
*,
|
|
714
|
+
project_file: str | Path | None = None,
|
|
715
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
716
|
+
workers: int = 0,
|
|
717
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
718
|
+
startup_timeout: float = DEFAULT_STARTUP_TIMEOUT,
|
|
719
|
+
) -> CacheMetadata:
|
|
591
720
|
canonical = str(Path(root).resolve())
|
|
592
721
|
project = str((Path(canonical) / project_file).resolve()) if project_file and not Path(project_file).is_absolute() else (str(Path(project_file).resolve()) if project_file else "")
|
|
593
722
|
existing = _read_metadata(canonical)
|
|
@@ -596,12 +725,32 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
596
725
|
_client_exchange(existing, {"action": "status", "_startup_probe": True})
|
|
597
726
|
except CacheClientError as error:
|
|
598
727
|
raise CacheClientError("unavailable", "Live cache daemon is unavailable.") from error
|
|
599
|
-
if (existing.project_file, existing.max_memory_bytes, existing.idle_timeout) != (
|
|
728
|
+
if (existing.project_file, existing.max_memory_bytes, existing.workers, existing.idle_timeout) != (
|
|
729
|
+
project,
|
|
730
|
+
max_memory_bytes,
|
|
731
|
+
workers,
|
|
732
|
+
idle_timeout,
|
|
733
|
+
):
|
|
600
734
|
raise CacheClientError("configuration_conflict", "A live cache daemon has conflicting configuration.")
|
|
601
735
|
return existing
|
|
602
736
|
if existing:
|
|
603
737
|
_remove_metadata_if_owned(existing)
|
|
604
|
-
|
|
738
|
+
else:
|
|
739
|
+
_remove_incompatible_metadata_if_stale(canonical)
|
|
740
|
+
command = [
|
|
741
|
+
sys.executable,
|
|
742
|
+
"-m",
|
|
743
|
+
"delphi_lsp.agent_cache",
|
|
744
|
+
"serve",
|
|
745
|
+
"--root",
|
|
746
|
+
canonical,
|
|
747
|
+
"--max-memory",
|
|
748
|
+
str(max_memory_bytes),
|
|
749
|
+
"--workers",
|
|
750
|
+
str(workers),
|
|
751
|
+
"--idle-timeout",
|
|
752
|
+
str(idle_timeout),
|
|
753
|
+
]
|
|
605
754
|
if project:
|
|
606
755
|
command.extend(("--project-file", project))
|
|
607
756
|
options: dict[str, object] = {"stdin": subprocess.DEVNULL, "stdout": subprocess.DEVNULL}
|
|
@@ -612,7 +761,7 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
612
761
|
with tempfile.TemporaryFile() as diagnostics:
|
|
613
762
|
options["stderr"] = diagnostics
|
|
614
763
|
process = subprocess.Popen(command, **options)
|
|
615
|
-
deadline = time.monotonic() +
|
|
764
|
+
deadline = time.monotonic() + startup_timeout
|
|
616
765
|
startup_ready = False
|
|
617
766
|
try:
|
|
618
767
|
while time.monotonic() < deadline:
|
|
@@ -649,14 +798,34 @@ def _start_cache_unlocked(root: str | Path, *, project_file: str | Path | None =
|
|
|
649
798
|
process.wait()
|
|
650
799
|
|
|
651
800
|
|
|
652
|
-
def start_cache(
|
|
653
|
-
|
|
654
|
-
|
|
801
|
+
def start_cache(
|
|
802
|
+
root: str | Path,
|
|
803
|
+
*,
|
|
804
|
+
project_file: str | Path | None = None,
|
|
805
|
+
max_memory_bytes: int = DEFAULT_MAX_MEMORY_BYTES,
|
|
806
|
+
workers: int = 0,
|
|
807
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT,
|
|
808
|
+
startup_timeout: float = DEFAULT_STARTUP_TIMEOUT,
|
|
809
|
+
) -> CacheMetadata:
|
|
810
|
+
if type(workers) is not int or not 0 <= workers <= 32:
|
|
811
|
+
raise ValueError("workers must be auto or an integer from 1 through 32.")
|
|
812
|
+
if not math.isfinite(startup_timeout) or startup_timeout <= 0:
|
|
813
|
+
raise ValueError("startup_timeout must be greater than zero.")
|
|
814
|
+
with _start_lock(root, startup_timeout):
|
|
815
|
+
return _start_cache_unlocked(
|
|
816
|
+
root,
|
|
817
|
+
project_file=project_file,
|
|
818
|
+
max_memory_bytes=max_memory_bytes,
|
|
819
|
+
workers=workers,
|
|
820
|
+
idle_timeout=idle_timeout,
|
|
821
|
+
startup_timeout=startup_timeout,
|
|
822
|
+
)
|
|
655
823
|
|
|
656
824
|
|
|
657
825
|
def query_cache(root: str | Path, request: dict[str, object]) -> CacheClientResponse:
|
|
658
826
|
metadata = _read_metadata(root)
|
|
659
827
|
if metadata is None:
|
|
828
|
+
_remove_incompatible_metadata_if_stale(root)
|
|
660
829
|
raise CacheClientError("cache_not_running", "Cache daemon is not running.")
|
|
661
830
|
if not _pid_alive(metadata.pid):
|
|
662
831
|
_remove_metadata_if_owned(metadata)
|
|
@@ -671,6 +840,7 @@ def cache_status(root: str | Path) -> dict[str, object]:
|
|
|
671
840
|
def stop_cache(root: str | Path) -> None:
|
|
672
841
|
metadata = _read_metadata(root)
|
|
673
842
|
if metadata is None:
|
|
843
|
+
_remove_incompatible_metadata_if_stale(root)
|
|
674
844
|
return
|
|
675
845
|
if not _pid_alive(metadata.pid):
|
|
676
846
|
_remove_metadata_if_owned(metadata)
|
|
@@ -681,11 +851,13 @@ def stop_cache(root: str | Path) -> None:
|
|
|
681
851
|
pass
|
|
682
852
|
deadline = time.monotonic() + 3
|
|
683
853
|
stopped = False
|
|
684
|
-
while
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
854
|
+
while time.monotonic() < deadline:
|
|
855
|
+
if _reap_child_if_exited(metadata.pid):
|
|
856
|
+
stopped = True
|
|
857
|
+
break
|
|
858
|
+
if not _pid_alive(metadata.pid):
|
|
859
|
+
stopped = True
|
|
860
|
+
break
|
|
689
861
|
time.sleep(0.05)
|
|
690
862
|
if not stopped and _pid_alive(metadata.pid):
|
|
691
863
|
raise CacheClientError("stop_failed", "Cache daemon did not stop.")
|
|
@@ -699,10 +871,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
699
871
|
serve.add_argument("--root", required=True)
|
|
700
872
|
serve.add_argument("--project-file", default="")
|
|
701
873
|
serve.add_argument("--max-memory", type=int, default=DEFAULT_MAX_MEMORY_BYTES)
|
|
874
|
+
serve.add_argument("--workers", type=int, default=0)
|
|
702
875
|
serve.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT)
|
|
703
876
|
args = parser.parse_args(argv)
|
|
704
877
|
if args.command == "serve":
|
|
705
|
-
run_cache_daemon(
|
|
878
|
+
run_cache_daemon(
|
|
879
|
+
args.root,
|
|
880
|
+
project_file=args.project_file,
|
|
881
|
+
max_memory_bytes=args.max_memory,
|
|
882
|
+
workers=args.workers,
|
|
883
|
+
idle_timeout=args.idle_timeout,
|
|
884
|
+
)
|
|
706
885
|
return 0
|
|
707
886
|
|
|
708
887
|
|