@deepseek-ai/dsh-lsp-stdio 0.1.2-rc.1 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +2 -3
- package/README.zh.md +2 -3
- package/lib/index.js +36 -34
- package/lib/types/connection.d.ts +6 -8
- package/lib/types/instance.d.ts +5 -4
- package/package.json +16 -16
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/lsp/lsp-stdio/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 61137e544ea7c9ba0d2458fc99a43eaa1c03aafb
|
|
6
|
+
README.zh.md: 39c327346c90d7efeee895035e35a76b17354cf5
|
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ This section explains the design decisions behind the provider and where the cod
|
|
|
91
91
|
- **Read before spawn.** The source is resolved, contained, and byte-bounded inside the workspace queue before any process is created, so a queued query sees current bytes when its turn starts and an invalid source cannot leave an idle process pooled.
|
|
92
92
|
- **One pooled process per canonical workspace.** Instances are single-flighted per `(server id, canonical workspace target)`; a transport failure retries the read-only query once on a fresh process after awaiting disposal.
|
|
93
93
|
- **Per-workspace serialization.** One abortable queue per workspace serializes source-read/open/query/close lifecycles; distinct workspaces run in parallel, and a cancellation that fails to stop a server terminates only that instance.
|
|
94
|
-
- **Bounded teardown.** Graceful `shutdown`/`exit` escalates through
|
|
94
|
+
- **Bounded teardown.** Graceful `shutdown`/`exit` escalates through the subprocess provider's managed-range termination procedure; quiescence is confirmed by awaiting that whole range, not by the termination request's outcome.
|
|
95
95
|
- **Execution-world pairing.** Servers launch through `ctx.subprocess` with `processId: null` (another machine or PID namespace must not monitor the harness), sources read through `ctx.fs`, and no `fs/observed` event is emitted — only the LSP result is model-visible.
|
|
96
96
|
|
|
97
97
|
### Source map
|
|
@@ -119,10 +119,9 @@ Initialization advertises UTF-16 positions, workspace folders and configuration,
|
|
|
119
119
|
<a id="further-exploration"></a>
|
|
120
120
|
## Further Exploration
|
|
121
121
|
|
|
122
|
-
Read these pages when the package-level contract is not enough. They move from the shared navigation model to the seam
|
|
122
|
+
Read these pages when the package-level contract is not enough. They move from the shared navigation model to the seam and the tool.
|
|
123
123
|
|
|
124
124
|
- [LSP navigation subsystem](../../../docs/subsystems/lsp.md) — operations, coordinates, requests and results, and `LspError` codes.
|
|
125
|
-
- [LSP capability seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md) — design rationale, alternatives, and deliberately deferred API.
|
|
126
125
|
- [dsh-lsp](../lsp/README.md) — the seam this provider registers against.
|
|
127
126
|
- [dsh-tool-lsp](../tool-lsp/README.md) — the model-facing tool over the seam.
|
|
128
127
|
- [lsp group map](../README.md) — the three-package family and its related documentation.
|
package/README.zh.md
CHANGED
|
@@ -91,7 +91,7 @@ kind: "package-reference"
|
|
|
91
91
|
- **先读后启动。** 源文件在工作区队列内先完成解析、包含关系检查与字节限制,然后才创建任何进程,因此排队查询只会在轮到自身时读取当前字节,无效源文件也不会留下空闲的池化进程。
|
|
92
92
|
- **每个规范工作区一个池化进程。** 实例按 `(server id, canonical workspace target)` 进行 single-flight;传输故障会在等待释放完成后于新进程上重试一次该只读查询。
|
|
93
93
|
- **逐工作区串行化。** 每个工作区一条可中止队列,串行执行源读取/打开/查询/关闭生命周期;不同工作区并行运行,无法停止服务器的取消只会终止该实例。
|
|
94
|
-
- **有边界的释放。** 优雅 `shutdown`/`exit`
|
|
94
|
+
- **有边界的释放。** 优雅 `shutdown`/`exit` 会升级到 subprocess 提供方的 managed-range 终止流程;是否完全停稳由等待整个 range 确认,而非由终止请求自身的结果确认。
|
|
95
95
|
- **执行世界配对。** 服务器通过 `ctx.subprocess` 启动,`processId: null`(另一台机器或 PID namespace 不得监视 harness);源文件通过 `ctx.fs` 读取;不发出 `fs/observed` 事件——只有 LSP 结果对模型可见。
|
|
96
96
|
|
|
97
97
|
### 源码地图
|
|
@@ -119,10 +119,9 @@ kind: "package-reference"
|
|
|
119
119
|
<a id="further-exploration"></a>
|
|
120
120
|
## 进一步探索
|
|
121
121
|
|
|
122
|
-
当包级约定不够用时阅读以下页面。它们从共享的导航模型逐步进入 seam
|
|
122
|
+
当包级约定不够用时阅读以下页面。它们从共享的导航模型逐步进入 seam 与工具。
|
|
123
123
|
|
|
124
124
|
- [LSP 导航子系统](../../../docs/subsystems/lsp.zh.md)——操作、坐标、请求与结果,以及 `LspError` code。
|
|
125
|
-
- [LSP 能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.zh.md)——设计原理、备选方案与刻意推迟的 API。
|
|
126
125
|
- [dsh-lsp](../lsp/README.zh.md)——本提供方注册到的 seam。
|
|
127
126
|
- [dsh-tool-lsp](../tool-lsp/README.zh.md)——基于该 seam 的面向模型工具。
|
|
128
127
|
- [lsp 组地图](../README.zh.md)——三个包的家族及其相关文档。
|
package/lib/index.js
CHANGED
|
@@ -229,8 +229,8 @@ function parseContentLength(headerText) {
|
|
|
229
229
|
* server→client requests: it answers `workspace/configuration` from static
|
|
230
230
|
* config, and rejects `workspace/applyEdit` (this host never applies edits or
|
|
231
231
|
* runs commands). It caps stderr, surfaces framing/decoder failures as a
|
|
232
|
-
* fatal close, and exposes
|
|
233
|
-
* instance owns teardown;
|
|
232
|
+
* fatal close, and exposes managed-range termination through the handle so the
|
|
233
|
+
* instance owns teardown; platform mechanics live in the subprocess
|
|
234
234
|
* Service Provider.
|
|
235
235
|
* @module @deepseek-ai/dsh-lsp-stdio/connection
|
|
236
236
|
*/
|
|
@@ -293,10 +293,6 @@ var LspConnection = class {
|
|
|
293
293
|
this.onStdout(chunk);
|
|
294
294
|
});
|
|
295
295
|
}
|
|
296
|
-
/** The child's pid, or `-1` when the spawn produced no pid (so signalling is a no-op). */
|
|
297
|
-
get pid() {
|
|
298
|
-
return this.handle.pid;
|
|
299
|
-
}
|
|
300
296
|
/** The retained stderr tail, for diagnostics on a failed server. */
|
|
301
297
|
get stderrTail() {
|
|
302
298
|
/* v8 ignore next -- the collect disposition always exposes a stderr reader; defensive. */
|
|
@@ -372,16 +368,16 @@ var LspConnection = class {
|
|
|
372
368
|
peekNextId() {
|
|
373
369
|
return this.nextId;
|
|
374
370
|
}
|
|
375
|
-
/** Terminate the server's
|
|
371
|
+
/** Terminate the server's provider-managed range (idempotent). */
|
|
376
372
|
terminate() {
|
|
377
373
|
this.handle.terminate();
|
|
378
374
|
}
|
|
379
375
|
/**
|
|
380
|
-
* Wait until the owned
|
|
376
|
+
* Wait until the owned managed range is empty.
|
|
381
377
|
* @param signal - optional bound for the wait.
|
|
382
|
-
* @returns `true` when the
|
|
378
|
+
* @returns `true` when the range is empty, or `false` when the signal aborted first.
|
|
383
379
|
*/
|
|
384
|
-
async
|
|
380
|
+
async waitForManagedRangeExit(signal) {
|
|
385
381
|
return await this.handle.waitForExit(signal);
|
|
386
382
|
}
|
|
387
383
|
onStdout(chunk) {
|
|
@@ -747,7 +743,7 @@ var LspInstance = class {
|
|
|
747
743
|
*/
|
|
748
744
|
query(request, source, signal) {
|
|
749
745
|
const run = abortable(this.queue, signal).then(() => this.runQuery(request, source, signal)).catch(async (error) => {
|
|
750
|
-
if (this.isTransportFailure(error)) await this.
|
|
746
|
+
if (this.isTransportFailure(error)) await this.awaitTeardownAttempt();
|
|
751
747
|
throw error;
|
|
752
748
|
});
|
|
753
749
|
this.queue = this.queue.then(() => run).then(() => void 0, () => void 0);
|
|
@@ -775,7 +771,7 @@ var LspInstance = class {
|
|
|
775
771
|
try {
|
|
776
772
|
await abortable(this.ready, signal);
|
|
777
773
|
} catch (error) {
|
|
778
|
-
if (!this.dead) await this.
|
|
774
|
+
if (!this.dead) await this.awaitTeardownAttempt();
|
|
779
775
|
throw error;
|
|
780
776
|
}
|
|
781
777
|
const capabilities = this.capabilities;
|
|
@@ -796,7 +792,7 @@ var LspInstance = class {
|
|
|
796
792
|
text: source.text
|
|
797
793
|
} }), signal);
|
|
798
794
|
} catch (error) {
|
|
799
|
-
await this.
|
|
795
|
+
await this.awaitTeardownAttempt();
|
|
800
796
|
throw error;
|
|
801
797
|
}
|
|
802
798
|
opened = true;
|
|
@@ -805,10 +801,8 @@ var LspInstance = class {
|
|
|
805
801
|
} finally {
|
|
806
802
|
if (opened && !this.dead) try {
|
|
807
803
|
await this.connection.notify("textDocument/didClose", { textDocument: { uri } });
|
|
808
|
-
} catch {
|
|
809
|
-
|
|
810
|
-
await this.startTeardown();
|
|
811
|
-
} catch {}
|
|
804
|
+
} catch (_closeFailure) {
|
|
805
|
+
await this.awaitTeardownAttempt();
|
|
812
806
|
}
|
|
813
807
|
}
|
|
814
808
|
}
|
|
@@ -848,7 +842,7 @@ var LspInstance = class {
|
|
|
848
842
|
grace.signal.addEventListener("abort", () => {
|
|
849
843
|
resolve(false);
|
|
850
844
|
}, { once: true });
|
|
851
|
-
})])) await this.
|
|
845
|
+
})])) await this.awaitTeardownAttempt();
|
|
852
846
|
} finally {
|
|
853
847
|
grace[Symbol.dispose]();
|
|
854
848
|
}
|
|
@@ -890,6 +884,12 @@ var LspInstance = class {
|
|
|
890
884
|
this.teardownPromise ??= this.tearDown();
|
|
891
885
|
return this.teardownPromise;
|
|
892
886
|
}
|
|
887
|
+
/** Await teardown while leaving its memoized failure for provider-level finalization. */
|
|
888
|
+
async awaitTeardownAttempt() {
|
|
889
|
+
try {
|
|
890
|
+
await this.startTeardown();
|
|
891
|
+
} catch (_teardownFailure) {}
|
|
892
|
+
}
|
|
893
893
|
async tearDown() {
|
|
894
894
|
const shutdownDeadline = deadline(void 0, this.spec.shutdownTimeoutMs, "LSP_SHUTDOWN");
|
|
895
895
|
try {
|
|
@@ -906,14 +906,13 @@ var LspInstance = class {
|
|
|
906
906
|
await abortable(this.connection.closed, signal);
|
|
907
907
|
}
|
|
908
908
|
/**
|
|
909
|
-
* Terminate the
|
|
910
|
-
*
|
|
911
|
-
*
|
|
912
|
-
* another timer — is the postcondition disposal owes its callers.
|
|
909
|
+
* Terminate the provider-managed range, then await the direct server result
|
|
910
|
+
* and whole-range quiescence. The awaits are unbounded on purpose because
|
|
911
|
+
* quiescence, not another timer, is the postcondition disposal owes callers.
|
|
913
912
|
*/
|
|
914
913
|
async forceTerminate() {
|
|
915
914
|
this.connection.terminate();
|
|
916
|
-
await Promise.all([this.connection.closed, this.connection.
|
|
915
|
+
await Promise.all([this.connection.closed, this.connection.waitForManagedRangeExit()]);
|
|
917
916
|
}
|
|
918
917
|
};
|
|
919
918
|
/** Server→client request methods this host acknowledges with an empty result (no dynamic registration). */
|
|
@@ -1109,20 +1108,23 @@ var LocalLspProvider = class {
|
|
|
1109
1108
|
const source = await readHostSource(this.fs, request.filePath, workspace, this.config.maxDocumentBytes, querySignal);
|
|
1110
1109
|
this.assertActive(querySignal);
|
|
1111
1110
|
let instance = this.instanceFor(workspaceKey, workspace);
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
await instance.dispose();
|
|
1117
|
-
this.evictIfCurrent(workspaceKey, instance);
|
|
1118
|
-
this.assertActive(querySignal);
|
|
1119
|
-
instance = this.instanceFor(workspaceKey, workspace);
|
|
1120
|
-
return await instance.query(request, source, querySignal);
|
|
1121
|
-
} finally {
|
|
1111
|
+
let canRetryTransport = true;
|
|
1112
|
+
for (;;) {
|
|
1113
|
+
const [queryOutcome] = await Promise.allSettled([instance.query(request, source, querySignal)]);
|
|
1114
|
+
let teardownOutcome;
|
|
1122
1115
|
if (instance.dead) {
|
|
1123
|
-
await instance.dispose();
|
|
1116
|
+
[teardownOutcome] = await Promise.allSettled([instance.dispose()]);
|
|
1124
1117
|
this.evictIfCurrent(workspaceKey, instance);
|
|
1125
1118
|
}
|
|
1119
|
+
if (teardownOutcome?.status === "rejected") {
|
|
1120
|
+
if (queryOutcome.status === "rejected") throw new AggregateError([queryOutcome.reason, teardownOutcome.reason], "LSP operation and teardown failed");
|
|
1121
|
+
throw teardownOutcome.reason;
|
|
1122
|
+
}
|
|
1123
|
+
if (queryOutcome.status === "fulfilled") return queryOutcome.value;
|
|
1124
|
+
if (!canRetryTransport || !instance.isTransportFailure(queryOutcome.reason)) throw queryOutcome.reason;
|
|
1125
|
+
canRetryTransport = false;
|
|
1126
|
+
this.assertActive(querySignal);
|
|
1127
|
+
instance = this.instanceFor(workspaceKey, workspace);
|
|
1126
1128
|
}
|
|
1127
1129
|
});
|
|
1128
1130
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* server→client requests: it answers `workspace/configuration` from static
|
|
5
5
|
* config, and rejects `workspace/applyEdit` (this host never applies edits or
|
|
6
6
|
* runs commands). It caps stderr, surfaces framing/decoder failures as a
|
|
7
|
-
* fatal close, and exposes
|
|
8
|
-
* instance owns teardown;
|
|
7
|
+
* fatal close, and exposes managed-range termination through the handle so the
|
|
8
|
+
* instance owns teardown; platform mechanics live in the subprocess
|
|
9
9
|
* Service Provider.
|
|
10
10
|
* @module @deepseek-ai/dsh-lsp-stdio/connection
|
|
11
11
|
*/
|
|
@@ -62,8 +62,6 @@ export declare class LspConnection {
|
|
|
62
62
|
* @param writer - message writer; tests inject callback failures without relying on OS pipe races.
|
|
63
63
|
*/
|
|
64
64
|
constructor(spec: ConnectionSpec, spawner: ConnectionSpawner, onServerRequest: (method: string, params: unknown) => Promise<unknown>, writer?: ConnectionWriter);
|
|
65
|
-
/** The child's pid, or `-1` when the spawn produced no pid (so signalling is a no-op). */
|
|
66
|
-
get pid(): number;
|
|
67
65
|
/** The retained stderr tail, for diagnostics on a failed server. */
|
|
68
66
|
get stderrTail(): string;
|
|
69
67
|
/** Whether the transport has failed even if the child close event has not arrived yet. */
|
|
@@ -98,14 +96,14 @@ export declare class LspConnection {
|
|
|
98
96
|
* @returns the numeric id the next request will be assigned.
|
|
99
97
|
*/
|
|
100
98
|
peekNextId(): number;
|
|
101
|
-
/** Terminate the server's
|
|
99
|
+
/** Terminate the server's provider-managed range (idempotent). */
|
|
102
100
|
terminate(): void;
|
|
103
101
|
/**
|
|
104
|
-
* Wait until the owned
|
|
102
|
+
* Wait until the owned managed range is empty.
|
|
105
103
|
* @param signal - optional bound for the wait.
|
|
106
|
-
* @returns `true` when the
|
|
104
|
+
* @returns `true` when the range is empty, or `false` when the signal aborted first.
|
|
107
105
|
*/
|
|
108
|
-
|
|
106
|
+
waitForManagedRangeExit(signal?: AbortSignal): Promise<boolean>;
|
|
109
107
|
private onStdout;
|
|
110
108
|
private dispatch;
|
|
111
109
|
private handleServerRequest;
|
package/lib/types/instance.d.ts
CHANGED
|
@@ -75,14 +75,15 @@ export declare class LspInstance {
|
|
|
75
75
|
dispose(): Promise<void>;
|
|
76
76
|
/** Publish disposal once and make every caller await the same quiescence boundary. */
|
|
77
77
|
private startTeardown;
|
|
78
|
+
/** Await teardown while leaving its memoized failure for provider-level finalization. */
|
|
79
|
+
private awaitTeardownAttempt;
|
|
78
80
|
private tearDown;
|
|
79
81
|
/** Best-effort LSP `shutdown`/`exit`, including process close, bounded by `signal`. */
|
|
80
82
|
private gracefulShutdown;
|
|
81
83
|
/**
|
|
82
|
-
* Terminate the
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* another timer — is the postcondition disposal owes its callers.
|
|
84
|
+
* Terminate the provider-managed range, then await the direct server result
|
|
85
|
+
* and whole-range quiescence. The awaits are unbounded on purpose because
|
|
86
|
+
* quiescence, not another timer, is the postcondition disposal owes callers.
|
|
86
87
|
*/
|
|
87
88
|
private forceTerminate;
|
|
88
89
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-lsp-stdio",
|
|
3
3
|
"description": "Generic stdio language-server provider for the DeepSeek Harness LSP capability seam (ctx.lsp) — spawns configured servers, translates JSON-RPC, and serves transient-open goToDefinition/findReferences/goToImplementation/hover queries in the host filesystem namespace",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,28 +28,28 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
31
|
-
"@deepseek-ai/dsh-brand": "^0.1.
|
|
32
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
33
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
34
|
-
"@deepseek-ai/dsh-lsp": "^0.1.
|
|
35
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.
|
|
36
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
31
|
+
"@deepseek-ai/dsh-brand": "^0.1.3-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-fs": "^0.1.3-alpha.2",
|
|
33
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
34
|
+
"@deepseek-ai/dsh-lsp": "^0.1.3-alpha.2",
|
|
35
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-util-values": "^0.1.
|
|
39
|
+
"@deepseek-ai/dsh-util-values": "^0.1.3-alpha.2",
|
|
40
40
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
44
|
"typescript-language-server": "^5.0.0",
|
|
45
45
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-fs
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
46
|
+
"@deepseek-ai/dsh-brand": "^0.1.3-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-fs": "^0.1.3-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-fs-local": "^0.1.3-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.1.3-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-lsp": "^0.1.3-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2"
|
|
54
54
|
}
|
|
55
55
|
}
|