dsh-vscode-mode 0.21.2 → 0.21.4
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.md +7 -3
- package/lib/client.js +182 -76
- package/lib/client.js.map +2 -2
- package/lib/index.js +65 -30
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +79 -32
- package/src/client/sidebar/panels/SvnPanel.ts +38 -30
- package/src/mcpPool.ts +62 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
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": [
|
|
@@ -281,28 +281,78 @@ interface SessionBridge {
|
|
|
281
281
|
scope?: (id: string) => unknown
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/**
|
|
285
|
-
|
|
284
|
+
/** Detailed admission evidence; none of these states proves task completion. */
|
|
285
|
+
export interface TaskSendResult {
|
|
286
|
+
status: 'accepted' | 'rejected' | 'unavailable' | 'unknown' | 'submitted' | 'cancelled'
|
|
287
|
+
/** Historical boolean from this same attempt, not an admission marker. */
|
|
288
|
+
legacyOk: boolean
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @private @author ddj 2026年10月29号 @param receipt Raw selected-channel receipt. @returns Explicit admission evidence and the historical boolean. */
|
|
292
|
+
function taskReceipt(receipt: unknown): TaskSendResult {
|
|
293
|
+
const ok = (receipt as { ok?: unknown } | null)?.ok
|
|
294
|
+
if (ok === true) return { status: 'accepted', legacyOk: true }
|
|
295
|
+
if (ok === false) return { status: 'rejected', legacyOk: false }
|
|
296
|
+
return { status: 'unknown', legacyOk: false }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @private @author ddj 2026年10月29号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Admission evidence without switching transport. @sideEffects Retains and releases exactly one generation. */
|
|
300
|
+
async function sendOwned(ctx: CtxLike, sessions: SessionBridge, id: string, text: string): Promise<TaskSendResult> {
|
|
286
301
|
const controller = new AbortController()
|
|
287
302
|
const reference = sessions.retain!(id, { source: 'controllerOperation', signal: controller.signal })
|
|
288
303
|
let released = false
|
|
289
|
-
|
|
304
|
+
let cleanupFailed = false
|
|
305
|
+
/** @private @author ddj 2026年10月29号 Cancels and releases this exact generation once; remembers unload cleanup failures. */
|
|
290
306
|
function release(): void {
|
|
291
307
|
if (released) return
|
|
292
308
|
released = true
|
|
293
309
|
controller.abort()
|
|
294
|
-
reference.release()
|
|
310
|
+
try { reference.release() } catch (error) { cleanupFailed = true; throw error }
|
|
295
311
|
}
|
|
296
312
|
let remove: (() => void) | undefined
|
|
313
|
+
let started = false
|
|
314
|
+
let result: TaskSendResult = { status: 'unavailable', legacyOk: false }
|
|
297
315
|
try {
|
|
298
316
|
remove = ctx.effect?.(() => release, 'vscode-mode: task session reference')
|
|
299
317
|
const binding = await reference.ready
|
|
300
|
-
if (controller.signal.aborted)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
318
|
+
if (controller.signal.aborted) result = { status: 'cancelled', legacyOk: false }
|
|
319
|
+
else {
|
|
320
|
+
started = true
|
|
321
|
+
result = taskReceipt(await binding.session.prompt([{ type: 'text', text }], 'queue', controller.signal))
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
result = { status: started ? 'unknown' : 'unavailable', legacyOk: false }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Snapshot cancellation before normal cleanup aborts our own signal.
|
|
328
|
+
const cancelled = controller.signal.aborted
|
|
329
|
+
try { release() } catch { cleanupFailed = true }
|
|
330
|
+
try { remove?.() } catch { cleanupFailed = true }
|
|
331
|
+
if (cleanupFailed) result = { status: started ? 'unknown' : 'unavailable', legacyOk: false }
|
|
332
|
+
if (cancelled) result = { status: 'cancelled', legacyOk: result.legacyOk }
|
|
333
|
+
return result
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @private @author ddj 2026年10月29号 @param ctx Service owner. @param id Fixed target. @param text Task body. @returns One legacy-channel attempt; no cross-channel retry. */
|
|
337
|
+
async function sendLegacy(ctx: CtxLike, id: string, text: string): Promise<TaskSendResult> {
|
|
338
|
+
const input = inputFor(ctx, id) as { setDraft?: (task: string) => void; submit?: () => void } | undefined
|
|
339
|
+
if (typeof input?.setDraft === 'function' && typeof input.submit === 'function') {
|
|
340
|
+
let submitted = false
|
|
341
|
+
try {
|
|
342
|
+
input.setDraft(text)
|
|
343
|
+
submitted = true
|
|
344
|
+
input.submit()
|
|
345
|
+
return { status: 'submitted', legacyOk: true }
|
|
346
|
+
} catch { return { status: submitted ? 'unknown' : 'unavailable', legacyOk: false } }
|
|
305
347
|
}
|
|
348
|
+
|
|
349
|
+
const remote = ctx.get('remote.session') as { prompt?: (req: unknown) => Promise<unknown> } | undefined
|
|
350
|
+
if (typeof remote?.prompt !== 'function') return httpTaskResult(id, text)
|
|
351
|
+
try {
|
|
352
|
+
const payload = { sessionId: id, mode: 'queue', content: [{ type: 'text', text }] }
|
|
353
|
+
const receipt = await remote.prompt(slotArg(payload))
|
|
354
|
+
return taskReceipt((receipt as { result?: unknown } | null)?.result ?? receipt)
|
|
355
|
+
} catch { return { status: 'unknown', legacyOk: false } }
|
|
306
356
|
}
|
|
307
357
|
|
|
308
358
|
/** 新建会话结果(ok=false 供调用方降级回当前对话注入)。 */
|
|
@@ -386,6 +436,11 @@ async function apiSessionCreate(cwd: string | undefined): Promise<string> {
|
|
|
386
436
|
* @returns 是否受理
|
|
387
437
|
*/
|
|
388
438
|
export async function apiSessionPrompt(sessionId: string, text: string): Promise<boolean> {
|
|
439
|
+
return (await httpTaskResult(sessionId, text)).legacyOk
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns HTTP receipt evidence; errors after request entry are never safe rejection. */
|
|
443
|
+
async function httpTaskResult(sessionId: string, text: string): Promise<TaskSendResult> {
|
|
389
444
|
try {
|
|
390
445
|
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
391
446
|
const res = await fetch(appUrl(SESSION_PROMPT_PATH), {
|
|
@@ -399,9 +454,9 @@ export async function apiSessionPrompt(sessionId: string, text: string): Promise
|
|
|
399
454
|
}),
|
|
400
455
|
})
|
|
401
456
|
const json = await res.json().catch(() => null) as { result?: { ok?: boolean } } | null
|
|
402
|
-
return json?.result
|
|
457
|
+
return taskReceipt(json?.result)
|
|
403
458
|
} catch {
|
|
404
|
-
return false
|
|
459
|
+
return { status: 'unknown', legacyOk: false }
|
|
405
460
|
}
|
|
406
461
|
}
|
|
407
462
|
|
|
@@ -466,8 +521,10 @@ export interface AddToConversation {
|
|
|
466
521
|
appendText(sessionId: string | undefined, text: string): Promise<AddOutcome>
|
|
467
522
|
/** 新建独立会话并切换(旁路任务隔离;失败 ok:false 供降级回当前对话)。 */
|
|
468
523
|
startDraftSession(cwd?: string): Promise<DraftSessionResult>
|
|
469
|
-
/**
|
|
524
|
+
/** Historical compatibility result, not proof that a task was accepted. */
|
|
470
525
|
sendTask(sessionId: string | undefined, text: string): Promise<boolean>
|
|
526
|
+
/** Optional on older injected facades; detailed callers must not infer acceptance from sendTask. */
|
|
527
|
+
sendTaskResult?(sessionId: string | undefined, text: string): Promise<TaskSendResult>
|
|
471
528
|
}
|
|
472
529
|
|
|
473
530
|
/**
|
|
@@ -640,32 +697,22 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
640
697
|
/**
|
|
641
698
|
* 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
|
|
642
699
|
* 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
|
|
643
|
-
* @author ddj 2026年
|
|
700
|
+
* @author ddj 2026年10月29号
|
|
644
701
|
* @param sessionId 目标会话 id
|
|
645
702
|
* @param text 任务文本
|
|
646
|
-
* @returns
|
|
703
|
+
* @returns 同一次调用的历史boolean语义,不能作为受理事实
|
|
647
704
|
*/
|
|
648
|
-
const sendTask: AddToConversation['sendTask'] = async (sessionId, text) =>
|
|
649
|
-
|
|
705
|
+
const sendTask: AddToConversation['sendTask'] = async (sessionId, text) => (await sendTaskResult(sessionId, text)).legacyOk
|
|
706
|
+
|
|
707
|
+
/** @private @author ddj 2026年10月29号 @param sessionId Fixed target. @param text Task body. @returns Detailed evidence from exactly one selected transport. */
|
|
708
|
+
async function sendTaskResult(sessionId: string | undefined, text: string): Promise<TaskSendResult> {
|
|
709
|
+
if (!sessionId) return { status: 'unavailable', legacyOk: false }
|
|
650
710
|
try {
|
|
651
711
|
const sessions = ctx.get('sessions') as SessionBridge | undefined
|
|
652
712
|
if (typeof sessions?.retain === 'function') return await sendOwned(ctx, sessions, sessionId, text)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const input = inputFor(ctx, sessionId) as { setDraft?: (task: string) => void; submit?: () => void } | undefined
|
|
656
|
-
if (typeof input?.setDraft === 'function' && typeof input.submit === 'function') {
|
|
657
|
-
input.setDraft(text)
|
|
658
|
-
input.submit()
|
|
659
|
-
return true
|
|
660
|
-
}
|
|
661
|
-
const remote = ctx.get('remote.session') as { prompt?: (req: unknown) => Promise<unknown> } | undefined
|
|
662
|
-
if (typeof remote?.prompt === 'function') {
|
|
663
|
-
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
664
|
-
return rpcOk(await remote.prompt(slotArg(payload))).ok
|
|
665
|
-
}
|
|
666
|
-
return await apiSessionPrompt(sessionId, text)
|
|
667
|
-
} catch { return false }
|
|
713
|
+
return await sendLegacy(ctx, sessionId, text)
|
|
714
|
+
} catch { return { status: 'unavailable', legacyOk: false } }
|
|
668
715
|
}
|
|
669
716
|
|
|
670
|
-
return { appendReference, appendText, startDraftSession, sendTask }
|
|
717
|
+
return { appendReference, appendText, startDraftSession, sendTask, sendTaskResult }
|
|
671
718
|
}
|
|
@@ -39,6 +39,7 @@ import type { ContextMenuEntry } from '../../ui/ContextMenu.js'
|
|
|
39
39
|
import { SvnAiPlanDialog } from '../../ui/SvnAiPlanDialog.js'
|
|
40
40
|
import { budgetGroupsOf } from './svnListBudget.js'
|
|
41
41
|
import type { SidebarCtx } from '../types.js'
|
|
42
|
+
import type { DraftSessionResult, TaskSendResult } from '../../addToConversation.js'
|
|
42
43
|
|
|
43
44
|
/** 未分组条目的分组键(changelist 为空)。 */
|
|
44
45
|
const NO_CHANGELIST = '(未分组)'
|
|
@@ -845,10 +846,9 @@ export function SvnPanel(props) {
|
|
|
845
846
|
|
|
846
847
|
/**
|
|
847
848
|
* 深度分析入口(02-deep-session-prompt 修正版):**新建独立会话**承载任务
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
* @author ddj 2026年09月23号
|
|
849
|
+
* (不污染当前对话),显式受理才等待方案;不明回执不重发或填入另一草稿。
|
|
850
|
+
* 未发送或明确拒绝时仅填当前草稿,提示用户手动发送,prompt使用实际草稿会话。
|
|
851
|
+
* @author ddj 2026年10月29号
|
|
852
852
|
*/
|
|
853
853
|
const runDeepPlan = React.useCallback(() => {
|
|
854
854
|
const live = ctxRef.current
|
|
@@ -858,34 +858,42 @@ export function SvnPanel(props) {
|
|
|
858
858
|
const since = Date.now()
|
|
859
859
|
const add = live?.addToConversation
|
|
860
860
|
if (!add?.appendText) { live?.notify?.('无法填入任务(无会话或输入框不可用)'); return }
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
})
|
|
861
|
+
|
|
862
|
+
/** @private @author ddj 2026年10月29号 @returns Draft insertion only; never submits or starts a task poll. */
|
|
863
|
+
async function draftCurrent(): Promise<void> {
|
|
864
|
+
if (!sessionId) { live?.notify?.('无法填入任务(无会话或输入框不可用)'); return }
|
|
865
|
+
try {
|
|
866
|
+
const outcome = await add.appendText(sessionId, buildAgentPrompt(wcRoot, sessionId))
|
|
867
|
+
if (outcome === 'ok' || outcome === 'busy') live?.notify?.('已填入当前草稿,请手动发送')
|
|
868
|
+
else live?.notify?.('发起失败:' + (outcome === 'unavailable' ? '无会话或输入框不可用' : '输入框忙,请重试'))
|
|
869
|
+
} catch { live?.notify?.('草稿填入失败,请检查当前输入框') }
|
|
871
870
|
}
|
|
872
|
-
/**
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
871
|
+
/** @private @author ddj 2026年10月29号 @param id Created target. @returns One detailed send decision; ambiguous receipts never retry. */
|
|
872
|
+
async function sendAndWatch(id): Promise<void> {
|
|
873
|
+
if (!id) { await draftCurrent(); return }
|
|
874
|
+
if (typeof add.sendTaskResult !== 'function') {
|
|
875
|
+
live?.notify?.('发送接口不支持受理确认,本次未自动发送,请手动处理')
|
|
876
|
+
return
|
|
877
|
+
}
|
|
878
|
+
let result: TaskSendResult | undefined
|
|
879
|
+
try { result = await add.sendTaskResult(id, buildAgentPrompt(wcRoot, id)) }
|
|
880
|
+
catch { result = { status: 'unknown', legacyOk: false } }
|
|
881
|
+
if (result?.status === 'accepted') {
|
|
882
|
+
live?.notify?.('已在新会话「AI 深度分析」受理任务,请在左侧会话列表点开查看进度')
|
|
883
|
+
startDeepPoll(since)
|
|
884
|
+
} else if (result?.status === 'rejected' || result?.status === 'unavailable') await draftCurrent()
|
|
885
|
+
else if (result?.status === 'cancelled') live?.notify?.('发送操作已取消,任务可能已提交,请检查目标会话:' + id)
|
|
886
|
+
else if (result?.status === 'submitted') live?.notify?.('已调用提交但受理未确认,请检查目标会话,不要重复发送:' + id)
|
|
887
|
+
else live?.notify?.('任务受理未确认,请检查目标会话,不要重复发送:' + id)
|
|
888
|
+
}
|
|
889
|
+
/** @private @author ddj 2026年10月29号 @returns Creates a target before sending; creation failure can only insert a manual draft. */
|
|
890
|
+
async function beginDeep(): Promise<void> {
|
|
891
|
+
let result: DraftSessionResult | undefined
|
|
892
|
+
try { result = await add.startDraftSession?.(wcRoot || undefined) }
|
|
893
|
+
catch { result = { ok: false } }
|
|
894
|
+
await sendAndWatch(result?.ok ? result.id : undefined)
|
|
884
895
|
}
|
|
885
|
-
void (
|
|
886
|
-
? add.startDraftSession(wcRoot || undefined)
|
|
887
|
-
: Promise.resolve({ ok: false })
|
|
888
|
-
).then((result) => { sendAndWatch(result?.ok ? result.id : undefined) })
|
|
896
|
+
void beginDeep()
|
|
889
897
|
}, [aiBusy, aiRun, deepWaiting, entries, sessionId, startDeepPoll])
|
|
890
898
|
|
|
891
899
|
// --endregion
|
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月
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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月
|
|
141
|
-
private async start(entry: PoolEntry): Promise<void> {
|
|
142
|
-
if (!entry.live || this.stopped)
|
|
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
|
|
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
|
-
|
|
167
|
-
await
|
|
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月
|
|
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
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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.
|
|
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
|
}
|