Files
tsl-devkit/docs/superpowers/plans/2026-07-13-lsp-core-lifecycle.md
T

1187 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<FixtureInitialize>());
},
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 $<TARGET_FILE:test_provider>)
```
- [ ] **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<protocol::RequestId> 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<protocol::integer>(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<void(RequestDispatcher&)>;
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<bool> 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<protocol::ResponseMessage>(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;生命周期用例 PASSshutdown 响应含 `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<protocol::LSPAny>(raw_message);
if (!any)
{
SendError(std::nullopt, protocol::ErrorCodes::ParseError, "Parse error");
return;
}
if (!any->Is<protocol::LSPObject>())
{
SendError(std::nullopt, protocol::ErrorCodes::InvalidRequest,
"Invalid Request");
return;
}
const auto& object = any->Get<protocol::LSPObject>();
const auto jsonrpc = object.find("jsonrpc");
if (jsonrpc == object.end() || !jsonrpc->second.Is<protocol::string>() ||
jsonrpc->second.Get<protocol::string>() != "2.0")
{
SendError(ExtractRequestId(object), protocol::ErrorCodes::InvalidRequest,
"Invalid Request");
return;
}
```
`ExtractRequestId` 只接受 integer/string
```cpp
std::optional<protocol::RequestId> ExtractRequestId(
const protocol::LSPObject& object)
{
const auto id = object.find("id");
if (id == object.end())
return std::nullopt;
if (id->second.Is<protocol::integer>())
return protocol::RequestId{id->second.Get<protocol::integer>()};
if (id->second.Is<protocol::string>())
return protocol::RequestId{id->second.Get<protocol::string>()};
return std::nullopt;
}
```
method 必须是 string;有 method+id 是
request,无 id 是 notificationresponse 必须有 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<FixtureBlock>());
dispatcher.RegisterRequestProvider(std::make_shared<FixtureThrow>());
dispatcher.RegisterNotificationProvider(
std::make_shared<FixtureThrowNotification>());
```
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<std::string, std::shared_ptr<ActiveRequest>> active_requests_;
```
请求键必须带类型:
```cpp
std::string RequestKey(const protocol::RequestId& id)
{
return std::visit([](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, protocol::integer>)
return "i:" + std::to_string(value);
else
return "s:" + value;
}, id);
}
```
`SubmitRequest` 先登记 shared state,解锁后 Submit,再绑定 handle
```cpp
auto state = std::make_shared<ActiveRequest>();
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<std::string> {
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/waitshutdown、
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 PASSLSP 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"
```