# LSP Core Lifecycle Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 LSP core 改为严格的生命周期状态机、可取消异步请求、规范 JSON-RPC 错误和不可恢复 framing,并移除旧 Provider/兜底路径。 **Architecture:** `LspServer` 独占生命周期和控制消息,Dispatcher 在构造时绑定 manager/scheduler,仅负责 Provider 路由。生产入口与测试入口分别注入 Provider 注册器;测试入口仍运行同一个 server 和 stdio framing,以确定性阻塞/异常 Provider 验证异步取消和错误响应。 **Tech Stack:** C++23 Modules、Glaze、Taskflow/AsyncExecutor、LSP 3.17、Tree-sitter、CMake/CTest、Python 3 --- ## Plan Meta - **Plan Group:** `lsp-core-lifecycle` - **Parent Plan:** none - **Verification Scope:** `tsl-server`、`test_provider`、`test_scheduler`、`test_core_server`、`test_cli_startup`、LSP JSON transport smoke - **Verification Gate:** 目标全部构建成功;上述 CTest 全部通过;core 无生命周期 bool、取消空分支、`"{}"`/固定 JSON 兜底或已删除 Provider 引用 - **Executor:** `executing-plans` - **Constraints:** `karpathy-guidelines`、`.agents`、`AGENT_RULES.md`、不保留兼容层、所有提交使用规范 emoji - **Worktree Note:** 当前工作区已有用户改动;保留所有无关修改。`lsp-server/src/cli/launcher.cppm` 已有 namespace/args_parser 改动,提交时只暂存本 Plan 新增的 registrar 和 `Run()` 退出码 hunk。 ### Task 1: 建立 core 生命周期所有权和可测试服务器入口 **Files:** - Create: `lsp-server/test/test_provider/core_server_fixture.cppm` - Create: `lsp-server/test/test_core_server.py` - Modify: `lsp-server/src/core/dispatcher.cppm` - Modify: `lsp-server/src/core/server.cppm` - Modify: `lsp-server/src/cli/launcher.cppm` - Modify: `lsp-server/src/provider/base/interface.cppm` - Modify: `lsp-server/src/provider/initialize/initialize.cppm` - Modify: `lsp-server/src/provider/manifest.cppm` - Delete: `lsp-server/src/provider/shutdown/shutdown.cppm` - Delete: `lsp-server/src/provider/exit/exit.cppm` - Delete: `lsp-server/src/provider/cancel_request/cancel_request.cppm` - Modify: `lsp-server/test/test_provider/test_main.cppm` - Modify: `lsp-server/test/test_provider/provider_misc_test.cppm` - Modify: `lsp-server/test/test_provider/provider_surface_test.cppm` - Modify: `lsp-server/test/test_provider/json_provider_coverage_test.cppm` - Modify: `lsp-server/test/test_provider/CMakeLists.txt` - Modify: `lsp-server/test/CMakeLists.txt` - [ ] **Step 1: 写生命周期 RED 测试和测试服务器模式** 在 `core_server_fixture.cppm` 定义只用于测试进程的初始化 Provider,并通过正式 `LspServer` 运行: ```cpp export module lsp.test.provider.core_server_fixture; import std; import spdlog; import lsp.codec.facade; import lsp.core.server; import lsp.provider.base.interface; import lsp.protocol; export namespace lsp::test::provider { int RunCoreServerFixture(); } namespace lsp::test::provider { class FixtureInitialize final : public core::IRequestProvider { public: std::string GetMethod() const override { return "initialize"; } std::string GetProviderName() const override { return "FixtureInitialize"; } std::string ProvideResponse(const protocol::RequestMessage& request, core::ExecutionContext&) override { protocol::ResponseMessage response; response.id = request.id; response.result = protocol::LSPAny(protocol::LSPObject{}); return codec::Serialize(response).value(); } }; int RunCoreServerFixture() { spdlog::set_level(spdlog::level::off); core::LspServer server( std::cin, std::cout, [](core::RequestDispatcher& dispatcher) { dispatcher.RegisterRequestProvider( std::make_shared()); }, 2, ""); return server.Run(); } } ``` 在 `test_main.cppm` 的正常测试输出前处理该模式: ```cpp if (arg == "--core-server-fixture") return lsp::test::provider::RunCoreServerFixture(); ``` 在 `test_core_server.py` 添加 framing 工具和生命周期用例: ```python def frame(message: dict) -> bytes: body = json.dumps(message, separators=(",", ":")).encode("utf-8") return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body def read_messages(data: bytes) -> list[dict]: messages = [] offset = 0 while offset < len(data): header_end = data.find(b"\r\n\r\n", offset) assert header_end >= 0 header = data[offset:header_end].decode("ascii") fields = dict(line.split(": ", 1) for line in header.split("\r\n")) length = int(fields["Content-Length"]) body_start = header_end + 4 body_end = body_start + length assert body_end <= len(data) messages.append(json.loads(data[body_start:body_end].decode("utf-8"))) offset = body_end return messages def response_by_id(data: bytes, request_id: int | str) -> dict: return next(message for message in read_messages(data) if message.get("id") == request_id) def run_raw(server: Path, payload: bytes) -> subprocess.CompletedProcess[bytes]: return subprocess.run( [str(server), "--core-server-fixture"], input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5, check=False, ) def run_batch(server: Path, messages: list[dict]) -> subprocess.CompletedProcess[bytes]: payload = b"".join(frame(message) for message in messages) return run_raw(server, payload) class LspClient: def __init__(self, server: Path) -> None: self.proc = subprocess.Popen( [str(server), "--core-server-fixture"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) self.buffer = b"" def send(self, message: dict) -> None: assert self.proc.stdin is not None self.proc.stdin.write(frame(message)) self.proc.stdin.flush() def read(self, timeout: float = 1.0) -> dict: assert self.proc.stdout is not None deadline = time.monotonic() + timeout while True: header_end = self.buffer.find(b"\r\n\r\n") if header_end >= 0: header = self.buffer[:header_end].decode("ascii") fields = dict(line.split(": ", 1) for line in header.split("\r\n")) length = int(fields["Content-Length"]) body_start = header_end + 4 body_end = body_start + length if len(self.buffer) >= body_end: body = self.buffer[body_start:body_end] self.buffer = self.buffer[body_end:] return json.loads(body.decode("utf-8")) remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError("timed out waiting for LSP response") ready, _, _ = select.select( [self.proc.stdout.fileno()], [], [], remaining) if not ready: continue chunk = os.read(self.proc.stdout.fileno(), 4096) if not chunk: raise RuntimeError("fixture closed stdout before a response") self.buffer += chunk def close_input(self) -> int: assert self.proc.stdin is not None self.proc.stdin.close() return self.proc.wait(timeout=5) def assert_lifecycle(server: Path) -> None: before_init = run_batch(server, [ {"jsonrpc": "2.0", "id": 1, "method": "shutdown"}, {"jsonrpc": "2.0", "method": "exit"}, ]) assert before_init.returncode == 1 assert response_by_id(before_init.stdout, 1)["error"]["code"] == -32002 repeated = run_batch(server, [ {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}, {"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {}}, {"jsonrpc": "2.0", "id": 3, "method": "shutdown"}, {"jsonrpc": "2.0", "method": "exit"}, ]) assert repeated.returncode == 0 assert response_by_id(repeated.stdout, 2)["error"]["code"] == -32600 assert response_by_id(repeated.stdout, 3)["result"] is None direct_exit = run_batch( server, [{"jsonrpc": "2.0", "method": "exit"}]) assert direct_exit.returncode == 1 client = LspClient(server) client.send({"jsonrpc": "2.0", "id": 11, "method": "initialize", "params": {}}) assert client.read()["id"] == 11 client.send({"jsonrpc": "2.0", "id": 12, "method": "shutdown"}) assert client.read()["result"] is None assert client.proc.poll() is None client.send({"jsonrpc": "2.0", "method": "exit"}) assert client.close_input() == 0 ``` 脚本导入 `argparse`、`json`、`os`、`select`、`subprocess`、`time` 和 `Path`, 解析必填 `--server` 后调用 `assert_lifecycle(Path(args.server))` 并返回 0。响应 解析严格按 `Content-Length` 切分,不按换行猜测 JSON 边界。 在 `test_provider/CMakeLists.txt` 的 source 和 Module file-set 中加入 fixture、 `server.cppm`、`bridge/win32_stdio.cppm`、`manager/bootstrap.cppm`;在父级 `test/CMakeLists.txt` 的 Python 分支注册: ```cmake add_test(NAME test_core_server COMMAND ${PYTHON3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/test_core_server.py --server $) ``` - [ ] **Step 2: 运行测试并确认 RED** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ``` Expected: FAIL,`LspServer` 还没有 stream/registrar 构造函数且 `Run()` 仍返回 `void`;失败来自新生命周期接口,而不是 Python 语法错误。 - [ ] **Step 3: 收紧 Dispatcher 构造与错误响应接口** 将 Dispatcher 改为构造即有效: ```cpp class ExecutionContext { public: ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler, manager::ManagerHub& manager_hub) : async_executor_(scheduler), manager_hub_(manager_hub) {} scheduler::async_executor::AsyncExecutor& GetScheduler() const; manager::ManagerHub& GetManagerHub() const; private: scheduler::async_executor::AsyncExecutor& async_executor_; manager::ManagerHub& manager_hub_; }; class RequestDispatcher { public: RequestDispatcher(scheduler::async_executor::AsyncExecutor& scheduler, manager::ManagerHub& manager_hub); private: scheduler::async_executor::AsyncExecutor& async_executor_; manager::ManagerHub& manager_hub_; }; ``` 删除 `ServerLifecycleEvent`、`LifecycleCallback`、两个 setter、callback 注册/通知 和所有 nullable dependency 分支。错误构造只保留一套实现: ```cpp std::string BuildErrorResponseMessage( std::optional id, protocol::ErrorCodes code, std::string_view message) { protocol::ResponseMessage response; response.id = std::move(id); response.error = protocol::ResponseError{ .jsonrpc = "2.0", .code = static_cast(code), .message = std::string(message), .data = std::nullopt, }; return transform::Serialize(response).value(); } std::string BuildErrorResponseMessage( const protocol::RequestMessage& request, protocol::ErrorCodes code, std::string_view message) { return BuildErrorResponseMessage(request.id, code, message); } ``` `HandleUnknownRequest` 调用该函数;序列化失败直接抛出,不返回 `"{}"` 或固定 JSON。 - [ ] **Step 4: 实现 server 状态机和控制消息** 增加可注入注册器、stream 引用和显式状态: ```cpp using ProviderRegistrar = std::function; enum class ServerState { kUninitialized, kRunning, kShutdownRequested, kExiting, }; LspServer(std::istream& input, std::ostream& output, ProviderRegistrar registrar, std::size_t concurrency, std::string interpreter_path); int Run(); ``` 成员按依赖顺序声明: ```cpp std::istream& input_; std::ostream& output_; manager::ManagerHub manager_hub_; scheduler::async_executor::AsyncExecutor async_executor_; RequestDispatcher dispatcher_; ServerState state_ = ServerState::kUninitialized; int exit_code_ = 1; std::atomic fatal_io_error_ = false; ``` 请求状态策略集中在 `HandleRequest`: ```cpp if (request.method == "initialize") { if (state_ != ServerState::kUninitialized) return SendError(request, protocol::ErrorCodes::InvalidRequest, "Server is already initialized"); const auto response = dispatcher_.Dispatch(request); SendMessage(response); const auto parsed = transform::Deserialize(response); if (!parsed || parsed->error) return; state_ = ServerState::kRunning; return; } if (request.method == "shutdown") { if (state_ == ServerState::kUninitialized) return SendError(request, protocol::ErrorCodes::ServerNotInitialized, "Server not initialized"); if (state_ != ServerState::kRunning) return SendError(request, protocol::ErrorCodes::InvalidRequest, "Shutdown already requested"); async_executor_.WaitAll(); manager_hub_.Shutdown(); protocol::ResponseMessage response; response.id = request.id; response.result = protocol::LSPAny(std::nullptr_t{}); SendMessage(transform::Serialize(response).value()); state_ = ServerState::kShutdownRequested; return; } ``` 生命周期控制分支之后,普通请求只允许在 `kRunning`: ```cpp if (state_ == ServerState::kUninitialized) { SendError(request, protocol::ErrorCodes::ServerNotInitialized, "Server not initialized"); return; } if (state_ == ServerState::kShutdownRequested) { SendError(request, protocol::ErrorCodes::InvalidRequest, "Server is shutting down"); return; } SendMessage(dispatcher_.Dispatch(request)); ``` notification 策略只有一个入口:`exit` 在任何状态都终止;`kUninitialized` 忽略 所有其他通知;`kShutdownRequested` 只允许 `exit`;`kRunning` 才 dispatch 普通通知。Task 3 再在普通通知之前插入 `$/cancelRequest` 控制分支。 `SendMessage` 使用注入的 output stream,并把写失败标为 fatal: ```cpp void LspServer::SendMessage(const std::string& message) { std::lock_guard lock(output_mutex_); output_ << "Content-Length: " << message.size() << "\r\n\r\n" << message << std::flush; if (!output_) { fatal_io_error_ = true; throw std::runtime_error("Failed to write LSP message"); } } ``` 主循环每次读取前检查 `fatal_io_error_` 并以 1 结束。同步写异常传播到 launcher; 异步 callback 写异常由 executor 记录,但 fatal flag 会在主循环下一控制点生效。 `exit` 不再 dispatch:保存旧状态,设置 `kExiting`,只有旧状态为 `kShutdownRequested` 时把 `exit_code_` 设为 0。EOF 保持 1。删除析构函数中的 状态写入、主循环 sleep、`RequiresSyncProcessing`、`CanProcessRequest` 和 生命周期事件入口。 - [ ] **Step 5: 删除 lifecycle Provider 和迁移调用点** 删除 shutdown/exit/cancel_request 三个 Module。`AllProviders` 移除 `Shutdown`; `Initialize::ProvideResponse` 删除事件触发,只在序列化失败时抛出。Provider bridge 只保留实际使用的接口别名: ```cpp using ExecutionContext = lsp::core::ExecutionContext; using IProvider = lsp::core::IProvider; using IRequestProvider = lsp::core::IRequestProvider; using INotificationProvider = lsp::core::INotificationProvider; using lsp::core::BuildErrorResponseMessage; ``` 测试环境统一改为: ```cpp struct ProviderEnv { scheduler::async_executor::AsyncExecutor scheduler{1}; manager::ManagerHub hub{}; core::RequestDispatcher dispatcher{scheduler, hub}; core::ExecutionContext context{scheduler, hub}; }; ``` 删除 provider misc/surface 中三个已删除 Provider 的 import、声明、注册、子进程 入口和断言;Initialize 测试只验证响应与 manager 状态。同步移除 test CMake 中的三个 Module。 launcher 显式注入正式注册器并返回 server 退出码: ```cpp core::LspServer server( std::cin, std::cout, provider::RegisterAllProviders, config.thread_count, config.interpreter_path); const int exit_code = server.Run(); spdlog::shutdown(); return exit_code; ``` - [ ] **Step 6: 运行 GREEN 验证** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure ``` Expected: build PASS;生命周期用例 PASS,shutdown 响应含 `result: null`,正常 关闭返回 0,异常退出返回 1。 - [ ] **Step 7: 提交生命周期改动** 只暂存本 Task 文件;launcher 使用逐 hunk 暂存,保留原有 namespace/args_parser 改动。检查后提交: ```bash git diff --cached --check git commit -m ":bug: fix(core): enforce strict LSP lifecycle" ``` ### Task 2: 严格分类 JSON-RPC 消息并统一请求错误 **Files:** - Modify: `lsp-server/test/test_core_server.py` - Modify: `lsp-server/src/core/server.cppm` - Modify: `lsp-server/src/core/dispatcher.cppm` - [ ] **Step 1: 写 ParseError、InvalidRequest 和 MethodNotFound RED 测试** 新增批量原始消息测试: ```python def raw_frame(body: bytes) -> bytes: return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body def assert_json_rpc_errors(server: Path) -> None: payload = b"".join([ raw_frame(b'{"jsonrpc":"2.0",'), raw_frame(b'[]'), raw_frame(b'{"jsonrpc":"1.0","id":7,"method":"initialize"}'), frame({"jsonrpc": "2.0", "id": 8, "method": "initialize", "params": {}}), frame({"jsonrpc": "2.0", "id": 9, "method": "missing/method"}), frame({"jsonrpc": "2.0", "id": 10, "method": "shutdown"}), frame({"jsonrpc": "2.0", "method": "exit"}), ]) result = run_raw(server, payload) assert result.returncode == 0 responses = read_messages(result.stdout) assert responses[0]["error"]["code"] == -32700 assert responses[0]["id"] is None assert responses[1]["error"]["code"] == -32600 assert response_by_id(result.stdout, 7)["error"]["code"] == -32600 assert response_by_id(result.stdout, 9)["error"]["code"] == -32601 ``` 把 `assert_json_rpc_errors(server)` 加入脚本 `main()`,确保 CTest 实际执行新用例。 - [ ] **Step 2: 运行测试并确认 RED** Run: ```bash ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure ``` Expected: FAIL,非法 JSON/结构当前只写日志,没有对应响应。 - [ ] **Step 3: 实现严格消息分类** `HandleMessage` 按以下顺序处理: ```cpp auto any = transform::Deserialize(raw_message); if (!any) { SendError(std::nullopt, protocol::ErrorCodes::ParseError, "Parse error"); return; } if (!any->Is()) { SendError(std::nullopt, protocol::ErrorCodes::InvalidRequest, "Invalid Request"); return; } const auto& object = any->Get(); const auto jsonrpc = object.find("jsonrpc"); if (jsonrpc == object.end() || !jsonrpc->second.Is() || jsonrpc->second.Get() != "2.0") { SendError(ExtractRequestId(object), protocol::ErrorCodes::InvalidRequest, "Invalid Request"); return; } ``` `ExtractRequestId` 只接受 integer/string: ```cpp std::optional ExtractRequestId( const protocol::LSPObject& object) { const auto id = object.find("id"); if (id == object.end()) return std::nullopt; if (id->second.Is()) return protocol::RequestId{id->second.Get()}; if (id->second.Is()) return protocol::RequestId{id->second.Get()}; return std::nullopt; } ``` method 必须是 string;有 method+id 是 request,无 id 是 notification;response 必须有 id 且 `result`/`error` 恰好一 个。不可分类的 response 只记录,不发送 response-to-response。 `SendError` 只负责调用公共 `BuildErrorResponseMessage` 后 `SendMessage`,不再 自己构造 `ResponseMessage`。 - [ ] **Step 4: 运行 GREEN 验证并提交** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure ``` Expected: PASS;错误码和 ID 与测试一致。 Commit: ```bash git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_core_server.py git diff --cached --check git commit -m ":bug: fix(core): return JSON-RPC protocol errors" ``` ### Task 3: 异步执行普通请求并精确取消 **Files:** - Modify: `lsp-server/test/test_provider/core_server_fixture.cppm` - Modify: `lsp-server/test/test_core_server.py` - Modify: `lsp-server/src/core/dispatcher.cppm` - Modify: `lsp-server/src/core/server.cppm` - [ ] **Step 1: 增加阻塞、异常 Provider 和取消 RED 测试** Fixture 注册两个请求 Provider 和一个通知 Provider: ```cpp class FixtureBlock final : public core::IRequestProvider { public: std::string GetMethod() const override { return "test/block"; } std::string GetProviderName() const override { return "FixtureBlock"; } std::string ProvideResponse(const protocol::RequestMessage& request, core::ExecutionContext& context) override { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (!context.GetStopToken().stop_requested() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(5)); protocol::ResponseMessage response; response.id = request.id; response.result = protocol::LSPAny(protocol::string("completed")); return codec::Serialize(response).value(); } }; class FixtureThrow final : public core::IRequestProvider { public: std::string GetMethod() const override { return "test/throw"; } std::string GetProviderName() const override { return "FixtureThrow"; } std::string ProvideResponse(const protocol::RequestMessage&, core::ExecutionContext&) override { throw std::runtime_error("fixture request failure"); } }; class FixtureThrowNotification final : public core::INotificationProvider { public: std::string GetMethod() const override { return "test/throwNotification"; } std::string GetProviderName() const override { return "FixtureThrowNotification"; } void HandleNotification(const protocol::NotificationMessage&, core::ExecutionContext&) override { throw std::runtime_error("fixture notification failure"); } }; ``` registrar 中加入: ```cpp dispatcher.RegisterRequestProvider(std::make_shared()); dispatcher.RegisterRequestProvider(std::make_shared()); dispatcher.RegisterNotificationProvider( std::make_shared()); ``` Python 交互测试必须覆盖: ```python client = LspClient(server) client.send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}) assert client.read()["id"] == 1 client.send({"jsonrpc": "2.0", "id": 2, "method": "test/block"}) client.send({"jsonrpc": "2.0", "id": "2", "method": "test/block"}) client.send({"jsonrpc": "2.0", "method": "$/cancelRequest", "params": {"id": 2}}) cancelled = client.read(timeout=1) assert cancelled["id"] == 2 assert cancelled["error"]["code"] == -32800 client.send({"jsonrpc": "2.0", "method": "$/cancelRequest", "params": {"id": "2"}}) string_cancelled = client.read(timeout=1) assert string_cancelled["id"] == "2" assert string_cancelled["error"]["code"] == -32800 client.send({"jsonrpc": "2.0", "id": 3, "method": "test/throw"}) failed = client.read(timeout=1) assert failed["error"]["code"] == -32603 client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"}) client.send({"jsonrpc": "2.0", "id": 4, "method": "test/block"}) duplicate = client.read(timeout=1) assert duplicate["id"] == 4 assert duplicate["error"]["code"] == -32600 client.send({"jsonrpc": "2.0", "method": "$/cancelRequest", "params": {"id": 4}}) cancelled_original = client.read(timeout=1) assert cancelled_original["error"]["code"] == -32800 client.send({"jsonrpc": "2.0", "method": "test/throwNotification"}) client.send({"jsonrpc": "2.0", "id": 5, "method": "shutdown"}) assert client.read(timeout=1)["id"] == 5 client.send({"jsonrpc": "2.0", "method": "exit"}) assert client.close_input() == 0 ``` 把该交互过程封装为 `assert_cancellation_and_failures(server)` 并加入脚本 `main()`。 - [ ] **Step 2: 运行测试并确认 RED** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure ``` Expected: FAIL;`GetStopToken` 尚不存在,或同步 block 请求使取消响应超过 1 秒。 - [ ] **Step 3: 向 ExecutionContext 传递 stop token** ```cpp ExecutionContext(scheduler::async_executor::AsyncExecutor& scheduler, manager::ManagerHub& manager_hub, std::stop_token stop_token = {}) : async_executor_(scheduler), manager_hub_(manager_hub), stop_token_(stop_token) {} std::stop_token GetStopToken() const { return stop_token_; } ``` `RequestDispatcher::Dispatch` 改为接收默认值为空的 `std::stop_token` 并用它 构造 context;同步 initialize 和 notification 使用空 token。 - [ ] **Step 4: 实现类型安全请求表和异步 callback** server 增加: ```cpp struct ActiveRequest { scheduler::async_executor::TaskHandle handle; }; std::mutex requests_mutex_; std::unordered_map> active_requests_; ``` 请求键必须带类型: ```cpp std::string RequestKey(const protocol::RequestId& id) { return std::visit([](const auto& value) { using T = std::decay_t; if constexpr (std::is_same_v) return "i:" + std::to_string(value); else return "s:" + value; }, id); } ``` `SubmitRequest` 先登记 shared state,解锁后 Submit,再绑定 handle: ```cpp auto state = std::make_shared(); bool duplicate = false; { std::lock_guard lock(requests_mutex_); if (active_requests_.contains(key)) duplicate = true; else active_requests_.emplace(key, state); } if (duplicate) { SendError(request, protocol::ErrorCodes::InvalidRequest, "Duplicate active request id"); return; } state->handle = async_executor_.Submit( "lsp-request:" + key, [this, request](std::stop_token stop_token) -> std::optional { return dispatcher_.Dispatch(request, stop_token); }, [this, request, key, state](const auto& result) { FinishRequest(request, key, state, result); }); ``` `FinishRequest` 先仅当映射仍指向同一 state 时删除登记并释放请求表锁,再对 completed/cancelled/failed 分别发送结果、`RequestCancelled`、`InternalError`。 这样输出异常不会留下陈旧登记,callback 也不会在请求表锁内发送消息。 `HandleCancelRequest` 严格读取 `CancelParams`,复制 handle 后解锁并调用 `Cancel()`。`DrainActiveRequests` 锁内复制 handles、锁外 cancel/wait;shutdown、 exit、EOF/framing 收尾都调用它。shutdown 随后调用 `async_executor_.WaitAll()`,保证 initialize/Provider 提交的后台任务也在 `ManagerHub::Shutdown()` 前结束。 - [ ] **Step 5: 捕获 notification 异常并运行 GREEN** notification dispatch 外围只记录异常: ```cpp try { dispatcher_.Dispatch(notification); } catch (const std::exception& error) { spdlog::error("Notification {} failed: {}", notification.method, error.what()); } catch (...) { spdlog::error("Notification {} failed with unknown exception", notification.method); } ``` Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server)$' --output-on-failure ``` Expected: PASS;两种 ID 分别取消,异常请求返回 -32603,异常通知不终止进程。 - [ ] **Step 6: 提交异步取消改动** ```bash git add lsp-server/src/core/server.cppm lsp-server/src/core/dispatcher.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_core_server.py git diff --cached --check git commit -m ":bug: fix(core): dispatch cancellable requests asynchronously" ``` ### Task 4: 严格验证 LSP framing **Files:** - Modify: `lsp-server/test/test_core_server.py` - Modify: `lsp-server/src/core/server.cppm` - [ ] **Step 1: 写 framing RED 测试** 对每个 payload 启动独立 fixture 进程并要求非零退出、stdout 无 LSP 响应: ```python invalid_payloads = [ b"Content-Length: 2junk\r\n\r\n{}", b"Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}", b"Content-Length: 0\r\n\r\n", b"Content-Length: 16777217\r\n\r\n", b"Content-Length: 10\r\n\r\n{}", b"Content-Length: 2\n\n{}", b"X-Length: 2\r\n\r\n{}", b"Content-Length: 2\r\nContent-Type: application/json\r\n\r\n{}", ] for payload in invalid_payloads: result = run_raw(server, payload) assert result.returncode == 1 assert result.stdout == b"" ``` 另加规范 `Content-Type: application/vscode-jsonrpc; charset=utf-8` 和连续两条 消息的通过用例,并把 `assert_framing(server)` 加入脚本 `main()`。 - [ ] **Step 2: 运行测试并确认 RED** Run: ```bash ctest --test-dir lsp-server/build/codex43-clean/Release -R '^test_core_server$' --output-on-failure ``` Expected: FAIL;部分数字、重复长度或 LF-only header 被旧读取器接受。 - [ ] **Step 3: 用明确读取结果和 from_chars 实现严格 framing** ```cpp enum class ReadStatus { kMessage, kEndOfStream, kFramingError, }; struct ReadResult { ReadStatus status; std::string message; }; static constexpr std::size_t kMaxMessageSize = 16U * 1024U * 1024U; ``` 每个 header 行必须以 `\r` 结束;移除该字符后解析。`Content-Length: ` 后的值 非空,`from_chars` 必须满足 `error == std::errc{}` 且 pointer 到达字符串末尾。 只允许一个 Content-Length 和最多一个精确规范 Content-Type。未知 header、空 header、零/超限长度和短 body 返回 `kFramingError`。 主循环处理: ```cpp const auto read = ReadMessage(); if (read.status != ReadStatus::kMessage) { exit_code_ = 1; break; } HandleMessage(read.message); ``` 删除 `stoul`、nullopt 重试、5ms sleep 和 framing 错误后的继续读取。 - [ ] **Step 4: 运行 GREEN 并提交** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate ``` Expected: PASS;非法 framing 全部非零退出且无响应,规范连续消息通过。 Commit: ```bash git add lsp-server/src/core/server.cppm lsp-server/test/test_core_server.py git diff --cached --check git commit -m ":bug: fix(core): reject invalid LSP framing" ``` ### Task 5: 将 diagnostics 字节列转换为 UTF-16 **Files:** - Modify: `lsp-server/test/test_provider/text_coordinates_test.cppm` - Modify: `lsp-server/src/utils/text_coordinates.cppm` - Modify: `lsp-server/src/core/server.cppm` - [ ] **Step 1: 写反向坐标 RED 测试** ```cpp TestResult TextCoordinatesTests::TestBytePointsToUtf16Positions() { const protocol::string content = "A中😀Z\n😀x"; ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 0U}, content), 0U, 0U); ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 4U}, content), 0U, 2U); ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 8U}, content), 0U, 4U); ExpectLspPosition(utils::text_coordinates::ToPosition({1U, 4U}, content), 1U, 2U); ExpectLspPosition(utils::text_coordinates::ToPosition({0U, 6U}, content), 0U, 2U); return {"", true, "ok"}; } ``` 在测试类声明和注册入口分别加入: ```cpp static TestResult TestBytePointsToUtf16Positions(); runner.addTest("text coordinates convert byte points to UTF-16 positions", TestBytePointsToUtf16Positions); ``` 测试 helper 只比较字段: ```cpp void ExpectLspPosition(const protocol::Position& actual, protocol::uinteger line, protocol::uinteger character) { assertEqual(line, actual.line, "LSP line should match"); assertEqual(character, actual.character, "LSP UTF-16 character should match"); } ``` - [ ] **Step 2: 运行测试并确认 RED** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider -j1 ``` Expected: FAIL,`ToPosition(TSPoint, string)` 尚未定义。 - [ ] **Step 3: 实现单一 UTF-8 解码路径的反向转换** 导出: ```cpp protocol::Position ToPosition(TSPoint point, const protocol::string& content); ``` 实现先扫描到 `point.row` 行首,再逐个调用已有 `DecodeCharacter`。只有完整字符的 末尾不超过目标 byte column 时才累计 `utf16_units`;目标落在字符内部时停止, 超出行尾时收敛到行尾。返回实际行号和累计 UTF-16 units,不复制第二套 UTF-8 判定代码。 `PublishDiagnostics` 转换起止点: ```cpp diagnostic.range.start = utils::text_coordinates::ToPosition( TSPoint{error.location.start_line, error.location.start_column}, content); diagnostic.range.end = utils::text_coordinates::ToPosition( TSPoint{error.location.end_line, error.location.end_column}, content); ``` - [ ] **Step 4: 运行 GREEN 并提交** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target test_provider tsl-server -j1 ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_provider|test_core_server)$' --output-on-failure ``` Expected: PASS;中文和 emoji 坐标符合 UTF-16。 Commit: ```bash git add lsp-server/src/utils/text_coordinates.cppm lsp-server/src/core/server.cppm lsp-server/test/test_provider/text_coordinates_test.cppm git diff --cached --check git commit -m ":bug: fix(core): convert diagnostics to UTF-16 positions" ``` ### Task 6: 格式化、全量验证和无效兜底专项复审 **Files:** - Modify: `memory-bank/progress.md` - Format: 本 Plan 已修改的 C++ 文件 - [ ] **Step 1: 格式化本 Plan 修改的 C++ 文件** Run: ```bash clang-format -i lsp-server/src/core/dispatcher.cppm lsp-server/src/core/server.cppm lsp-server/src/provider/base/interface.cppm lsp-server/src/provider/initialize/initialize.cppm lsp-server/src/provider/manifest.cppm lsp-server/src/utils/text_coordinates.cppm lsp-server/test/test_provider/core_server_fixture.cppm lsp-server/test/test_provider/test_main.cppm lsp-server/test/test_provider/provider_misc_test.cppm lsp-server/test/test_provider/provider_surface_test.cppm lsp-server/test/test_provider/json_provider_coverage_test.cppm lsp-server/test/test_provider/text_coordinates_test.cppm ``` 检查格式化没有改动用户的无关 hunk;若有,恢复那些 hunk 到格式化前内容,仅 保留本 Plan 行。 - [ ] **Step 2: 构建所有影响目标** Run: ```bash cmake --build lsp-server/build/codex43-clean/Release --target tsl-server test_scheduler test_provider -j1 ``` Expected: exit 0,无编译错误。 - [ ] **Step 3: 运行完整相关测试门** Run: ```bash ctest --test-dir lsp-server/build/codex43-clean/Release -R '^(test_scheduler|test_provider|test_core_server|test_cli_startup)$' --output-on-failure python lsp-server/test/run_lsp_json_tests.py --server lsp-server/build/codex43-clean/Release/src/tsl-server --no-validate ``` Expected: 4/4 CTest PASS,LSP transport smoke exit 0。已知 `test_ast_script`、`test_symbol_script`、`test_semantic_script` 以及 rename 结果校验不在本 Plan gate 中。 - [ ] **Step 4: 专项搜索无效兜底、兼容层和死代码** Run: ```bash rg -n 'return "\{\}"|value_or\("\{\}"\)|TO[D]O|RequiresSyncProcessing|CanProcessRequest|SetRequestScheduler|SetManagerHub|ServerLifecycleEvent|LifecycleCallback|provider\.(shutdown|exit|cancel_request)' lsp-server/src/core lsp-server/src/provider lsp-server/test/test_provider ``` Expected: 无命中。再分别检查生产和测试 sleep: ```bash rg -n 'sleep_for' lsp-server/src/core rg -n 'sleep_for' lsp-server/test/test_provider/core_server_fixture.cppm ``` Expected: core 无命中;fixture 恰好命中确定性阻塞 Provider。再运行: ```bash rg -n 'catch\s*\(\.\.\.\)|catch\s*\(const std::exception' lsp-server/src/core ``` Expected: 每个 catch 都能对应“请求转 InternalError”“通知只记录”或“主循环致命 错误退出”,不存在捕获后 sleep/继续的分支。 - [ ] **Step 5: 检查最终 diff 和提交遗漏** Run: ```bash git diff --check git status --short git log -6 --oneline ``` Expected: 本 Plan 产品/测试改动已提交;用户原有未提交文件仍存在且未混入提交; 新增提交均带匹配 type 的 emoji。 若格式化或复审产生必要修正,先重新执行 Step 2/3,再提交: ```bash git commit -m ":art: style(core): normalize lifecycle implementation" ``` - [ ] **Step 6: 写回主循环状态** Run: ```bash python docs/standards/playbook/scripts/main_loop.py finish -plan docs/superpowers/plans/2026-07-13-lsp-core-lifecycle.md -status done -progress memory-bank/progress.md ``` 更新 `memory-bank/progress.md` 上半部分摘要,记录生命周期状态机、请求取消、 framing、UTF-16 diagnostics 和验证结果;只暂存该文件并提交: ```bash git add memory-bank/progress.md git diff --cached --check git commit -m ":memo: docs(progress): finish LSP core lifecycle plan" ```