dsh-vscode-mode 0.21.1 → 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/README.md +7 -3
- package/lib/client.js +66 -59
- package/lib/client.js.map +2 -2
- package/lib/index.js +1368 -1296
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +70 -45
- package/src/mcpDiagnostic.ts +42 -0
- package/src/mcpPool.ts +67 -29
- package/src/mcpRuntime.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.21.
|
|
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": [
|
|
@@ -206,7 +206,10 @@ export function refOf(mention: string, range: RefRange | undefined): string {
|
|
|
206
206
|
: `${mention} L${range.startLine}-${range.endLine}`
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/** 构造 DSH
|
|
209
|
+
/** 构造 DSH 文件引用插入载荷,模型文本、剪贴板投影与降级文本共用完整行区间。
|
|
210
|
+
* @public
|
|
211
|
+
* @author ddj 2026年10月28号
|
|
212
|
+
* @returns 引用载荷与含可选行区间的完整引用文本
|
|
210
213
|
* @param path 目标路径(相对 cwd 或绝对)
|
|
211
214
|
* @param cwd 会话工作区目录(可选)
|
|
212
215
|
* @param range 行区间(可选)
|
|
@@ -218,12 +221,13 @@ export function buildFileRef(
|
|
|
218
221
|
range?: RefRange,
|
|
219
222
|
appearance: RefAppearance = 'file',
|
|
220
223
|
): { reference: ReferenceInsertLike; mention: string } {
|
|
221
|
-
const mention = mentionOf(path, cwd)
|
|
224
|
+
const mention = refOf(mentionOf(path, cwd), range)
|
|
225
|
+
|
|
222
226
|
return {
|
|
223
227
|
mention,
|
|
224
228
|
reference: {
|
|
225
229
|
source: REF_SOURCE,
|
|
226
|
-
ref:
|
|
230
|
+
ref: mention,
|
|
227
231
|
label: labelOf(path, range),
|
|
228
232
|
appearance,
|
|
229
233
|
clipboardText: mention,
|
|
@@ -264,6 +268,41 @@ export interface TokenSpanLike {
|
|
|
264
268
|
/** ctx 依赖的最小面(sessions.scope + conversation.input)。 */
|
|
265
269
|
export interface CtxLike {
|
|
266
270
|
get: (name: string) => unknown
|
|
271
|
+
effect?: (setup: () => () => void, label?: string) => (() => void)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Public Controller subset; old Hosts need not provide retain/binding. */
|
|
275
|
+
interface SessionBridge {
|
|
276
|
+
retain?: (id: string, options: { source: 'controllerOperation'; signal: AbortSignal }) => {
|
|
277
|
+
ready: Promise<{ session: { prompt(content: { type: 'text'; text: string }[], mode: 'queue', signal: AbortSignal): Promise<{ ok: boolean }> } }>
|
|
278
|
+
release(): void
|
|
279
|
+
}
|
|
280
|
+
binding?: (id: string) => { ctx: unknown } | undefined
|
|
281
|
+
scope?: (id: string) => unknown
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** @private @author ddj 2026年10月28号 @param ctx Lifecycle owner. @param sessions Public Controller. @param id Fixed target. @param text Task body. @returns Acceptance; failures never retry another transport. @sideEffects Retains one generation until completion or unload. */
|
|
285
|
+
async function sendOwned(ctx: CtxLike, sessions: SessionBridge, id: string, text: string): Promise<boolean> {
|
|
286
|
+
const controller = new AbortController()
|
|
287
|
+
const reference = sessions.retain!(id, { source: 'controllerOperation', signal: controller.signal })
|
|
288
|
+
let released = false
|
|
289
|
+
/** @private @author ddj 2026年10月28号 Cancel pending admission and release this exact generation once. */
|
|
290
|
+
function release(): void {
|
|
291
|
+
if (released) return
|
|
292
|
+
released = true
|
|
293
|
+
controller.abort()
|
|
294
|
+
reference.release()
|
|
295
|
+
}
|
|
296
|
+
let remove: (() => void) | undefined
|
|
297
|
+
try {
|
|
298
|
+
remove = ctx.effect?.(() => release, 'vscode-mode: task session reference')
|
|
299
|
+
const binding = await reference.ready
|
|
300
|
+
if (controller.signal.aborted) return false
|
|
301
|
+
return (await binding.session.prompt([{ type: 'text', text }], 'queue', controller.signal)).ok === true
|
|
302
|
+
} finally {
|
|
303
|
+
release()
|
|
304
|
+
remove?.()
|
|
305
|
+
}
|
|
267
306
|
}
|
|
268
307
|
|
|
269
308
|
/** 新建会话结果(ok=false 供调用方降级回当前对话注入)。 */
|
|
@@ -368,8 +407,9 @@ export async function apiSessionPrompt(sessionId: string, text: string): Promise
|
|
|
368
407
|
|
|
369
408
|
/**
|
|
370
409
|
* 新建独立会话承载旁路任务(02-deep-session-prompt:深度分析不污染当前对话)。
|
|
371
|
-
*
|
|
372
|
-
*
|
|
410
|
+
* 优先公开 sessions.create;旧 Host 按能力选择 remote 或 wire,调用后不换通道重试。
|
|
411
|
+
* 受理状态不明时返回 ok:false,不重复创建会话。
|
|
412
|
+
* @author ddj 2026年10月28号
|
|
373
413
|
* @author ddj 2026年09月23号
|
|
374
414
|
* @param ctx 客户端服务上下文(remote.session + sessions,仅回落用)
|
|
375
415
|
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
@@ -391,21 +431,20 @@ export async function newDraftSession(ctx: CtxLike, cwd: string | undefined): Pr
|
|
|
391
431
|
: ''
|
|
392
432
|
if (id) return { ok: true, id }
|
|
393
433
|
}
|
|
394
|
-
|
|
434
|
+
// A create call may have succeeded remotely even when its receipt is unavailable.
|
|
435
|
+
if (typeof sessions?.create === 'function') return { ok: false }
|
|
436
|
+
} catch { return { ok: false } }
|
|
395
437
|
try {
|
|
396
|
-
//
|
|
438
|
+
// Legacy Hosts select one known transport; never retry a non-idempotent create.
|
|
397
439
|
const remote = ctx.get('remote.session') as { create?: (req: unknown) => Promise<unknown> } | undefined
|
|
398
440
|
if (typeof remote?.create === 'function') {
|
|
399
441
|
const payload = cwd ? { cwd } : {}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (r.ok && typeof id?.sessionId === 'string') return { ok: true, id: id.sessionId }
|
|
405
|
-
} catch { /* 单形态失败试下一形态 */ }
|
|
406
|
-
}
|
|
442
|
+
const receipt = rpcOk(await remote.create(slotArg(payload)))
|
|
443
|
+
const created = receipt.value as { sessionId?: unknown } | undefined
|
|
444
|
+
if (receipt.ok && typeof created?.sessionId === 'string') return { ok: true, id: created.sessionId }
|
|
445
|
+
return { ok: false }
|
|
407
446
|
}
|
|
408
|
-
} catch {
|
|
447
|
+
} catch { return { ok: false } }
|
|
409
448
|
// 回落 2:wire 直调(创建成功但 store 无感,open 切换不保证;任务发送仍可用)
|
|
410
449
|
try {
|
|
411
450
|
const id = await apiSessionCreate(cwd)
|
|
@@ -441,9 +480,9 @@ export interface AddToConversation {
|
|
|
441
480
|
export function inputFor(ctx: CtxLike, sessionId: string | undefined): InputLike | undefined {
|
|
442
481
|
if (!sessionId) return undefined
|
|
443
482
|
try {
|
|
444
|
-
const sessions = ctx.get('sessions') as
|
|
483
|
+
const sessions = ctx.get('sessions') as SessionBridge | undefined
|
|
445
484
|
const conversation = ctx.get('conversation') as { input?: { for: (actx: unknown) => unknown } } | undefined
|
|
446
|
-
const actx = sessions?.scope?.(sessionId)
|
|
485
|
+
const actx = typeof sessions?.binding === 'function' ? sessions.binding(sessionId)?.ctx : sessions?.scope?.(sessionId)
|
|
447
486
|
const shell = actx && conversation?.input?.for?.(actx)
|
|
448
487
|
// 探针须是我们的**实际依赖**(insertReference),而不是会被整篇重建的 setDraft
|
|
449
488
|
return (shell && typeof (shell as InputLike).insertReference === 'function') ? shell as InputLike : undefined
|
|
@@ -569,13 +608,15 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
569
608
|
/**
|
|
570
609
|
* 新建独立会话(02-deep-session-prompt:旁路任务不污染当前对话)。
|
|
571
610
|
* 闭包持有真服务 ctx(面板 SidebarCtx 无 .get,会话操作必须经此门面);
|
|
572
|
-
*
|
|
611
|
+
* 新版仅返回已登记身份,视图选择由宿主负责;旧版保留可用的 open/select。
|
|
612
|
+
* @author ddj 2026年10月28号
|
|
573
613
|
* @author ddj 2026年09月23号
|
|
574
614
|
* @param cwd 新会话工作区目录(与当前会话同工作区)
|
|
575
615
|
* @returns 新会话结果(失败供降级)
|
|
576
616
|
*/
|
|
577
617
|
const startDraftSession: AddToConversation['startDraftSession'] = async (cwd) => {
|
|
578
618
|
const result = await newDraftSession(ctx, cwd)
|
|
619
|
+
if (typeof (ctx.get('sessions') as SessionBridge | undefined)?.retain === 'function') return result
|
|
579
620
|
if (result.ok && result.id) {
|
|
580
621
|
try {
|
|
581
622
|
// 主动铸造 scope(scope 对新会话惰性铸造,inputFor/composer 面依赖它)
|
|
@@ -598,48 +639,32 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
598
639
|
|
|
599
640
|
/**
|
|
600
641
|
* 发送任务给目标会话 agent(点击即发送方案:任务落地会话即非 blank,列表可见)。
|
|
601
|
-
*
|
|
602
|
-
* @author ddj 2026年09月23号
|
|
642
|
+
* 新版持有目标 Controller 到 prompt 结束;旧版按能力选择唯一通道,不跨通道重发。
|
|
643
|
+
* @author ddj 2026年09月23号 / 2026年10月28号
|
|
603
644
|
* @param sessionId 目标会话 id
|
|
604
645
|
* @param text 任务文本
|
|
605
646
|
* @returns 是否受理
|
|
606
647
|
*/
|
|
607
648
|
const sendTask: AddToConversation['sendTask'] = async (sessionId, text) => {
|
|
608
649
|
if (!sessionId) return false
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
650
|
+
try {
|
|
651
|
+
const sessions = ctx.get('sessions') as SessionBridge | undefined
|
|
652
|
+
if (typeof sessions?.retain === 'function') return await sendOwned(ctx, sessions, sessionId, text)
|
|
653
|
+
|
|
654
|
+
// Legacy composer remains an exclusive channel; never overwrite the draft and then retry elsewhere.
|
|
655
|
+
const input = inputFor(ctx, sessionId) as { setDraft?: (task: string) => void; submit?: () => void } | undefined
|
|
656
|
+
if (typeof input?.setDraft === 'function' && typeof input.submit === 'function') {
|
|
614
657
|
input.setDraft(text)
|
|
615
658
|
input.submit()
|
|
616
659
|
return true
|
|
617
660
|
}
|
|
618
|
-
return false
|
|
619
|
-
}
|
|
620
|
-
try {
|
|
621
|
-
for (let attempt = 3; attempt > 0; attempt--) {
|
|
622
|
-
if (await composerOnce()) return true
|
|
623
|
-
if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 500))
|
|
624
|
-
}
|
|
625
|
-
} catch { /* 降级下一通道 */ }
|
|
626
|
-
// 兜底 1:remote.session service 面(多形态容错)
|
|
627
|
-
try {
|
|
628
661
|
const remote = ctx.get('remote.session') as { prompt?: (req: unknown) => Promise<unknown> } | undefined
|
|
629
662
|
if (typeof remote?.prompt === 'function') {
|
|
630
663
|
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
if (rpcOk(await remote.prompt(arg)).ok) return true
|
|
634
|
-
} catch { /* 单形态失败试下一形态 */ }
|
|
635
|
-
}
|
|
664
|
+
return rpcOk(await remote.prompt(slotArg(payload))).ok
|
|
636
665
|
}
|
|
637
|
-
} catch { /* 降级下一通道 */ }
|
|
638
|
-
// 兜底 2:HTTP wire
|
|
639
|
-
try {
|
|
640
666
|
return await apiSessionPrompt(sessionId, text)
|
|
641
|
-
} catch {
|
|
642
|
-
return false
|
|
667
|
+
} catch { return false }
|
|
643
668
|
}
|
|
644
669
|
|
|
645
670
|
return { appendReference, appendText, startDraftSession, sendTask }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Bounded credential-safe MCP failure diagnostics. @author ddj 2026年10月28号 */
|
|
2
|
+
import type { MpcConfig } from './shared/mcp.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format an initialization failure without serializing the MCP configuration.
|
|
6
|
+
* @public @author ddj 2026年10月28号
|
|
7
|
+
* @param error Original failure and optional cause chain.
|
|
8
|
+
* @param config Effective configuration, used only to remove known secret values.
|
|
9
|
+
* @returns Bounded single-line diagnostic; URLs and explicit credentials are removed.
|
|
10
|
+
*/
|
|
11
|
+
export function mcpErrorText(error: unknown, config: MpcConfig): string {
|
|
12
|
+
const messages: string[] = []
|
|
13
|
+
const seen = new Set<unknown>()
|
|
14
|
+
let cause = error
|
|
15
|
+
while (cause !== undefined && !seen.has(cause) && messages.length < 4) {
|
|
16
|
+
seen.add(cause)
|
|
17
|
+
if (cause instanceof Error) {
|
|
18
|
+
const code = (cause as Error & { code?: unknown }).code
|
|
19
|
+
messages.push(cause.name + ': ' + cause.message + (typeof code === 'string' ? ' [' + code + ']' : ''))
|
|
20
|
+
cause = cause.cause
|
|
21
|
+
} else {
|
|
22
|
+
messages.push(typeof cause === 'string' ? cause : 'Unknown MCP failure')
|
|
23
|
+
break
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let text = messages.join(' <- ')
|
|
28
|
+
const secrets = [...Object.values(config.env ?? {}), ...Object.values(config.headers ?? {})]
|
|
29
|
+
// Arguments may contain credentials; never reproduce their non-option values in an error.
|
|
30
|
+
for (const argument of config.args ?? []) {
|
|
31
|
+
if (!argument.startsWith('-')) secrets.push(argument)
|
|
32
|
+
}
|
|
33
|
+
secrets.sort((left, right) => right.length - left.length)
|
|
34
|
+
for (const secret of secrets) {
|
|
35
|
+
if (secret) text = text.split(secret).join('[REDACTED]')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
text = text.replace(/https?:\/\/[^\s"'<>]+/gi, '[URL]')
|
|
39
|
+
.replace(/\b(authorization|api[-_]?key|token|password|secret)\s*[:=]\s*[^\s,;]+/gi, '$1=[REDACTED]')
|
|
40
|
+
.replace(/[\r\n\x00-\x1f\x7f]+/g, ' ')
|
|
41
|
+
return text.slice(0, 2048)
|
|
42
|
+
}
|
package/src/mcpPool.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Connection ownership, bounded cold starts and workspace leases. @author ddj 2026年10月20号 */
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { McpBridge } from './mcpBridge.js'
|
|
4
|
+
import { debugRecord } from './debugLog.js'
|
|
5
|
+
import { mcpErrorText } from './mcpDiagnostic.js'
|
|
4
6
|
import type { McpPolicy, McpPhase, MpcConfig } from './shared/mcp.js'
|
|
5
7
|
import type { Ctx } from './store.js'
|
|
6
8
|
|
|
@@ -10,6 +12,11 @@ export interface PoolSpec {
|
|
|
10
12
|
config: MpcConfig
|
|
11
13
|
policy: McpPolicy
|
|
12
14
|
}
|
|
15
|
+
/** Startup caller settlement, independent from the owned resource lifetime. */
|
|
16
|
+
interface StartupResult {
|
|
17
|
+
resolve(): void
|
|
18
|
+
reject(error: unknown): void
|
|
19
|
+
}
|
|
13
20
|
export interface PoolEntry {
|
|
14
21
|
key: string
|
|
15
22
|
spec: PoolSpec
|
|
@@ -25,6 +32,9 @@ export interface PoolEntry {
|
|
|
25
32
|
bridge?: McpBridge
|
|
26
33
|
fiber?: any
|
|
27
34
|
ready: Promise<void>
|
|
35
|
+
/** Entire queued mount/cleanup lifetime; rejection means closure remains unconfirmed. */
|
|
36
|
+
quiescence: Promise<void>
|
|
37
|
+
cleanupFailed?: boolean
|
|
28
38
|
close?: Promise<void>
|
|
29
39
|
idle?: ReturnType<typeof setTimeout>
|
|
30
40
|
live: boolean
|
|
@@ -93,7 +103,7 @@ export class McpPool {
|
|
|
93
103
|
}
|
|
94
104
|
if (entry && !entry.live) throw new Error(entry.error ?? 'MCP 旧连接仍在释放,暂不能创建替代实例')
|
|
95
105
|
if (!entry) {
|
|
96
|
-
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() }
|
|
97
107
|
this.entries.set(key, entry)
|
|
98
108
|
entry.ready = this.schedule(entry)
|
|
99
109
|
void entry.ready.catch(() => undefined)
|
|
@@ -109,16 +119,27 @@ export class McpPool {
|
|
|
109
119
|
} }
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
/** @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. */
|
|
113
123
|
private schedule(entry: PoolEntry): Promise<void> {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
118
138
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
}
|
|
140
|
+
this.queue.push(run)
|
|
141
|
+
this.pump()
|
|
142
|
+
return ready
|
|
122
143
|
}
|
|
123
144
|
|
|
124
145
|
/** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
|
|
@@ -135,9 +156,9 @@ export class McpPool {
|
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
/** @private @author ddj 2026年10月
|
|
139
|
-
private async start(entry: PoolEntry): Promise<void> {
|
|
140
|
-
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 }
|
|
141
162
|
entry.phase = 'loading'
|
|
142
163
|
entry.phaseAt = Date.now()
|
|
143
164
|
entry.startAt = entry.phaseAt
|
|
@@ -153,31 +174,41 @@ export class McpPool {
|
|
|
153
174
|
entry.phase = 'ready'
|
|
154
175
|
entry.finishedAt = Date.now()
|
|
155
176
|
entry.connectedAt = entry.finishedAt
|
|
177
|
+
result.resolve()
|
|
156
178
|
} catch (error) {
|
|
157
|
-
entry.error = 'MCP
|
|
158
|
-
|
|
179
|
+
if (!entry.cleanupFailed) entry.error = 'MCP 初始化失败或超时;旧连接清理尚未确认,暂不能重建,请查看 Host 日志'
|
|
180
|
+
const diagnostic = 'MCP initialization failed; phase=' + entry.phase + '; transport=' + entry.spec.config.transport
|
|
181
|
+
+ '; elapsedMs=' + (Date.now() - entry.startAt) + '; ' + mcpErrorText(error, entry.spec.config)
|
|
182
|
+
debugRecord(this.ctx, entry.spec.workspace, diagnostic, 'warn')
|
|
159
183
|
// Invalidation precedes disposal; neither late module load nor callbacks may publish.
|
|
160
184
|
entry.live = false
|
|
161
185
|
entry.phase = 'stopping'
|
|
162
|
-
|
|
163
|
-
await
|
|
164
|
-
entry.phase = 'error'
|
|
165
|
-
entry.finishedAt = Date.now()
|
|
166
|
-
entry.retryAt = Date.now() + 5000
|
|
167
|
-
// Failed generations stay invalid; acquire can replace them only after confirmed closure and cooldown.
|
|
168
|
-
throw new Error(entry.error)
|
|
186
|
+
result.reject(new Error(entry.error))
|
|
187
|
+
await this.finishFailure(entry, work)
|
|
169
188
|
} finally {
|
|
170
189
|
if (timer) clearTimeout(timer)
|
|
171
190
|
}
|
|
172
191
|
}
|
|
173
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
|
+
|
|
174
204
|
/** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
|
|
175
205
|
private async mount(entry: PoolEntry): Promise<void> {
|
|
176
206
|
const official = await this.load()
|
|
177
207
|
if (!entry.live || this.stopped) return
|
|
178
208
|
const bridge = await this.make()
|
|
179
|
-
if (!entry.live || this.stopped) { await bridge.dispose(); return }
|
|
180
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 }
|
|
181
212
|
entry.phase = 'starting'
|
|
182
213
|
entry.phaseAt = Date.now()
|
|
183
214
|
entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config)
|
|
@@ -185,10 +216,14 @@ export class McpPool {
|
|
|
185
216
|
if (entry.fiber?.state !== undefined && entry.fiber.state !== 2) throw new Error('MCP 插件未完成激活')
|
|
186
217
|
}
|
|
187
218
|
|
|
188
|
-
/** @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. */
|
|
189
220
|
private async closeBridge(entry: PoolEntry): Promise<void> {
|
|
190
221
|
if (entry.close) return entry.close
|
|
191
|
-
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)
|
|
192
227
|
entry.close = close
|
|
193
228
|
try {
|
|
194
229
|
await close
|
|
@@ -196,6 +231,7 @@ export class McpPool {
|
|
|
196
231
|
entry.fiber = undefined
|
|
197
232
|
entry.close = undefined
|
|
198
233
|
} catch (error) {
|
|
234
|
+
entry.cleanupFailed = true
|
|
199
235
|
entry.error = 'MCP 释放未确认,禁止重建,请查看 Host 日志'
|
|
200
236
|
this.ctx.logger?.warn?.('MCP cleanup failed: ' + String(error))
|
|
201
237
|
throw error
|
|
@@ -216,10 +252,10 @@ export class McpPool {
|
|
|
216
252
|
entry.live = false
|
|
217
253
|
entry.phase = 'stopping'
|
|
218
254
|
if (entry.idle) clearTimeout(entry.idle)
|
|
219
|
-
await this.closeBridge(entry)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
223
259
|
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key)
|
|
224
260
|
}
|
|
225
261
|
|
|
@@ -239,7 +275,9 @@ export class McpPool {
|
|
|
239
275
|
/** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
|
|
240
276
|
async dispose(): Promise<void> {
|
|
241
277
|
this.stopped = true
|
|
242
|
-
await Promise.
|
|
278
|
+
const closures = await Promise.allSettled([...this.entries.values()].map((entry) => this.retire(entry)))
|
|
243
279
|
await Promise.all([...this.jobs])
|
|
280
|
+
const failure = closures.find((closure) => closure.status === 'rejected')
|
|
281
|
+
if (failure?.status === 'rejected') throw failure.reason
|
|
244
282
|
}
|
|
245
283
|
}
|
package/src/mcpRuntime.ts
CHANGED
|
@@ -265,8 +265,17 @@ export class McpRuntime {
|
|
|
265
265
|
instance ??= this.reserve(agent, record, path, name, def)
|
|
266
266
|
await waitMcp(instance.ready, signal)
|
|
267
267
|
if (!this.valid(agent, record, name, instance)) throw new Error('项目 MCP 配置已失效')
|
|
268
|
-
const
|
|
269
|
-
|
|
268
|
+
const registry = agent.ctx.get('tools')
|
|
269
|
+
// Discovery is not step admission; the official executor still owns execution permission.
|
|
270
|
+
let visible: ReadonlySet<string>
|
|
271
|
+
if (typeof registry?.definitions === 'function') {
|
|
272
|
+
visible = new Set(registry.definitions(agent).map((tool: { name: string }) => tool.name))
|
|
273
|
+
} else if (typeof registry?.view === 'function') {
|
|
274
|
+
visible = new Set(registry.view(agent).visible.keys())
|
|
275
|
+
} else {
|
|
276
|
+
throw new Error('项目 MCP 工具目录不可用:缺少公开 definitions 或旧版 view 接口')
|
|
277
|
+
}
|
|
278
|
+
const tools = [...instance.lease.entry.bridge!.tools.values()].filter((tool) => visible.has(tool.name))
|
|
270
279
|
.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }))
|
|
271
280
|
return { serverName: name, tools, resources: instance.lease.entry.bridge!.resources.has(name), nextRequest: 'Use these tools in the next model request; PTC must start a new program to refresh its SDK.' }
|
|
272
281
|
}
|