dsh-agent-plugins-market 0.6.2 → 0.7.0
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 +117 -111
- package/README.zh.md +112 -98
- package/client/client.js +547 -540
- package/client/client.js.map +1 -1
- package/lib/application/catalog-context.js +181 -0
- package/lib/application/catalog.js +217 -1110
- package/lib/application/details.js +6 -6
- package/lib/application/install-store.js +95 -0
- package/lib/application/lsp-service.js +97 -0
- package/lib/application/mcp-service.js +246 -0
- package/lib/application/panel-resources.js +29 -12
- package/lib/application/ports.js +21 -0
- package/lib/application/project-agent-roles.js +1 -1
- package/lib/application/snapshot-cache.js +160 -0
- package/lib/application/source-store.js +521 -0
- package/lib/catalog/archive.js +34 -29
- package/lib/catalog/component-files.js +14 -6
- package/lib/catalog/fs-probes.js +43 -0
- package/lib/catalog/manifests.js +53 -92
- package/lib/catalog/native-project.js +2 -2
- package/lib/catalog/paths.js +66 -55
- package/lib/catalog/project-config.js +2 -1
- package/lib/catalog/project-hooks.js +9 -1
- package/lib/catalog/scan-resolvers.js +29 -45
- package/lib/catalog/skills-parse.js +14 -10
- package/lib/catalog/source-catalog.js +3 -2
- package/lib/catalog/suite-components.js +5 -5
- package/lib/catalog/suite-scanner.js +3 -1
- package/lib/catalog/surfaces.js +5 -26
- package/lib/catalog/validate.js +7 -11
- package/lib/contracts/market.js +1 -0
- package/lib/index.js +91 -208
- package/lib/model/types.js +26 -3
- package/lib/routes.js +45 -15
- package/lib/runtime/agent-role-names.js +70 -0
- package/lib/runtime/agent-role-router.js +116 -102
- package/lib/runtime/commands-mounts.js +9 -6
- package/lib/runtime/deadline.js +28 -0
- package/lib/runtime/feedback-tool.js +94 -43
- package/lib/runtime/hooks-mounts.js +9 -11
- package/lib/runtime/host-locale.js +10 -21
- package/lib/runtime/lsp-direct-config.js +19 -19
- package/lib/runtime/lsp-mounts.js +159 -68
- package/lib/runtime/lsp-server-state.js +4 -4
- package/lib/runtime/lsp-status.js +1 -1
- package/lib/runtime/mcp-auth-record.js +42 -0
- package/lib/runtime/mcp-backend.js +6 -5
- package/lib/runtime/mcp-client/config.js +0 -1
- package/lib/runtime/mcp-client/connection.js +32 -37
- package/lib/runtime/mcp-client/host-contract.js +10 -0
- package/lib/runtime/mcp-client/oauth.js +5 -7
- package/lib/runtime/mcp-client/plugin-identity.js +28 -0
- package/lib/runtime/mcp-client/projection.js +202 -0
- package/lib/runtime/mcp-client/tools.js +4 -189
- package/lib/runtime/mcp-config.js +29 -10
- package/lib/runtime/mcp-direct-config.js +33 -16
- package/lib/runtime/mcp-mounts.js +35 -75
- package/lib/runtime/mcp-overrides.js +5 -5
- package/lib/runtime/mcp-redaction.js +3 -2
- package/lib/runtime/mcp-status.js +7 -8
- package/lib/runtime/mount-lifecycle.js +73 -0
- package/lib/runtime/profile-seam.js +321 -0
- package/lib/runtime/project-runtime.js +11 -8
- package/lib/runtime/reconcile-scheduler.js +99 -0
- package/lib/runtime/reconciler.js +4 -0
- package/lib/runtime/server-config.js +6 -4
- package/lib/runtime/settings-namespace.js +136 -0
- package/lib/runtime/skills-provider.js +5 -3
- package/lib/runtime/source-auto-update.js +70 -0
- package/lib/{model/state.js → runtime/state-store.js} +9 -11
- package/lib/runtime/storage-migration.js +35 -18
- package/lib/runtime/subagent-catalog.js +5 -4
- package/lib/runtime/user-commands.js +2 -2
- package/lib/runtime/user-panels.js +31 -21
- package/lib/runtime/user-store.js +12 -13
- package/lib/types/application/catalog-context.d.ts +118 -0
- package/lib/types/application/catalog.d.ts +99 -304
- package/lib/types/application/install-store.d.ts +21 -0
- package/lib/types/application/lsp-service.d.ts +36 -0
- package/lib/types/application/mcp-service.d.ts +71 -0
- package/lib/types/application/panel-resources.d.ts +8 -0
- package/lib/types/application/ports.d.ts +72 -0
- package/lib/types/application/queries.d.ts +13 -26
- package/lib/types/application/snapshot-cache.d.ts +51 -0
- package/lib/types/application/source-store.d.ts +100 -0
- package/lib/types/catalog/archive.d.ts +3 -11
- package/lib/types/catalog/component-files.d.ts +4 -0
- package/lib/types/catalog/fs-probes.d.ts +10 -0
- package/lib/types/catalog/manifests.d.ts +17 -16
- package/lib/types/catalog/native-project.d.ts +2 -2
- package/lib/types/catalog/paths.d.ts +47 -15
- package/lib/types/catalog/scan-pipeline.d.ts +3 -3
- package/lib/types/catalog/scan-resolvers.d.ts +3 -9
- package/lib/types/catalog/skills-parse.d.ts +0 -7
- package/lib/types/catalog/source-catalog.d.ts +3 -3
- package/lib/types/catalog/suite-scanner.d.ts +8 -6
- package/lib/types/catalog/validate.d.ts +0 -2
- package/lib/types/contracts/lsp-status.d.ts +35 -0
- package/lib/types/contracts/market.d.ts +3 -2
- package/lib/types/contracts/mcp-status.d.ts +4 -0
- package/lib/types/model/types.d.ts +29 -5
- package/lib/types/runtime/agent-role-names.d.ts +6 -0
- package/lib/types/runtime/agent-role-router.d.ts +54 -51
- package/lib/types/runtime/deadline.d.ts +18 -0
- package/lib/types/runtime/feedback-tool.d.ts +33 -11
- package/lib/types/runtime/hooks-mounts.d.ts +1 -1
- package/lib/types/runtime/host-locale.d.ts +1 -1
- package/lib/types/runtime/lsp-direct-config.d.ts +3 -3
- package/lib/types/runtime/lsp-mounts.d.ts +62 -19
- package/lib/types/runtime/lsp-status.d.ts +2 -1
- package/lib/types/runtime/mcp-auth-record.d.ts +29 -0
- package/lib/types/runtime/mcp-backend.d.ts +7 -4
- package/lib/types/runtime/mcp-client/config.d.ts +0 -2
- package/lib/types/runtime/mcp-client/host-contract.d.ts +78 -0
- package/lib/types/runtime/mcp-client/oauth.d.ts +1 -1
- package/lib/types/runtime/mcp-client/plugin-identity.d.ts +16 -0
- package/lib/types/runtime/mcp-client/projection.d.ts +45 -0
- package/lib/types/runtime/mcp-client/tools.d.ts +5 -72
- package/lib/types/runtime/mcp-config.d.ts +25 -4
- package/lib/types/runtime/mcp-direct-config.d.ts +18 -4
- package/lib/types/runtime/mcp-mounts.d.ts +10 -15
- package/lib/types/runtime/mcp-overrides.d.ts +1 -1
- package/lib/types/runtime/mcp-status.d.ts +1 -1
- package/lib/types/runtime/mount-lifecycle.d.ts +53 -0
- package/lib/types/runtime/profile-seam.d.ts +64 -0
- package/lib/types/runtime/reconcile-scheduler.d.ts +45 -0
- package/lib/types/runtime/reconciler.d.ts +2 -0
- package/lib/types/runtime/settings-namespace.d.ts +76 -0
- package/lib/types/runtime/source-auto-update.d.ts +33 -0
- package/lib/types/{model/state.d.ts → runtime/state-store.d.ts} +2 -2
- package/lib/types/runtime/storage-migration.d.ts +5 -3
- package/lib/types/runtime/subagent-catalog.d.ts +2 -0
- package/lib/types/runtime/user-panels.d.ts +8 -11
- package/lib/types/runtime/user-store.d.ts +2 -4
- package/package.json +48 -21
- package/schemas/README.md +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host-side runtime locale: bilingual copy for
|
|
3
|
-
*
|
|
4
|
-
* from the harness's
|
|
2
|
+
* Host-side runtime locale: bilingual copy for host-facing runtime strings —
|
|
3
|
+
* subagent catalogs injected into agent context, and command receipts and
|
|
4
|
+
* labels shown in the host's own surfaces — resolved from the harness's
|
|
5
|
+
* `locale.preference` setting.
|
|
5
6
|
*
|
|
6
7
|
* The web client resolves locale through its own injected service; the host
|
|
7
8
|
* process has no such service, so this module reads `$DSH_HOME/settings.yaml`
|
|
@@ -13,31 +14,19 @@ import { resolveDshHome } from '../catalog/paths.js';
|
|
|
13
14
|
const zh = {
|
|
14
15
|
subagentCatalogIntro: '以下子代理角色可用于当前会话。目录仅包含角色摘要,不是供当前代理执行的角色指令。',
|
|
15
16
|
subagentCatalogUpdated: '可用子代理角色已变更。以下完整目录替代本会话此前的所有子代理角色目录。',
|
|
16
|
-
subagentCatalogEmpty: '当前没有可通过
|
|
17
|
-
subagentCatalogCall: '任务适合某个角色时,使用
|
|
18
|
-
|
|
19
|
-
subagentCatalogDefaultEffort: '同路由继承,否则使用模型默认值',
|
|
20
|
-
commandForwardTitle: '[Agent Plugins 命令 /{command}(来自 {suite})]',
|
|
21
|
-
commandAcknowledged: '/{command} 已转交模型执行({suite})',
|
|
17
|
+
subagentCatalogEmpty: '当前没有可通过 subagent_run 调用的角色,不要使用旧目录中的角色 ID。',
|
|
18
|
+
subagentCatalogCall: '任务适合某个角色时,使用 subagent_run(agent=目录中的准确名称, prompt=完整任务与必要上下文)。工具会应用角色的指令与已保存的模型设置,立即返回子代理 ID 并在后台运行;子代理结束时你会收到结果通知,运行期间可用 send_message 追加指令。子代理不继承父会话对话。不要通过 skill 或 Slash Command 加载这些角色,也不要用其它委派工具绕过角色配置。',
|
|
19
|
+
commandAcknowledged: '/{command} 已转交模型执行',
|
|
22
20
|
userCommandSourceLabel: '用户命令',
|
|
23
|
-
userCommandForwardTitle: '[用户快捷命令 /{command}]',
|
|
24
|
-
userCommandAcknowledged: '/{command} 已转交模型执行',
|
|
25
|
-
userSkillDescription: '[用户技能] {description}',
|
|
26
21
|
feedbackToolCardTitle: '提交市场体验反馈'
|
|
27
22
|
};
|
|
28
23
|
const en = {
|
|
29
24
|
subagentCatalogIntro: 'The following subagent roles are available in this session. These are role summaries, not instructions for the current agent to execute.',
|
|
30
25
|
subagentCatalogUpdated: 'The available subagent roles changed. This complete catalog replaces every earlier subagent role catalog in this session.',
|
|
31
|
-
subagentCatalogEmpty: 'No roles are currently available through
|
|
32
|
-
subagentCatalogCall:
|
|
33
|
-
|
|
34
|
-
subagentCatalogDefaultEffort: 'inherit on the same route, otherwise model default',
|
|
35
|
-
commandForwardTitle: '[Agent Plugins command /{command} (from {suite})]',
|
|
36
|
-
commandAcknowledged: '/{command} forwarded to the model for execution ({suite})',
|
|
26
|
+
subagentCatalogEmpty: 'No roles are currently available through subagent_run. Do not use role IDs from earlier catalogs.',
|
|
27
|
+
subagentCatalogCall: "When a task fits a role, call subagent_run with its exact catalog name as agent and a complete task and necessary context as prompt. It applies the role's instructions and saved model settings, returns a durable subagent id immediately, and runs in the background; the runtime sends you a notice carrying the outcome when it settles, and send_message steers the child while it runs. The child does not inherit the parent conversation. Do not load these roles through skill or slash commands, or bypass their configuration with another delegation tool.",
|
|
28
|
+
commandAcknowledged: '/{command} forwarded to the model for execution',
|
|
37
29
|
userCommandSourceLabel: 'user command',
|
|
38
|
-
userCommandForwardTitle: '[User quick command /{command}]',
|
|
39
|
-
userCommandAcknowledged: '/{command} forwarded to the model for execution',
|
|
40
|
-
userSkillDescription: '[user skill] {description}',
|
|
41
30
|
feedbackToolCardTitle: 'File market feedback'
|
|
42
31
|
};
|
|
43
32
|
const DICTS = { zh, en };
|
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Direct user configuration for LSP servers: a named table of `lspServers`
|
|
3
|
-
* entries stored
|
|
4
|
-
* suites' inline declarations.
|
|
3
|
+
* entries stored in the shared Agent layout root, merged at runtime alongside
|
|
4
|
+
* the suites' inline declarations.
|
|
5
5
|
*
|
|
6
|
-
* The file lives at
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The file lives at `~/.agents/lsp.json` and is the single source for the
|
|
7
|
+
* `direct` kind on the LSP status surface. Parsing reuses the same fail-closed
|
|
8
|
+
* `lsp-spec` rules as suite declarations; a broken file degrades to no direct
|
|
9
|
+
* servers plus a diagnostic, never a thrown discovery.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
13
14
|
import { parseLspServers } from '../catalog/lsp-spec.js';
|
|
14
15
|
/** The direct LSP server configuration file path. */
|
|
15
|
-
export function lspServersPath(
|
|
16
|
-
return join(
|
|
16
|
+
export function lspServersPath(agentsRoot) {
|
|
17
|
+
return join(agentsRoot, 'lsp.json');
|
|
17
18
|
}
|
|
18
19
|
/** Load the user-configured LSP servers; unreadable or invalid files yield an empty table. */
|
|
19
|
-
export async function loadLspServers(
|
|
20
|
+
export async function loadLspServers(agentsRoot) {
|
|
20
21
|
let raw;
|
|
21
22
|
try {
|
|
22
|
-
raw = JSON.parse(await readFile(lspServersPath(
|
|
23
|
+
raw = JSON.parse(await readFile(lspServersPath(agentsRoot), 'utf8'));
|
|
23
24
|
}
|
|
24
25
|
catch (error) {
|
|
25
|
-
return { servers: {}, errors: error.code === 'ENOENT' ? [] : [`lsp
|
|
26
|
+
return { servers: {}, errors: error.code === 'ENOENT' ? [] : [`lsp.json: ${error instanceof Error ? error.message : String(error)}`] };
|
|
26
27
|
}
|
|
27
28
|
const errors = [];
|
|
28
29
|
const table = raw?.['lspServers'];
|
|
29
30
|
if (table === undefined || table === null || Array.isArray(table) || typeof table !== 'object')
|
|
30
|
-
return { servers: {}, errors: ['lsp
|
|
31
|
+
return { servers: {}, errors: ['lsp.json: lspServers must be an object'] };
|
|
31
32
|
const servers = parseLspServers(table, errors);
|
|
32
|
-
return { servers, errors: errors.map(error => `lsp
|
|
33
|
+
return { servers, errors: errors.map(error => `lsp.json: ${error}`) };
|
|
33
34
|
}
|
|
34
35
|
/** Validate and persist the user-configured LSP servers; returns normalized specs. */
|
|
35
|
-
export async function saveLspServers(
|
|
36
|
+
export async function saveLspServers(agentsRoot, raw) {
|
|
36
37
|
const errors = [];
|
|
37
38
|
const table = raw?.['lspServers'];
|
|
38
39
|
const servers = parseLspServers(table, errors);
|
|
39
40
|
if (errors.length > 0) {
|
|
40
41
|
throw new Error(`invalid lspServers: ${errors[0]}`);
|
|
41
42
|
}
|
|
42
|
-
const path = lspServersPath(
|
|
43
|
-
await mkdir(dirname(path), { recursive: true });
|
|
43
|
+
const path = lspServersPath(agentsRoot);
|
|
44
44
|
const persisted = Object.fromEntries(Object.entries(servers).map(([name, { key: _key, ...config }]) => [name, config]));
|
|
45
|
-
await
|
|
45
|
+
await writeFileAtomic(path, `${JSON.stringify({ lspServers: persisted }, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
46
46
|
return { servers };
|
|
47
47
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { DIRECT_LSP_SUITE_ID } from './lsp-status.js';
|
|
2
|
+
import { SerialPassQueue, RetryScheduler } from './mount-lifecycle.js';
|
|
3
|
+
import { describeLegacySeam, findLegacyLspSeams } from './profile-seam.js';
|
|
2
4
|
import { qualifiedSuiteId } from '../catalog/paths.js';
|
|
3
|
-
|
|
4
|
-
* Bounded retry schedule for a failed mount/unmount, mirroring the MCP
|
|
5
|
-
* mounts: a permanently broken server must stop consuming attempt budget,
|
|
6
|
-
* and the schedule resets with every reconcile pass.
|
|
7
|
-
*/
|
|
8
|
-
const RETRY_SCHEDULE_MS = [1_500, 5_000, 15_000, 45_000, 120_000];
|
|
9
|
-
const MAX_RETRY_ATTEMPTS = RETRY_SCHEDULE_MS.length;
|
|
5
|
+
import { effectiveSurfaces } from '../model/types.js';
|
|
10
6
|
/** Build one dsh-lsp-stdio Config.servers entry from a normalized spec. */
|
|
11
7
|
export function toLspServerConfig(spec) {
|
|
12
8
|
return {
|
|
@@ -18,50 +14,77 @@ export function toLspServerConfig(spec) {
|
|
|
18
14
|
...(spec.configuration === undefined ? {} : { configuration: spec.configuration })
|
|
19
15
|
};
|
|
20
16
|
}
|
|
21
|
-
/** Import
|
|
17
|
+
/** Import specifiers kept as string literals so the host packages stay dynamically loaded. */
|
|
22
18
|
const LSP_STDIO_IMPORT = '@deepseek-ai/dsh-lsp-stdio';
|
|
19
|
+
const LSP_SERVICE_IMPORT = '@deepseek-ai/dsh-lsp';
|
|
20
|
+
const LSP_TOOL_IMPORT = '@deepseek-ai/dsh-tool-lsp';
|
|
21
|
+
/**
|
|
22
|
+
* Build one lazy loader for a host package. The specifier stays a literal in a
|
|
23
|
+
* top-level `const` so an absent package degrades to `undefined` instead of
|
|
24
|
+
* failing the import of this module.
|
|
25
|
+
*/
|
|
26
|
+
function lazyImport(specifier) {
|
|
27
|
+
return async () => {
|
|
28
|
+
try {
|
|
29
|
+
return (await import(/* @vite-ignore */ specifier));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
23
36
|
export class LspMountRegistry {
|
|
24
37
|
ctx;
|
|
25
38
|
live = new Map();
|
|
26
39
|
/** Serialize mount and unmount passes so a disable cannot race an in-flight spawn. */
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
retries
|
|
30
|
-
/** Attempt count per mount key; reset whenever a retry succeeds. */
|
|
31
|
-
attempts = new Map();
|
|
40
|
+
passes = new SerialPassQueue();
|
|
41
|
+
/** Delayed re-attempts for mounts that are not live yet. */
|
|
42
|
+
retries;
|
|
32
43
|
/** Snapshot of the last reconciled suite set, replayed by retry passes. */
|
|
33
44
|
lastEnabled = [];
|
|
34
45
|
/** Host module loader; overridable for tests. */
|
|
35
46
|
loadHost;
|
|
47
|
+
/**
|
|
48
|
+
* Reads the profile files when a seam conflict needs explaining. Injectable
|
|
49
|
+
* because it touches the machine's own `$DSH_HOME`, so a test must not
|
|
50
|
+
* inherit whatever the developer running it happens to have configured.
|
|
51
|
+
*/
|
|
52
|
+
locateSeams;
|
|
53
|
+
/** `ctx.lsp` seam loader, mounted only when the profile supplies no service. */
|
|
54
|
+
loadService;
|
|
55
|
+
/** `lsp` tool loader, mounted only when the profile publishes no such tool. */
|
|
56
|
+
loadTool;
|
|
57
|
+
/** Capability plugins this registry mounted, in mount order, disposed with the last server. */
|
|
58
|
+
capability = [];
|
|
59
|
+
/** Whether the capability seam was already resolved for the current wanted set. */
|
|
60
|
+
capabilityReady = false;
|
|
61
|
+
/** Why the capability seam is unavailable; re-attempted on the next reconcile pass. */
|
|
62
|
+
capabilityFailure;
|
|
36
63
|
/** Latest diagnostic per suite id; cleared when its suite mounts or leaves the wanted set. */
|
|
37
64
|
lastDiagnostics = new Map();
|
|
38
65
|
/** Direct (user-configured) server provider; defaults to none. */
|
|
39
66
|
directProvider = async () => ({});
|
|
40
67
|
disabledProvider = async () => new Set();
|
|
41
68
|
disabledSnapshot = new Set();
|
|
42
|
-
constructor(ctx, loadHost) {
|
|
69
|
+
constructor(ctx, loadHost, loadService, loadTool, locateSeams) {
|
|
43
70
|
this.ctx = ctx;
|
|
44
|
-
this.loadHost =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
});
|
|
71
|
+
this.loadHost = loadHost ?? lazyImport(LSP_STDIO_IMPORT);
|
|
72
|
+
this.loadService = loadService ?? lazyImport(LSP_SERVICE_IMPORT);
|
|
73
|
+
this.loadTool = loadTool ?? lazyImport(LSP_TOOL_IMPORT);
|
|
74
|
+
this.locateSeams = locateSeams ?? findLegacyLspSeams;
|
|
75
|
+
this.retries = new RetryScheduler({
|
|
76
|
+
replay: () => this.reconcile(this.lastEnabled),
|
|
77
|
+
log: message => this.ctx.logger?.warn?.(`[dsh-agent-plugins-market] ${message}`)
|
|
78
|
+
});
|
|
54
79
|
}
|
|
55
80
|
/** Queue one reconciliation behind any in-flight mount/unmount pass. */
|
|
56
81
|
reconcile(enabledSuites) {
|
|
57
|
-
|
|
58
|
-
this.reconcileQueue = run.then(() => undefined, () => undefined);
|
|
59
|
-
return run;
|
|
82
|
+
return this.passes.run(() => this.reconcileNow(enabledSuites));
|
|
60
83
|
}
|
|
61
84
|
/** Mount/unmount LSP servers to match the enabled suites plus direct config exactly. */
|
|
62
85
|
async reconcileNow(enabledSuites) {
|
|
63
86
|
this.lastEnabled = [...enabledSuites];
|
|
64
|
-
const active = enabledSuites.filter(suite => suite.activeSurfaces
|
|
87
|
+
const active = enabledSuites.filter(suite => suite.activeSurfaces.lsp !== false);
|
|
65
88
|
const wanted = new Map();
|
|
66
89
|
const diagnostics = [];
|
|
67
90
|
const disabled = await this.disabledProvider();
|
|
@@ -102,29 +125,39 @@ export class LspMountRegistry {
|
|
|
102
125
|
surfaces: { skills: 0, mcp: 0, hooks: 0, commands: 0, agents: 0, lsp: directKeys.length },
|
|
103
126
|
dimension: 'user',
|
|
104
127
|
enabled: true,
|
|
128
|
+
// Direct servers carry no install state and no per-surface
|
|
129
|
+
// overrides, so every surface keeps its enabled default.
|
|
130
|
+
activeSurfaces: effectiveSurfaces(undefined),
|
|
105
131
|
errors: []
|
|
106
132
|
},
|
|
107
133
|
config
|
|
108
134
|
});
|
|
109
135
|
}
|
|
136
|
+
// The last remaining server releases the capability seam; the first one mounts it.
|
|
137
|
+
if (wanted.size === 0)
|
|
138
|
+
await this.releaseCapability();
|
|
110
139
|
for (const [key, live] of [...this.live]) {
|
|
111
|
-
|
|
140
|
+
const target = wanted.get(key);
|
|
141
|
+
if (target === undefined || live.fingerprint !== JSON.stringify(target.config)) {
|
|
112
142
|
const reason = await this.unmount(key, live);
|
|
113
143
|
this.lastDiagnostics.delete(key);
|
|
114
144
|
if (reason !== undefined)
|
|
115
145
|
diagnostics.push({ suiteId: key, serverKey: live.serverKeys.join(','), reason, code: 'unmount-failed' });
|
|
116
146
|
}
|
|
117
147
|
}
|
|
148
|
+
const capabilityFailure = wanted.size > 0 ? await this.ensureCapability() : undefined;
|
|
118
149
|
for (const [key, entry] of wanted) {
|
|
119
150
|
if (this.live.has(key)) {
|
|
120
151
|
this.lastDiagnostics.delete(key);
|
|
121
152
|
continue;
|
|
122
153
|
}
|
|
123
|
-
const failure =
|
|
154
|
+
const failure = capabilityFailure === undefined
|
|
155
|
+
? await this.mountWith(key, entry.suite, entry.config)
|
|
156
|
+
: { suiteId: key, serverKey: Object.keys(entry.config).join(','), reason: capabilityFailure.reason, code: capabilityFailure.code };
|
|
124
157
|
if (failure !== undefined) {
|
|
125
158
|
diagnostics.push(failure);
|
|
126
159
|
this.lastDiagnostics.set(key, failure);
|
|
127
|
-
this.
|
|
160
|
+
this.retries.schedule({ key, label: failure.suiteId, ...(failure.code === 'host-missing' ? {} : { reason: failure.reason }) });
|
|
128
161
|
}
|
|
129
162
|
else {
|
|
130
163
|
this.lastDiagnostics.delete(key);
|
|
@@ -132,6 +165,98 @@ export class LspMountRegistry {
|
|
|
132
165
|
}
|
|
133
166
|
return diagnostics;
|
|
134
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Mount the capability seam this plugin delivers end to end: `ctx.lsp`, then the model-facing
|
|
170
|
+
* `lsp` tool.
|
|
171
|
+
*
|
|
172
|
+
* The plugin never asks what the profile already carries. Its dependency declaration decides
|
|
173
|
+
* the version, so deferring to a seam another layer registered would silently run whatever
|
|
174
|
+
* version that layer picked. Both mounts are therefore unconditional; a seam that is already
|
|
175
|
+
* taken comes back as a `seam-conflict` diagnostic naming the layer to remove, which is the
|
|
176
|
+
* only way the aligned copy can own it.
|
|
177
|
+
* @returns the failure, or undefined when the seam is ready.
|
|
178
|
+
*/
|
|
179
|
+
async ensureCapability() {
|
|
180
|
+
// A previous failure is re-attempted on the next pass: the user can fix a profile and the
|
|
181
|
+
// retry schedule re-runs reconcile, while a module that still cannot load fails just as fast.
|
|
182
|
+
if (this.capabilityReady && this.capabilityFailure === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
this.capabilityReady = true;
|
|
185
|
+
this.capabilityFailure = undefined;
|
|
186
|
+
const ctx = this.ctx;
|
|
187
|
+
if (typeof ctx.plugin !== 'function')
|
|
188
|
+
return (this.capabilityFailure = { reason: 'the host context does not support dynamic plugin mounting', code: 'host-missing' });
|
|
189
|
+
for (const [load, specifier] of [
|
|
190
|
+
[this.loadService, LSP_SERVICE_IMPORT],
|
|
191
|
+
[this.loadTool, LSP_TOOL_IMPORT]
|
|
192
|
+
]) {
|
|
193
|
+
const failure = await this.mountCapability(load, specifier);
|
|
194
|
+
if (failure !== undefined)
|
|
195
|
+
return (this.capabilityFailure = failure);
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
/** Mount one capability plugin and remember it for teardown with the last server. */
|
|
200
|
+
async mountCapability(load, specifier) {
|
|
201
|
+
const module = await load();
|
|
202
|
+
if (module === undefined)
|
|
203
|
+
return { reason: `the ${specifier} package is not installed in this profile`, code: 'host-missing' };
|
|
204
|
+
const ctx = this.ctx;
|
|
205
|
+
let handle;
|
|
206
|
+
try {
|
|
207
|
+
handle = ctx.plugin(module.default ?? module, {});
|
|
208
|
+
await handle.await();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// A failed startup can leave a half-mounted handle behind; drop it before reporting.
|
|
212
|
+
if (handle !== undefined) {
|
|
213
|
+
try {
|
|
214
|
+
await handle.dispose();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Ignore teardown errors: the startup failure is the real signal.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
+
// `service "lsp" has been registered` / `tool "lsp" is already registered`: another layer
|
|
222
|
+
// owns the seam. Report the layer to drop rather than falling back to its version.
|
|
223
|
+
if (/already registered|has been registered/.test(message)) {
|
|
224
|
+
return { reason: await this.seamConflictReason(specifier, message), code: 'seam-conflict' };
|
|
225
|
+
}
|
|
226
|
+
return { reason: `mount failed: ${message}`, code: 'mount-failed' };
|
|
227
|
+
}
|
|
228
|
+
this.capability.push(handle);
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Explain a taken seam, naming the profile layer to remove when one can be
|
|
233
|
+
* found on disk. Releases before the plugin provisioned LSP itself told users
|
|
234
|
+
* to add that layer by hand, so the message points at the exact file and rows
|
|
235
|
+
* rather than at "the profile" in general.
|
|
236
|
+
*/
|
|
237
|
+
async seamConflictReason(specifier, message) {
|
|
238
|
+
const head = `the profile already registers its own ${specifier} (${message})`;
|
|
239
|
+
const seams = await this.locateSeams().catch(() => []);
|
|
240
|
+
const seam = seams[0];
|
|
241
|
+
if (seam === undefined) {
|
|
242
|
+
return `${head}; remove that layer from the profile (a manual \`cordis.patch.yml\` row, or a profile dependency on the package) so this plugin's aligned copy owns the seam`;
|
|
243
|
+
}
|
|
244
|
+
return `${head}; ${describeLegacySeam(seam)} — remove those rows (the LSP panel offers a one-click removal) so this plugin's aligned copy owns the seam`;
|
|
245
|
+
}
|
|
246
|
+
/** Dispose the capability seam this registry mounted, once no wanted server needs it. */
|
|
247
|
+
async releaseCapability() {
|
|
248
|
+
const handles = this.capability.splice(0);
|
|
249
|
+
this.capabilityReady = false;
|
|
250
|
+
this.capabilityFailure = undefined;
|
|
251
|
+
for (const handle of handles.reverse()) {
|
|
252
|
+
try {
|
|
253
|
+
await handle.dispose();
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
this.ctx.logger?.warn?.(`[dsh-agent-plugins-market] LSP capability teardown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
135
260
|
/** Install the direct (user-configured) server provider used at reconcile time. */
|
|
136
261
|
setDirectProvider(provider) {
|
|
137
262
|
this.directProvider = provider;
|
|
@@ -202,49 +327,15 @@ export class LspMountRegistry {
|
|
|
202
327
|
return reason;
|
|
203
328
|
}
|
|
204
329
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Schedule a delayed re-attempt for a mount that still is not live. A
|
|
207
|
-
* `host-missing` failure passes no diagnostic and is never retried; the
|
|
208
|
-
* schedule is bounded and resets with every reconcile pass.
|
|
209
|
-
*/
|
|
210
|
-
scheduleRetry(key, failure) {
|
|
211
|
-
const pending = this.retries.get(key);
|
|
212
|
-
if (pending !== undefined)
|
|
213
|
-
clearTimeout(pending);
|
|
214
|
-
this.retries.delete(key);
|
|
215
|
-
if (failure === undefined || failure.code === 'host-missing') {
|
|
216
|
-
this.attempts.delete(key);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const attempt = (this.attempts.get(key) ?? 0) + 1;
|
|
220
|
-
this.attempts.set(key, attempt);
|
|
221
|
-
if (attempt > MAX_RETRY_ATTEMPTS) {
|
|
222
|
-
this.ctx.logger?.warn?.(`[dsh-agent-plugins-market] ${failure.suiteId}: giving up after ${MAX_RETRY_ATTEMPTS} attempts — ${failure.reason}`);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const delay = RETRY_SCHEDULE_MS[attempt - 1] ?? RETRY_SCHEDULE_MS[RETRY_SCHEDULE_MS.length - 1];
|
|
226
|
-
const timer = setTimeout(() => {
|
|
227
|
-
this.retries.delete(key);
|
|
228
|
-
// Re-run against the last known suite set: a retry must not resurrect
|
|
229
|
-
// servers of a suite that has since been disabled or uninstalled.
|
|
230
|
-
void this.reconcile(this.lastEnabled).catch(() => { });
|
|
231
|
-
}, delay);
|
|
232
|
-
timer.unref?.();
|
|
233
|
-
this.retries.set(key, timer);
|
|
234
|
-
}
|
|
235
330
|
/** Dispose every live mount after queued reconciliation passes settle. */
|
|
236
331
|
async disposeAll() {
|
|
237
|
-
|
|
332
|
+
await this.passes.run(async () => {
|
|
238
333
|
for (const [key, live] of [...this.live]) {
|
|
239
334
|
await this.unmount(key, live);
|
|
240
335
|
}
|
|
336
|
+
await this.releaseCapability();
|
|
241
337
|
});
|
|
242
|
-
this.reconcileQueue = run.then(() => undefined, () => undefined);
|
|
243
|
-
await run;
|
|
244
|
-
for (const timer of this.retries.values())
|
|
245
|
-
clearTimeout(timer);
|
|
246
338
|
this.retries.clear();
|
|
247
|
-
this.attempts.clear();
|
|
248
339
|
}
|
|
249
340
|
}
|
|
250
|
-
const HOST_MISSING_REASON =
|
|
341
|
+
const HOST_MISSING_REASON = `the ${LSP_STDIO_IMPORT} package could not be loaded from this profile; reinstall the plugin (\`dsh plugin --profile <profile> add dsh-agent-plugins-market\`) so its LSP dependencies are provisioned`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
3
4
|
export function lspServerStatePath(dataRoot) {
|
|
4
5
|
return join(dataRoot, 'lsp-server-state.json');
|
|
5
6
|
}
|
|
@@ -14,6 +15,5 @@ export async function loadDisabledLspServers(dataRoot) {
|
|
|
14
15
|
}
|
|
15
16
|
export async function saveDisabledLspServers(dataRoot, disabled) {
|
|
16
17
|
const path = lspServerStatePath(dataRoot);
|
|
17
|
-
await
|
|
18
|
-
await writeFile(path, `${JSON.stringify({ disabled: [...new Set(disabled)].sort() }, null, 2)}\n`, 'utf8');
|
|
18
|
+
await writeFileAtomic(path, `${JSON.stringify({ disabled: [...new Set(disabled)].sort() }, null, 2)}\n`, { mode: 0o600, dirMode: 0o700 });
|
|
19
19
|
}
|
|
@@ -40,7 +40,7 @@ export function buildLspStatus(suites, registry, direct = { servers: {}, errors:
|
|
|
40
40
|
if (suite.lsp === undefined || suite.installedAt === undefined || !suite.enabled)
|
|
41
41
|
continue;
|
|
42
42
|
const suiteKey = qualifiedSuiteId(suite.sourceId, suite.id);
|
|
43
|
-
const disabled = suite.activeSurfaces
|
|
43
|
+
const disabled = suite.activeSurfaces.lsp === false;
|
|
44
44
|
const diagnostic = diagnostics.get(suiteKey);
|
|
45
45
|
const { state, reason, retryable } = deriveState(disabled, diagnostic, anyLive);
|
|
46
46
|
if (state === 'mounted')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credential record one MCP server's OAuth grant lives under.
|
|
3
|
+
*
|
|
4
|
+
* `runtime/mcp-client/oauth.ts` writes those records; this module addresses
|
|
5
|
+
* the same key so a grant can be dropped without waking the transport that
|
|
6
|
+
* wrote it. The scope and the name fold must stay identical to that writer's,
|
|
7
|
+
* or the delete silently misses the record.
|
|
8
|
+
*
|
|
9
|
+
* @module runtime/mcp-auth-record
|
|
10
|
+
*/
|
|
11
|
+
import { credentialKey } from './mcp-client/host-seams.js';
|
|
12
|
+
/** Credential-record scope for MCP OAuth state; per-server ids follow it. */
|
|
13
|
+
export const MCP_AUTH_RECORD_SCOPE = 'mcp-auth';
|
|
14
|
+
/**
|
|
15
|
+
* Fold a `serverName` into a credential-key id segment. Server names carry
|
|
16
|
+
* `__` separators (e.g. `cloudflare__cloudflare-api`), and the key grammar
|
|
17
|
+
* admits only `[a-z0-9-]`, so non-conforming characters collapse to `-` —
|
|
18
|
+
* deterministic, so the same server always reads and writes one record.
|
|
19
|
+
*/
|
|
20
|
+
export function mcpAuthRecordId(serverName) {
|
|
21
|
+
const folded = serverName
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '');
|
|
25
|
+
return folded === '' ? 'server' : folded;
|
|
26
|
+
}
|
|
27
|
+
/** The credential record key one MCP server's OAuth grant is stored under. */
|
|
28
|
+
export function mcpAuthRecordKey(serverName) {
|
|
29
|
+
return credentialKey(MCP_AUTH_RECORD_SCOPE, mcpAuthRecordId(serverName));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Drop one MCP server's OAuth grant record so the next mount re-runs the
|
|
33
|
+
* browser authorization — the path for "I picked too narrow a scope".
|
|
34
|
+
* @param store - the host credentials service, when one is mounted.
|
|
35
|
+
* @throws when the credentials service is not mounted.
|
|
36
|
+
*/
|
|
37
|
+
export async function deleteMcpAuthGrant(store, serverName) {
|
|
38
|
+
const records = store;
|
|
39
|
+
if (typeof records?.deleteRecord !== 'function')
|
|
40
|
+
throw new Error('credentials service is not mounted');
|
|
41
|
+
await records.deleteRecord(mcpAuthRecordKey(serverName));
|
|
42
|
+
}
|
|
@@ -17,19 +17,20 @@ import { readFile } from 'node:fs/promises';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import z from '@deepseek-ai/schemastery';
|
|
20
|
-
export const MCP_BACKENDS = ['builtin', 'host'];
|
|
21
20
|
/** Settings namespace this plugin registers; the key the plugin-config tab pairs our card by. */
|
|
22
21
|
export const MCP_SETTINGS_NAMESPACE = 'dsh-agent-plugins-market';
|
|
23
|
-
/** Schema of the market settings namespace: the
|
|
22
|
+
/** Schema of the market settings namespace: the switches this plugin's config card serves. */
|
|
24
23
|
export const MarketSettingsSchema = z.object({
|
|
25
24
|
/** ON (default) = the built-in bridge with OAuth and SSE; OFF = host client compat mode. */
|
|
26
25
|
mcpEnhanced: z.boolean().default(true),
|
|
27
|
-
/** Read native Agent layouts under the project root; default
|
|
28
|
-
scanProjectLayouts: z.boolean().default(
|
|
26
|
+
/** Read native Agent layouts under the project root; OFF (default) scans configured sources only. */
|
|
27
|
+
scanProjectLayouts: z.boolean().default(false),
|
|
29
28
|
/** Download region for GitHub acquisition; `auto` follows the interface language. */
|
|
30
29
|
downloadRegion: z.union([z.const('auto'), z.const('global'), z.const('china')]).default('auto'),
|
|
31
30
|
/** ON (default) = the `report_market_issue` model tool is registered; OFF = unregistered. */
|
|
32
|
-
feedbackEnabled: z.boolean().default(true)
|
|
31
|
+
feedbackEnabled: z.boolean().default(true),
|
|
32
|
+
/** ON = refresh every configured source in the background; OFF (default) = never. */
|
|
33
|
+
autoUpdateSources: z.boolean().default(false)
|
|
33
34
|
});
|
|
34
35
|
/** Path of the legacy persisted settings file under the plugin data root. */
|
|
35
36
|
export function marketSettingsPath(dataRoot) {
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module runtime/mcp-client/config
|
|
10
10
|
*/
|
|
11
|
-
export { RECONNECT_DEFAULTS } from './connection.js';
|
|
12
11
|
/** Default timeout for individual MCP tool calls (ms). */
|
|
13
12
|
export const DEFAULT_TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
14
13
|
/** Valid `serverName`, kept below the public tool-name budget. */
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
24
24
|
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
25
25
|
import { MAX_TIMER_DELAY_MS } from './host-seams.js';
|
|
26
|
+
import { PLUGIN_NAME, PLUGIN_VERSION } from './plugin-identity.js';
|
|
26
27
|
import { createTransport } from './transport.js';
|
|
27
28
|
import { syncTools } from './tools.js';
|
|
28
29
|
/** Defaults shared by the config and {@link resolveReconnectPolicy}. */
|
|
@@ -197,7 +198,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
197
198
|
* @param startup - Whether this is the bridge's activation attempt.
|
|
198
199
|
*/
|
|
199
200
|
async function connectGeneration(startup) {
|
|
200
|
-
const generation = new Client({ name:
|
|
201
|
+
const generation = new Client({ name: PLUGIN_NAME, version: PLUGIN_VERSION }, { capabilities: {} });
|
|
201
202
|
const closed = Promise.withResolvers();
|
|
202
203
|
let attemptSettled = false;
|
|
203
204
|
let closeObserved = false;
|
|
@@ -228,6 +229,31 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
228
229
|
host.logger.error(`${label}: tool re-sync failed: ${String(error)}`);
|
|
229
230
|
}
|
|
230
231
|
});
|
|
232
|
+
/**
|
|
233
|
+
* Retire a generation whose attempt failed: close it, wait for the
|
|
234
|
+
* transport's own close signal, then hand it to the disconnect path. A
|
|
235
|
+
* generation that will not close stops reconnection outright — respawning
|
|
236
|
+
* would overlap the server process still running behind it.
|
|
237
|
+
*/
|
|
238
|
+
async function teardownFailedAttempt() {
|
|
239
|
+
try {
|
|
240
|
+
await generation.close();
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* transport already gone */
|
|
244
|
+
}
|
|
245
|
+
const quiesced = hasClosed() || (await waitForClose(closed.promise));
|
|
246
|
+
attemptSettled = true;
|
|
247
|
+
if (!isCurrent(generation))
|
|
248
|
+
return;
|
|
249
|
+
if (quiesced) {
|
|
250
|
+
generationDown(generation);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
client = undefined;
|
|
254
|
+
clientClosed = undefined;
|
|
255
|
+
host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes; reload the plugin or restart the Host to retry`);
|
|
256
|
+
}
|
|
231
257
|
const { transport, oauthProvider } = createTransport(config, credentials, (message) => {
|
|
232
258
|
host.logger.info(message);
|
|
233
259
|
});
|
|
@@ -261,11 +287,9 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
261
287
|
attemptSettled = true;
|
|
262
288
|
if (!isCurrent(generation))
|
|
263
289
|
return;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
if (!legSettled) {
|
|
290
|
+
// The leg failed, or the transport closed while it ran: no generation
|
|
291
|
+
// is left to finish establishing.
|
|
292
|
+
if (hasClosed() || !legSettled) {
|
|
269
293
|
generationDown(generation);
|
|
270
294
|
return;
|
|
271
295
|
}
|
|
@@ -277,20 +301,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
277
301
|
catch (syncError) {
|
|
278
302
|
if (isCurrent(generation))
|
|
279
303
|
host.logger.warn(`${label}: tool sync after authorization failed: ${String(syncError)}`);
|
|
280
|
-
|
|
281
|
-
await generation.close();
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
/* transport already gone */
|
|
285
|
-
}
|
|
286
|
-
const quiescedSync = hasClosed() || (await waitForClose(closed.promise));
|
|
287
|
-
if (!quiescedSync) {
|
|
288
|
-
client = undefined;
|
|
289
|
-
clientClosed = undefined;
|
|
290
|
-
host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
generationDown(generation);
|
|
304
|
+
await teardownFailedAttempt();
|
|
294
305
|
return;
|
|
295
306
|
}
|
|
296
307
|
if (hasClosed()) {
|
|
@@ -304,23 +315,7 @@ export function startConnection(host, config, policy, credentials) {
|
|
|
304
315
|
host.logger.info(`${label}: authorized and re-synced tools (attempt ${failedAttempts}/${policy.maxAttempts})`);
|
|
305
316
|
return;
|
|
306
317
|
}
|
|
307
|
-
|
|
308
|
-
await generation.close();
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
/* transport already gone */
|
|
312
|
-
}
|
|
313
|
-
const quiesced = hasClosed() || (await waitForClose(closed.promise));
|
|
314
|
-
attemptSettled = true;
|
|
315
|
-
if (!isCurrent(generation))
|
|
316
|
-
return;
|
|
317
|
-
if (!quiesced) {
|
|
318
|
-
client = undefined;
|
|
319
|
-
clientClosed = undefined;
|
|
320
|
-
host.logger.error(`${label}: failed generation did not close within ${GENERATION_CLOSE_TIMEOUT_MS}ms — reconnect stopped to avoid overlapping server processes; reload the plugin or restart the Host to retry`);
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
generationDown(generation);
|
|
318
|
+
await teardownFailedAttempt();
|
|
324
319
|
return;
|
|
325
320
|
}
|
|
326
321
|
attemptSettled = true;
|