dsh-vscode-mode 0.21.2 → 0.21.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.21.2",
3
+ "version": "0.21.3",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
package/src/mcpPool.ts CHANGED
@@ -12,6 +12,11 @@ export interface PoolSpec {
12
12
  config: MpcConfig
13
13
  policy: McpPolicy
14
14
  }
15
+ /** Startup caller settlement, independent from the owned resource lifetime. */
16
+ interface StartupResult {
17
+ resolve(): void
18
+ reject(error: unknown): void
19
+ }
15
20
  export interface PoolEntry {
16
21
  key: string
17
22
  spec: PoolSpec
@@ -27,6 +32,9 @@ export interface PoolEntry {
27
32
  bridge?: McpBridge
28
33
  fiber?: any
29
34
  ready: Promise<void>
35
+ /** Entire queued mount/cleanup lifetime; rejection means closure remains unconfirmed. */
36
+ quiescence: Promise<void>
37
+ cleanupFailed?: boolean
30
38
  close?: Promise<void>
31
39
  idle?: ReturnType<typeof setTimeout>
32
40
  live: boolean
@@ -95,7 +103,7 @@ export class McpPool {
95
103
  }
96
104
  if (entry && !entry.live) throw new Error(entry.error ?? 'MCP 旧连接仍在释放,暂不能创建替代实例')
97
105
  if (!entry) {
98
- entry = { key, spec, refs: 0, phase: 'queued', queuedAt: Date.now(), phaseAt: Date.now(), ready: Promise.resolve(), live: true, calls: Promise.resolve() }
106
+ entry = { key, spec, refs: 0, phase: 'queued', queuedAt: Date.now(), phaseAt: Date.now(), ready: Promise.resolve(), quiescence: Promise.resolve(), live: true, calls: Promise.resolve() }
99
107
  this.entries.set(key, entry)
100
108
  entry.ready = this.schedule(entry)
101
109
  void entry.ready.catch(() => undefined)
@@ -111,16 +119,27 @@ export class McpPool {
111
119
  } }
112
120
  }
113
121
 
114
- /** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
122
+ /** @private @author ddj 2026年10月29号 @param entry New entry. @returns Caller result, independent from the two-slot cleanup lifetime. */
115
123
  private schedule(entry: PoolEntry): Promise<void> {
116
- return new Promise<void>((resolve, reject) => {
117
- /** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
118
- const run = async (): Promise<void> => {
119
- try { await this.start(entry); resolve() } catch (error) { reject(error) }
124
+ let result!: StartupResult
125
+ const ready = new Promise<void>((resolve, reject) => { result = { resolve, reject } })
126
+ let lifetime!: StartupResult
127
+ entry.quiescence = new Promise<void>((resolve, reject) => { lifetime = { resolve, reject } })
128
+ void entry.quiescence.catch(() => undefined)
129
+
130
+ /** @private @author ddj 2026年10月29号 Runs and observes the complete owned mount even after caller rejection. */
131
+ const run = async (): Promise<void> => {
132
+ try {
133
+ await this.start(entry, result)
134
+ lifetime.resolve()
135
+ } catch (error) {
136
+ result.reject(error)
137
+ lifetime.reject(error)
120
138
  }
121
- this.queue.push(run)
122
- this.pump()
123
- })
139
+ }
140
+ this.queue.push(run)
141
+ this.pump()
142
+ return ready
124
143
  }
125
144
 
126
145
  /** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
@@ -137,9 +156,9 @@ export class McpPool {
137
156
  }
138
157
  }
139
158
 
140
- /** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
141
- private async start(entry: PoolEntry): Promise<void> {
142
- if (!entry.live || this.stopped) throw new Error('MCP 启动已取消')
159
+ /** @private @author ddj 2026年10月29号 @param entry Connection to initialize. @param result Caller settlement. @returns Full startup and cleanup lifetime. */
160
+ private async start(entry: PoolEntry, result: StartupResult): Promise<void> {
161
+ if (!entry.live || this.stopped) { result.reject(new Error('MCP 启动已取消')); return }
143
162
  entry.phase = 'loading'
144
163
  entry.phaseAt = Date.now()
145
164
  entry.startAt = entry.phaseAt
@@ -155,33 +174,41 @@ export class McpPool {
155
174
  entry.phase = 'ready'
156
175
  entry.finishedAt = Date.now()
157
176
  entry.connectedAt = entry.finishedAt
177
+ result.resolve()
158
178
  } catch (error) {
159
- entry.error = 'MCP 初始化失败或超时,请查看 Host 日志'
179
+ if (!entry.cleanupFailed) entry.error = 'MCP 初始化失败或超时;旧连接清理尚未确认,暂不能重建,请查看 Host 日志'
160
180
  const diagnostic = 'MCP initialization failed; phase=' + entry.phase + '; transport=' + entry.spec.config.transport
161
181
  + '; elapsedMs=' + (Date.now() - entry.startAt) + '; ' + mcpErrorText(error, entry.spec.config)
162
182
  debugRecord(this.ctx, entry.spec.workspace, diagnostic, 'warn')
163
183
  // Invalidation precedes disposal; neither late module load nor callbacks may publish.
164
184
  entry.live = false
165
185
  entry.phase = 'stopping'
166
- const closing = this.closeBridge(entry)
167
- await Promise.allSettled([closing, work])
168
- entry.phase = 'error'
169
- entry.finishedAt = Date.now()
170
- entry.retryAt = Date.now() + 5000
171
- // Failed generations stay invalid; acquire can replace them only after confirmed closure and cooldown.
172
- throw new Error(entry.error)
186
+ result.reject(new Error(entry.error))
187
+ await this.finishFailure(entry, work)
173
188
  } finally {
174
189
  if (timer) clearTimeout(timer)
175
190
  }
176
191
  }
177
192
 
193
+ /** @private @author ddj 2026年10月29号 @param entry Failed generation. @param work Entire mount, including late factory cleanup. @returns Confirmed quiescence; throws when closure is unconfirmed. */
194
+ private async finishFailure(entry: PoolEntry, work: Promise<void>): Promise<void> {
195
+ await Promise.allSettled([this.closeBridge(entry), work])
196
+ if (entry.cleanupFailed) throw new Error(entry.error)
197
+
198
+ entry.phase = 'error'
199
+ entry.finishedAt = Date.now()
200
+ entry.retryAt = entry.finishedAt + 5000
201
+ entry.error = 'MCP 初始化失败或超时,请查看 Host 日志'
202
+ }
203
+
178
204
  /** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
179
205
  private async mount(entry: PoolEntry): Promise<void> {
180
206
  const official = await this.load()
181
207
  if (!entry.live || this.stopped) return
182
208
  const bridge = await this.make()
183
- if (!entry.live || this.stopped) { await bridge.dispose(); return }
184
209
  entry.bridge = bridge
210
+ // Keep late factory ownership visible, including synchronous or rejected disposal failures.
211
+ if (!entry.live || this.stopped) { await this.closeBridge(entry); return }
185
212
  entry.phase = 'starting'
186
213
  entry.phaseAt = Date.now()
187
214
  entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config)
@@ -189,10 +216,14 @@ export class McpPool {
189
216
  if (entry.fiber?.state !== undefined && entry.fiber.state !== 2) throw new Error('MCP 插件未完成激活')
190
217
  }
191
218
 
192
- /** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
219
+ /** @private @author ddj 2026年10月29号 @param entry Connection to close. @returns Shared closure barrier; synchronous throws and rejections retain a fenced entry. */
193
220
  private async closeBridge(entry: PoolEntry): Promise<void> {
194
221
  if (entry.close) return entry.close
195
- const close = entry.bridge?.dispose() ?? Promise.resolve()
222
+ const bridge = entry.bridge
223
+ if (!bridge) return
224
+ /** @private @author ddj 2026年10月29号 @returns Deferred disposal so synchronous throws are owned by the shared barrier. */
225
+ const dispose = (): Promise<void> => bridge.dispose()
226
+ const close = Promise.resolve().then(dispose)
196
227
  entry.close = close
197
228
  try {
198
229
  await close
@@ -200,6 +231,7 @@ export class McpPool {
200
231
  entry.fiber = undefined
201
232
  entry.close = undefined
202
233
  } catch (error) {
234
+ entry.cleanupFailed = true
203
235
  entry.error = 'MCP 释放未确认,禁止重建,请查看 Host 日志'
204
236
  this.ctx.logger?.warn?.('MCP cleanup failed: ' + String(error))
205
237
  throw error
@@ -220,10 +252,10 @@ export class McpPool {
220
252
  entry.live = false
221
253
  entry.phase = 'stopping'
222
254
  if (entry.idle) clearTimeout(entry.idle)
223
- await this.closeBridge(entry)
224
- await entry.ready.catch(() => undefined)
225
- // A late mount checks live before assigning its bridge.
226
- await entry.calls.catch(() => undefined)
255
+ const closures = await Promise.allSettled([this.closeBridge(entry), entry.quiescence, entry.calls])
256
+ const failure = closures.find((closure) => closure.status === 'rejected')
257
+ if (failure?.status === 'rejected') throw failure.reason
258
+ // Only the whole mount barrier proves that no late factory can still create a scope.
227
259
  if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key)
228
260
  }
229
261
 
@@ -243,7 +275,9 @@ export class McpPool {
243
275
  /** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
244
276
  async dispose(): Promise<void> {
245
277
  this.stopped = true
246
- await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)))
278
+ const closures = await Promise.allSettled([...this.entries.values()].map((entry) => this.retire(entry)))
247
279
  await Promise.all([...this.jobs])
280
+ const failure = closures.find((closure) => closure.status === 'rejected')
281
+ if (failure?.status === 'rejected') throw failure.reason
248
282
  }
249
283
  }