From 543a41c47e6925811a1dfd3f8747e23dd35e7c0e Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 26 Apr 2026 18:26:03 +0800 Subject: [PATCH 01/12] feat(server): reorganize src/server/ into subdirectories and add agentic protocol Reorganize the flat src/server/ directory into four subdirectories: - workspace/ (workspace, config) - compiler/ (compiler, compile_graph, indexer, protocol) - worker/ (worker_pool, stateful/stateless_worker, worker_common) - lsp/ (master_server, session) Add initial agentic protocol support: - --agentic flag enables a TCP listener (reusing --host/--port) for agent connections alongside the LSP pipe, with validation that it is only allowed in pipe mode - Multi-peer support: agent connections/disconnections are handled independently without affecting the LSP session - First agentic request: agentic/compileCommand returns the compile command (file, directory, arguments) for a given filesystem path Co-Authored-By: Claude Opus 4.6 --- src/clice.cc | 24 +++++++- src/server/agentic_protocol.h | 30 ++++++++++ src/server/{ => compiler}/compile_graph.cpp | 2 +- src/server/{ => compiler}/compile_graph.h | 0 src/server/{ => compiler}/compiler.cpp | 4 +- src/server/{ => compiler}/compiler.h | 6 +- src/server/{ => compiler}/indexer.cpp | 10 ++-- src/server/{ => compiler}/indexer.h | 2 +- src/server/{ => compiler}/protocol.h | 0 src/server/{ => lsp}/master_server.cpp | 59 ++++++++++++++++++- src/server/{ => lsp}/master_server.h | 25 ++++++-- src/server/{ => lsp}/session.h | 2 +- src/server/{ => worker}/stateful_worker.cpp | 6 +- src/server/{ => worker}/stateful_worker.h | 0 src/server/{ => worker}/stateless_worker.cpp | 6 +- src/server/{ => worker}/stateless_worker.h | 0 src/server/{ => worker}/worker_common.h | 0 src/server/{ => worker}/worker_pool.cpp | 2 +- src/server/{ => worker}/worker_pool.h | 2 +- src/server/{ => workspace}/config.cpp | 2 +- src/server/{ => workspace}/config.h | 0 src/server/{ => workspace}/workspace.cpp | 2 +- src/server/{ => workspace}/workspace.h | 4 +- .../compile_graph_integration_tests.cpp | 2 +- tests/unit/server/compile_graph_tests.cpp | 2 +- tests/unit/server/config_tests.cpp | 2 +- tests/unit/server/module_worker_tests.cpp | 2 +- tests/unit/server/pch_worker_tests.cpp | 2 +- tests/unit/server/stateful_worker_tests.cpp | 2 +- tests/unit/server/stateless_worker_tests.cpp | 2 +- tests/unit/server/worker_test_helpers.h | 2 +- 31 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 src/server/agentic_protocol.h rename src/server/{ => compiler}/compile_graph.cpp (99%) rename src/server/{ => compiler}/compile_graph.h (100%) rename src/server/{ => compiler}/compiler.cpp (99%) rename src/server/{ => compiler}/compiler.h (97%) rename src/server/{ => compiler}/indexer.cpp (99%) rename src/server/{ => compiler}/indexer.h (99%) rename src/server/{ => compiler}/protocol.h (100%) rename src/server/{ => lsp}/master_server.cpp (94%) rename src/server/{ => lsp}/master_server.h (74%) rename src/server/{ => lsp}/session.h (98%) rename src/server/{ => worker}/stateful_worker.cpp (98%) rename src/server/{ => worker}/stateful_worker.h (100%) rename src/server/{ => worker}/stateless_worker.cpp (98%) rename src/server/{ => worker}/stateless_worker.h (100%) rename src/server/{ => worker}/worker_common.h (100%) rename src/server/{ => worker}/worker_pool.cpp (99%) rename src/server/{ => worker}/worker_pool.h (99%) rename src/server/{ => workspace}/config.cpp (99%) rename src/server/{ => workspace}/config.h (100%) rename src/server/{ => workspace}/workspace.cpp (99%) rename src/server/{ => workspace}/workspace.h (99%) diff --git a/src/clice.cc b/src/clice.cc index d09e5b127..d5a1da5d9 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -4,9 +4,9 @@ #include #include -#include "server/master_server.h" -#include "server/stateful_worker.h" -#include "server/stateless_worker.h" +#include "server/lsp/master_server.h" +#include "server/worker/stateful_worker.h" +#include "server/worker/stateless_worker.h" #include "support/logging.h" #include "kota/async/async.h" @@ -62,6 +62,11 @@ struct Options { DecoFlag(names = {"-v", "--version"}, help = "Show version", required = false) version; + + DecoFlag(names = {"--agentic"}, + help = "Enable agentic protocol listener on host:port (pipe mode only)", + required = false) + agentic; }; } // namespace clice @@ -110,6 +115,13 @@ int main(int argc, const char** argv) { return 1; } + auto agentic = opts.agentic.value_or(false); + + if(agentic && *opts.mode != "pipe") { + LOG_ERROR("--agentic is only supported in pipe mode"); + return 1; + } + std::string self_path = argv[0]; auto& mode = *opts.mode; @@ -153,6 +165,12 @@ int main(int argc, const char** argv) { clice::MasterServer server(loop, peer, std::move(self_path)); server.register_handlers(); + if(agentic) { + auto host = opts.host.value_or("127.0.0.1"); + auto port = opts.port.value_or(50051); + loop.schedule(server.listen_for_agents(std::move(host), port)); + } + loop.schedule(peer.run()); loop.run(); return 0; diff --git a/src/server/agentic_protocol.h b/src/server/agentic_protocol.h new file mode 100644 index 000000000..b17fa9689 --- /dev/null +++ b/src/server/agentic_protocol.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "kota/ipc/protocol.h" + +namespace clice::agentic { + +struct CompileCommandParams { + std::string path; +}; + +struct CompileCommandResult { + std::string file; + std::string directory; + std::vector arguments; +}; + +} // namespace clice::agentic + +namespace kota::ipc::protocol { + +template <> +struct RequestTraits { + using Result = clice::agentic::CompileCommandResult; + constexpr inline static std::string_view method = "agentic/compileCommand"; +}; + +} // namespace kota::ipc::protocol diff --git a/src/server/compile_graph.cpp b/src/server/compiler/compile_graph.cpp similarity index 99% rename from src/server/compile_graph.cpp rename to src/server/compiler/compile_graph.cpp index ac7f1f9e0..39d4070dc 100644 --- a/src/server/compile_graph.cpp +++ b/src/server/compiler/compile_graph.cpp @@ -1,4 +1,4 @@ -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" #include diff --git a/src/server/compile_graph.h b/src/server/compiler/compile_graph.h similarity index 100% rename from src/server/compile_graph.h rename to src/server/compiler/compile_graph.h diff --git a/src/server/compiler.cpp b/src/server/compiler/compiler.cpp similarity index 99% rename from src/server/compiler.cpp rename to src/server/compiler/compiler.cpp index 0686049b4..a6d485e15 100644 --- a/src/server/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -1,4 +1,4 @@ -#include "server/compiler.h" +#include "server/compiler/compiler.h" #include #include @@ -6,7 +6,7 @@ #include "command/search_config.h" #include "index/tu_index.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "support/filesystem.h" #include "support/logging.h" #include "syntax/include_resolver.h" diff --git a/src/server/compiler.h b/src/server/compiler/compiler.h similarity index 97% rename from src/server/compiler.h rename to src/server/compiler/compiler.h index c0c7b71d5..dcd645dac 100644 --- a/src/server/compiler.h +++ b/src/server/compiler/compiler.h @@ -8,9 +8,9 @@ #include #include "command/command.h" -#include "server/session.h" -#include "server/worker_pool.h" -#include "server/workspace.h" +#include "server/lsp/session.h" +#include "server/worker/worker_pool.h" +#include "server/workspace/workspace.h" #include "syntax/completion.h" #include "kota/async/async.h" diff --git a/src/server/indexer.cpp b/src/server/compiler/indexer.cpp similarity index 99% rename from src/server/indexer.cpp rename to src/server/compiler/indexer.cpp index f46aa386a..e1ddacb5c 100644 --- a/src/server/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -1,4 +1,4 @@ -#include "server/indexer.h" +#include "server/compiler/indexer.h" #include #include @@ -6,10 +6,10 @@ #include #include "index/tu_index.h" -#include "server/compiler.h" -#include "server/protocol.h" -#include "server/session.h" -#include "server/worker_pool.h" +#include "server/compiler/compiler.h" +#include "server/compiler/protocol.h" +#include "server/lsp/session.h" +#include "server/worker/worker_pool.h" #include "support/filesystem.h" #include "support/logging.h" diff --git a/src/server/indexer.h b/src/server/compiler/indexer.h similarity index 99% rename from src/server/indexer.h rename to src/server/compiler/indexer.h index 7b6a621b1..8e7ef5d7e 100644 --- a/src/server/indexer.h +++ b/src/server/compiler/indexer.h @@ -9,7 +9,7 @@ #include "semantic/relation_kind.h" #include "semantic/symbol_kind.h" -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include "kota/async/async.h" #include "kota/ipc/codec/json.h" diff --git a/src/server/protocol.h b/src/server/compiler/protocol.h similarity index 100% rename from src/server/protocol.h rename to src/server/compiler/protocol.h diff --git a/src/server/master_server.cpp b/src/server/lsp/master_server.cpp similarity index 94% rename from src/server/master_server.cpp rename to src/server/lsp/master_server.cpp index 6255cec6f..59545d66b 100644 --- a/src/server/master_server.cpp +++ b/src/server/lsp/master_server.cpp @@ -1,4 +1,4 @@ -#include "server/master_server.h" +#include "server/lsp/master_server.h" #include #include @@ -7,7 +7,8 @@ #include #include "semantic/symbol_kind.h" -#include "server/protocol.h" +#include "server/agentic_protocol.h" +#include "server/compiler/protocol.h" #include "support/filesystem.h" #include "support/logging.h" @@ -918,4 +919,58 @@ void MasterServer::register_handlers() { }); } +void MasterServer::register_agent_handlers(kota::ipc::JsonPeer& agent_peer) { + using namespace agentic; + + agent_peer.on_request( + [this](RequestContext&, + const CompileCommandParams& params) -> RequestResult { + std::string directory; + std::vector arguments; + if(!compiler.fill_compile_args(params.path, directory, arguments)) { + co_return kota::outcome_error( + kota::ipc::Error{std::format("no compile command found for {}", params.path)}); + } + + co_return CompileCommandResult{ + .file = params.path, + .directory = std::move(directory), + .arguments = std::move(arguments), + }; + }); +} + +kota::task<> MasterServer::listen_for_agents(std::string host, int port) { + auto acceptor = kota::tcp::listen(host, port, {}, loop); + if(!acceptor) { + LOG_ERROR("failed to listen for agents on {}:{}", host, port); + co_return; + } + + LOG_INFO("Agentic protocol listening on {}:{}", host, port); + + while(true) { + auto conn = co_await acceptor->accept(); + if(!conn.has_value()) { + break; + } + + LOG_INFO("Agent connected"); + + auto transport = std::make_unique(std::move(*conn)); + auto peer = std::make_unique(loop, std::move(transport)); + register_agent_handlers(*peer); + + auto* peer_ptr = peer.get(); + auto it = agent_connections.emplace(agent_connections.end(), + AgentConnection{nullptr, std::move(peer)}); + + loop.schedule([this, peer_ptr, it]() -> kota::task<> { + co_await peer_ptr->run(); + LOG_INFO("Agent disconnected"); + agent_connections.erase(it); + }()); + } +} + } // namespace clice diff --git a/src/server/master_server.h b/src/server/lsp/master_server.h similarity index 74% rename from src/server/master_server.h rename to src/server/lsp/master_server.h index 94106fffa..5f9fc7fc9 100644 --- a/src/server/master_server.h +++ b/src/server/lsp/master_server.h @@ -1,19 +1,21 @@ #pragma once #include +#include #include #include #include -#include "server/compiler.h" -#include "server/indexer.h" -#include "server/session.h" -#include "server/worker_pool.h" -#include "server/workspace.h" +#include "server/compiler/compiler.h" +#include "server/compiler/indexer.h" +#include "server/lsp/session.h" +#include "server/worker/worker_pool.h" +#include "server/workspace/workspace.h" #include "kota/async/async.h" #include "kota/codec/json/json.h" #include "kota/ipc/peer.h" +#include "kota/ipc/transport.h" #include "llvm/ADT/DenseMap.h" namespace clice { @@ -47,7 +49,20 @@ class MasterServer { void register_handlers(); + /// Start accepting agent connections on the given host:port. + /// Each agent gets `agentic/*` handlers registered. + /// Agent disconnections are handled gracefully without affecting the server. + kota::task<> listen_for_agents(std::string host, int port); + private: + void register_agent_handlers(kota::ipc::JsonPeer& agent_peer); + + struct AgentConnection { + std::unique_ptr transport; + std::unique_ptr peer; + }; + + std::list agent_connections; kota::event_loop& loop; kota::ipc::JsonPeer& peer; diff --git a/src/server/session.h b/src/server/lsp/session.h similarity index 98% rename from src/server/session.h rename to src/server/lsp/session.h index 487ed38f3..0d66c688c 100644 --- a/src/server/session.h +++ b/src/server/lsp/session.h @@ -5,7 +5,7 @@ #include #include -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include "kota/async/async.h" #include "llvm/ADT/SmallVector.h" diff --git a/src/server/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp similarity index 98% rename from src/server/stateful_worker.cpp rename to src/server/worker/stateful_worker.cpp index 8337a0eab..d641fd2ce 100644 --- a/src/server/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -1,4 +1,4 @@ -#include "server/stateful_worker.h" +#include "server/worker/stateful_worker.h" #include #include @@ -10,8 +10,8 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/protocol.h" -#include "server/worker_common.h" +#include "server/compiler/protocol.h" +#include "server/worker/worker_common.h" #include "support/logging.h" #include "kota/async/async.h" diff --git a/src/server/stateful_worker.h b/src/server/worker/stateful_worker.h similarity index 100% rename from src/server/stateful_worker.h rename to src/server/worker/stateful_worker.h diff --git a/src/server/stateless_worker.cpp b/src/server/worker/stateless_worker.cpp similarity index 98% rename from src/server/stateless_worker.cpp rename to src/server/worker/stateless_worker.cpp index 7d85f8fcf..2f81eb960 100644 --- a/src/server/stateless_worker.cpp +++ b/src/server/worker/stateless_worker.cpp @@ -1,10 +1,10 @@ -#include "server/stateless_worker.h" +#include "server/worker/stateless_worker.h" #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/protocol.h" -#include "server/worker_common.h" +#include "server/compiler/protocol.h" +#include "server/worker/worker_common.h" #include "support/logging.h" #include "kota/async/async.h" diff --git a/src/server/stateless_worker.h b/src/server/worker/stateless_worker.h similarity index 100% rename from src/server/stateless_worker.h rename to src/server/worker/stateless_worker.h diff --git a/src/server/worker_common.h b/src/server/worker/worker_common.h similarity index 100% rename from src/server/worker_common.h rename to src/server/worker/worker_common.h diff --git a/src/server/worker_pool.cpp b/src/server/worker/worker_pool.cpp similarity index 99% rename from src/server/worker_pool.cpp rename to src/server/worker/worker_pool.cpp index e4beb9725..1ece69e75 100644 --- a/src/server/worker_pool.cpp +++ b/src/server/worker/worker_pool.cpp @@ -1,4 +1,4 @@ -#include "server/worker_pool.h" +#include "server/worker/worker_pool.h" #include #include diff --git a/src/server/worker_pool.h b/src/server/worker/worker_pool.h similarity index 99% rename from src/server/worker_pool.h rename to src/server/worker/worker_pool.h index 0ba58734c..5bba9bfeb 100644 --- a/src/server/worker_pool.h +++ b/src/server/worker/worker_pool.h @@ -6,7 +6,7 @@ #include #include -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "kota/async/async.h" #include "kota/ipc/codec/bincode.h" diff --git a/src/server/config.cpp b/src/server/workspace/config.cpp similarity index 99% rename from src/server/config.cpp rename to src/server/workspace/config.cpp index ed767be23..ba44088d9 100644 --- a/src/server/config.cpp +++ b/src/server/workspace/config.cpp @@ -1,4 +1,4 @@ -#include "server/config.h" +#include "server/workspace/config.h" #include diff --git a/src/server/config.h b/src/server/workspace/config.h similarity index 100% rename from src/server/config.h rename to src/server/workspace/config.h diff --git a/src/server/workspace.cpp b/src/server/workspace/workspace.cpp similarity index 99% rename from src/server/workspace.cpp rename to src/server/workspace/workspace.cpp index 3781b7846..5a9640595 100644 --- a/src/server/workspace.cpp +++ b/src/server/workspace/workspace.cpp @@ -1,4 +1,4 @@ -#include "server/workspace.h" +#include "server/workspace/workspace.h" #include #include diff --git a/src/server/workspace.h b/src/server/workspace/workspace.h similarity index 99% rename from src/server/workspace.h rename to src/server/workspace/workspace.h index 75a89ed28..5023eecd1 100644 --- a/src/server/workspace.h +++ b/src/server/workspace/workspace.h @@ -11,8 +11,8 @@ #include "index/merged_index.h" #include "index/project_index.h" #include "semantic/relation_kind.h" -#include "server/compile_graph.h" -#include "server/config.h" +#include "server/compiler/compile_graph.h" +#include "server/workspace/config.h" #include "support/path_pool.h" #include "syntax/dependency_graph.h" diff --git a/tests/unit/server/compile_graph_integration_tests.cpp b/tests/unit/server/compile_graph_integration_tests.cpp index 559e0557f..e1c768f57 100644 --- a/tests/unit/server/compile_graph_integration_tests.cpp +++ b/tests/unit/server/compile_graph_integration_tests.cpp @@ -3,7 +3,7 @@ #include "test/test.h" #include "command/command.h" #include "compile/compilation.h" -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" #include "support/path_pool.h" #include "syntax/dependency_graph.h" #include "syntax/scan.h" diff --git a/tests/unit/server/compile_graph_tests.cpp b/tests/unit/server/compile_graph_tests.cpp index 5b562e672..680927f11 100644 --- a/tests/unit/server/compile_graph_tests.cpp +++ b/tests/unit/server/compile_graph_tests.cpp @@ -1,7 +1,7 @@ #include #include "test/test.h" -#include "server/compile_graph.h" +#include "server/compiler/compile_graph.h" namespace clice::testing { namespace { diff --git a/tests/unit/server/config_tests.cpp b/tests/unit/server/config_tests.cpp index d9181fa8a..8fd9adcae 100644 --- a/tests/unit/server/config_tests.cpp +++ b/tests/unit/server/config_tests.cpp @@ -2,7 +2,7 @@ #include "test/temp_dir.h" #include "test/test.h" -#include "server/config.h" +#include "server/workspace/config.h" #include "support/filesystem.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index d9e73251b..2af58732b 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "server/worker_test_helpers.h" namespace clice::testing { diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index 37273e3c5..d05935beb 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "server/worker_test_helpers.h" #include "syntax/scan.h" diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index d16f6c622..1a51169de 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "server/worker_test_helpers.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index 0cc8b4848..4f280af45 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "server/worker_test_helpers.h" #include "kota/codec/bincode/bincode.h" diff --git a/tests/unit/server/worker_test_helpers.h b/tests/unit/server/worker_test_helpers.h index 729c87dc9..dfffae31e 100644 --- a/tests/unit/server/worker_test_helpers.h +++ b/tests/unit/server/worker_test_helpers.h @@ -11,7 +11,7 @@ #include "test/temp_dir.h" #include "command/argument_parser.h" #include "command/command.h" -#include "server/protocol.h" +#include "server/compiler/protocol.h" #include "support/filesystem.h" #include "kota/async/async.h" From ed2b20c99f6ee60253b6dd2dd05f84021ed9982b Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 26 Apr 2026 19:34:12 +0800 Subject: [PATCH 02/12] feat(server): split MasterServer into service layer and move protocols to src/protocol/ Reorganize server architecture: extract LSP handler registration into LSPClient and agentic handlers into AgentClient, keeping MasterServer as pure state. Move all protocol definitions (worker, extension, agentic) into src/protocol/. Add TCP listener for agentic connections in pipe mode and stub --mode agentic for future CLI agent client. Co-Authored-By: Claude Opus 4.6 --- src/clice.cc | 112 +--- .../agentic_protocol.h => protocol/agentic.h} | 0 src/protocol/extension.h | 42 ++ .../compiler/protocol.h => protocol/worker.h} | 39 -- src/server/compiler/compiler.cpp | 13 +- src/server/compiler/compiler.h | 10 +- src/server/compiler/indexer.cpp | 4 +- src/server/lsp/master_server.h | 96 --- src/server/service/agent_client.cpp | 37 ++ src/server/service/agent_client.h | 18 + .../lsp_client.cpp} | 571 ++++++------------ src/server/service/lsp_client.h | 22 + src/server/service/master_server.cpp | 249 ++++++++ src/server/service/master_server.h | 66 ++ src/server/{lsp => service}/session.h | 0 src/server/worker/stateful_worker.cpp | 2 +- src/server/worker/stateless_worker.cpp | 2 +- src/server/worker/worker_pool.h | 2 +- tests/unit/server/module_worker_tests.cpp | 2 +- tests/unit/server/pch_worker_tests.cpp | 2 +- tests/unit/server/stateful_worker_tests.cpp | 2 +- tests/unit/server/stateless_worker_tests.cpp | 2 +- tests/unit/server/worker_test_helpers.h | 2 +- 23 files changed, 659 insertions(+), 636 deletions(-) rename src/{server/agentic_protocol.h => protocol/agentic.h} (100%) create mode 100644 src/protocol/extension.h rename src/{server/compiler/protocol.h => protocol/worker.h} (87%) delete mode 100644 src/server/lsp/master_server.h create mode 100644 src/server/service/agent_client.cpp create mode 100644 src/server/service/agent_client.h rename src/server/{lsp/master_server.cpp => service/lsp_client.cpp} (56%) create mode 100644 src/server/service/lsp_client.h create mode 100644 src/server/service/master_server.cpp create mode 100644 src/server/service/master_server.h rename src/server/{lsp => service}/session.h (100%) diff --git a/src/clice.cc b/src/clice.cc index d5a1da5d9..cd48ab8d3 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -4,17 +4,12 @@ #include #include -#include "server/lsp/master_server.h" +#include "server/service/master_server.h" #include "server/worker/stateful_worker.h" #include "server/worker/stateless_worker.h" #include "support/logging.h" -#include "kota/async/async.h" #include "kota/deco/deco.h" -#include "kota/ipc/codec/json.h" -#include "kota/ipc/peer.h" -#include "kota/ipc/recording_transport.h" -#include "kota/ipc/transport.h" namespace clice { @@ -22,7 +17,7 @@ using kota::deco::decl::KVStyle; struct Options { DecoKV(style = KVStyle::JoinedOrSeparate, - help = "Running mode: pipe, socket, stateless-worker, stateful-worker", + help = "Running mode: pipe, socket, agentic, stateless-worker, stateful-worker", required = false) mode; @@ -62,20 +57,12 @@ struct Options { DecoFlag(names = {"-v", "--version"}, help = "Show version", required = false) version; - - DecoFlag(names = {"--agentic"}, - help = "Enable agentic protocol listener on host:port (pipe mode only)", - required = false) - agentic; }; } // namespace clice int main(int argc, const char** argv) { #ifndef _WIN32 - // On POSIX systems, ignore SIGPIPE so that writing to a closed pipe - // (e.g. when the LSP client disconnects) returns EPIPE instead of - // killing the process. This is standard practice for pipe-based servers. signal(SIGPIPE, SIG_IGN); #endif @@ -115,15 +102,6 @@ int main(int argc, const char** argv) { return 1; } - auto agentic = opts.agentic.value_or(false); - - if(agentic && *opts.mode != "pipe") { - LOG_ERROR("--agentic is only supported in pipe mode"); - return 1; - } - - std::string self_path = argv[0]; - auto& mode = *opts.mode; auto worker_name = opts.worker_name.value_or(""); @@ -143,83 +121,19 @@ int main(int argc, const char** argv) { log_dir); } - if(mode == "pipe") { - clice::logging::stderr_logger("master", clice::logging::options); - - kota::event_loop loop; - - auto transport = kota::ipc::StreamTransport::open_stdio(loop); - if(!transport) { - LOG_ERROR("failed to open stdio transport"); - return 1; - } - - std::unique_ptr final_transport = std::move(*transport); - if(opts.record.has_value()) { - final_transport = - std::make_unique(std::move(final_transport), - *opts.record); - } - - kota::ipc::JsonPeer peer(loop, std::move(final_transport)); - clice::MasterServer server(loop, peer, std::move(self_path)); - server.register_handlers(); - - if(agentic) { - auto host = opts.host.value_or("127.0.0.1"); - auto port = opts.port.value_or(50051); - loop.schedule(server.listen_for_agents(std::move(host), port)); - } - - loop.schedule(peer.run()); - loop.run(); - return 0; + if(mode == "pipe" || mode == "socket") { + clice::ServerOptions server_opts; + server_opts.mode = mode; + server_opts.host = opts.host.value_or("127.0.0.1"); + server_opts.port = opts.port.value_or(50051); + server_opts.self_path = argv[0]; + server_opts.record = opts.record.value_or(""); + return clice::run_server_mode(server_opts); } - if(mode == "socket") { - clice::logging::stderr_logger("master", clice::logging::options); - - kota::event_loop loop; - - auto host = opts.host.value_or("127.0.0.1"); - auto port = opts.port.value_or(50051); - - auto acceptor = kota::tcp::listen(host, port, {}, loop); - if(!acceptor) { - LOG_ERROR("failed to listen on {}:{}", host, port); - return 1; - } - - LOG_INFO("Listening on {}:{} ...", host, port); - - auto task = [&]() -> kota::task<> { - auto client = co_await acceptor->accept(); - if(!client.has_value()) { - LOG_ERROR("failed to accept connection"); - loop.stop(); - co_return; - } - - LOG_INFO("Client connected"); - - std::unique_ptr transport = - std::make_unique(std::move(client.value())); - if(opts.record.has_value()) { - transport = std::make_unique(std::move(transport), - *opts.record); - } - kota::ipc::JsonPeer peer(loop, std::move(transport)); - clice::MasterServer server(loop, peer, std::string(self_path)); - server.register_handlers(); - - co_await peer.run(); - peer.close(); - loop.stop(); - }; - - loop.schedule(task()); - loop.run(); - return 0; + if(mode == "agentic") { + LOG_ERROR("agentic client mode is not yet implemented"); + return 1; } LOG_ERROR("unknown mode '{}'", mode); diff --git a/src/server/agentic_protocol.h b/src/protocol/agentic.h similarity index 100% rename from src/server/agentic_protocol.h rename to src/protocol/agentic.h diff --git a/src/protocol/extension.h b/src/protocol/extension.h new file mode 100644 index 000000000..8ded8bea5 --- /dev/null +++ b/src/protocol/extension.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace clice::ext { + +struct ContextItem { + std::string label; + std::string description; + std::string uri; +}; + +struct QueryContextParams { + std::string uri; + std::optional offset; +}; + +struct QueryContextResult { + std::vector contexts; + int total; +}; + +struct CurrentContextParams { + std::string uri; +}; + +struct CurrentContextResult { + std::optional context; +}; + +struct SwitchContextParams { + std::string uri; + std::string context_uri; +}; + +struct SwitchContextResult { + bool success; +}; + +} // namespace clice::ext diff --git a/src/server/compiler/protocol.h b/src/protocol/worker.h similarity index 87% rename from src/server/compiler/protocol.h rename to src/protocol/worker.h index 1a2774c78..e5a3a7293 100644 --- a/src/server/compiler/protocol.h +++ b/src/protocol/worker.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -10,7 +9,6 @@ #include "syntax/token.h" #include "kota/codec/json/json.h" -#include "kota/ipc/lsp/protocol.h" #include "kota/ipc/protocol.h" namespace clice::worker { @@ -122,43 +120,6 @@ struct EvictedParams { } // namespace clice::worker -namespace clice::ext { - -struct ContextItem { - std::string label; - std::string description; - std::string uri; -}; - -struct QueryContextParams { - std::string uri; - std::optional offset; -}; - -struct QueryContextResult { - std::vector contexts; - int total; -}; - -struct CurrentContextParams { - std::string uri; -}; - -struct CurrentContextResult { - std::optional context; -}; - -struct SwitchContextParams { - std::string uri; - std::string context_uri; -}; - -struct SwitchContextResult { - bool success; -}; - -} // namespace clice::ext - namespace kota::ipc::protocol { template <> diff --git a/src/server/compiler/compiler.cpp b/src/server/compiler/compiler.cpp index a6d485e15..c4853f666 100644 --- a/src/server/compiler/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -6,7 +6,7 @@ #include "command/search_config.h" #include "index/tu_index.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "support/filesystem.h" #include "support/logging.h" #include "syntax/include_resolver.h" @@ -28,11 +28,10 @@ using serde_raw = kota::codec::RawValue; /// Detect whether the cursor is inside a preamble directive (include/import). Compiler::Compiler(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, Workspace& workspace, WorkerPool& pool, llvm::DenseMap& sessions) : - loop(loop), peer(peer), workspace(workspace), pool(pool), sessions(sessions) {} + loop(loop), workspace(workspace), pool(pool), sessions(sessions) {} Compiler::~Compiler() { workspace.cancel_all(); @@ -410,6 +409,8 @@ std::string uri_to_path(const std::string& uri) { void Compiler::publish_diagnostics(const std::string& uri, int version, const kota::codec::RawValue& diagnostics_json) { + if(!peer) + return; std::vector diagnostics; if(!diagnostics_json.empty()) { auto status = kota::codec::json::from_json(diagnostics_json.data, diagnostics); @@ -421,14 +422,16 @@ void Compiler::publish_diagnostics(const std::string& uri, params.uri = uri; params.version = version; params.diagnostics = std::move(diagnostics); - peer.send_notification(params); + peer->send_notification(params); } void Compiler::clear_diagnostics(const std::string& uri) { + if(!peer) + return; protocol::PublishDiagnosticsParams params; params.uri = uri; params.diagnostics = {}; - peer.send_notification(params); + peer->send_notification(params); } kota::task Compiler::ensure_pch(Session& session, diff --git a/src/server/compiler/compiler.h b/src/server/compiler/compiler.h index dcd645dac..7083b36ff 100644 --- a/src/server/compiler/compiler.h +++ b/src/server/compiler/compiler.h @@ -8,7 +8,7 @@ #include #include "command/command.h" -#include "server/lsp/session.h" +#include "server/service/session.h" #include "server/worker/worker_pool.h" #include "server/workspace/workspace.h" #include "syntax/completion.h" @@ -50,10 +50,14 @@ std::string uri_to_path(const std::string& uri); class Compiler { public: Compiler(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, Workspace& workspace, WorkerPool& pool, llvm::DenseMap& sessions); + + void set_peer(kota::ipc::JsonPeer* p) { + peer = p; + } + ~Compiler(); void init_compile_graph(); @@ -125,7 +129,7 @@ class Compiler { private: kota::event_loop& loop; - kota::ipc::JsonPeer& peer; + kota::ipc::JsonPeer* peer = nullptr; Workspace& workspace; WorkerPool& pool; llvm::DenseMap& sessions; diff --git a/src/server/compiler/indexer.cpp b/src/server/compiler/indexer.cpp index e1ddacb5c..c9937c828 100644 --- a/src/server/compiler/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -6,9 +6,9 @@ #include #include "index/tu_index.h" +#include "protocol/worker.h" #include "server/compiler/compiler.h" -#include "server/compiler/protocol.h" -#include "server/lsp/session.h" +#include "server/service/session.h" #include "server/worker/worker_pool.h" #include "support/filesystem.h" #include "support/logging.h" diff --git a/src/server/lsp/master_server.h b/src/server/lsp/master_server.h deleted file mode 100644 index 5f9fc7fc9..000000000 --- a/src/server/lsp/master_server.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "server/compiler/compiler.h" -#include "server/compiler/indexer.h" -#include "server/lsp/session.h" -#include "server/worker/worker_pool.h" -#include "server/workspace/workspace.h" - -#include "kota/async/async.h" -#include "kota/codec/json/json.h" -#include "kota/ipc/peer.h" -#include "kota/ipc/transport.h" -#include "llvm/ADT/DenseMap.h" - -namespace clice { - -enum class ServerLifecycle : std::uint8_t { - Uninitialized, - Initialized, - Ready, - ShuttingDown, - Exited, -}; - -/// Top-level LSP server — the single orchestration point for the language -/// server process. -/// -/// Responsibilities: -/// - Owns the two-layer state model: Workspace (disk truth) and Sessions -/// (per-open-file volatile state). -/// - Manages Session lifecycle directly: didOpen creates, didChange mutates, -/// didSave syncs to Workspace, didClose destroys. -/// - Dispatches compilation and feature queries to Compiler. -/// - Dispatches index lookups and background indexing to Indexer. -/// -/// Design principle: -/// Open files are never depended upon by other files. Dependencies always -/// point to disk files. The only path from Session to Workspace is didSave. -class MasterServer { -public: - MasterServer(kota::event_loop& loop, kota::ipc::JsonPeer& peer, std::string self_path); - ~MasterServer(); - - void register_handlers(); - - /// Start accepting agent connections on the given host:port. - /// Each agent gets `agentic/*` handlers registered. - /// Agent disconnections are handled gracefully without affecting the server. - kota::task<> listen_for_agents(std::string host, int port); - -private: - void register_agent_handlers(kota::ipc::JsonPeer& agent_peer); - - struct AgentConnection { - std::unique_ptr transport; - std::unique_ptr peer; - }; - - std::list agent_connections; - kota::event_loop& loop; - kota::ipc::JsonPeer& peer; - - /// Persistent project-wide state (config, CDB, path pool, dependency - /// graphs, compilation caches, symbol index). - Workspace workspace; - - /// Per-file editing sessions, keyed by server-level path_id. - llvm::DenseMap sessions; - - /// Worker process pool for offloading compilation and queries. - WorkerPool pool; - - /// Compilation lifecycle manager (reads/writes workspace and sessions). - Compiler compiler; - - /// Index query and background scheduling (reads from workspace and sessions). - Indexer indexer; - - ServerLifecycle lifecycle = ServerLifecycle::Uninitialized; - std::string self_path; - std::string workspace_root; - std::string session_log_dir; - std::string init_options_json; ///< Raw JSON from initializationOptions, consumed once. - - void load_workspace(); - - using RawResult = kota::task; -}; - -} // namespace clice diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp new file mode 100644 index 000000000..d83a9f926 --- /dev/null +++ b/src/server/service/agent_client.cpp @@ -0,0 +1,37 @@ +#include "server/service/agent_client.h" + +#include +#include +#include + +#include "protocol/agentic.h" +#include "server/service/master_server.h" + +namespace clice { + +using kota::ipc::RequestResult; +using RequestContext = kota::ipc::JsonPeer::RequestContext; + +AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) : + server(server), peer(peer) { + using namespace agentic; + + peer.on_request( + [this](RequestContext&, + const CompileCommandParams& params) -> RequestResult { + std::string directory; + std::vector arguments; + if(!this->server.compiler.fill_compile_args(params.path, directory, arguments)) { + co_return kota::outcome_error( + kota::ipc::Error{std::format("no compile command found for {}", params.path)}); + } + + co_return CompileCommandResult{ + .file = params.path, + .directory = std::move(directory), + .arguments = std::move(arguments), + }; + }); +} + +} // namespace clice diff --git a/src/server/service/agent_client.h b/src/server/service/agent_client.h new file mode 100644 index 000000000..a3232b90d --- /dev/null +++ b/src/server/service/agent_client.h @@ -0,0 +1,18 @@ +#pragma once + +#include "kota/ipc/codec/json.h" + +namespace clice { + +class MasterServer; + +class AgentClient { +public: + AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer); + +private: + MasterServer& server; + kota::ipc::JsonPeer& peer; +}; + +} // namespace clice diff --git a/src/server/lsp/master_server.cpp b/src/server/service/lsp_client.cpp similarity index 56% rename from src/server/lsp/master_server.cpp rename to src/server/service/lsp_client.cpp index 59545d66b..12e12eeaa 100644 --- a/src/server/lsp/master_server.cpp +++ b/src/server/service/lsp_client.cpp @@ -1,4 +1,4 @@ -#include "server/lsp/master_server.h" +#include "server/service/lsp_client.h" #include #include @@ -6,9 +6,10 @@ #include #include +#include "protocol/extension.h" +#include "protocol/worker.h" #include "semantic/symbol_kind.h" -#include "server/agentic_protocol.h" -#include "server/compiler/protocol.h" +#include "server/service/master_server.h" #include "support/filesystem.h" #include "support/logging.h" @@ -17,7 +18,6 @@ #include "kota/ipc/lsp/protocol.h" #include "kota/ipc/lsp/uri.h" #include "kota/meta/enum.h" -#include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/Process.h" @@ -30,177 +30,38 @@ using kota::ipc::RequestResult; using RequestContext = kota::ipc::JsonPeer::RequestContext; using serde_raw = kota::codec::RawValue; -/// Serialize a value to a JSON RawValue using LSP config. template static serde_raw to_raw(const T& value) { auto json = kota::codec::json::to_json(value); return serde_raw{json ? std::move(*json) : "null"}; } -MasterServer::MasterServer(kota::event_loop& loop, - kota::ipc::JsonPeer& peer, - std::string self_path) : - loop(loop), peer(peer), pool(loop), compiler(loop, peer, workspace, pool, sessions), - indexer(loop, - workspace, - sessions, - pool, - compiler, - [this](uint32_t proj_path_id) { - // Bridge project-level path_id to server-level path_id. - // The two PathPools may assign different IDs to the same path. - auto path = workspace.project_index.path_pool.path(proj_path_id); - auto server_id = workspace.path_pool.intern(path); - return sessions.contains(server_id); - }), - self_path(std::move(self_path)) {} - -MasterServer::~MasterServer() = default; - -void MasterServer::load_workspace() { - if(workspace_root.empty()) - return; - - auto& cfg = workspace.config.project; - - if(!cfg.cache_dir.empty()) { - auto ec = llvm::sys::fs::create_directories(cfg.cache_dir); - if(ec) { - LOG_WARN("Failed to create cache directory {}: {}", - std::string_view(cfg.cache_dir), - ec.message()); - } else { - LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir)); - } - - for(auto* subdir: {"cache/pch", "cache/pcm"}) { - auto dir = path::join(cfg.cache_dir, subdir); - if(auto ec2 = llvm::sys::fs::create_directories(dir)) - LOG_WARN("Failed to create {}: {}", dir, ec2.message()); - } - - workspace.cleanup_cache(); - workspace.load_cache(); - } - - // Discover compile_commands.json: configured paths first, then auto-scan. - std::string cdb_path; - for(auto& configured: cfg.compile_commands_paths) { - // Each entry can be a file or a directory containing compile_commands.json. - if(llvm::sys::fs::is_directory(configured)) { - auto candidate = path::join(configured, "compile_commands.json"); - if(llvm::sys::fs::exists(candidate)) { - cdb_path = std::move(candidate); - break; - } - } else if(llvm::sys::fs::exists(configured)) { - cdb_path = configured; - break; - } else { - LOG_WARN("Configured compile_commands_path not found: {}", configured); - } - } - - // Auto-scan: workspace root + all immediate subdirectories. - if(cdb_path.empty()) { - auto try_candidate = [&](llvm::StringRef dir) -> bool { - auto candidate = path::join(dir, "compile_commands.json"); - if(llvm::sys::fs::exists(candidate)) { - cdb_path = std::move(candidate); - return true; - } - return false; - }; - - if(!try_candidate(workspace_root)) { - std::error_code ec; - for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec; - it.increment(ec)) { - if(it->type() == llvm::sys::fs::file_type::directory_file) { - if(try_candidate(it->path())) - break; - } - } - } - } - - if(cdb_path.empty()) { - LOG_WARN("No compile_commands.json found in workspace {}", workspace_root); - return; - } - - auto count = workspace.cdb.load(cdb_path); - LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count); - - auto report = scan_dependency_graph(workspace.cdb, - workspace.path_pool, - workspace.dep_graph, - /*cache=*/nullptr, - [this](llvm::StringRef path, - std::vector& append, - std::vector& remove) { - workspace.config.match_rules(path, append, remove); - }); - workspace.dep_graph.build_reverse_map(); - - auto unresolved = report.includes_found - report.includes_resolved; - double accuracy = - report.includes_found > 0 - ? 100.0 * static_cast(report.includes_resolved) / report.includes_found - : 100.0; - LOG_INFO( - "Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves", - report.elapsed_ms, - report.total_files, - report.source_files, - report.header_files, - report.total_edges, - report.includes_resolved, - report.includes_found, - accuracy, - report.waves); - if(unresolved > 0) - LOG_WARN("{} unresolved includes", unresolved); - - workspace.build_module_map(); - indexer.load(cfg.index_dir); - - if(*cfg.enable_indexing) { - for(auto& entry: workspace.cdb.get_entries()) { - auto file = workspace.cdb.resolve_path(entry.file); - auto server_id = workspace.path_pool.intern(file); - indexer.enqueue(server_id); - } - indexer.schedule(); - } +LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) { + server.compiler.set_peer(&peer); + server.indexer.set_peer(&peer); - compiler.init_compile_graph(); -} - -void MasterServer::register_handlers() { using StringVec = std::vector; peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params) -> RequestResult { - if(lifecycle != ServerLifecycle::Uninitialized) { + if(this->server.lifecycle != ServerLifecycle::Uninitialized) { co_return kota::outcome_error(protocol::Error{"Server already initialized"}); } auto& init = params.lsp__initialize_params; if(init.root_uri.has_value()) { - workspace_root = uri_to_path(*init.root_uri); + this->server.workspace_root = uri_to_path(*init.root_uri); } - // Capture initializationOptions as raw JSON for config loading. if(init.initialization_options.has_value()) { auto json = kota::codec::json::to_json(*init.initialization_options); if(json) - init_options_json = std::move(*json); + this->server.init_options_json = std::move(*json); } - lifecycle = ServerLifecycle::Initialized; - LOG_INFO("Initialized with workspace: {}", workspace_root); + this->server.lifecycle = ServerLifecycle::Initialized; + LOG_INFO("Initialized with workspace: {}", this->server.workspace_root); protocol::InitializeResult result; auto& caps = result.capabilities; @@ -223,7 +84,6 @@ void MasterServer::register_handlers() { caps.signature_help_provider = protocol::SignatureHelpOptions{ .trigger_characters = StringVec{"(", ")", "{", "}", "<", ">", ","}, }; - /// FIXME: In the future, we would support work done progress. caps.declaration_provider = protocol::DeclarationOptions{ .work_done_progress = false, }; @@ -279,27 +139,21 @@ void MasterServer::register_handlers() { }); peer.on_notification([this](const protocol::InitializedParams& params) { - // Config priority: initializationOptions > clice.toml > defaults. - // Load the workspace config (with defaults applied) first, then overlay - // any initializationOptions on top so fields not mentioned in the JSON - // keep the values from clice.toml — kotatsu's deserializer only touches - // fields that are present in the input. - workspace.config = Config::load_from_workspace(workspace_root); - if(!init_options_json.empty()) { - if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) { + auto& srv = this->server; + + srv.workspace.config = Config::load_from_workspace(srv.workspace_root); + if(!srv.init_options_json.empty()) { + if(auto ov = kota::codec::json::parse(srv.init_options_json, srv.workspace.config); + !ov) { LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string()); } else { - // Re-run apply_defaults so overridden strings get workspace - // substitution and `compiled_rules` is rebuilt if `rules` - // changed. Defaults are gated on zero/empty sentinels, so - // existing values from the overlay are preserved. - workspace.config.apply_defaults(workspace_root); + srv.workspace.config.apply_defaults(srv.workspace_root); LOG_INFO("Applied initializationOptions overlay"); } - init_options_json.clear(); + srv.init_options_json.clear(); } - auto& cfg = workspace.config.project; + auto& cfg = srv.workspace.config.project; if(!cfg.logging_dir.empty()) { auto now = std::chrono::system_clock::now(); @@ -307,7 +161,7 @@ void MasterServer::register_handlers() { auto session_dir = path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid)); logging::file_logger("master", session_dir, logging::options); - session_log_dir = session_dir; + srv.session_log_dir = session_dir; } LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)", @@ -316,62 +170,61 @@ void MasterServer::register_handlers() { *cfg.idle_timeout_ms); WorkerPoolOptions pool_opts; - pool_opts.self_path = self_path; + pool_opts.self_path = srv.self_path; pool_opts.stateful_count = cfg.stateful_worker_count; pool_opts.stateless_count = cfg.stateless_worker_count; pool_opts.worker_memory_limit = cfg.worker_memory_limit; - pool_opts.log_dir = session_log_dir; - if(!pool.start(pool_opts)) { + pool_opts.log_dir = srv.session_log_dir; + if(!srv.pool.start(pool_opts)) { LOG_ERROR("Failed to start worker pool"); return; } - lifecycle = ServerLifecycle::Ready; + srv.lifecycle = ServerLifecycle::Ready; - compiler.on_indexing_needed = [this]() { - indexer.schedule(); + srv.compiler.on_indexing_needed = [&srv]() { + srv.indexer.schedule(); }; - indexer.set_peer(&peer); - indexer.set_max_concurrency(cfg.stateless_worker_count.value); + srv.indexer.set_peer(&this->peer); + srv.indexer.set_max_concurrency(cfg.stateless_worker_count.value); - load_workspace(); + srv.load_workspace(); }); peer.on_request( [this](RequestContext& ctx, const protocol::ShutdownParams& params) -> RequestResult { - lifecycle = ServerLifecycle::ShuttingDown; + this->server.lifecycle = ServerLifecycle::ShuttingDown; LOG_INFO("Shutdown requested"); co_return nullptr; }); peer.on_notification([this](const protocol::ExitParams& params) { - lifecycle = ServerLifecycle::Exited; + auto& srv = this->server; + srv.lifecycle = ServerLifecycle::Exited; LOG_INFO("Exit notification received"); - indexer.save(workspace.config.project.index_dir); - workspace.save_cache(); + srv.indexer.save(srv.workspace.config.project.index_dir); + srv.workspace.save_cache(); - loop.schedule([this]() -> kota::task<> { - co_await pool.stop(); - loop.stop(); + srv.loop.schedule([&srv]() -> kota::task<> { + co_await srv.pool.stop(); + srv.loop.stop(); }()); }); - /// Document lifecycle — handled directly by MasterServer. - peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - auto [it, inserted] = sessions.try_emplace(path_id); + auto [it, inserted] = srv.sessions.try_emplace(path_id); auto& session = it->second; if(!inserted) { - // DenseMap tombstone may retain stale data — reset to a fresh Session. session = Session{}; } session.path_id = path_id; @@ -383,14 +236,15 @@ void MasterServer::register_handlers() { }); peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - auto it = sessions.find(path_id); - if(it == sessions.end()) + auto it = srv.sessions.find(path_id); + if(it == srv.sessions.end()) return; auto& session = it->second; @@ -427,142 +281,146 @@ void MasterServer::register_handlers() { worker::DocumentUpdateParams update; update.path = path; update.version = session.version; - pool.notify_stateful(path_id, update); + srv.pool.notify_stateful(path_id, update); }); peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - workspace.on_file_closed(path_id); - pool.notify_stateful(path_id, worker::EvictParams{path}); + srv.workspace.on_file_closed(path_id); + srv.pool.notify_stateful(path_id, worker::EvictParams{path}); - // Clear diagnostics for the closed file. protocol::PublishDiagnosticsParams diag_params; diag_params.uri = params.text_document.uri; - peer.send_notification(diag_params); + this->peer.send_notification(diag_params); - sessions.erase(path_id); + srv.sessions.erase(path_id); - indexer.enqueue(path_id); - indexer.schedule(); + srv.indexer.enqueue(path_id); + srv.indexer.schedule(); LOG_DEBUG("didClose: {}", path); }); peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) { - if(lifecycle != ServerLifecycle::Ready) + auto& srv = this->server; + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); - auto dirtied = workspace.on_file_saved(path_id); + auto dirtied = srv.workspace.on_file_saved(path_id); for(auto dirty_id: dirtied) { - if(auto sit = sessions.find(dirty_id); sit != sessions.end()) { + if(auto sit = srv.sessions.find(dirty_id); sit != srv.sessions.end()) { sit->second.ast_dirty = true; } else { - indexer.enqueue(dirty_id); + srv.indexer.enqueue(dirty_id); } } - // Invalidate header contexts for sessions whose host is this file. - for(auto& [hdr_id, session]: sessions) { + for(auto& [hdr_id, session]: srv.sessions) { if(session.header_context && session.header_context->host_path_id == path_id) { session.header_context.reset(); session.ast_dirty = true; } } - indexer.schedule(); + srv.indexer.schedule(); LOG_DEBUG("didSave: {}", path); }); - /// Feature requests — stateful forwarding. - peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::Hover, - sit->second, - params.text_document_position_params.position); - }); - - peer.on_request([this](RequestContext& ctx, - const protocol::SemanticTokensParams& params) -> RawResult { - auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::SemanticTokens, sit->second); + co_return co_await srv.compiler.forward_query( + worker::QueryKind::Hover, + sit->second, + params.text_document_position_params.position); }); peer.on_request( - [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { + [this](RequestContext& ctx, const protocol::SemanticTokensParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::InlayHints, - sit->second, - {}, - params.range); + co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, + sit->second); }); peer.on_request( - [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult { + [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::FoldingRange, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints, + sit->second, + {}, + params.range); }); peer.on_request([this](RequestContext& ctx, - const protocol::DocumentSymbolParams& params) -> RawResult { + const protocol::FoldingRangeParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::DocumentSymbol, sit->second); + co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, sit->second); }); + peer.on_request( + [this](RequestContext& ctx, const protocol::DocumentSymbolParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) + co_return serde_raw{"null"}; + co_return co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, + sit->second); + }); + peer.on_request([this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; auto& session = sit->second; - auto result = co_await compiler.forward_query(worker::QueryKind::DocumentLink, session); + auto result = co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, session); if(!result.has_value()) co_return serde_raw{"null"}; - // Merge document links from PCH if available. auto& links = result.value(); - // Re-lookup session after co_await since iterators may be invalidated. - auto sit2 = sessions.find(path_id); - if(sit2 != sessions.end() && sit2->second.pch_ref) { - auto pch_it = workspace.pch_cache.find(sit2->second.pch_ref->path_id); - if(pch_it != workspace.pch_cache.end() && !pch_it->second.document_links_json.empty()) { + auto sit2 = srv.sessions.find(path_id); + if(sit2 != srv.sessions.end() && sit2->second.pch_ref) { + auto pch_it = srv.workspace.pch_cache.find(sit2->second.pch_ref->path_id); + if(pch_it != srv.workspace.pch_cache.end() && + !pch_it->second.document_links_json.empty()) { auto& pch_json = pch_it->second.document_links_json; - // Merge two JSON arrays. if(!links.data.empty() && links.data != "null" && links.data.size() > 2) { - // "[a,b]" + "[c,d]" -> "[a,b,c,d]" - links.data.pop_back(); // remove trailing ']' + links.data.pop_back(); links.data += ','; - links.data.append(pch_json.begin() + 1, pch_json.end()); // skip '[' + links.data.append(pch_json.begin() + 1, pch_json.end()); } else { links.data = pch_json; } @@ -571,17 +429,17 @@ void MasterServer::register_handlers() { co_return std::move(links); }); - peer.on_request( - [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult { - auto path = uri_to_path(params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::CodeAction, sit->second); - }); + peer.on_request([this](RequestContext& ctx, + const protocol::CodeActionParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) + co_return serde_raw{"null"}; + co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, sit->second); + }); - /// Helper: resolve URI to path, path_id, and Session pointer. auto resolve_uri = [this](const std::string& uri) { struct Result { std::string path; @@ -589,22 +447,22 @@ void MasterServer::register_handlers() { Session* session; }; auto path = uri_to_path(uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - Session* session = (sit != sessions.end()) ? &sit->second : nullptr; + auto path_id = this->server.workspace.path_pool.intern(path); + auto sit = this->server.sessions.find(path_id); + Session* session = (sit != this->server.sessions.end()) ? &sit->second : nullptr; return Result{std::move(path), path_id, session}; }; auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) { auto [path, path_id, session] = resolve_uri(uri); - return indexer.lookup_symbol(uri, path, pos, session); + return this->server.indexer.lookup_symbol(uri, path, pos, session); }; auto query_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos, RelationKind kind) -> std::vector { auto [path, path_id, session] = resolve_uri(uri); - return indexer.query_relations(path, pos, kind, session); + return this->server.indexer.query_relations(path, pos, kind, session); }; auto resolve_item = @@ -613,11 +471,9 @@ void MasterServer::register_handlers() { const protocol::Range& range, const std::optional& data) -> std::optional { auto [path, path_id, session] = resolve_uri(uri); - return indexer.resolve_hierarchy_item(uri, path, range, data, session); + return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session); }; - /// Feature requests — index-based with AST fallback. - peer.on_request([this, query_at](RequestContext& ctx, const protocol::DefinitionParams& params) -> RawResult { auto& uri = params.text_document_position_params.text_document.uri; @@ -628,14 +484,15 @@ void MasterServer::register_handlers() { co_return to_raw(result); } + auto& srv = this->server; auto path = uri_to_path(uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - co_return co_await compiler.forward_query(worker::QueryKind::GoToDefinition, - sit->second, - pos); + co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition, + sit->second, + pos); }); peer.on_request([this, query_at](RequestContext& ctx, @@ -672,38 +529,37 @@ void MasterServer::register_handlers() { co_return serde_raw{"null"}; }); - /// Feature requests — stateless forwarding. + peer.on_request([this](RequestContext& ctx, + const protocol::CompletionParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document_position_params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) + co_return serde_raw{"null"}; + auto pause = srv.indexer.scoped_pause(); + auto result = + co_await srv.compiler.handle_completion(params.text_document_position_params.position, + sit->second); + co_return std::move(result); + }); peer.on_request( - [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult { + [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) + auto path_id = srv.workspace.path_pool.intern(path); + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) co_return serde_raw{"null"}; - auto pause = indexer.scoped_pause(); + auto pause = srv.indexer.scoped_pause(); auto result = - co_await compiler.handle_completion(params.text_document_position_params.position, + co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp, + params.text_document_position_params.position, sit->second); co_return std::move(result); }); - peer.on_request([this](RequestContext& ctx, - const protocol::SignatureHelpParams& params) -> RawResult { - auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = workspace.path_pool.intern(path); - auto sit = sessions.find(path_id); - if(sit == sessions.end()) - co_return serde_raw{"null"}; - auto pause = indexer.scoped_pause(); - auto result = co_await compiler.forward_build(worker::BuildKind::SignatureHelp, - params.text_document_position_params.position, - sit->second); - co_return std::move(result); - }); - - /// Hierarchy queries — index-based. - peer.on_request( [this, lookup_at](RequestContext& ctx, const protocol::CallHierarchyPrepareParams& params) -> RawResult { @@ -727,7 +583,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_incoming_calls(info->hash); + auto results = this->server.indexer.find_incoming_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -739,7 +595,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_outgoing_calls(info->hash); + auto results = this->server.indexer.find_outgoing_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -769,7 +625,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_supertypes(info->hash); + auto results = this->server.indexer.find_supertypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -781,7 +637,7 @@ void MasterServer::register_handlers() { auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = indexer.find_subtypes(info->hash); + auto results = this->server.indexer.find_subtypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -789,29 +645,28 @@ void MasterServer::register_handlers() { peer.on_request( [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult { - auto results = indexer.search_symbols(params.query); + auto results = this->server.indexer.search_symbols(params.query); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); }); - /// clice/ extension commands. - peer.on_request( "clice/queryContext", [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); int offset_val = std::max(0, params.offset.value_or(0)); constexpr int page_size = 10; ext::QueryContextResult result; std::vector all_items; - auto hosts = workspace.dep_graph.find_host_sources(path_id); + auto hosts = srv.workspace.dep_graph.find_host_sources(path_id); for(auto host_id: hosts) { - auto host_path = workspace.path_pool.resolve(host_id); - auto host_cdb = workspace.cdb.lookup(host_path, {.suppress_logging = true}); + auto host_path = srv.workspace.path_pool.resolve(host_id); + auto host_cdb = srv.workspace.cdb.lookup(host_path, {.suppress_logging = true}); if(host_cdb.empty()) continue; auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path)); @@ -825,7 +680,7 @@ void MasterServer::register_handlers() { } if(hosts.empty()) { - auto entries = workspace.cdb.lookup(path, {.suppress_logging = true}); + auto entries = srv.workspace.cdb.lookup(path, {.suppress_logging = true}); for(std::size_t i = 0; i < entries.size(); ++i) { auto& cmd = entries[i]; auto argv = cmd.to_argv(); @@ -867,13 +722,14 @@ void MasterServer::register_handlers() { peer.on_request( "clice/currentContext", [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); ext::CurrentContextResult result; - auto sit = sessions.find(path_id); - if(sit != sessions.end() && sit->second.active_context) { - auto ctx_path = workspace.path_pool.resolve(*sit->second.active_context); + auto sit = srv.sessions.find(path_id); + if(sit != srv.sessions.end() && sit->second.active_context) { + auto ctx_path = srv.workspace.path_pool.resolve(*sit->second.active_context); auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path)); if(ctx_uri_opt) { ext::ContextItem item; @@ -889,21 +745,22 @@ void MasterServer::register_handlers() { peer.on_request( "clice/switchContext", [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult { + auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = workspace.path_pool.intern(path); + auto path_id = srv.workspace.path_pool.intern(path); auto context_path = uri_to_path(params.context_uri); - auto context_path_id = workspace.path_pool.intern(context_path); + auto context_path_id = srv.workspace.path_pool.intern(context_path); ext::SwitchContextResult result; - auto context_cdb = workspace.cdb.lookup(context_path, {.suppress_logging = true}); + auto context_cdb = srv.workspace.cdb.lookup(context_path, {.suppress_logging = true}); if(context_cdb.empty()) { result.success = false; co_return to_raw(result); } - auto sit = sessions.find(path_id); - if(sit == sessions.end()) { + auto sit = srv.sessions.find(path_id); + if(sit == srv.sessions.end()) { result.success = false; co_return to_raw(result); } @@ -919,58 +776,4 @@ void MasterServer::register_handlers() { }); } -void MasterServer::register_agent_handlers(kota::ipc::JsonPeer& agent_peer) { - using namespace agentic; - - agent_peer.on_request( - [this](RequestContext&, - const CompileCommandParams& params) -> RequestResult { - std::string directory; - std::vector arguments; - if(!compiler.fill_compile_args(params.path, directory, arguments)) { - co_return kota::outcome_error( - kota::ipc::Error{std::format("no compile command found for {}", params.path)}); - } - - co_return CompileCommandResult{ - .file = params.path, - .directory = std::move(directory), - .arguments = std::move(arguments), - }; - }); -} - -kota::task<> MasterServer::listen_for_agents(std::string host, int port) { - auto acceptor = kota::tcp::listen(host, port, {}, loop); - if(!acceptor) { - LOG_ERROR("failed to listen for agents on {}:{}", host, port); - co_return; - } - - LOG_INFO("Agentic protocol listening on {}:{}", host, port); - - while(true) { - auto conn = co_await acceptor->accept(); - if(!conn.has_value()) { - break; - } - - LOG_INFO("Agent connected"); - - auto transport = std::make_unique(std::move(*conn)); - auto peer = std::make_unique(loop, std::move(transport)); - register_agent_handlers(*peer); - - auto* peer_ptr = peer.get(); - auto it = agent_connections.emplace(agent_connections.end(), - AgentConnection{nullptr, std::move(peer)}); - - loop.schedule([this, peer_ptr, it]() -> kota::task<> { - co_await peer_ptr->run(); - LOG_INFO("Agent disconnected"); - agent_connections.erase(it); - }()); - } -} - } // namespace clice diff --git a/src/server/service/lsp_client.h b/src/server/service/lsp_client.h new file mode 100644 index 000000000..a252770eb --- /dev/null +++ b/src/server/service/lsp_client.h @@ -0,0 +1,22 @@ +#pragma once + +#include "kota/async/async.h" +#include "kota/codec/json/json.h" +#include "kota/ipc/codec/json.h" + +namespace clice { + +class MasterServer; + +class LSPClient { +public: + LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer); + +private: + using RawResult = kota::task; + + MasterServer& server; + kota::ipc::JsonPeer& peer; +}; + +} // namespace clice diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp new file mode 100644 index 000000000..75d6b11ee --- /dev/null +++ b/src/server/service/master_server.cpp @@ -0,0 +1,249 @@ +#include "server/service/master_server.h" + +#include +#include +#include +#include + +#include "server/service/agent_client.h" +#include "server/service/lsp_client.h" +#include "support/filesystem.h" +#include "support/logging.h" + +#include "kota/async/async.h" +#include "kota/ipc/codec/json.h" +#include "kota/ipc/recording_transport.h" +#include "kota/ipc/transport.h" +#include "llvm/Support/FileSystem.h" + +namespace clice { + +MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) : + loop(loop), pool(loop), compiler(loop, workspace, pool, sessions), + indexer(loop, + workspace, + sessions, + pool, + compiler, + [this](uint32_t proj_path_id) { + auto path = workspace.project_index.path_pool.path(proj_path_id); + auto server_id = workspace.path_pool.intern(path); + return sessions.contains(server_id); + }), + self_path(std::move(self_path)) {} + +MasterServer::~MasterServer() = default; + +void MasterServer::load_workspace() { + if(workspace_root.empty()) + return; + + auto& cfg = workspace.config.project; + + if(!cfg.cache_dir.empty()) { + auto ec = llvm::sys::fs::create_directories(cfg.cache_dir); + if(ec) { + LOG_WARN("Failed to create cache directory {}: {}", + std::string_view(cfg.cache_dir), + ec.message()); + } else { + LOG_INFO("Cache directory: {}", std::string_view(cfg.cache_dir)); + } + + for(auto* subdir: {"cache/pch", "cache/pcm"}) { + auto dir = path::join(cfg.cache_dir, subdir); + if(auto ec2 = llvm::sys::fs::create_directories(dir)) + LOG_WARN("Failed to create {}: {}", dir, ec2.message()); + } + + workspace.cleanup_cache(); + workspace.load_cache(); + } + + std::string cdb_path; + for(auto& configured: cfg.compile_commands_paths) { + if(llvm::sys::fs::is_directory(configured)) { + auto candidate = path::join(configured, "compile_commands.json"); + if(llvm::sys::fs::exists(candidate)) { + cdb_path = std::move(candidate); + break; + } + } else if(llvm::sys::fs::exists(configured)) { + cdb_path = configured; + break; + } else { + LOG_WARN("Configured compile_commands_path not found: {}", configured); + } + } + + if(cdb_path.empty()) { + auto try_candidate = [&](llvm::StringRef dir) -> bool { + auto candidate = path::join(dir, "compile_commands.json"); + if(llvm::sys::fs::exists(candidate)) { + cdb_path = std::move(candidate); + return true; + } + return false; + }; + + if(!try_candidate(workspace_root)) { + std::error_code ec; + for(llvm::sys::fs::directory_iterator it(workspace_root, ec), end; it != end && !ec; + it.increment(ec)) { + if(it->type() == llvm::sys::fs::file_type::directory_file) { + if(try_candidate(it->path())) + break; + } + } + } + } + + if(cdb_path.empty()) { + LOG_WARN("No compile_commands.json found in workspace {}", workspace_root); + return; + } + + auto count = workspace.cdb.load(cdb_path); + LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count); + + auto report = scan_dependency_graph(workspace.cdb, + workspace.path_pool, + workspace.dep_graph, + /*cache=*/nullptr, + [this](llvm::StringRef path, + std::vector& append, + std::vector& remove) { + workspace.config.match_rules(path, append, remove); + }); + workspace.dep_graph.build_reverse_map(); + + auto unresolved = report.includes_found - report.includes_resolved; + double accuracy = + report.includes_found > 0 + ? 100.0 * static_cast(report.includes_resolved) / report.includes_found + : 100.0; + LOG_INFO( + "Dependency scan: {}ms, {} files ({} source + {} header), " "{} edges, {}/{} resolved ({:.1f}%), {} waves", + report.elapsed_ms, + report.total_files, + report.source_files, + report.header_files, + report.total_edges, + report.includes_resolved, + report.includes_found, + accuracy, + report.waves); + if(unresolved > 0) + LOG_WARN("{} unresolved includes", unresolved); + + workspace.build_module_map(); + indexer.load(cfg.index_dir); + + if(*cfg.enable_indexing) { + for(auto& entry: workspace.cdb.get_entries()) { + auto file = workspace.cdb.resolve_path(entry.file); + auto server_id = workspace.path_pool.intern(file); + indexer.enqueue(server_id); + } + indexer.schedule(); + } + + compiler.init_compile_graph(); +} + +struct Connection { + std::unique_ptr peer; + std::unique_ptr lsp_client; + std::unique_ptr agent_client; +}; + +static kota::task<> accept_connections(kota::event_loop& loop, + MasterServer& server, + kota::tcp::acceptor acceptor, + bool register_lsp, + std::list& connections) { + while(true) { + auto conn = co_await acceptor.accept(); + if(!conn.has_value()) + break; + + LOG_INFO("Client connected"); + + auto transport = std::make_unique(std::move(*conn)); + auto peer = std::make_unique(loop, std::move(transport)); + + std::unique_ptr lsp; + if(register_lsp) + lsp = std::make_unique(server, *peer); + auto agent = std::make_unique(server, *peer); + + auto* peer_ptr = peer.get(); + auto it = connections.emplace(connections.end(), + Connection{ + .peer = std::move(peer), + .lsp_client = std::move(lsp), + .agent_client = std::move(agent), + }); + + loop.schedule([peer_ptr, &connections, it]() -> kota::task<> { + co_await peer_ptr->run(); + LOG_INFO("Client disconnected"); + connections.erase(it); + }()); + } +} + +int run_server_mode(const ServerOptions& opts) { + logging::stderr_logger("master", logging::options); + + kota::event_loop loop; + MasterServer server(loop, opts.self_path); + std::list connections; + + if(opts.mode == "pipe") { + auto transport = kota::ipc::StreamTransport::open_stdio(loop); + if(!transport) { + LOG_ERROR("failed to open stdio transport"); + return 1; + } + + std::unique_ptr final_transport = std::move(*transport); + if(!opts.record.empty()) { + final_transport = + std::make_unique(std::move(final_transport), + opts.record); + } + + kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport)); + LSPClient lsp_client(server, lsp_peer); + + auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); + if(acceptor) { + LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port); + loop.schedule( + accept_connections(loop, server, std::move(*acceptor), false, connections)); + } + + loop.schedule(lsp_peer.run()); + loop.run(); + return 0; + } + + if(opts.mode == "socket") { + auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); + if(!acceptor) { + LOG_ERROR("failed to listen on {}:{}", opts.host, opts.port); + return 1; + } + + LOG_INFO("Listening on {}:{} ...", opts.host, opts.port); + loop.schedule(accept_connections(loop, server, std::move(*acceptor), true, connections)); + loop.run(); + return 0; + } + + LOG_ERROR("unknown server mode '{}'", opts.mode); + return 1; +} + +} // namespace clice diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h new file mode 100644 index 000000000..858d2dae7 --- /dev/null +++ b/src/server/service/master_server.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +#include "server/compiler/compiler.h" +#include "server/compiler/indexer.h" +#include "server/service/session.h" +#include "server/worker/worker_pool.h" +#include "server/workspace/workspace.h" + +#include "kota/async/async.h" +#include "llvm/ADT/DenseMap.h" + +namespace clice { + +enum class ServerLifecycle : std::uint8_t { + Uninitialized, + Initialized, + Ready, + ShuttingDown, + Exited, +}; + +/// Core server state — owns the two-layer state model (Workspace + Sessions), +/// the worker pool, compilation engine, and indexer. +/// +/// Does NOT own any transport or peer. Protocol-specific handler registration +/// is done by LSPClient and AgentClient, which access server state via friend. +class MasterServer { + friend class LSPClient; + friend class AgentClient; + +public: + MasterServer(kota::event_loop& loop, std::string self_path); + ~MasterServer(); + +private: + void load_workspace(); + + kota::event_loop& loop; + + Workspace workspace; + llvm::DenseMap sessions; + WorkerPool pool; + Compiler compiler; + Indexer indexer; + + ServerLifecycle lifecycle = ServerLifecycle::Uninitialized; + std::string self_path; + std::string workspace_root; + std::string session_log_dir; + std::string init_options_json; +}; + +struct ServerOptions { + std::string mode; + std::string host = "127.0.0.1"; + int port = 50051; + std::string self_path; + std::string record; +}; + +int run_server_mode(const ServerOptions& opts); + +} // namespace clice diff --git a/src/server/lsp/session.h b/src/server/service/session.h similarity index 100% rename from src/server/lsp/session.h rename to src/server/service/session.h diff --git a/src/server/worker/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp index d641fd2ce..96fa5386c 100644 --- a/src/server/worker/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -10,7 +10,7 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker/worker_common.h" #include "support/logging.h" diff --git a/src/server/worker/stateless_worker.cpp b/src/server/worker/stateless_worker.cpp index 2f81eb960..a0b9b25c1 100644 --- a/src/server/worker/stateless_worker.cpp +++ b/src/server/worker/stateless_worker.cpp @@ -3,7 +3,7 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker/worker_common.h" #include "support/logging.h" diff --git a/src/server/worker/worker_pool.h b/src/server/worker/worker_pool.h index 5bba9bfeb..5eaf3c28c 100644 --- a/src/server/worker/worker_pool.h +++ b/src/server/worker/worker_pool.h @@ -6,7 +6,7 @@ #include #include -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "kota/async/async.h" #include "kota/ipc/codec/bincode.h" diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index 2af58732b..51da31ffa 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker_test_helpers.h" namespace clice::testing { diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index d05935beb..e15c86cea 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker_test_helpers.h" #include "syntax/scan.h" diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index 1a51169de..a7e104632 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index 4f280af45..b78d515db 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/bincode/bincode.h" diff --git a/tests/unit/server/worker_test_helpers.h b/tests/unit/server/worker_test_helpers.h index dfffae31e..8e012ba82 100644 --- a/tests/unit/server/worker_test_helpers.h +++ b/tests/unit/server/worker_test_helpers.h @@ -11,7 +11,7 @@ #include "test/temp_dir.h" #include "command/argument_parser.h" #include "command/command.h" -#include "server/compiler/protocol.h" +#include "protocol/worker.h" #include "support/filesystem.h" #include "kota/async/async.h" From e736eb37d2a147c74ce7d7629cbd2a78f25c13ae Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 26 Apr 2026 20:10:59 +0800 Subject: [PATCH 03/12] test(agentic): add integration tests for agentic TCP protocol Add AgenticClient utility for JSON-RPC over TCP and three integration tests for agentic/compileCommand: known file, unknown file fallback, and multiple sequential requests. Update conftest to pass --port with a dynamic free port so the agentic listener starts in pipe mode. Co-Authored-By: Claude Opus 4.6 --- tests/conftest.py | 33 +++++++++-- tests/integration/agentic/__init__.py | 0 tests/integration/agentic/test_agentic.py | 31 +++++++++++ tests/integration/utils/agentic_client.py | 67 +++++++++++++++++++++++ 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 tests/integration/agentic/__init__.py create mode 100644 tests/integration/agentic/test_agentic.py create mode 100644 tests/integration/utils/agentic_client.py diff --git a/tests/conftest.py b/tests/conftest.py index ada56698a..06b18e1cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,14 @@ import asyncio import json import shutil +import socket import subprocess import sys from pathlib import Path import pytest +from tests.integration.utils.agentic_client import AgenticClient from tests.integration.utils.client import CliceClient @@ -91,19 +93,30 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non return path +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +@pytest.fixture +def agentic_port() -> int: + return _find_free_port() + + @pytest.fixture async def client( - request: pytest.FixtureRequest, executable: Path, workspace: Path | None + request: pytest.FixtureRequest, + executable: Path, + workspace: Path | None, + agentic_port: int, ): """Spawn clice server, auto-initialize if @pytest.mark.workspace is present.""" config = request.config mode = config.getoption("--mode") + host = config.getoption("--host") - cmd = [str(executable), "--mode", mode] - if mode == "socket": - host = config.getoption("--host") - port = config.getoption("--port") - cmd += ["--host", host, "--port", str(port)] + cmd = [str(executable), "--mode", mode, "--host", host, "--port", str(agentic_port)] c = CliceClient() await c.start_io(*cmd) @@ -122,6 +135,14 @@ async def client( await _shutdown_client(c) +@pytest.fixture +async def agentic(agentic_port: int, client) -> AgenticClient: + """Connect to the agentic TCP endpoint of a running server.""" + ac = await AgenticClient.connect("127.0.0.1", agentic_port) + yield ac + await ac.close() + + def generate_cdb(workspace: Path) -> None: """Generate compile_commands.json using CMake with Ninja backend.""" cmake = shutil.which("cmake") diff --git a/tests/integration/agentic/__init__.py b/tests/integration/agentic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py new file mode 100644 index 000000000..84191cc9c --- /dev/null +++ b/tests/integration/agentic/test_agentic.py @@ -0,0 +1,31 @@ +"""Tests for the agentic TCP protocol.""" + +import pytest + + +@pytest.mark.workspace("hello_world") +async def test_compile_command(agentic, workspace): + main_cpp = str(workspace / "main.cpp") + resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) + result = resp["result"] + assert result["file"] == main_cpp + assert result["directory"] == str(workspace) + assert len(result["arguments"]) > 0 + + +@pytest.mark.workspace("hello_world") +async def test_compile_command_fallback(agentic, workspace): + resp = await agentic.request( + "agentic/compileCommand", {"path": "/nonexistent/file.cpp"} + ) + result = resp["result"] + assert result["file"] == "/nonexistent/file.cpp" + + +@pytest.mark.workspace("hello_world") +async def test_multiple_requests(agentic, workspace): + main_cpp = str(workspace / "main.cpp") + for _ in range(3): + resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) + assert "result" in resp + assert resp["result"]["file"] == main_cpp diff --git a/tests/integration/utils/agentic_client.py b/tests/integration/utils/agentic_client.py new file mode 100644 index 000000000..fb8c316c7 --- /dev/null +++ b/tests/integration/utils/agentic_client.py @@ -0,0 +1,67 @@ +"""Lightweight async JSON-RPC client for the agentic TCP protocol.""" + +import asyncio +import json +import re + + +class AgenticClient: + """Connects to the clice agentic TCP endpoint and sends JSON-RPC requests.""" + + def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + self._reader = reader + self._writer = writer + self._next_id = 1 + + @classmethod + async def connect(cls, host: str, port: int, *, timeout: float = 10.0): + deadline = asyncio.get_event_loop().time() + timeout + while True: + try: + reader, writer = await asyncio.open_connection(host, port) + return cls(reader, writer) + except (ConnectionRefusedError, OSError): + if asyncio.get_event_loop().time() >= deadline: + raise + await asyncio.sleep(0.1) + + async def request( + self, method: str, params: dict, *, timeout: float = 30.0 + ) -> dict: + msg_id = self._next_id + self._next_id += 1 + payload = json.dumps( + { + "jsonrpc": "2.0", + "id": msg_id, + "method": method, + "params": params, + } + ) + encoded = payload.encode("utf-8") + self._writer.write( + f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii") + encoded + ) + await self._writer.drain() + response = await asyncio.wait_for(self._read_message(), timeout=timeout) + assert response is not None, "connection closed before response" + assert response.get("id") == msg_id + return response + + async def _read_message(self) -> dict | None: + header = b"" + while True: + line = await self._reader.readline() + if not line: + return None + header += line + if header.endswith(b"\r\n\r\n"): + break + match = re.search(rb"Content-Length:\s*(\d+)", header) + if not match: + return None + return json.loads(await self._reader.readexactly(int(match.group(1)))) + + async def close(self): + self._writer.close() + await self._writer.wait_closed() From c83ba7e2dbfbeac4fa2b43c8b2cc0e416f1af89a Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 26 Apr 2026 21:51:13 +0800 Subject: [PATCH 04/12] fix(server): use capture-less lambda coroutines to prevent use-after-free Temporary lambdas used as coroutines have their captures accessed through a dangling this pointer after the lambda is destroyed. Pass values as function parameters instead (copied into the coroutine frame by the standard). Also make the agentic TCP listener opt-in (--port > 0) to avoid acceptor leaks in smoke tests, add LSPClient destructor to reset peer pointers, and default-initialize extension protocol scalars. Co-Authored-By: Claude Opus 4.6 --- src/clice.cc | 8 +++++--- src/protocol/extension.h | 4 ++-- src/server/service/lsp_client.cpp | 22 ++++++++++++--------- src/server/service/lsp_client.h | 1 + src/server/service/master_server.cpp | 24 ++++++++++++++--------- src/server/service/master_server.h | 2 +- tests/conftest.py | 3 ++- tests/integration/utils/agentic_client.py | 7 +++++-- 8 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/clice.cc b/src/clice.cc index cd48ab8d3..21cfd3e3f 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -24,8 +24,10 @@ struct Options { DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode address", required = false) host = "127.0.0.1"; - DecoKV(style = KVStyle::JoinedOrSeparate, help = "Socket mode port", required = false) - port = 50051; + DecoKV(style = KVStyle::JoinedOrSeparate, + help = "Agentic TCP port (0 = disabled)", + required = false) + port = 0; DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--log-level", "--log-level="}, @@ -125,7 +127,7 @@ int main(int argc, const char** argv) { clice::ServerOptions server_opts; server_opts.mode = mode; server_opts.host = opts.host.value_or("127.0.0.1"); - server_opts.port = opts.port.value_or(50051); + server_opts.port = opts.port.value_or(0); server_opts.self_path = argv[0]; server_opts.record = opts.record.value_or(""); return clice::run_server_mode(server_opts); diff --git a/src/protocol/extension.h b/src/protocol/extension.h index 8ded8bea5..7ed2cbbd4 100644 --- a/src/protocol/extension.h +++ b/src/protocol/extension.h @@ -19,7 +19,7 @@ struct QueryContextParams { struct QueryContextResult { std::vector contexts; - int total; + int total = 0; }; struct CurrentContextParams { @@ -36,7 +36,7 @@ struct SwitchContextParams { }; struct SwitchContextResult { - bool success; + bool success = false; }; } // namespace clice::ext diff --git a/src/server/service/lsp_client.cpp b/src/server/service/lsp_client.cpp index 12e12eeaa..027c10ae7 100644 --- a/src/server/service/lsp_client.cpp +++ b/src/server/service/lsp_client.cpp @@ -200,18 +200,17 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s co_return nullptr; }); - peer.on_notification([this](const protocol::ExitParams& params) { - auto& srv = this->server; - srv.lifecycle = ServerLifecycle::Exited; + peer.on_notification([srv = &this->server](const protocol::ExitParams& params) { + srv->lifecycle = ServerLifecycle::Exited; LOG_INFO("Exit notification received"); - srv.indexer.save(srv.workspace.config.project.index_dir); - srv.workspace.save_cache(); + srv->indexer.save(srv->workspace.config.project.index_dir); + srv->workspace.save_cache(); - srv.loop.schedule([&srv]() -> kota::task<> { - co_await srv.pool.stop(); - srv.loop.stop(); - }()); + srv->loop.schedule([](MasterServer* s) -> kota::task<> { + co_await s->pool.stop(); + s->loop.stop(); + }(srv)); }); peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { @@ -776,4 +775,9 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s }); } +LSPClient::~LSPClient() { + server.compiler.set_peer(nullptr); + server.indexer.set_peer(nullptr); +} + } // namespace clice diff --git a/src/server/service/lsp_client.h b/src/server/service/lsp_client.h index a252770eb..9e8a449ef 100644 --- a/src/server/service/lsp_client.h +++ b/src/server/service/lsp_client.h @@ -11,6 +11,7 @@ class MasterServer; class LSPClient { public: LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer); + ~LSPClient(); private: using RawResult = kota::task; diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp index 75d6b11ee..2e391a461 100644 --- a/src/server/service/master_server.cpp +++ b/src/server/service/master_server.cpp @@ -185,11 +185,13 @@ static kota::task<> accept_connections(kota::event_loop& loop, .agent_client = std::move(agent), }); - loop.schedule([peer_ptr, &connections, it]() -> kota::task<> { - co_await peer_ptr->run(); + loop.schedule([](kota::ipc::JsonPeer* p, + std::list& conns, + std::list::iterator pos) -> kota::task<> { + co_await p->run(); LOG_INFO("Client disconnected"); - connections.erase(it); - }()); + conns.erase(pos); + }(peer_ptr, connections, it)); } } @@ -217,11 +219,15 @@ int run_server_mode(const ServerOptions& opts) { kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport)); LSPClient lsp_client(server, lsp_peer); - auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); - if(acceptor) { - LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port); - loop.schedule( - accept_connections(loop, server, std::move(*acceptor), false, connections)); + if(opts.port > 0) { + auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); + if(acceptor) { + LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port); + loop.schedule( + accept_connections(loop, server, std::move(*acceptor), false, connections)); + } else { + LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port); + } } loop.schedule(lsp_peer.run()); diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h index 858d2dae7..fef52966a 100644 --- a/src/server/service/master_server.h +++ b/src/server/service/master_server.h @@ -56,7 +56,7 @@ class MasterServer { struct ServerOptions { std::string mode; std::string host = "127.0.0.1"; - int port = 50051; + int port = 0; std::string self_path; std::string record; }; diff --git a/tests/conftest.py b/tests/conftest.py index 06b18e1cd..0ef7eed6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,8 +167,9 @@ def generate_cdb(workspace: Path) -> None: async def make_client(executable: Path, workspace: Path) -> CliceClient: """Spawn a fresh clice server and initialize it. For multi-session tests.""" + port = _find_free_port() c = CliceClient() - await c.start_io(str(executable), "--mode", "pipe") + await c.start_io(str(executable), "--mode", "pipe", "--port", str(port)) await c.initialize(workspace) return c diff --git a/tests/integration/utils/agentic_client.py b/tests/integration/utils/agentic_client.py index fb8c316c7..0d49f59f2 100644 --- a/tests/integration/utils/agentic_client.py +++ b/tests/integration/utils/agentic_client.py @@ -63,5 +63,8 @@ async def _read_message(self) -> dict | None: return json.loads(await self._reader.readexactly(int(match.group(1)))) async def close(self): - self._writer.close() - await self._writer.wait_closed() + try: + self._writer.close() + await self._writer.wait_closed() + except (ConnectionError, OSError): + pass From 7f9c743845afd06302f2077def2f85359bfc3bd0 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 26 Apr 2026 22:36:22 +0800 Subject: [PATCH 05/12] fix(tests): use posix paths in agentic tests for Windows compatibility Co-Authored-By: Claude Opus 4.6 --- tests/integration/agentic/test_agentic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py index 84191cc9c..cbca5f884 100644 --- a/tests/integration/agentic/test_agentic.py +++ b/tests/integration/agentic/test_agentic.py @@ -5,11 +5,11 @@ @pytest.mark.workspace("hello_world") async def test_compile_command(agentic, workspace): - main_cpp = str(workspace / "main.cpp") + main_cpp = (workspace / "main.cpp").as_posix() resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) result = resp["result"] assert result["file"] == main_cpp - assert result["directory"] == str(workspace) + assert result["directory"] == workspace.as_posix() assert len(result["arguments"]) > 0 @@ -24,7 +24,7 @@ async def test_compile_command_fallback(agentic, workspace): @pytest.mark.workspace("hello_world") async def test_multiple_requests(agentic, workspace): - main_cpp = str(workspace / "main.cpp") + main_cpp = (workspace / "main.cpp").as_posix() for _ in range(3): resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) assert "result" in resp From 06070a24ec7346bad951540a25ea1047883b83ee Mon Sep 17 00:00:00 2001 From: ykiko Date: Mon, 27 Apr 2026 01:09:26 +0800 Subject: [PATCH 06/12] refactor(server): make agentic mode a TCP client and split into own module - Rewrite `--mode agentic` as a one-shot TCP client that connects to a running server's agentic port, sends a compileCommand request, prints the JSON result to stdout, and exits. - Extract agentic client code into src/server/service/agentic.cpp/h. - Replace inline coroutine lambdas with named functions and when_all. - Rewrite integration tests to use subprocess.run instead of a custom JSON-RPC client; delete agentic_client.py. Co-Authored-By: Claude Opus 4.6 --- src/clice.cc | 20 ++++++- src/server/service/agentic.cpp | 55 ++++++++++++++++++ src/server/service/agentic.h | 11 ++++ src/server/service/master_server.cpp | 24 +++++--- src/server/service/master_server.h | 2 + tests/conftest.py | 53 ++++++++++------- tests/integration/agentic/test_agentic.py | 55 +++++++++++++----- tests/integration/utils/agentic_client.py | 70 ----------------------- 8 files changed, 176 insertions(+), 114 deletions(-) create mode 100644 src/server/service/agentic.cpp create mode 100644 src/server/service/agentic.h delete mode 100644 tests/integration/utils/agentic_client.py diff --git a/src/clice.cc b/src/clice.cc index 21cfd3e3f..0f0bfcb77 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -4,6 +4,7 @@ #include #include +#include "server/service/agentic.h" #include "server/service/master_server.h" #include "server/worker/stateful_worker.h" #include "server/worker/stateless_worker.h" @@ -40,6 +41,11 @@ struct Options { required = false) record; + DecoKV(style = KVStyle::JoinedOrSeparate, + help = "File path for agentic queries", + required = false) + path; + // Internal options (passed from master to worker processes) DecoKV(style = KVStyle::JoinedOrSeparate, names = {"--worker-memory-limit", "--worker-memory-limit="}, @@ -134,8 +140,18 @@ int main(int argc, const char** argv) { } if(mode == "agentic") { - LOG_ERROR("agentic client mode is not yet implemented"); - return 1; + auto host = opts.host.value_or("127.0.0.1"); + auto port = opts.port.value_or(0); + auto path = opts.path.value_or(""); + if(port <= 0) { + LOG_ERROR("--port is required for agentic mode"); + return 1; + } + if(path.empty()) { + LOG_ERROR("--path is required for agentic mode"); + return 1; + } + return clice::run_agentic_mode(host, port, path); } LOG_ERROR("unknown mode '{}'", mode); diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp new file mode 100644 index 000000000..0ec79df49 --- /dev/null +++ b/src/server/service/agentic.cpp @@ -0,0 +1,55 @@ +#include "server/service/agentic.h" + +#include +#include + +#include "protocol/agentic.h" +#include "support/logging.h" + +#include "kota/async/async.h" +#include "kota/ipc/codec/json.h" +#include "kota/ipc/transport.h" + +namespace clice { + +static kota::task<> agentic_request(kota::ipc::JsonPeer& peer, int& exit_code, std::string path) { + auto result = + co_await peer.send_request(agentic::CompileCommandParams{.path = std::move(path)}); + + if(!result) { + LOG_ERROR("request failed: {}", result.error().message); + } else { + auto json = kota::codec::json::to_string(*result); + std::println("{}", json ? *json : "null"); + exit_code = 0; + } + + peer.close(); +} + +static kota::task<> agentic_client(kota::event_loop& loop, + int& exit_code, + std::string host, + int port, + std::string path) { + auto transport = co_await kota::ipc::StreamTransport::connect_tcp(host, port, loop); + if(!transport) { + LOG_ERROR("failed to connect to {}:{}", host, port); + co_return; + } + + kota::ipc::JsonPeer peer(loop, std::move(*transport)); + co_await kota::when_all(peer.run(), agentic_request(peer, exit_code, std::move(path))); +} + +int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path) { + logging::stderr_logger("agentic", logging::options); + + kota::event_loop loop; + int exit_code = 1; + loop.schedule(agentic_client(loop, exit_code, host.str(), port, path.str())); + loop.run(); + return exit_code; +} + +} // namespace clice diff --git a/src/server/service/agentic.h b/src/server/service/agentic.h new file mode 100644 index 000000000..b2c625b45 --- /dev/null +++ b/src/server/service/agentic.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "llvm/ADT/StringRef.h" + +namespace clice { + +int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path); + +} // namespace clice diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp index 2e391a461..d2e2ba978 100644 --- a/src/server/service/master_server.cpp +++ b/src/server/service/master_server.cpp @@ -34,6 +34,14 @@ MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) : MasterServer::~MasterServer() = default; +void MasterServer::initialize(llvm::StringRef root) { + workspace_root = root.str(); + workspace.config = Config::load_from_workspace(workspace_root); + workspace.config.apply_defaults(workspace_root); + lifecycle = ServerLifecycle::Ready; + load_workspace(); +} + void MasterServer::load_workspace() { if(workspace_root.empty()) return; @@ -157,6 +165,14 @@ struct Connection { std::unique_ptr agent_client; }; +static kota::task<> run_connection(kota::ipc::JsonPeer* peer, + std::list& connections, + std::list::iterator pos) { + co_await peer->run(); + LOG_INFO("Client disconnected"); + connections.erase(pos); +} + static kota::task<> accept_connections(kota::event_loop& loop, MasterServer& server, kota::tcp::acceptor acceptor, @@ -185,13 +201,7 @@ static kota::task<> accept_connections(kota::event_loop& loop, .agent_client = std::move(agent), }); - loop.schedule([](kota::ipc::JsonPeer* p, - std::list& conns, - std::list::iterator pos) -> kota::task<> { - co_await p->run(); - LOG_INFO("Client disconnected"); - conns.erase(pos); - }(peer_ptr, connections, it)); + loop.schedule(run_connection(peer_ptr, connections, it)); } } diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h index fef52966a..8c1938eac 100644 --- a/src/server/service/master_server.h +++ b/src/server/service/master_server.h @@ -35,6 +35,8 @@ class MasterServer { MasterServer(kota::event_loop& loop, std::string self_path); ~MasterServer(); + void initialize(llvm::StringRef root); + private: void load_workspace(); diff --git a/tests/conftest.py b/tests/conftest.py index 0ef7eed6d..d50097d42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ import pytest -from tests.integration.utils.agentic_client import AgenticClient from tests.integration.utils.client import CliceClient @@ -93,30 +92,18 @@ def workspace(request: pytest.FixtureRequest, test_data_dir: Path) -> Path | Non return path -def _find_free_port() -> int: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("127.0.0.1", 0)) - return s.getsockname()[1] - - -@pytest.fixture -def agentic_port() -> int: - return _find_free_port() - - @pytest.fixture async def client( request: pytest.FixtureRequest, executable: Path, workspace: Path | None, - agentic_port: int, ): """Spawn clice server, auto-initialize if @pytest.mark.workspace is present.""" config = request.config mode = config.getoption("--mode") host = config.getoption("--host") - cmd = [str(executable), "--mode", mode, "--host", host, "--port", str(agentic_port)] + cmd = [str(executable), "--mode", mode, "--host", host] c = CliceClient() await c.start_io(*cmd) @@ -135,12 +122,37 @@ async def client( await _shutdown_client(c) +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + @pytest.fixture -async def agentic(agentic_port: int, client) -> AgenticClient: - """Connect to the agentic TCP endpoint of a running server.""" - ac = await AgenticClient.connect("127.0.0.1", agentic_port) - yield ac - await ac.close() +async def agentic( + request: pytest.FixtureRequest, + executable: Path, + workspace: Path | None, +): + """Start a server with agentic TCP port, yield (executable, host, port).""" + host = "127.0.0.1" + port = _find_free_port() + cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)] + + c = CliceClient() + await c.start_io(*cmd) + + if workspace is not None: + init_options_marker = request.node.get_closest_marker("init_options") + init_options = dict(init_options_marker.args[0]) if init_options_marker else {} + project = dict(init_options.get("project", {})) + project.setdefault("cache_dir", str(workspace / ".clice")) + init_options["project"] = project + await c.initialize(workspace, initialization_options=init_options) + + yield executable, host, port + + await _shutdown_client(c) def generate_cdb(workspace: Path) -> None: @@ -167,9 +179,8 @@ def generate_cdb(workspace: Path) -> None: async def make_client(executable: Path, workspace: Path) -> CliceClient: """Spawn a fresh clice server and initialize it. For multi-session tests.""" - port = _find_free_port() c = CliceClient() - await c.start_io(str(executable), "--mode", "pipe", "--port", str(port)) + await c.start_io(str(executable), "--mode", "pipe") await c.initialize(workspace) return c diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py index cbca5f884..2226ccccb 100644 --- a/tests/integration/agentic/test_agentic.py +++ b/tests/integration/agentic/test_agentic.py @@ -1,31 +1,58 @@ -"""Tests for the agentic TCP protocol.""" +"""Tests for the agentic CLI client.""" + +import json +import subprocess import pytest +def run_agentic(executable, host, port, path, timeout=10): + result = subprocess.run( + [ + str(executable), + "--mode", + "agentic", + "--host", + host, + "--port", + str(port), + "--path", + path, + ], + capture_output=True, + text=True, + timeout=timeout, + ) + return result + + @pytest.mark.workspace("hello_world") async def test_compile_command(agentic, workspace): + executable, host, port = agentic main_cpp = (workspace / "main.cpp").as_posix() - resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) - result = resp["result"] - assert result["file"] == main_cpp - assert result["directory"] == workspace.as_posix() - assert len(result["arguments"]) > 0 + result = run_agentic(executable, host, port, main_cpp) + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == main_cpp + assert data["directory"] == workspace.as_posix() + assert len(data["arguments"]) > 0 @pytest.mark.workspace("hello_world") async def test_compile_command_fallback(agentic, workspace): - resp = await agentic.request( - "agentic/compileCommand", {"path": "/nonexistent/file.cpp"} - ) - result = resp["result"] - assert result["file"] == "/nonexistent/file.cpp" + executable, host, port = agentic + result = run_agentic(executable, host, port, "/nonexistent/file.cpp") + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == "/nonexistent/file.cpp" @pytest.mark.workspace("hello_world") async def test_multiple_requests(agentic, workspace): + executable, host, port = agentic main_cpp = (workspace / "main.cpp").as_posix() for _ in range(3): - resp = await agentic.request("agentic/compileCommand", {"path": main_cpp}) - assert "result" in resp - assert resp["result"]["file"] == main_cpp + result = run_agentic(executable, host, port, main_cpp) + assert result.returncode == 0, f"stderr: {result.stderr}" + data = json.loads(result.stdout) + assert data["file"] == main_cpp diff --git a/tests/integration/utils/agentic_client.py b/tests/integration/utils/agentic_client.py deleted file mode 100644 index 0d49f59f2..000000000 --- a/tests/integration/utils/agentic_client.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Lightweight async JSON-RPC client for the agentic TCP protocol.""" - -import asyncio -import json -import re - - -class AgenticClient: - """Connects to the clice agentic TCP endpoint and sends JSON-RPC requests.""" - - def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): - self._reader = reader - self._writer = writer - self._next_id = 1 - - @classmethod - async def connect(cls, host: str, port: int, *, timeout: float = 10.0): - deadline = asyncio.get_event_loop().time() + timeout - while True: - try: - reader, writer = await asyncio.open_connection(host, port) - return cls(reader, writer) - except (ConnectionRefusedError, OSError): - if asyncio.get_event_loop().time() >= deadline: - raise - await asyncio.sleep(0.1) - - async def request( - self, method: str, params: dict, *, timeout: float = 30.0 - ) -> dict: - msg_id = self._next_id - self._next_id += 1 - payload = json.dumps( - { - "jsonrpc": "2.0", - "id": msg_id, - "method": method, - "params": params, - } - ) - encoded = payload.encode("utf-8") - self._writer.write( - f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii") + encoded - ) - await self._writer.drain() - response = await asyncio.wait_for(self._read_message(), timeout=timeout) - assert response is not None, "connection closed before response" - assert response.get("id") == msg_id - return response - - async def _read_message(self) -> dict | None: - header = b"" - while True: - line = await self._reader.readline() - if not line: - return None - header += line - if header.endswith(b"\r\n\r\n"): - break - match = re.search(rb"Content-Length:\s*(\d+)", header) - if not match: - return None - return json.loads(await self._reader.readexactly(int(match.group(1)))) - - async def close(self): - try: - self._writer.close() - await self._writer.wait_closed() - except (ConnectionError, OSError): - pass From c11d24971fb974b03119476291869174a82d570f Mon Sep 17 00:00:00 2001 From: ykiko Date: Mon, 27 Apr 2026 01:48:12 +0800 Subject: [PATCH 07/12] refactor(server): move src/protocol/ into src/server/protocol/ Co-Authored-By: Claude Opus 4.6 --- src/server/compiler/compiler.cpp | 2 +- src/server/compiler/indexer.cpp | 2 +- src/{ => server}/protocol/agentic.h | 0 src/{ => server}/protocol/extension.h | 0 src/{ => server}/protocol/worker.h | 0 src/server/service/agent_client.cpp | 2 +- src/server/service/agentic.cpp | 2 +- src/server/service/lsp_client.cpp | 4 ++-- src/server/worker/stateful_worker.cpp | 2 +- src/server/worker/stateless_worker.cpp | 2 +- src/server/worker/worker_pool.h | 2 +- tests/unit/server/module_worker_tests.cpp | 2 +- tests/unit/server/pch_worker_tests.cpp | 2 +- tests/unit/server/stateful_worker_tests.cpp | 2 +- tests/unit/server/stateless_worker_tests.cpp | 2 +- tests/unit/server/worker_test_helpers.h | 2 +- 16 files changed, 14 insertions(+), 14 deletions(-) rename src/{ => server}/protocol/agentic.h (100%) rename src/{ => server}/protocol/extension.h (100%) rename src/{ => server}/protocol/worker.h (100%) diff --git a/src/server/compiler/compiler.cpp b/src/server/compiler/compiler.cpp index c4853f666..ddf3d5527 100644 --- a/src/server/compiler/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -6,7 +6,7 @@ #include "command/search_config.h" #include "index/tu_index.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "support/filesystem.h" #include "support/logging.h" #include "syntax/include_resolver.h" diff --git a/src/server/compiler/indexer.cpp b/src/server/compiler/indexer.cpp index c9937c828..442dac18f 100644 --- a/src/server/compiler/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -6,8 +6,8 @@ #include #include "index/tu_index.h" -#include "protocol/worker.h" #include "server/compiler/compiler.h" +#include "server/protocol/worker.h" #include "server/service/session.h" #include "server/worker/worker_pool.h" #include "support/filesystem.h" diff --git a/src/protocol/agentic.h b/src/server/protocol/agentic.h similarity index 100% rename from src/protocol/agentic.h rename to src/server/protocol/agentic.h diff --git a/src/protocol/extension.h b/src/server/protocol/extension.h similarity index 100% rename from src/protocol/extension.h rename to src/server/protocol/extension.h diff --git a/src/protocol/worker.h b/src/server/protocol/worker.h similarity index 100% rename from src/protocol/worker.h rename to src/server/protocol/worker.h diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp index d83a9f926..bae366def 100644 --- a/src/server/service/agent_client.cpp +++ b/src/server/service/agent_client.cpp @@ -4,7 +4,7 @@ #include #include -#include "protocol/agentic.h" +#include "server/protocol/agentic.h" #include "server/service/master_server.h" namespace clice { diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp index 0ec79df49..d4282a3bf 100644 --- a/src/server/service/agentic.cpp +++ b/src/server/service/agentic.cpp @@ -3,7 +3,7 @@ #include #include -#include "protocol/agentic.h" +#include "server/protocol/agentic.h" #include "support/logging.h" #include "kota/async/async.h" diff --git a/src/server/service/lsp_client.cpp b/src/server/service/lsp_client.cpp index 027c10ae7..07ff824eb 100644 --- a/src/server/service/lsp_client.cpp +++ b/src/server/service/lsp_client.cpp @@ -6,9 +6,9 @@ #include #include -#include "protocol/extension.h" -#include "protocol/worker.h" #include "semantic/symbol_kind.h" +#include "server/protocol/extension.h" +#include "server/protocol/worker.h" #include "server/service/master_server.h" #include "support/filesystem.h" #include "support/logging.h" diff --git a/src/server/worker/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp index 96fa5386c..3cda3289f 100644 --- a/src/server/worker/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -10,7 +10,7 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker/worker_common.h" #include "support/logging.h" diff --git a/src/server/worker/stateless_worker.cpp b/src/server/worker/stateless_worker.cpp index a0b9b25c1..91a537042 100644 --- a/src/server/worker/stateless_worker.cpp +++ b/src/server/worker/stateless_worker.cpp @@ -3,7 +3,7 @@ #include "compile/compilation.h" #include "feature/feature.h" #include "index/tu_index.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker/worker_common.h" #include "support/logging.h" diff --git a/src/server/worker/worker_pool.h b/src/server/worker/worker_pool.h index 5eaf3c28c..faa03717d 100644 --- a/src/server/worker/worker_pool.h +++ b/src/server/worker/worker_pool.h @@ -6,7 +6,7 @@ #include #include -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "kota/async/async.h" #include "kota/ipc/codec/bincode.h" diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index 51da31ffa..5ddfc0896 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" namespace clice::testing { diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index e15c86cea..279e8dab0 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "syntax/scan.h" diff --git a/tests/unit/server/stateful_worker_tests.cpp b/tests/unit/server/stateful_worker_tests.cpp index a7e104632..a3ed6a4ac 100644 --- a/tests/unit/server/stateful_worker_tests.cpp +++ b/tests/unit/server/stateful_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/json/json.h" diff --git a/tests/unit/server/stateless_worker_tests.cpp b/tests/unit/server/stateless_worker_tests.cpp index b78d515db..4782c48e4 100644 --- a/tests/unit/server/stateless_worker_tests.cpp +++ b/tests/unit/server/stateless_worker_tests.cpp @@ -2,7 +2,7 @@ #include #include "test/test.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "server/worker_test_helpers.h" #include "kota/codec/bincode/bincode.h" diff --git a/tests/unit/server/worker_test_helpers.h b/tests/unit/server/worker_test_helpers.h index 8e012ba82..6108b58e3 100644 --- a/tests/unit/server/worker_test_helpers.h +++ b/tests/unit/server/worker_test_helpers.h @@ -11,7 +11,7 @@ #include "test/temp_dir.h" #include "command/argument_parser.h" #include "command/command.h" -#include "protocol/worker.h" +#include "server/protocol/worker.h" #include "support/filesystem.h" #include "kota/async/async.h" From ef117cd330132d210db0d2a27f7b6fc5279aed8c Mon Sep 17 00:00:00 2001 From: ykiko Date: Tue, 28 Apr 2026 22:30:32 +0800 Subject: [PATCH 08/12] fix(server): extend JsonPeer lifetime beyond coroutine frame in agentic client Move JsonPeer ownership to the caller via unique_ptr& out-parameter so the peer outlives the when_all scope and avoids use-after-free. Co-Authored-By: Claude Opus 4.6 --- src/server/service/agentic.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp index d4282a3bf..00388e502 100644 --- a/src/server/service/agentic.cpp +++ b/src/server/service/agentic.cpp @@ -1,5 +1,6 @@ #include "server/service/agentic.h" +#include #include #include @@ -29,6 +30,7 @@ static kota::task<> agentic_request(kota::ipc::JsonPeer& peer, int& exit_code, s static kota::task<> agentic_client(kota::event_loop& loop, int& exit_code, + std::unique_ptr& peer_out, std::string host, int port, std::string path) { @@ -38,8 +40,9 @@ static kota::task<> agentic_client(kota::event_loop& loop, co_return; } - kota::ipc::JsonPeer peer(loop, std::move(*transport)); - co_await kota::when_all(peer.run(), agentic_request(peer, exit_code, std::move(path))); + peer_out = std::make_unique(loop, std::move(*transport)); + co_await kota::when_all(peer_out->run(), + agentic_request(*peer_out, exit_code, std::move(path))); } int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path) { @@ -47,7 +50,8 @@ int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path) { kota::event_loop loop; int exit_code = 1; - loop.schedule(agentic_client(loop, exit_code, host.str(), port, path.str())); + std::unique_ptr peer; + loop.schedule(agentic_client(loop, exit_code, peer, host.str(), port, path.str())); loop.run(); return exit_code; } From 32d376dcb36e7e0dda77b56353215061517d334a Mon Sep 17 00:00:00 2001 From: ykiko Date: Fri, 1 May 2026 19:32:39 +0800 Subject: [PATCH 09/12] refactor(server): replace fire-and-forget schedule with task_group - Compiler: detached compile tasks now spawn into a task_group member, with stop() for graceful shutdown instead of raw loop.schedule() - MasterServer: connection handlers use a local task_group inside accept_connections instead of fire-and-forget scheduling - Indexer: replace hand-written inflight/finished/completion_event counters with task_group; monitor_resources uses cancellation_token via with_token instead of generation counter polling - WorkerPool: monitor tasks use task_group, removing manual alive_count_/all_exited_ tracking; stop() simplified to join() - Agentic client: remove unnecessary loop parameter, use event_loop::current() inside coroutine Co-Authored-By: Claude Opus 4.6 --- src/server/compiler/compiler.cpp | 222 +++++++++++++-------------- src/server/compiler/compiler.h | 6 + src/server/compiler/indexer.cpp | 48 +++--- src/server/compiler/indexer.h | 14 +- src/server/service/agentic.cpp | 6 +- src/server/service/lsp_client.cpp | 1 + src/server/service/master_server.cpp | 15 +- src/server/worker/worker_pool.cpp | 22 +-- src/server/worker/worker_pool.h | 3 +- 9 files changed, 152 insertions(+), 185 deletions(-) diff --git a/src/server/compiler/compiler.cpp b/src/server/compiler/compiler.cpp index ddf3d5527..346bf4326 100644 --- a/src/server/compiler/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -37,6 +37,11 @@ Compiler::~Compiler() { workspace.cancel_all(); } +kota::task<> Compiler::stop() { + compile_tasks.cancel(); + co_await compile_tasks.join(); +} + void Compiler::init_compile_graph() { if(workspace.path_to_module.empty()) { LOG_INFO("No C++20 modules detected, skipping CompileGraph"); @@ -632,6 +637,101 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef deps) { /// Called lazily by forward_query() / forward_build() before every /// feature request (hover, semantic tokens, etc.). Guarantees that when it /// returns true the stateful worker assigned to `path_id` holds an up-to-date +kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr pc) { + auto find_session = [&]() -> Session* { + auto it = sessions.find(pid); + return it != sessions.end() ? &it->second : nullptr; + }; + + auto* sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + auto finish_compile = [&]() { + auto* s = find_session(); + if(s && s->compiling == pc) { + s->compiling.reset(); + } + LOG_INFO("ensure_compiled: finish path_id={}", pid); + pc->done.set(); + }; + + auto gen = sess->generation; + LOG_INFO("ensure_compiled: starting compile path_id={} gen={}", pid, gen); + + auto file_path = std::string(workspace.path_pool.resolve(pid)); + auto uri = lsp::URI::from_file_path(file_path); + std::string uri_str = uri.has_value() ? uri->str() : file_path; + + worker::CompileParams params; + params.path = file_path; + params.version = sess->version; + params.text = sess->text; + if(!fill_compile_args(file_path, params.directory, params.arguments, sess)) { + finish_compile(); + co_return; + } + + if(!co_await ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) { + LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); + finish_compile(); + co_return; + } + + sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + auto result = co_await pool.send_stateful(pid, params); + + sess = find_session(); + if(!sess) { + pc->done.set(); + co_return; + } + + if(sess->generation != gen) { + LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", + sess->generation, + gen, + uri_str); + finish_compile(); + co_return; + } + + if(!result.has_value()) { + LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message); + clear_diagnostics(uri_str); + finish_compile(); + co_return; + } + + sess->ast_dirty = false; + pc->succeeded = true; + record_deps(*sess, result.value().deps); + + if(!result.value().tu_index_data.empty()) { + auto tu_index = index::TUIndex::from(result.value().tu_index_data.data()); + OpenFileIndex ofi; + ofi.file_index = std::move(tu_index.main_file_index); + ofi.symbols = std::move(tu_index.symbols); + ofi.content = sess->text; + ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16); + sess->file_index = std::move(ofi); + } + + auto version = sess->version; + finish_compile(); + + publish_diagnostics(uri_str, version, result.value().diagnostics); + if(on_indexing_needed) + on_indexing_needed(); +} + /// AST and diagnostics have been published to the client. /// /// Lifecycle overview (pull-based model): @@ -651,9 +751,9 @@ void Compiler::record_deps(Session& session, llvm::ArrayRef deps) { /// worker); every other file is read from disk by the compiler. /// /// Concurrency: multiple concurrent feature requests for the same file will -/// each call ensure_compiled(). The first one launches a detached compile -/// task via loop.schedule(); subsequent ones wait on the shared event. -/// The detached task cannot be cancelled by LSP $/cancelRequest, preventing +/// each call ensure_compiled(). The first one spawns a compile task into the +/// Compiler's task_group; subsequent ones wait on the shared event. +/// The spawned task is not cancelled by LSP $/cancelRequest, preventing /// the race where cancellation wakes all waiters and they all start compiles. kota::task Compiler::ensure_compiled(Session& session) { auto path_id = session.path_id; @@ -682,124 +782,12 @@ kota::task Compiler::ensure_compiled(Session& session) { co_return true; } - // No compile in flight and AST is dirty — launch a detached compile task. - // The detached task is scheduled via loop.schedule() so it is NOT subject - // to LSP $/cancelRequest cancellation. This eliminates the race where - // cancellation fires the RAII guard, waking all waiters simultaneously - // and causing them all to start new compiles. auto pending_compile = std::make_shared(); session.compiling = pending_compile; - LOG_INFO("ensure_compiled: launching detached compile path_id={} gen={}", - path_id, - session.generation); - - // Capture path_id by value so the detached lambda can re-lookup the session - // from the sessions map after co_await (DenseMap may invalidate pointers). - loop.schedule([](Compiler* self, - std::uint32_t pid, - std::shared_ptr pc) -> kota::task<> { - // Re-lookup session from the sessions map (pointer may have been - // invalidated by DenseMap growth during co_await). - auto find_session = [&]() -> Session* { - auto it = self->sessions.find(pid); - return it != self->sessions.end() ? &it->second : nullptr; - }; - - auto* sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - auto finish_compile = [&]() { - auto* s = find_session(); - if(s && s->compiling == pc) { - s->compiling.reset(); - } - LOG_INFO("ensure_compiled: finish_compile (detached) path_id={}", pid); - pc->done.set(); - }; - - auto gen = sess->generation; - LOG_INFO("ensure_compiled: starting compile (detached) path_id={} gen={}", pid, gen); - - auto file_path = std::string(self->workspace.path_pool.resolve(pid)); - auto uri = lsp::URI::from_file_path(file_path); - std::string uri_str = uri.has_value() ? uri->str() : file_path; - - worker::CompileParams params; - params.path = file_path; - params.version = sess->version; - params.text = sess->text; - if(!self->fill_compile_args(file_path, params.directory, params.arguments, sess)) { - finish_compile(); - co_return; - } - - if(!co_await self - ->ensure_deps(*sess, params.directory, params.arguments, params.pch, params.pcms)) { - LOG_WARN("Dependency preparation failed for {}, skipping compile", uri_str); - finish_compile(); - co_return; - } - - // Re-lookup after co_await (DenseMap may have grown). - sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - auto result = co_await self->pool.send_stateful(pid, params); - - // Re-lookup after co_await. - sess = find_session(); - if(!sess) { - pc->done.set(); - co_return; - } - - if(sess->generation != gen) { - LOG_INFO("ensure_compiled: generation mismatch ({} vs {}) for {}", - sess->generation, - gen, - uri_str); - finish_compile(); - co_return; - } - - if(!result.has_value()) { - LOG_WARN("Compile failed for {}: {}", uri_str, result.error().message); - self->clear_diagnostics(uri_str); - finish_compile(); - co_return; - } - - sess->ast_dirty = false; - pc->succeeded = true; - self->record_deps(*sess, result.value().deps); - - // Store open file index from the stateful worker's TUIndex. - if(!result.value().tu_index_data.empty()) { - auto tu_index = index::TUIndex::from(result.value().tu_index_data.data()); - OpenFileIndex ofi; - ofi.file_index = std::move(tu_index.main_file_index); - ofi.symbols = std::move(tu_index.symbols); - ofi.content = sess->text; - ofi.mapper.emplace(ofi.content, lsp::PositionEncoding::UTF16); - sess->file_index = std::move(ofi); - } - - auto version = sess->version; - finish_compile(); + LOG_INFO("ensure_compiled: launching compile path_id={} gen={}", path_id, session.generation); - // Publish diagnostics AFTER marking compile as done, so that concurrent - // forward_query() calls can proceed immediately. - self->publish_diagnostics(uri_str, version, result.value().diagnostics); - if(self->on_indexing_needed) - self->on_indexing_needed(); - }(this, path_id, pending_compile)); + compile_tasks.spawn(run_compile(path_id, pending_compile)); // Wait for the detached compile to finish. If this wait is cancelled // by LSP $/cancelRequest, the detached task continues unaffected. diff --git a/src/server/compiler/compiler.h b/src/server/compiler/compiler.h index 7083b36ff..8fdbd69c5 100644 --- a/src/server/compiler/compiler.h +++ b/src/server/compiler/compiler.h @@ -100,7 +100,12 @@ class Compiler { /// Callback invoked when indexing should be scheduled. std::function on_indexing_needed; + /// Cancel in-flight compile tasks and wait for them to finish. + kota::task<> stop(); + private: + kota::task<> run_compile(std::uint32_t path_id, std::shared_ptr pc); + kota::task ensure_deps(Session& session, const std::string& directory, const std::vector& arguments, @@ -133,6 +138,7 @@ class Compiler { Workspace& workspace; WorkerPool& pool; llvm::DenseMap& sessions; + kota::task_group<> compile_tasks{loop}; }; } // namespace clice diff --git a/src/server/compiler/indexer.cpp b/src/server/compiler/indexer.cpp index 442dac18f..4ef2b65fc 100644 --- a/src/server/compiler/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -694,18 +694,14 @@ kota::task<> Indexer::index_one(std::uint32_t server_path_id) { } } -kota::task<> Indexer::monitor_resources(std::uint32_t generation) { - while(generation == monitor_generation) { - co_await kota::sleep(std::chrono::milliseconds(3000), loop); - - if(generation != monitor_generation) - break; +kota::task<> Indexer::monitor_resources() { + while(true) { + co_await kota::sleep(std::chrono::milliseconds(3000)); auto mem = kota::sys::memory(); if(mem.total == 0) continue; - // Respect cgroup/container limits when present. auto effective_total = (mem.constrained > 0 && mem.constrained < mem.total) ? mem.constrained : mem.total; auto ratio = static_cast(mem.available) / static_cast(effective_total); @@ -736,22 +732,23 @@ kota::task<> Indexer::run_background_indexing() { } indexing_active = true; - ++monitor_generation; - loop.schedule(monitor_resources(monitor_generation)); - // Put module interface units first so their PCMs are built before - // non-module files that might import them. + kota::cancellation_source monitor_cancel; + kota::task_group<> index_group(loop); + index_group.spawn(kota::with_token(monitor_resources(), monitor_cancel.token())); + std::stable_partition( index_queue.begin() + index_queue_pos, index_queue.end(), [this](std::uint32_t id) { return workspace.path_to_module.contains(id); }); auto batch = index_queue.size() - index_queue_pos; + std::size_t inflight = 0; std::size_t dispatched = 0; std::size_t completed = 0; - finished = 0; + std::size_t finished = 0; + kota::event completion_event; - // Progress reporting via LSP $/progress. std::optional> progress; if(peer) { progress.emplace(*peer, protocol::ProgressToken(std::string("clice/backgroundIndex"))); @@ -764,17 +761,13 @@ kota::task<> Indexer::run_background_indexing() { } while(index_queue_pos < index_queue.size() || inflight > 0) { - // Dispatch new tasks up to max_concurrent. while(index_queue_pos < index_queue.size() && inflight < max_concurrent) { - // Wait if paused by a user request. if(pause_depth > 0) { co_await resume_event.wait(); } auto server_path_id = index_queue[index_queue_pos++]; - // Quick pre-filter: skip open files and fresh files without - // consuming a concurrency slot. auto file_path = std::string(workspace.path_pool.resolve(server_path_id)); if(sessions.contains(server_path_id) || !need_update(file_path)) { ++completed; @@ -784,27 +777,26 @@ kota::task<> Indexer::run_background_indexing() { ++inflight; ++dispatched; - // Launch the index task. On completion it decrements - // inflight, bumps finished, and signals the event. - loop.schedule([](Indexer* self, std::uint32_t id, kota::event& done) -> kota::task<> { + index_group.spawn([](Indexer* self, + std::uint32_t id, + std::size_t& inflight_ref, + std::size_t& finished_ref, + kota::event& done) -> kota::task<> { co_await self->index_one(id); - --self->inflight; - ++self->finished; + --inflight_ref; + ++finished_ref; done.set(); - }(this, server_path_id, completion_event)); + }(this, server_path_id, inflight, finished, completion_event)); } if(inflight == 0) break; - // Wait for at least one task to finish. co_await completion_event.wait(); completion_event.reset(); - // Drain all completions that occurred since last wake. completed += std::exchange(finished, 0); - // Report progress. if(progress) { auto pct = batch > 0 ? static_cast(completed * 100 / batch) : 100; progress->report(std::format("{}/{} files", completed, batch), pct); @@ -815,8 +807,10 @@ kota::task<> Indexer::run_background_indexing() { progress->end(std::format("Indexed {} files", dispatched)); } + monitor_cancel.cancel(); + co_await index_group.join(); + indexing_active = false; - ++monitor_generation; // Stop the monitor coroutine. LOG_INFO("Background indexing complete: {} files dispatched", dispatched); save(workspace.config.project.index_dir); } diff --git a/src/server/compiler/indexer.h b/src/server/compiler/indexer.h index 8e7ef5d7e..1e76395b4 100644 --- a/src/server/compiler/indexer.h +++ b/src/server/compiler/indexer.h @@ -231,27 +231,15 @@ class Indexer { /// Concurrency control for background indexing. std::size_t max_concurrent = 2; std::size_t baseline_concurrent = 2; - std::size_t inflight = 0; - std::size_t finished = 0; ///< Incremented by each completed dispatch task. /// Pause/resume: when paused, new index tasks wait on this event. /// Uses a counter so nested pause/resume pairs work correctly. std::size_t pause_depth = 0; kota::event resume_event{true}; - /// Completion event — signalled by each finished dispatch task so the - /// main loop can wake up. Must be a member (not local to the coroutine) - /// because inflight tasks capture it by reference and may outlive the - /// coroutine frame during server shutdown. - kota::event completion_event; - - /// Generation counter — incremented each run so a stale monitor_resources - /// coroutine can detect that its owning run has ended. - std::uint32_t monitor_generation = 0; - kota::task<> run_background_indexing(); kota::task<> index_one(std::uint32_t server_path_id); - kota::task<> monitor_resources(std::uint32_t generation); + kota::task<> monitor_resources(); }; } // namespace clice diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp index 00388e502..02fd72f64 100644 --- a/src/server/service/agentic.cpp +++ b/src/server/service/agentic.cpp @@ -28,12 +28,12 @@ static kota::task<> agentic_request(kota::ipc::JsonPeer& peer, int& exit_code, s peer.close(); } -static kota::task<> agentic_client(kota::event_loop& loop, - int& exit_code, +static kota::task<> agentic_client(int& exit_code, std::unique_ptr& peer_out, std::string host, int port, std::string path) { + auto& loop = kota::event_loop::current(); auto transport = co_await kota::ipc::StreamTransport::connect_tcp(host, port, loop); if(!transport) { LOG_ERROR("failed to connect to {}:{}", host, port); @@ -51,7 +51,7 @@ int run_agentic_mode(llvm::StringRef host, int port, llvm::StringRef path) { kota::event_loop loop; int exit_code = 1; std::unique_ptr peer; - loop.schedule(agentic_client(loop, exit_code, peer, host.str(), port, path.str())); + loop.schedule(agentic_client(exit_code, peer, host.str(), port, path.str())); loop.run(); return exit_code; } diff --git a/src/server/service/lsp_client.cpp b/src/server/service/lsp_client.cpp index 07ff824eb..a73c83bb5 100644 --- a/src/server/service/lsp_client.cpp +++ b/src/server/service/lsp_client.cpp @@ -208,6 +208,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s srv->workspace.save_cache(); srv->loop.schedule([](MasterServer* s) -> kota::task<> { + co_await s->compiler.stop(); co_await s->pool.stop(); s->loop.stop(); }(srv)); diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp index d2e2ba978..a218884d7 100644 --- a/src/server/service/master_server.cpp +++ b/src/server/service/master_server.cpp @@ -173,11 +173,13 @@ static kota::task<> run_connection(kota::ipc::JsonPeer* peer, connections.erase(pos); } -static kota::task<> accept_connections(kota::event_loop& loop, - MasterServer& server, +static kota::task<> accept_connections(MasterServer& server, kota::tcp::acceptor acceptor, bool register_lsp, std::list& connections) { + auto& loop = kota::event_loop::current(); + kota::task_group<> connection_group(loop); + while(true) { auto conn = co_await acceptor.accept(); if(!conn.has_value()) @@ -201,8 +203,10 @@ static kota::task<> accept_connections(kota::event_loop& loop, .agent_client = std::move(agent), }); - loop.schedule(run_connection(peer_ptr, connections, it)); + connection_group.spawn(run_connection(peer_ptr, connections, it)); } + + co_await connection_group.join(); } int run_server_mode(const ServerOptions& opts) { @@ -233,8 +237,7 @@ int run_server_mode(const ServerOptions& opts) { auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop); if(acceptor) { LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port); - loop.schedule( - accept_connections(loop, server, std::move(*acceptor), false, connections)); + loop.schedule(accept_connections(server, std::move(*acceptor), false, connections)); } else { LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port); } @@ -253,7 +256,7 @@ int run_server_mode(const ServerOptions& opts) { } LOG_INFO("Listening on {}:{} ...", opts.host, opts.port); - loop.schedule(accept_connections(loop, server, std::move(*acceptor), true, connections)); + loop.schedule(accept_connections(server, std::move(*acceptor), true, connections)); loop.run(); return 0; } diff --git a/src/server/worker/worker_pool.cpp b/src/server/worker/worker_pool.cpp index 1ece69e75..fccd1af8a 100644 --- a/src/server/worker/worker_pool.cpp +++ b/src/server/worker/worker_pool.cpp @@ -108,7 +108,6 @@ bool WorkerPool::spawn_worker(const std::string& self_path, auto& w = workers.back(); w.alive = true; - ++alive_count_; loop.schedule(w.peer->run()); return true; @@ -122,14 +121,14 @@ bool WorkerPool::start(const WorkerPoolOptions& options) { if(!spawn_worker(options.self_path, false, 0)) { return false; } - loop.schedule(monitor_worker(stateless_workers.size() - 1, false)); + monitor_group.spawn(monitor_worker(stateless_workers.size() - 1, false)); } for(std::uint32_t i = 0; i < options.stateful_count; ++i) { if(!spawn_worker(options.self_path, true, options.worker_memory_limit)) { return false; } - loop.schedule(monitor_worker(stateful_workers.size() - 1, true)); + monitor_group.spawn(monitor_worker(stateful_workers.size() - 1, true)); } // Register evicted notification handler for each stateful worker @@ -151,23 +150,17 @@ kota::task<> WorkerPool::stop() { LOG_INFO("WorkerPool stopping..."); shutting_down_ = true; - // Close output pipes to signal workers to exit gracefully. for(auto& w: stateless_workers) w.peer->close_output(); for(auto& w: stateful_workers) w.peer->close_output(); - // Send SIGTERM. monitor_worker coroutines handle the wait. for(auto& w: stateless_workers) w.proc.kill(SIGTERM); for(auto& w: stateful_workers) w.proc.kill(SIGTERM); - // Wait until all monitor_worker coroutines have finished. - if(alive_count_ > 0) { - all_exited_.reset(); - co_await all_exited_.wait(); - } + co_await monitor_group.join(); LOG_INFO("WorkerPool stopped"); } @@ -242,13 +235,9 @@ kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) { auto result = co_await w.proc.wait(); w.alive = false; - --alive_count_; - if(shutting_down_) { - if(alive_count_ == 0) - all_exited_.set(); + if(shutting_down_) co_return; - } if(result.has_value()) { auto& exit = result.value(); @@ -342,7 +331,6 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) { }; auto& w = workers[index]; - ++alive_count_; loop.schedule(w.peer->run()); if(stateful) { @@ -352,7 +340,7 @@ bool WorkerPool::respawn_worker(std::size_t index, bool stateful) { }); } - loop.schedule(monitor_worker(index, stateful)); + monitor_group.spawn(monitor_worker(index, stateful)); LOG_INFO("Worker {} restarted (attempt {})", worker_name, old_restart_count); return true; diff --git a/src/server/worker/worker_pool.h b/src/server/worker/worker_pool.h index faa03717d..c6948989f 100644 --- a/src/server/worker/worker_pool.h +++ b/src/server/worker/worker_pool.h @@ -83,8 +83,7 @@ class WorkerPool { std::size_t pick_least_loaded(); bool shutting_down_ = false; - std::size_t alive_count_ = 0; - kota::event all_exited_{true}; // Signalled when alive_count_ reaches 0. + kota::task_group<> monitor_group{loop}; WorkerPoolOptions options_; std::string log_dir_; From 8849d0e40cac0066b832721d6288384d3a730b5f Mon Sep 17 00:00:00 2001 From: ykiko Date: Fri, 1 May 2026 22:35:48 +0800 Subject: [PATCH 10/12] chore: pin kotatsu to exact commit hash for reproducibility Pin to e024f3b which includes the variant dispatch fix (#129). Co-Authored-By: Claude Opus 4.6 --- cmake/package.cmake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/package.cmake b/cmake/package.cmake index c056e75df..1f7f06da2 100644 --- a/cmake/package.cmake +++ b/cmake/package.cmake @@ -41,8 +41,7 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE) FetchContent_Declare( kotatsu GIT_REPOSITORY https://github.com/clice-io/kotatsu - GIT_TAG main - GIT_SHALLOW TRUE + GIT_TAG e024f3b427a554502c4aa015952800a03ca4384b ) set(KOTA_ENABLE_ZEST ON) From e3df300ce5e213c9c8824a0c9f0523ceba2054fd Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 2 May 2026 00:26:22 +0800 Subject: [PATCH 11/12] refactor(server): extract MasterServer public API and harden tests - Remove `friend class LSPClient/AgentClient` from MasterServer, replace with explicit public accessors and coordination methods (open_session, close_session, find_session, on_file_saved, schedule_shutdown, etc.) - Enforce single LSP connection in socket mode - Delete dead MasterServer::initialize() and duplicate set_peer call - Unify lambda capture style in lsp_client.cpp (this-> consistently) - Fix stale doc reference from src/server/protocol.h to src/server/protocol/ - Add agentic error-path integration tests (connection_refused, concurrent) - Fix flaky test_touch_without_content_change_skips_recompile: default cache_dir to workspace/.clice in CliceClient.initialize() to prevent stale PCH from global cache causing silent compilation failures Co-Authored-By: Claude Opus 4.6 --- docs/en/architecture.md | 2 +- src/server/service/agent_client.cpp | 2 +- src/server/service/lsp_client.cpp | 398 +++++++++------------- src/server/service/master_server.cpp | 127 ++++++- src/server/service/master_server.h | 103 +++++- tests/integration/agentic/test_agentic.py | 29 ++ tests/integration/utils/client.py | 9 +- 7 files changed, 413 insertions(+), 257 deletions(-) diff --git a/docs/en/architecture.md b/docs/en/architecture.md index 9a9b0bff4..dacbe21f2 100644 --- a/docs/en/architecture.md +++ b/docs/en/architecture.md @@ -153,7 +153,7 @@ String values support `${workspace}` substitution. ## IPC Protocol -The master and workers communicate using custom RPC messages defined in `src/server/protocol.h`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type. +The master and workers communicate using custom RPC messages defined in `src/server/protocol/`. Each message type has a `RequestTraits` or `NotificationTraits` specialization that defines the method name and result type. ### Stateful Worker Messages diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp index bae366def..85572a4ff 100644 --- a/src/server/service/agent_client.cpp +++ b/src/server/service/agent_client.cpp @@ -21,7 +21,7 @@ AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) : const CompileCommandParams& params) -> RequestResult { std::string directory; std::vector arguments; - if(!this->server.compiler.fill_compile_args(params.path, directory, arguments)) { + if(!this->server.get_compiler().fill_compile_args(params.path, directory, arguments)) { co_return kota::outcome_error( kota::ipc::Error{std::format("no compile command found for {}", params.path)}); } diff --git a/src/server/service/lsp_client.cpp b/src/server/service/lsp_client.cpp index a73c83bb5..587487765 100644 --- a/src/server/service/lsp_client.cpp +++ b/src/server/service/lsp_client.cpp @@ -37,31 +37,32 @@ static serde_raw to_raw(const T& value) { } LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) { - server.compiler.set_peer(&peer); - server.indexer.set_peer(&peer); + server.get_compiler().set_peer(&peer); + server.get_indexer().set_peer(&peer); using StringVec = std::vector; peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params) -> RequestResult { - if(this->server.lifecycle != ServerLifecycle::Uninitialized) { + auto& srv = this->server; + if(srv.get_lifecycle() != ServerLifecycle::Uninitialized) { co_return kota::outcome_error(protocol::Error{"Server already initialized"}); } auto& init = params.lsp__initialize_params; if(init.root_uri.has_value()) { - this->server.workspace_root = uri_to_path(*init.root_uri); + srv.set_workspace_root(uri_to_path(*init.root_uri)); } if(init.initialization_options.has_value()) { auto json = kota::codec::json::to_json(*init.initialization_options); if(json) - this->server.init_options_json = std::move(*json); + srv.set_init_options(std::move(*json)); } - this->server.lifecycle = ServerLifecycle::Initialized; - LOG_INFO("Initialized with workspace: {}", this->server.workspace_root); + srv.set_lifecycle(ServerLifecycle::Initialized); + LOG_INFO("Initialized with workspace: {}", srv.get_workspace_root()); protocol::InitializeResult result; auto& caps = result.capabilities; @@ -138,96 +139,34 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s co_return result; }); - peer.on_notification([this](const protocol::InitializedParams& params) { - auto& srv = this->server; - - srv.workspace.config = Config::load_from_workspace(srv.workspace_root); - if(!srv.init_options_json.empty()) { - if(auto ov = kota::codec::json::parse(srv.init_options_json, srv.workspace.config); - !ov) { - LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string()); - } else { - srv.workspace.config.apply_defaults(srv.workspace_root); - LOG_INFO("Applied initializationOptions overlay"); - } - srv.init_options_json.clear(); - } - - auto& cfg = srv.workspace.config.project; - - if(!cfg.logging_dir.empty()) { - auto now = std::chrono::system_clock::now(); - auto pid = llvm::sys::Process::getProcessId(); - auto session_dir = - path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid)); - logging::file_logger("master", session_dir, logging::options); - srv.session_log_dir = session_dir; - } - - LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)", - cfg.stateful_worker_count.value, - cfg.stateless_worker_count.value, - *cfg.idle_timeout_ms); - - WorkerPoolOptions pool_opts; - pool_opts.self_path = srv.self_path; - pool_opts.stateful_count = cfg.stateful_worker_count; - pool_opts.stateless_count = cfg.stateless_worker_count; - pool_opts.worker_memory_limit = cfg.worker_memory_limit; - pool_opts.log_dir = srv.session_log_dir; - if(!srv.pool.start(pool_opts)) { - LOG_ERROR("Failed to start worker pool"); - return; - } - - srv.lifecycle = ServerLifecycle::Ready; - - srv.compiler.on_indexing_needed = [&srv]() { - srv.indexer.schedule(); - }; - - srv.indexer.set_peer(&this->peer); - srv.indexer.set_max_concurrency(cfg.stateless_worker_count.value); - - srv.load_workspace(); + peer.on_notification([this]([[maybe_unused]] const protocol::InitializedParams& params) { + this->server.initialize(); }); peer.on_request( [this](RequestContext& ctx, const protocol::ShutdownParams& params) -> RequestResult { - this->server.lifecycle = ServerLifecycle::ShuttingDown; + this->server.set_lifecycle(ServerLifecycle::ShuttingDown); LOG_INFO("Shutdown requested"); co_return nullptr; }); - peer.on_notification([srv = &this->server](const protocol::ExitParams& params) { - srv->lifecycle = ServerLifecycle::Exited; + peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) { + auto& srv = this->server; + srv.set_lifecycle(ServerLifecycle::Exited); LOG_INFO("Exit notification received"); - - srv->indexer.save(srv->workspace.config.project.index_dir); - srv->workspace.save_cache(); - - srv->loop.schedule([](MasterServer* s) -> kota::task<> { - co_await s->compiler.stop(); - co_await s->pool.stop(); - s->loop.stop(); - }(srv)); + srv.schedule_shutdown(); }); peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { auto& srv = this->server; - if(srv.lifecycle != ServerLifecycle::Ready) + if(srv.get_lifecycle() != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); + auto path_id = srv.intern_path(path); - auto [it, inserted] = srv.sessions.try_emplace(path_id); - auto& session = it->second; - if(!inserted) { - session = Session{}; - } - session.path_id = path_id; + auto& session = srv.open_session(path_id); session.version = params.text_document.version; session.text = params.text_document.text; session.generation++; @@ -237,18 +176,17 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) { auto& srv = this->server; - if(srv.lifecycle != ServerLifecycle::Ready) + if(srv.get_lifecycle() != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); + auto path_id = srv.intern_path(path); - auto it = srv.sessions.find(path_id); - if(it == srv.sessions.end()) + auto* session = srv.find_session(path_id); + if(!session) return; - auto& session = it->second; - session.version = params.text_document.version; + session->version = params.text_document.version; for(auto& change: params.content_changes) { std::visit( @@ -256,82 +194,51 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s using T = std::remove_cvref_t; if constexpr(std::is_same_v) { - session.text = c.text; + session->text = c.text; } else { auto& range = c.range; - lsp::PositionMapper mapper(session.text, lsp::PositionEncoding::UTF16); + lsp::PositionMapper mapper(session->text, lsp::PositionEncoding::UTF16); auto start = mapper.to_offset(range.start); auto end = mapper.to_offset(range.end); if(start && end && *start <= *end) { - session.text.replace(*start, *end - *start, c.text); + session->text.replace(*start, *end - *start, c.text); } } }, change); } - session.generation++; - session.ast_dirty = true; + session->generation++; + session->ast_dirty = true; LOG_DEBUG("didChange: path={} version={} gen={}", path, - session.version, - session.generation); + session->version, + session->generation); worker::DocumentUpdateParams update; update.path = path; - update.version = session.version; - srv.pool.notify_stateful(path_id, update); + update.version = session->version; + srv.get_pool().notify_stateful(path_id, update); }); peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) { auto& srv = this->server; - if(srv.lifecycle != ServerLifecycle::Ready) + if(srv.get_lifecycle() != ServerLifecycle::Ready) return; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - - srv.workspace.on_file_closed(path_id); - srv.pool.notify_stateful(path_id, worker::EvictParams{path}); - - protocol::PublishDiagnosticsParams diag_params; - diag_params.uri = params.text_document.uri; - this->peer.send_notification(diag_params); - - srv.sessions.erase(path_id); - - srv.indexer.enqueue(path_id); - srv.indexer.schedule(); - - LOG_DEBUG("didClose: {}", path); + auto path_id = srv.intern_path(uri_to_path(params.text_document.uri)); + srv.close_session(path_id, this->peer); }); peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) { auto& srv = this->server; - if(srv.lifecycle != ServerLifecycle::Ready) + if(srv.get_lifecycle() != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - - auto dirtied = srv.workspace.on_file_saved(path_id); - for(auto dirty_id: dirtied) { - if(auto sit = srv.sessions.find(dirty_id); sit != srv.sessions.end()) { - sit->second.ast_dirty = true; - } else { - srv.indexer.enqueue(dirty_id); - } - } - - for(auto& [hdr_id, session]: srv.sessions) { - if(session.header_context && session.header_context->host_path_id == path_id) { - session.header_context.reset(); - session.ast_dirty = true; - } - } - - srv.indexer.schedule(); + auto path_id = srv.intern_path(path); + srv.on_file_saved(path_id); LOG_DEBUG("didSave: {}", path); }); @@ -339,13 +246,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query( + co_return co_await srv.get_compiler().forward_query( worker::QueryKind::Hover, - sit->second, + *session, params.text_document_position_params.position); }); @@ -353,69 +260,70 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const protocol::SemanticTokensParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, - sit->second); + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::SemanticTokens, + *session); }); peer.on_request( [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints, - sit->second, - {}, - params.range); + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::InlayHints, + *session, + {}, + params.range); }); - peer.on_request([this](RequestContext& ctx, - const protocol::FoldingRangeParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) - co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, sit->second); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::FoldingRange, + *session); + }); peer.on_request( [this](RequestContext& ctx, const protocol::DocumentSymbolParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, - sit->second); + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::DocumentSymbol, + *session); }); peer.on_request([this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - auto& session = sit->second; - auto result = co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, session); + auto result = + co_await srv.get_compiler().forward_query(worker::QueryKind::DocumentLink, *session); if(!result.has_value()) co_return serde_raw{"null"}; auto& links = result.value(); - auto sit2 = srv.sessions.find(path_id); - if(sit2 != srv.sessions.end() && sit2->second.pch_ref) { - auto pch_it = srv.workspace.pch_cache.find(sit2->second.pch_ref->path_id); - if(pch_it != srv.workspace.pch_cache.end() && - !pch_it->second.document_links_json.empty()) { + auto* session2 = srv.find_session(path_id); + if(session2 && session2->pch_ref) { + auto& pch_cache = srv.get_workspace().pch_cache; + auto pch_it = pch_cache.find(session2->pch_ref->path_id); + if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) { auto& pch_json = pch_it->second.document_links_json; if(!links.data.empty() && links.data != "null" && links.data.size() > 2) { links.data.pop_back(); @@ -429,16 +337,17 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s co_return std::move(links); }); - peer.on_request([this](RequestContext& ctx, - const protocol::CodeActionParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) - co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, sit->second); - }); + peer.on_request( + [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::CodeAction, + *session); + }); auto resolve_uri = [this](const std::string& uri) { struct Result { @@ -447,22 +356,21 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s Session* session; }; auto path = uri_to_path(uri); - auto path_id = this->server.workspace.path_pool.intern(path); - auto sit = this->server.sessions.find(path_id); - Session* session = (sit != this->server.sessions.end()) ? &sit->second : nullptr; + auto path_id = this->server.intern_path(path); + auto* session = this->server.find_session(path_id); return Result{std::move(path), path_id, session}; }; auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) { auto [path, path_id, session] = resolve_uri(uri); - return this->server.indexer.lookup_symbol(uri, path, pos, session); + return this->server.get_indexer().lookup_symbol(uri, path, pos, session); }; auto query_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos, RelationKind kind) -> std::vector { auto [path, path_id, session] = resolve_uri(uri); - return this->server.indexer.query_relations(path, pos, kind, session); + return this->server.get_indexer().query_relations(path, pos, kind, session); }; auto resolve_item = @@ -471,7 +379,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s const protocol::Range& range, const std::optional& data) -> std::optional { auto [path, path_id, session] = resolve_uri(uri); - return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session); + return this->server.get_indexer().resolve_hierarchy_item(uri, path, range, data, session); }; peer.on_request([this, query_at](RequestContext& ctx, @@ -486,13 +394,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto& srv = this->server; auto path = uri_to_path(uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition, - sit->second, - pos); + co_return co_await srv.get_compiler().forward_query(worker::QueryKind::GoToDefinition, + *session, + pos); }); peer.on_request([this, query_at](RequestContext& ctx, @@ -529,37 +437,37 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s co_return serde_raw{"null"}; }); + peer.on_request( + [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document_position_params.text_document.uri); + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + auto pause = srv.get_indexer().scoped_pause(); + auto result = co_await srv.get_compiler().handle_completion( + params.text_document_position_params.position, + *session); + co_return std::move(result); + }); + peer.on_request([this](RequestContext& ctx, - const protocol::CompletionParams& params) -> RawResult { + const protocol::SignatureHelpParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) + auto path_id = srv.intern_path(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - auto pause = srv.indexer.scoped_pause(); + auto pause = srv.get_indexer().scoped_pause(); auto result = - co_await srv.compiler.handle_completion(params.text_document_position_params.position, - sit->second); + co_await srv.get_compiler().forward_build(worker::BuildKind::SignatureHelp, + params.text_document_position_params.position, + *session); co_return std::move(result); }); - peer.on_request( - [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.workspace.path_pool.intern(path); - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) - co_return serde_raw{"null"}; - auto pause = srv.indexer.scoped_pause(); - auto result = - co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp, - params.text_document_position_params.position, - sit->second); - co_return std::move(result); - }); - peer.on_request( [this, lookup_at](RequestContext& ctx, const protocol::CallHierarchyPrepareParams& params) -> RawResult { @@ -583,7 +491,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.indexer.find_incoming_calls(info->hash); + auto results = this->server.get_indexer().find_incoming_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -595,7 +503,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.indexer.find_outgoing_calls(info->hash); + auto results = this->server.get_indexer().find_outgoing_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -625,7 +533,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.indexer.find_supertypes(info->hash); + auto results = this->server.get_indexer().find_supertypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -637,7 +545,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.indexer.find_subtypes(info->hash); + auto results = this->server.get_indexer().find_subtypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -645,7 +553,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_request( [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult { - auto results = this->server.indexer.search_symbols(params.query); + auto results = this->server.get_indexer().search_symbols(params.query); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -656,17 +564,18 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.workspace.path_pool.intern(path); + auto path_id = srv.intern_path(path); int offset_val = std::max(0, params.offset.value_or(0)); constexpr int page_size = 10; ext::QueryContextResult result; std::vector all_items; - auto hosts = srv.workspace.dep_graph.find_host_sources(path_id); + auto& ws = srv.get_workspace(); + auto hosts = ws.dep_graph.find_host_sources(path_id); for(auto host_id: hosts) { - auto host_path = srv.workspace.path_pool.resolve(host_id); - auto host_cdb = srv.workspace.cdb.lookup(host_path, {.suppress_logging = true}); + auto host_path = ws.path_pool.resolve(host_id); + auto host_cdb = ws.cdb.lookup(host_path, {.suppress_logging = true}); if(host_cdb.empty()) continue; auto host_uri_opt = lsp::URI::from_file_path(std::string(host_path)); @@ -680,7 +589,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s } if(hosts.empty()) { - auto entries = srv.workspace.cdb.lookup(path, {.suppress_logging = true}); + auto entries = ws.cdb.lookup(path, {.suppress_logging = true}); for(std::size_t i = 0; i < entries.size(); ++i) { auto& cmd = entries[i]; auto argv = cmd.to_argv(); @@ -724,12 +633,12 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.workspace.path_pool.intern(path); + auto path_id = srv.intern_path(path); ext::CurrentContextResult result; - auto sit = srv.sessions.find(path_id); - if(sit != srv.sessions.end() && sit->second.active_context) { - auto ctx_path = srv.workspace.path_pool.resolve(*sit->second.active_context); + auto* session = srv.find_session(path_id); + if(session && session->active_context) { + auto ctx_path = srv.resolve_path(*session->active_context); auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path)); if(ctx_uri_opt) { ext::ContextItem item; @@ -747,29 +656,30 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.workspace.path_pool.intern(path); + auto path_id = srv.intern_path(path); auto context_path = uri_to_path(params.context_uri); - auto context_path_id = srv.workspace.path_pool.intern(context_path); + auto context_path_id = srv.intern_path(context_path); ext::SwitchContextResult result; - auto context_cdb = srv.workspace.cdb.lookup(context_path, {.suppress_logging = true}); + auto& ws = srv.get_workspace(); + auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true}); if(context_cdb.empty()) { result.success = false; co_return to_raw(result); } - auto sit = srv.sessions.find(path_id); - if(sit == srv.sessions.end()) { + auto* session = srv.find_session(path_id); + if(!session) { result.success = false; co_return to_raw(result); } - sit->second.active_context = context_path_id; - sit->second.header_context.reset(); - sit->second.pch_ref.reset(); - sit->second.ast_deps.reset(); - sit->second.ast_dirty = true; + session->active_context = context_path_id; + session->header_context.reset(); + session->pch_ref.reset(); + session->ast_deps.reset(); + session->ast_dirty = true; result.success = true; co_return to_raw(result); @@ -777,8 +687,8 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s } LSPClient::~LSPClient() { - server.compiler.set_peer(nullptr); - server.indexer.set_peer(nullptr); + server.get_compiler().set_peer(nullptr); + server.get_indexer().set_peer(nullptr); } } // namespace clice diff --git a/src/server/service/master_server.cpp b/src/server/service/master_server.cpp index a218884d7..9e6fbbc9a 100644 --- a/src/server/service/master_server.cpp +++ b/src/server/service/master_server.cpp @@ -5,19 +5,27 @@ #include #include +#include "server/protocol/worker.h" #include "server/service/agent_client.h" #include "server/service/lsp_client.h" #include "support/filesystem.h" #include "support/logging.h" #include "kota/async/async.h" +#include "kota/codec/json/json.h" #include "kota/ipc/codec/json.h" +#include "kota/ipc/lsp/protocol.h" +#include "kota/ipc/lsp/uri.h" #include "kota/ipc/recording_transport.h" #include "kota/ipc/transport.h" #include "llvm/Support/FileSystem.h" +#include "llvm/Support/Process.h" namespace clice { +namespace lsp = kota::ipc::lsp; +namespace protocol = kota::ipc::protocol; + MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) : loop(loop), pool(loop), compiler(loop, workspace, pool, sessions), indexer(loop, @@ -34,14 +42,122 @@ MasterServer::MasterServer(kota::event_loop& loop, std::string self_path) : MasterServer::~MasterServer() = default; -void MasterServer::initialize(llvm::StringRef root) { - workspace_root = root.str(); +void MasterServer::initialize() { workspace.config = Config::load_from_workspace(workspace_root); - workspace.config.apply_defaults(workspace_root); + if(!init_options_json.empty()) { + if(auto ov = kota::codec::json::parse(init_options_json, workspace.config); !ov) { + LOG_WARN("Failed to apply initializationOptions: {}", ov.error().to_string()); + } else { + workspace.config.apply_defaults(workspace_root); + LOG_INFO("Applied initializationOptions overlay"); + } + init_options_json.clear(); + } + + auto& cfg = workspace.config.project; + + if(!cfg.logging_dir.empty()) { + auto now = std::chrono::system_clock::now(); + auto pid = llvm::sys::Process::getProcessId(); + session_log_dir = + path::join(cfg.logging_dir, std::format("{:%Y-%m-%d_%H-%M-%S}_{}", now, pid)); + logging::file_logger("master", session_log_dir, logging::options); + } + + LOG_INFO("Server ready (stateful={}, stateless={}, idle={}ms)", + cfg.stateful_worker_count.value, + cfg.stateless_worker_count.value, + *cfg.idle_timeout_ms); + + WorkerPoolOptions pool_opts; + pool_opts.self_path = self_path; + pool_opts.stateful_count = cfg.stateful_worker_count; + pool_opts.stateless_count = cfg.stateless_worker_count; + pool_opts.worker_memory_limit = cfg.worker_memory_limit; + pool_opts.log_dir = session_log_dir; + if(!pool.start(pool_opts)) { + LOG_ERROR("Failed to start worker pool"); + return; + } + lifecycle = ServerLifecycle::Ready; + + compiler.on_indexing_needed = [this]() { + indexer.schedule(); + }; + + indexer.set_max_concurrency(cfg.stateless_worker_count.value); + load_workspace(); } +Session* MasterServer::find_session(std::uint32_t path_id) { + auto it = sessions.find(path_id); + return it != sessions.end() ? &it->second : nullptr; +} + +Session& MasterServer::open_session(std::uint32_t path_id) { + auto [it, inserted] = sessions.try_emplace(path_id); + auto& session = it->second; + if(!inserted) + session = Session{}; + session.path_id = path_id; + return session; +} + +void MasterServer::close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer) { + namespace protocol = kota::ipc::protocol; + + auto path = workspace.path_pool.resolve(path_id); + workspace.on_file_closed(path_id); + pool.notify_stateful(path_id, worker::EvictParams{std::string(path)}); + + protocol::PublishDiagnosticsParams diag_params; + auto uri = lsp::URI::from_file_path(std::string(path)); + if(uri) + diag_params.uri = uri->str(); + diag_params.diagnostics = {}; + peer.send_notification(diag_params); + + sessions.erase(path_id); + + indexer.enqueue(path_id); + indexer.schedule(); + + LOG_DEBUG("didClose: {}", path); +} + +void MasterServer::on_file_saved(std::uint32_t path_id) { + auto dirtied = workspace.on_file_saved(path_id); + for(auto dirty_id: dirtied) { + if(auto* session = find_session(dirty_id)) { + session->ast_dirty = true; + } else { + indexer.enqueue(dirty_id); + } + } + + for(auto& [hdr_id, session]: sessions) { + if(session.header_context && session.header_context->host_path_id == path_id) { + session.header_context.reset(); + session.ast_dirty = true; + } + } + + indexer.schedule(); +} + +void MasterServer::schedule_shutdown() { + indexer.save(workspace.config.project.index_dir); + workspace.save_cache(); + + loop.schedule([this]() -> kota::task<> { + co_await compiler.stop(); + co_await pool.stop(); + loop.stop(); + }()); +} + void MasterServer::load_workspace() { if(workspace_root.empty()) return; @@ -179,6 +295,7 @@ static kota::task<> accept_connections(MasterServer& server, std::list& connections) { auto& loop = kota::event_loop::current(); kota::task_group<> connection_group(loop); + bool lsp_registered = false; while(true) { auto conn = co_await acceptor.accept(); @@ -191,8 +308,10 @@ static kota::task<> accept_connections(MasterServer& server, auto peer = std::make_unique(loop, std::move(transport)); std::unique_ptr lsp; - if(register_lsp) + if(register_lsp && !lsp_registered) { lsp = std::make_unique(server, *peer); + lsp_registered = true; + } auto agent = std::make_unique(server, *peer); auto* peer_ptr = peer.get(); diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h index 8c1938eac..0d76c8d1e 100644 --- a/src/server/service/master_server.h +++ b/src/server/service/master_server.h @@ -11,6 +11,7 @@ #include "kota/async/async.h" #include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringRef.h" namespace clice { @@ -26,16 +27,108 @@ enum class ServerLifecycle : std::uint8_t { /// the worker pool, compilation engine, and indexer. /// /// Does NOT own any transport or peer. Protocol-specific handler registration -/// is done by LSPClient and AgentClient, which access server state via friend. +/// is done by LSPClient and AgentClient, which call public methods here. class MasterServer { - friend class LSPClient; - friend class AgentClient; - public: MasterServer(kota::event_loop& loop, std::string self_path); ~MasterServer(); - void initialize(llvm::StringRef root); + // --- Lifecycle --- + + ServerLifecycle get_lifecycle() const { + return lifecycle; + } + + void set_lifecycle(ServerLifecycle state) { + lifecycle = state; + } + + // --- Initialization --- + + void set_workspace_root(std::string root) { + workspace_root = std::move(root); + } + + llvm::StringRef get_workspace_root() const { + return workspace_root; + } + + void set_init_options(std::string json) { + init_options_json = std::move(json); + } + + void set_session_log_dir(std::string dir) { + session_log_dir = std::move(dir); + } + + llvm::StringRef get_session_log_dir() const { + return session_log_dir; + } + + llvm::StringRef get_self_path() const { + return self_path; + } + + /// Apply initialization options to config and prepare workspace. + /// Called from the LSP `initialized` handler. + void initialize(); + + // --- Path operations --- + + std::uint32_t intern_path(llvm::StringRef path) { + return workspace.path_pool.intern(path); + } + + llvm::StringRef resolve_path(std::uint32_t path_id) { + return workspace.path_pool.resolve(path_id); + } + + // --- Session management --- + + Session* find_session(std::uint32_t path_id); + + Session& open_session(std::uint32_t path_id); + + void close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer); + + /// Called on didSave — marks dependent sessions dirty and enqueues + /// affected files for re-indexing. + void on_file_saved(std::uint32_t path_id); + + /// Iterate all sessions (e.g. for header context invalidation). + template + void for_each_session(F&& fn) { + for(auto& [id, session]: sessions) { + fn(id, session); + } + } + + // --- Shutdown --- + + /// Schedule graceful shutdown (stop compiler, pool, event loop). + void schedule_shutdown(); + + // --- Sub-component access --- + + Workspace& get_workspace() { + return workspace; + } + + Compiler& get_compiler() { + return compiler; + } + + Indexer& get_indexer() { + return indexer; + } + + WorkerPool& get_pool() { + return pool; + } + + kota::event_loop& get_loop() { + return loop; + } private: void load_workspace(); diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py index 2226ccccb..3eff6c074 100644 --- a/tests/integration/agentic/test_agentic.py +++ b/tests/integration/agentic/test_agentic.py @@ -1,7 +1,9 @@ """Tests for the agentic CLI client.""" import json +import socket import subprocess +from concurrent.futures import ThreadPoolExecutor import pytest @@ -56,3 +58,30 @@ async def test_multiple_requests(agentic, workspace): assert result.returncode == 0, f"stderr: {result.stderr}" data = json.loads(result.stdout) assert data["file"] == main_cpp + + +async def test_connection_refused(executable): + """Connecting to a port with no server should fail with non-zero exit.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + free_port = s.getsockname()[1] + result = run_agentic(executable, "127.0.0.1", free_port, "/some/file.cpp") + assert result.returncode != 0 + + +@pytest.mark.workspace("hello_world") +async def test_concurrent_connections(agentic, workspace): + """Multiple agentic clients connecting simultaneously should all succeed.""" + executable, host, port = agentic + main_cpp = (workspace / "main.cpp").as_posix() + + def do_request(_): + return run_agentic(executable, host, port, main_cpp) + + with ThreadPoolExecutor(max_workers=4) as pool: + results = list(pool.map(do_request, range(4))) + + for r in results: + assert r.returncode == 0, f"stderr: {r.stderr}" + data = json.loads(r.stdout) + assert data["file"] == main_cpp diff --git a/tests/integration/utils/client.py b/tests/integration/utils/client.py index 7b4319c04..58bcace66 100644 --- a/tests/integration/utils/client.py +++ b/tests/integration/utils/client.py @@ -92,13 +92,18 @@ async def initialize( *, initialization_options: dict | None = None, ) -> InitializeResult: + if initialization_options is None: + initialization_options = {} + project = dict(initialization_options.get("project", {})) + project.setdefault("cache_dir", str(workspace / ".clice")) + initialization_options["project"] = project + params = InitializeParams( capabilities=ClientCapabilities(), root_uri=workspace.as_uri(), workspace_folders=[WorkspaceFolder(uri=workspace.as_uri(), name="test")], ) - if initialization_options is not None: - params.initialization_options = initialization_options + params.initialization_options = initialization_options result = await self.initialize_async(params) self.initialized(InitializedParams()) self.init_result = result From 426d679becd3a28360fa276eaa0a31ff41645df4 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sat, 2 May 2026 00:39:17 +0800 Subject: [PATCH 12/12] refactor(server): revert MasterServer to friend class, remove separator comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trivial getters/setters on MasterServer added no value over direct member access via friend — revert to `friend class LSPClient; friend class AgentClient;`. Also remove all `// --- ... ---` decorative separator comments project-wide. Co-Authored-By: Claude Opus 4.6 --- src/compile/tidy.cpp | 4 - src/semantic/resolver.cpp | 2 - src/server/service/agent_client.cpp | 2 +- src/server/service/lsp_client.cpp | 250 +++++++++--------- src/server/service/master_server.h | 92 +------ .../unit/semantic/template_resolver_tests.cpp | 2 - tests/unit/server/module_worker_tests.cpp | 2 - tests/unit/server/pch_worker_tests.cpp | 2 - tests/unit/syntax/scan_tests.cpp | 7 +- 9 files changed, 129 insertions(+), 234 deletions(-) diff --git a/src/compile/tidy.cpp b/src/compile/tidy.cpp index 1b516e38d..b0e180e2c 100644 --- a/src/compile/tidy.cpp +++ b/src/compile/tidy.cpp @@ -92,15 +92,11 @@ tidy::ClangTidyOptions create_options() { // include-cleaner is directly integrated in IncludeCleaner.cpp "-misc-include-cleaner", - // ----- False Positives ----- - // Check relies on seeing ifndef/define/endif directives, // clangd doesn't replay those when using a preamble. "-llvm-header-guard", "-modernize-macro-to-enum", - // ----- Crashing Checks ----- - // Check can choke on invalid (intermediate) c++ // code, which is often the case when clangd // tries to build an AST. diff --git a/src/semantic/resolver.cpp b/src/semantic/resolver.cpp index 49875212c..120e523dd 100644 --- a/src/semantic/resolver.cpp +++ b/src/semantic/resolver.cpp @@ -1111,8 +1111,6 @@ class PseudoInstantiator : public clang::TreeTransform { return Base::TransformDecltypeType(TLB, TL); } - // --- State --- - private: clang::Sema& sema; clang::ASTContext& context; diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp index 85572a4ff..bae366def 100644 --- a/src/server/service/agent_client.cpp +++ b/src/server/service/agent_client.cpp @@ -21,7 +21,7 @@ AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) : const CompileCommandParams& params) -> RequestResult { std::string directory; std::vector arguments; - if(!this->server.get_compiler().fill_compile_args(params.path, directory, arguments)) { + if(!this->server.compiler.fill_compile_args(params.path, directory, arguments)) { co_return kota::outcome_error( kota::ipc::Error{std::format("no compile command found for {}", params.path)}); } diff --git a/src/server/service/lsp_client.cpp b/src/server/service/lsp_client.cpp index 587487765..d71e41446 100644 --- a/src/server/service/lsp_client.cpp +++ b/src/server/service/lsp_client.cpp @@ -37,32 +37,32 @@ static serde_raw to_raw(const T& value) { } LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(server), peer(peer) { - server.get_compiler().set_peer(&peer); - server.get_indexer().set_peer(&peer); + server.compiler.set_peer(&peer); + server.indexer.set_peer(&peer); using StringVec = std::vector; peer.on_request([this](RequestContext& ctx, const protocol::InitializeParams& params) -> RequestResult { auto& srv = this->server; - if(srv.get_lifecycle() != ServerLifecycle::Uninitialized) { + if(srv.lifecycle != ServerLifecycle::Uninitialized) { co_return kota::outcome_error(protocol::Error{"Server already initialized"}); } auto& init = params.lsp__initialize_params; if(init.root_uri.has_value()) { - srv.set_workspace_root(uri_to_path(*init.root_uri)); + srv.workspace_root = uri_to_path(*init.root_uri); } if(init.initialization_options.has_value()) { auto json = kota::codec::json::to_json(*init.initialization_options); if(json) - srv.set_init_options(std::move(*json)); + srv.init_options_json = std::move(*json); } - srv.set_lifecycle(ServerLifecycle::Initialized); - LOG_INFO("Initialized with workspace: {}", srv.get_workspace_root()); + srv.lifecycle = ServerLifecycle::Initialized; + LOG_INFO("Initialized with workspace: {}", srv.workspace_root); protocol::InitializeResult result; auto& caps = result.capabilities; @@ -146,25 +146,25 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_request( [this](RequestContext& ctx, const protocol::ShutdownParams& params) -> RequestResult { - this->server.set_lifecycle(ServerLifecycle::ShuttingDown); + this->server.lifecycle = ServerLifecycle::ShuttingDown; LOG_INFO("Shutdown requested"); co_return nullptr; }); peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) { auto& srv = this->server; - srv.set_lifecycle(ServerLifecycle::Exited); + srv.lifecycle = ServerLifecycle::Exited; LOG_INFO("Exit notification received"); srv.schedule_shutdown(); }); peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) { auto& srv = this->server; - if(srv.get_lifecycle() != ServerLifecycle::Ready) + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto& session = srv.open_session(path_id); session.version = params.text_document.version; @@ -176,11 +176,11 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_notification([this](const protocol::DidChangeTextDocumentParams& params) { auto& srv = this->server; - if(srv.get_lifecycle() != ServerLifecycle::Ready) + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) @@ -219,25 +219,25 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s worker::DocumentUpdateParams update; update.path = path; update.version = session->version; - srv.get_pool().notify_stateful(path_id, update); + srv.pool.notify_stateful(path_id, update); }); peer.on_notification([this](const protocol::DidCloseTextDocumentParams& params) { auto& srv = this->server; - if(srv.get_lifecycle() != ServerLifecycle::Ready) + if(srv.lifecycle != ServerLifecycle::Ready) return; - auto path_id = srv.intern_path(uri_to_path(params.text_document.uri)); + auto path_id = srv.workspace.path_pool.intern(uri_to_path(params.text_document.uri)); srv.close_session(path_id, this->peer); }); peer.on_notification([this](const protocol::DidSaveTextDocumentParams& params) { auto& srv = this->server; - if(srv.get_lifecycle() != ServerLifecycle::Ready) + if(srv.lifecycle != ServerLifecycle::Ready) return; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); srv.on_file_saved(path_id); LOG_DEBUG("didSave: {}", path); @@ -246,107 +246,103 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_request([this](RequestContext& ctx, const protocol::HoverParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query( + co_return co_await srv.compiler.forward_query( worker::QueryKind::Hover, *session, params.text_document_position_params.position); }); - peer.on_request( - [this](RequestContext& ctx, const protocol::SemanticTokensParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); - auto* session = srv.find_session(path_id); - if(!session) - co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::SemanticTokens, - *session); - }); + peer.on_request([this](RequestContext& ctx, + const protocol::SemanticTokensParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + co_return co_await srv.compiler.forward_query(worker::QueryKind::SemanticTokens, *session); + }); peer.on_request( [this](RequestContext& ctx, const protocol::InlayHintParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::InlayHints, - *session, - {}, - params.range); - }); - - peer.on_request( - [this](RequestContext& ctx, const protocol::FoldingRangeParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); - auto* session = srv.find_session(path_id); - if(!session) - co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::FoldingRange, - *session); - }); - - peer.on_request( - [this](RequestContext& ctx, const protocol::DocumentSymbolParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); - auto* session = srv.find_session(path_id); - if(!session) - co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::DocumentSymbol, - *session); + co_return co_await srv.compiler.forward_query(worker::QueryKind::InlayHints, + *session, + {}, + params.range); }); peer.on_request([this](RequestContext& ctx, - const protocol::DocumentLinkParams& params) -> RawResult { + const protocol::FoldingRangeParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - auto result = - co_await srv.get_compiler().forward_query(worker::QueryKind::DocumentLink, *session); - if(!result.has_value()) + co_return co_await srv.compiler.forward_query(worker::QueryKind::FoldingRange, *session); + }); + + peer.on_request([this](RequestContext& ctx, + const protocol::DocumentSymbolParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) co_return serde_raw{"null"}; - auto& links = result.value(); - auto* session2 = srv.find_session(path_id); - if(session2 && session2->pch_ref) { - auto& pch_cache = srv.get_workspace().pch_cache; - auto pch_it = pch_cache.find(session2->pch_ref->path_id); - if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) { - auto& pch_json = pch_it->second.document_links_json; - if(!links.data.empty() && links.data != "null" && links.data.size() > 2) { - links.data.pop_back(); - links.data += ','; - links.data.append(pch_json.begin() + 1, pch_json.end()); - } else { - links.data = pch_json; + co_return co_await srv.compiler.forward_query(worker::QueryKind::DocumentSymbol, *session); + }); + + peer.on_request( + [this](RequestContext& ctx, const protocol::DocumentLinkParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + auto result = + co_await srv.compiler.forward_query(worker::QueryKind::DocumentLink, *session); + if(!result.has_value()) + co_return serde_raw{"null"}; + auto& links = result.value(); + auto* session2 = srv.find_session(path_id); + if(session2 && session2->pch_ref) { + auto& pch_cache = srv.workspace.pch_cache; + auto pch_it = pch_cache.find(session2->pch_ref->path_id); + if(pch_it != pch_cache.end() && !pch_it->second.document_links_json.empty()) { + auto& pch_json = pch_it->second.document_links_json; + if(!links.data.empty() && links.data != "null" && links.data.size() > 2) { + links.data.pop_back(); + links.data += ','; + links.data.append(pch_json.begin() + 1, pch_json.end()); + } else { + links.data = pch_json; + } } } - } - co_return std::move(links); - }); + co_return std::move(links); + }); peer.on_request( [this](RequestContext& ctx, const protocol::CodeActionParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::CodeAction, - *session); + co_return co_await srv.compiler.forward_query(worker::QueryKind::CodeAction, *session); }); auto resolve_uri = [this](const std::string& uri) { @@ -356,21 +352,21 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s Session* session; }; auto path = uri_to_path(uri); - auto path_id = this->server.intern_path(path); + auto path_id = this->server.workspace.path_pool.intern(path); auto* session = this->server.find_session(path_id); return Result{std::move(path), path_id, session}; }; auto lookup_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos) { auto [path, path_id, session] = resolve_uri(uri); - return this->server.get_indexer().lookup_symbol(uri, path, pos, session); + return this->server.indexer.lookup_symbol(uri, path, pos, session); }; auto query_at = [this, resolve_uri](const std::string& uri, const protocol::Position& pos, RelationKind kind) -> std::vector { auto [path, path_id, session] = resolve_uri(uri); - return this->server.get_indexer().query_relations(path, pos, kind, session); + return this->server.indexer.query_relations(path, pos, kind, session); }; auto resolve_item = @@ -379,7 +375,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s const protocol::Range& range, const std::optional& data) -> std::optional { auto [path, path_id, session] = resolve_uri(uri); - return this->server.get_indexer().resolve_hierarchy_item(uri, path, range, data, session); + return this->server.indexer.resolve_hierarchy_item(uri, path, range, data, session); }; peer.on_request([this, query_at](RequestContext& ctx, @@ -394,13 +390,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto& srv = this->server; auto path = uri_to_path(uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - co_return co_await srv.get_compiler().forward_query(worker::QueryKind::GoToDefinition, - *session, - pos); + co_return co_await srv.compiler.forward_query(worker::QueryKind::GoToDefinition, + *session, + pos); }); peer.on_request([this, query_at](RequestContext& ctx, @@ -437,37 +433,37 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s co_return serde_raw{"null"}; }); - peer.on_request( - [this](RequestContext& ctx, const protocol::CompletionParams& params) -> RawResult { - auto& srv = this->server; - auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.intern_path(path); - auto* session = srv.find_session(path_id); - if(!session) - co_return serde_raw{"null"}; - auto pause = srv.get_indexer().scoped_pause(); - auto result = co_await srv.get_compiler().handle_completion( - params.text_document_position_params.position, - *session); - co_return std::move(result); - }); - peer.on_request([this](RequestContext& ctx, - const protocol::SignatureHelpParams& params) -> RawResult { + const protocol::CompletionParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.text_document_position_params.text_document.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto* session = srv.find_session(path_id); if(!session) co_return serde_raw{"null"}; - auto pause = srv.get_indexer().scoped_pause(); + auto pause = srv.indexer.scoped_pause(); auto result = - co_await srv.get_compiler().forward_build(worker::BuildKind::SignatureHelp, - params.text_document_position_params.position, - *session); + co_await srv.compiler.handle_completion(params.text_document_position_params.position, + *session); co_return std::move(result); }); + peer.on_request( + [this](RequestContext& ctx, const protocol::SignatureHelpParams& params) -> RawResult { + auto& srv = this->server; + auto path = uri_to_path(params.text_document_position_params.text_document.uri); + auto path_id = srv.workspace.path_pool.intern(path); + auto* session = srv.find_session(path_id); + if(!session) + co_return serde_raw{"null"}; + auto pause = srv.indexer.scoped_pause(); + auto result = + co_await srv.compiler.forward_build(worker::BuildKind::SignatureHelp, + params.text_document_position_params.position, + *session); + co_return std::move(result); + }); + peer.on_request( [this, lookup_at](RequestContext& ctx, const protocol::CallHierarchyPrepareParams& params) -> RawResult { @@ -491,7 +487,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.get_indexer().find_incoming_calls(info->hash); + auto results = this->server.indexer.find_incoming_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -503,7 +499,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.get_indexer().find_outgoing_calls(info->hash); + auto results = this->server.indexer.find_outgoing_calls(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -533,7 +529,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.get_indexer().find_supertypes(info->hash); + auto results = this->server.indexer.find_supertypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -545,7 +541,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s auto info = resolve_item(params.item.uri, params.item.range, params.item.data); if(!info) co_return serde_raw{"null"}; - auto results = this->server.get_indexer().find_subtypes(info->hash); + auto results = this->server.indexer.find_subtypes(info->hash); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -553,7 +549,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s peer.on_request( [this](RequestContext& ctx, const protocol::WorkspaceSymbolParams& params) -> RawResult { - auto results = this->server.get_indexer().search_symbols(params.query); + auto results = this->server.indexer.search_symbols(params.query); if(results.empty()) co_return serde_raw{"null"}; co_return to_raw(results); @@ -564,14 +560,14 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::QueryContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); int offset_val = std::max(0, params.offset.value_or(0)); constexpr int page_size = 10; ext::QueryContextResult result; std::vector all_items; - auto& ws = srv.get_workspace(); + auto& ws = srv.workspace; auto hosts = ws.dep_graph.find_host_sources(path_id); for(auto host_id: hosts) { auto host_path = ws.path_pool.resolve(host_id); @@ -633,12 +629,12 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::CurrentContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); ext::CurrentContextResult result; auto* session = srv.find_session(path_id); if(session && session->active_context) { - auto ctx_path = srv.resolve_path(*session->active_context); + auto ctx_path = srv.workspace.path_pool.resolve(*session->active_context); auto ctx_uri_opt = lsp::URI::from_file_path(std::string(ctx_path)); if(ctx_uri_opt) { ext::ContextItem item; @@ -656,13 +652,13 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s [this](RequestContext& ctx, const ext::SwitchContextParams& params) -> RawResult { auto& srv = this->server; auto path = uri_to_path(params.uri); - auto path_id = srv.intern_path(path); + auto path_id = srv.workspace.path_pool.intern(path); auto context_path = uri_to_path(params.context_uri); - auto context_path_id = srv.intern_path(context_path); + auto context_path_id = srv.workspace.path_pool.intern(context_path); ext::SwitchContextResult result; - auto& ws = srv.get_workspace(); + auto& ws = srv.workspace; auto context_cdb = ws.cdb.lookup(context_path, {.suppress_logging = true}); if(context_cdb.empty()) { result.success = false; @@ -687,8 +683,8 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s } LSPClient::~LSPClient() { - server.get_compiler().set_peer(nullptr); - server.get_indexer().set_peer(nullptr); + server.compiler.set_peer(nullptr); + server.indexer.set_peer(nullptr); } } // namespace clice diff --git a/src/server/service/master_server.h b/src/server/service/master_server.h index 0d76c8d1e..4e7566ee6 100644 --- a/src/server/service/master_server.h +++ b/src/server/service/master_server.h @@ -27,109 +27,25 @@ enum class ServerLifecycle : std::uint8_t { /// the worker pool, compilation engine, and indexer. /// /// Does NOT own any transport or peer. Protocol-specific handler registration -/// is done by LSPClient and AgentClient, which call public methods here. +/// is done by LSPClient and AgentClient, which access private members directly. class MasterServer { + friend class LSPClient; + friend class AgentClient; + public: MasterServer(kota::event_loop& loop, std::string self_path); ~MasterServer(); - // --- Lifecycle --- - - ServerLifecycle get_lifecycle() const { - return lifecycle; - } - - void set_lifecycle(ServerLifecycle state) { - lifecycle = state; - } - - // --- Initialization --- - - void set_workspace_root(std::string root) { - workspace_root = std::move(root); - } - - llvm::StringRef get_workspace_root() const { - return workspace_root; - } - - void set_init_options(std::string json) { - init_options_json = std::move(json); - } - - void set_session_log_dir(std::string dir) { - session_log_dir = std::move(dir); - } - - llvm::StringRef get_session_log_dir() const { - return session_log_dir; - } - - llvm::StringRef get_self_path() const { - return self_path; - } - - /// Apply initialization options to config and prepare workspace. - /// Called from the LSP `initialized` handler. void initialize(); - // --- Path operations --- - - std::uint32_t intern_path(llvm::StringRef path) { - return workspace.path_pool.intern(path); - } - - llvm::StringRef resolve_path(std::uint32_t path_id) { - return workspace.path_pool.resolve(path_id); - } - - // --- Session management --- - Session* find_session(std::uint32_t path_id); - Session& open_session(std::uint32_t path_id); - void close_session(std::uint32_t path_id, kota::ipc::JsonPeer& peer); - /// Called on didSave — marks dependent sessions dirty and enqueues - /// affected files for re-indexing. void on_file_saved(std::uint32_t path_id); - /// Iterate all sessions (e.g. for header context invalidation). - template - void for_each_session(F&& fn) { - for(auto& [id, session]: sessions) { - fn(id, session); - } - } - - // --- Shutdown --- - - /// Schedule graceful shutdown (stop compiler, pool, event loop). void schedule_shutdown(); - // --- Sub-component access --- - - Workspace& get_workspace() { - return workspace; - } - - Compiler& get_compiler() { - return compiler; - } - - Indexer& get_indexer() { - return indexer; - } - - WorkerPool& get_pool() { - return pool; - } - - kota::event_loop& get_loop() { - return loop; - } - private: void load_workspace(); diff --git a/tests/unit/semantic/template_resolver_tests.cpp b/tests/unit/semantic/template_resolver_tests.cpp index 6bfe3c067..3ca2a4744 100644 --- a/tests/unit/semantic/template_resolver_tests.cpp +++ b/tests/unit/semantic/template_resolver_tests.cpp @@ -456,8 +456,6 @@ TEST_CASE(BasePackExpansion) { )code"); } -// --- Robustness tests for edge cases found during stress testing --- - TEST_CASE(RecursiveBaseClass) { // Regression test: callback_traits inherits callback_traits, // creating infinite recursion through lookupInBases. CTD cycle detection must bail out. diff --git a/tests/unit/server/module_worker_tests.cpp b/tests/unit/server/module_worker_tests.cpp index 5ddfc0896..5d115ded9 100644 --- a/tests/unit/server/module_worker_tests.cpp +++ b/tests/unit/server/module_worker_tests.cpp @@ -29,7 +29,6 @@ TEST_CASE(BuildPCMThenCompileWithImport) { tmp.touch("consumer.cpp", "import Hello;\n" "int main() { return hello()[0]; }\n"); auto consumer = tmp.path("consumer.cpp"); - // --- Phase 1: Build PCM via stateless worker --- WorkerHandle sl; ASSERT_TRUE(sl.spawn("stateless-worker")); @@ -63,7 +62,6 @@ TEST_CASE(BuildPCMThenCompileWithImport) { ASSERT_TRUE(phase1_done); ASSERT_FALSE(pcm_path.empty()); - // --- Phase 2: Compile consumer with the PCM via stateful worker --- WorkerHandle sf; ASSERT_TRUE(sf.spawn("stateful-worker")); diff --git a/tests/unit/server/pch_worker_tests.cpp b/tests/unit/server/pch_worker_tests.cpp index 279e8dab0..2d2a1cae0 100644 --- a/tests/unit/server/pch_worker_tests.cpp +++ b/tests/unit/server/pch_worker_tests.cpp @@ -30,7 +30,6 @@ TEST_CASE(BuildPCHThenCompile) { auto dir = std::string(tmp.root); - // --- Phase 1: Build PCH via stateless worker --- WorkerHandle sl; ASSERT_TRUE(sl.spawn("stateless-worker")); @@ -69,7 +68,6 @@ TEST_CASE(BuildPCHThenCompile) { // Verify the PCH file exists on disk. ASSERT_TRUE(llvm::sys::fs::exists(pch_path)); - // --- Phase 2: Compile with PCH via stateful worker --- WorkerHandle sf; ASSERT_TRUE(sf.spawn("stateful-worker")); diff --git a/tests/unit/syntax/scan_tests.cpp b/tests/unit/syntax/scan_tests.cpp index 347fd2843..cca19291f 100644 --- a/tests/unit/syntax/scan_tests.cpp +++ b/tests/unit/syntax/scan_tests.cpp @@ -291,8 +291,6 @@ int x; TEST_SUITE(PreambleComplete) { -// --- #include completeness --- - TEST_CASE(CompleteQuotedInclude) { llvm::StringRef content = "#include \"foo.h\"\nint x;"; auto bound = compute_preamble_bound(content); @@ -341,8 +339,7 @@ TEST_CASE(MultipleIncludesLastIncomplete) { EXPECT_FALSE(is_preamble_complete(content, bound)); } -// --- C++20 module statements --- -// Note: compute_preamble_bound does not include import/export lines in its +// compute_preamble_bound does not include import/export lines in its // bound, so we pass manual bounds covering the relevant lines. TEST_CASE(CompleteImport) { @@ -381,8 +378,6 @@ TEST_CASE(CompleteExportImport) { EXPECT_TRUE(is_preamble_complete(content, 19)); } -// --- Edge cases --- - TEST_CASE(EmptyPreamble) { llvm::StringRef content = "int x;"; EXPECT_TRUE(is_preamble_complete(content, 0));