lark-remote 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.12] - 2026-08-29
9
+
10
+ ### 修复
11
+
12
+ - opencode ACP stream error 实时上报:tail opencode 日志即时捕获错误行,配额类错误秒级报错,不再挂起卡片等待超时
13
+
14
+ ### 变更
15
+
16
+ - codex 会话历史读取轻量化:长会话生成轻量摘要,模型目录异步加载,减少启动阻塞
17
+ - CI 覆盖率门禁纳入 `src/index.ts`,覆盖率统计更完整
18
+
19
+ ## [0.1.11] - 2026-08-24
20
+
21
+ ### 新增
22
+
23
+ - kimi ACP 支持 Bash 命令自主执行:`terminal/*` reverse RPC 将 Bash 工具下放到本地子进程(一次性 `bash -c <script>`),yolo/auto 模式自主跑命令,manual 模式仍走审批;run 卡片显示带真实命令与本地输出的 Bash 面板,并过滤 kimi 自产的双份 Bash 通知
24
+
25
+ ### 变更
26
+
27
+ - kimi ACP 能力声明:`fs` 保持关闭(走本地磁盘兜底,避免未实现的 `fs/*` RPC 报 `METHOD_NOT_FOUND`),`terminal` 打开
28
+ - 文档清理:移除设计文档中对内部文档名(`lessons-redlines`)与本地仓库路径(`/Users/.../lark-remote`)的引用
29
+
8
30
  ## [0.1.10] - 2026-08-22
9
31
 
10
32
  ### 新增
@@ -75,14 +75,36 @@ export declare function parseCodexModelsOutput(stdout: string): BundledModelInfo
75
75
  * 缓存:键含 binary/home/mode/models.json mtime:size + config.toml mtime:size
76
76
  * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
77
77
  * 指纹为空(P1-1/P3-10)。
78
+ *
79
+ * 同步路径(卡片构建用):缓存命中直接返回;若有一个后台异步加载
80
+ * (`loadCodexCatalogModelsAsync` / 启动 warm)在途,则不阻塞事件循环、不重复
81
+ * spawn,直接返回 [] 走既有 fallback(FALLBACK_MODELS / currentModel)——该
82
+ * [] 状态与命令失败/超时的已有语义一致,由调用方既有 fallback 兜底。
78
83
  */
79
84
  export declare function getCodexCatalogModels(codexHome?: string): BundledModelInfo[];
85
+ /**
86
+ * 异步加载模型目录(execFile,不阻塞事件循环),写入与同步路径共享的
87
+ * `catalogCache`。按缓存 key 复用 in-flight Promise:并发调用(多个 warm /
88
+ * 显式调用)只 spawn 一次 `codex debug models`。
89
+ *
90
+ * 与同步路径的职责划分:启动时用 `warmCodexCatalogCache` 后台预热,使首次打开
91
+ * Codex 配置卡(同步读)时缓存已温、无需 8s execFileSync 阻塞;若用户在 warm
92
+ * 完成前打开卡片,同步路径会检测到 in-flight 并直接返回 [] fallback。
93
+ */
94
+ export declare function loadCodexCatalogModelsAsync(codexHome?: string): Promise<BundledModelInfo[]>;
95
+ /**
96
+ * 启动/后台预热:fire-and-forget 调用异步加载,消除首次打开 Codex 配置卡时的
97
+ * 8s 同步阻塞。非致命——失败/未安装 codex 时静默落负缓存。
98
+ */
99
+ export declare function warmCodexCatalogCache(codexHome?: string): void;
80
100
  /**
81
101
  * Test utility: clear the catalog cache. Tests that exercise the bundled
82
102
  * command path (`--bundled`) directly use `invalidateCodexBundledTestCache`
83
103
  * from `tests/lib/codex-bundled-test-helpers.ts`.
84
104
  */
85
105
  export declare function invalidateCodexBundledCache(): void;
106
+ /** Test utility: clear in-flight async catalog loads (see async loader tests). */
107
+ export declare function _clearCodexCatalogInFlightForTest(): void;
86
108
  /**
87
109
  * Get supported reasoning effort options for a model.
88
110
  * @param codexHome 可选 codex 配置目录覆盖(与 loadCodexConfig 的 codexHome 对齐,
@@ -8,7 +8,7 @@
8
8
  * - catalog 模式(config.toml 声明 model_catalog_json):模型列表/档位来自活动目录
9
9
  * - 非 catalog 模式:openai 用 bundled 全量;anthropic 仅在用户显式配置时存在
10
10
  */
11
- import { execFileSync } from 'node:child_process';
11
+ import { execFileSync, execFile } from 'node:child_process';
12
12
  import fs from 'node:fs';
13
13
  import os from 'node:os';
14
14
  import path from 'node:path';
@@ -46,6 +46,8 @@ const BUNDLED_CACHE_TTL_MS = 60 * 60 * 1000;
46
46
  /** 失败/空结果负缓存 TTL:避免每次卡片构建同步阻塞最长 8s(P3-5) */
47
47
  const NEGATIVE_CACHE_TTL_MS = 30 * 1000;
48
48
  let catalogCache = null;
49
+ /** In-flight async catalog loads, keyed by cache key — dedup concurrent warm ups. */
50
+ const catalogInFlight = new Map();
49
51
  /**
50
52
  * Read config.toml `model_catalog_json` path (tilde-expanded).
51
53
  * 返回语义:
@@ -153,8 +155,89 @@ export function parseCodexModelsOutput(stdout) {
153
155
  * 缓存:键含 binary/home/mode/models.json mtime:size + config.toml mtime:size
154
156
  * (P3-4/P3-15/P3-5);成功结果 TTL 1h,失败/空结果短 TTL 负缓存;stat 异常时
155
157
  * 指纹为空(P1-1/P3-10)。
158
+ *
159
+ * 同步路径(卡片构建用):缓存命中直接返回;若有一个后台异步加载
160
+ * (`loadCodexCatalogModelsAsync` / 启动 warm)在途,则不阻塞事件循环、不重复
161
+ * spawn,直接返回 [] 走既有 fallback(FALLBACK_MODELS / currentModel)——该
162
+ * [] 状态与命令失败/超时的已有语义一致,由调用方既有 fallback 兜底。
156
163
  */
157
164
  export function getCodexCatalogModels(codexHome) {
165
+ const { key, bundled } = computeCodexCatalogKey(codexHome);
166
+ const cached = readCachedCatalog(key);
167
+ if (cached !== undefined)
168
+ return cached;
169
+ // in-flight 异步加载进行中:不 execFileSync(会阻塞 8s),返回 [] 交给
170
+ // 调用方既有 fallback;in-flight 完成后下次读取命中缓存。
171
+ if (catalogInFlight.has(key)) {
172
+ return [];
173
+ }
174
+ const now = Date.now();
175
+ try {
176
+ const stdout = execFileSync('codex', catalogArgs(bundled), {
177
+ encoding: 'utf-8',
178
+ timeout: 8_000,
179
+ maxBuffer: 4 * 1024 * 1024,
180
+ env: process.env,
181
+ stdio: ['ignore', 'pipe', 'ignore'],
182
+ });
183
+ const models = parseCodexModelsOutput(stdout);
184
+ catalogCache = { key, models, ts: now, failed: models.length === 0 };
185
+ return models;
186
+ }
187
+ catch (err) {
188
+ getLogger().warn(`[codex-config] models unavailable for binary "codex": ${err.message}`);
189
+ catalogCache = { key, models: [], ts: now, failed: true };
190
+ return [];
191
+ }
192
+ }
193
+ /**
194
+ * 异步加载模型目录(execFile,不阻塞事件循环),写入与同步路径共享的
195
+ * `catalogCache`。按缓存 key 复用 in-flight Promise:并发调用(多个 warm /
196
+ * 显式调用)只 spawn 一次 `codex debug models`。
197
+ *
198
+ * 与同步路径的职责划分:启动时用 `warmCodexCatalogCache` 后台预热,使首次打开
199
+ * Codex 配置卡(同步读)时缓存已温、无需 8s execFileSync 阻塞;若用户在 warm
200
+ * 完成前打开卡片,同步路径会检测到 in-flight 并直接返回 [] fallback。
201
+ */
202
+ export async function loadCodexCatalogModelsAsync(codexHome) {
203
+ const { key, bundled } = computeCodexCatalogKey(codexHome);
204
+ const cached = readCachedCatalog(key);
205
+ if (cached !== undefined)
206
+ return cached;
207
+ const inFlight = catalogInFlight.get(key);
208
+ if (inFlight)
209
+ return inFlight;
210
+ const p = (async () => {
211
+ const now = Date.now();
212
+ try {
213
+ const stdout = await execFileAsync('codex', catalogArgs(bundled));
214
+ const models = parseCodexModelsOutput(stdout);
215
+ catalogCache = { key, models, ts: now, failed: models.length === 0 };
216
+ return models;
217
+ }
218
+ catch (err) {
219
+ getLogger().warn(`[codex-config] models unavailable for binary "codex": ${err.message}`);
220
+ catalogCache = { key, models: [], ts: now, failed: true };
221
+ return [];
222
+ }
223
+ })();
224
+ catalogInFlight.set(key, p);
225
+ try {
226
+ return await p;
227
+ }
228
+ finally {
229
+ catalogInFlight.delete(key);
230
+ }
231
+ }
232
+ /**
233
+ * 启动/后台预热:fire-and-forget 调用异步加载,消除首次打开 Codex 配置卡时的
234
+ * 8s 同步阻塞。非致命——失败/未安装 codex 时静默落负缓存。
235
+ */
236
+ export function warmCodexCatalogCache(codexHome) {
237
+ void loadCodexCatalogModelsAsync(codexHome);
238
+ }
239
+ /** 计算目录缓存键(binary/home/mode + 指纹),同步与异步路径共享。 */
240
+ function computeCodexCatalogKey(codexHome) {
158
241
  const binary = 'codex';
159
242
  const home = resolveCodexHome(codexHome);
160
243
  const catalogPath = readModelCatalogJsonPath(home);
@@ -179,6 +262,10 @@ export function getCodexCatalogModels(codexHome) {
179
262
  }
180
263
  }
181
264
  const key = `${binary}\u0000${home}\u0000${bundled ? 'bundled' : 'active'}\u0000${catalogFingerprint}\u0000${configFingerprint}`;
265
+ return { key, bundled };
266
+ }
267
+ /** 读取有效缓存(含负缓存 TTL);命中返回模型,否则 undefined。 */
268
+ function readCachedCatalog(key) {
182
269
  const now = Date.now();
183
270
  if (catalogCache && catalogCache.key === key) {
184
271
  const ttl = catalogCache.failed ? NEGATIVE_CACHE_TTL_MS : BUNDLED_CACHE_TTL_MS;
@@ -186,24 +273,27 @@ export function getCodexCatalogModels(codexHome) {
186
273
  return catalogCache.models;
187
274
  }
188
275
  }
189
- try {
190
- const args = bundled ? ['debug', 'models', '--bundled'] : ['debug', 'models'];
191
- const stdout = execFileSync(binary, args, {
276
+ return undefined;
277
+ }
278
+ /** `codex debug models` 参数(--bundled 与否)。 */
279
+ function catalogArgs(bundled) {
280
+ return bundled ? ['debug', 'models', '--bundled'] : ['debug', 'models'];
281
+ }
282
+ /** Promisify execFile with the same 8s timeout / 4MB cap as the sync path. */
283
+ function execFileAsync(file, args) {
284
+ return new Promise((resolve, reject) => {
285
+ execFile(file, args, {
192
286
  encoding: 'utf-8',
193
287
  timeout: 8_000,
194
288
  maxBuffer: 4 * 1024 * 1024,
195
289
  env: process.env,
196
- stdio: ['ignore', 'pipe', 'ignore'],
290
+ }, (err, stdout) => {
291
+ if (err)
292
+ reject(err);
293
+ else
294
+ resolve(stdout);
197
295
  });
198
- const models = parseCodexModelsOutput(stdout);
199
- catalogCache = { key, models, ts: now, failed: models.length === 0 };
200
- return models;
201
- }
202
- catch (err) {
203
- getLogger().warn(`[codex-config] models unavailable for binary "${binary}": ${err.message}`);
204
- catalogCache = { key, models: [], ts: now, failed: true };
205
- return [];
206
- }
296
+ });
207
297
  }
208
298
  /**
209
299
  * Test utility: clear the catalog cache. Tests that exercise the bundled
@@ -213,6 +303,10 @@ export function getCodexCatalogModels(codexHome) {
213
303
  export function invalidateCodexBundledCache() {
214
304
  catalogCache = null;
215
305
  }
306
+ /** Test utility: clear in-flight async catalog loads (see async loader tests). */
307
+ export function _clearCodexCatalogInFlightForTest() {
308
+ catalogInFlight.clear();
309
+ }
216
310
  /**
217
311
  * Get supported reasoning effort options for a model.
218
312
  * @param codexHome 可选 codex 配置目录覆盖(与 loadCodexConfig 的 codexHome 对齐,
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { FeishuConnector } from './connector/index.js';
6
6
  import { ClaudeRunner, CodexAppServerRunner, OpencodeAcpRunner, PiRpcRunner, KimiAcpRunner, DshRunner, } from './runner/index.js';
7
7
  import { AgentRegistry } from './runner/registry.js';
8
8
  import { probeAllAgents } from './runner/probe.js';
9
+ import { warmCodexCatalogCache } from './config/codex-config.js';
9
10
  import { SessionReaderRegistry, SessionStore } from './session/index.js';
10
11
  import { ClaudeSessionReader, CodexSessionReader, OpencodeSessionReader, PiSessionReader, KimiSessionReader, DshSessionReader, } from './session/index.js';
11
12
  import { CommandRouter, isImmediateAction } from './router/index.js';
@@ -289,6 +290,11 @@ function initializeRunner(config, configDir, cliArgs) {
289
290
  .catch(() => {
290
291
  // Probe failure is non-fatal; /config card will retry on open.
291
292
  });
293
+ // Warm the codex model catalog in the background so the first Codex /config
294
+ // card open reads a warm cache instead of blocking the event loop for up to
295
+ // 8s (execFileSync cold path). Fire-and-forget; failure lands in negative
296
+ // cache and the sync card path falls back gracefully.
297
+ warmCodexCatalogCache();
292
298
  return { agentRegistry, sessionReaderRegistry };
293
299
  }
294
300
  /** Setup message and card action handlers. */
@@ -3108,12 +3108,19 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3108
3108
  const sessionMeta = new Map();
3109
3109
  const prefetchCount = Math.min(RESUME_CONTENT_PREFETCH, allSessions.length);
3110
3110
  for (const s of allSessions.slice(0, prefetchCount)) {
3111
- // Use the same reader as the listSessions call above
3112
- const content = reader.readSessionContent(s.sessionId, cwd);
3111
+ // 优先走轻量摘要(只取标题/用量,不构造 events)——codex 实现了
3112
+ // readSessionSummary,避免对前 N 条各做一次全量 JSONL 解析来构造
3113
+ // 立即被丢弃的 events 数组。其他 agent 无此方法时回退全量读取再取字段。
3114
+ const meta = reader.readSessionSummary
3115
+ ? reader.readSessionSummary(s.sessionId, cwd)
3116
+ : (() => {
3117
+ const c = reader.readSessionContent(s.sessionId, cwd);
3118
+ return { aiTitle: c.aiTitle, recap: c.recap, displayTitle: c.displayTitle };
3119
+ })();
3113
3120
  sessionMeta.set(s.sessionId, {
3114
- aiTitle: content.aiTitle,
3115
- recap: content.recap,
3116
- displayTitle: content.displayTitle,
3121
+ aiTitle: meta.aiTitle,
3122
+ recap: meta.recap,
3123
+ displayTitle: meta.displayTitle,
3117
3124
  });
3118
3125
  }
3119
3126
  const now = Date.now();
@@ -39,7 +39,7 @@ export declare const RpcErrorCode: {
39
39
  export interface InitializeParams {
40
40
  /** Protocol version — must be 1 (number, not string). */
41
41
  protocolVersion: number;
42
- /** Client capabilities: explicitly declare what we support (all off). */
42
+ /** Client capabilities: declare only reverse-RPC surfaces we implement. */
43
43
  clientCapabilities: {
44
44
  fs: {
45
45
  readTextFile: boolean;
@@ -209,6 +209,12 @@ export declare const ServerRequestMethod: {
209
209
  readonly REQUEST_PERMISSION: "session/request_permission";
210
210
  /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
211
211
  readonly ELICITATION_CREATE: "elicitation/create";
212
+ /** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
213
+ readonly TERMINAL_CREATE: "terminal/create";
214
+ readonly TERMINAL_OUTPUT: "terminal/output";
215
+ readonly TERMINAL_WAIT_FOR_EXIT: "terminal/wait_for_exit";
216
+ readonly TERMINAL_KILL: "terminal/kill";
217
+ readonly TERMINAL_RELEASE: "terminal/release";
212
218
  };
213
219
  export interface PermissionOption {
214
220
  /** Opaque option identifier — echo back in response outcome.optionId. */
@@ -269,3 +275,34 @@ export interface ElicitationCreateResponse {
269
275
  /** accept 时的答案,key 为 q0..qn(按问题顺序)。 */
270
276
  content?: Record<string, string | string[]>;
271
277
  }
278
+ export interface TerminalCreateParams {
279
+ sessionId: string;
280
+ /** 恒为 'bash'(服务端 spawn 只接受 bash -c)。 */
281
+ command: string;
282
+ /** ['-c', <script>]:-c 在 args[0],脚本在 args[1]。 */
283
+ args: string[];
284
+ env?: Array<{
285
+ name: string;
286
+ value: string;
287
+ }>;
288
+ /** 可能为 null → 回退 session cwd。 */
289
+ cwd?: string | null;
290
+ /** 输出缓冲上限(字节),默认 4MB。 */
291
+ outputByteLimit?: number;
292
+ }
293
+ export interface TerminalOutputParams {
294
+ sessionId: string;
295
+ terminalId: string;
296
+ }
297
+ export interface TerminalWaitForExitParams {
298
+ sessionId: string;
299
+ terminalId: string;
300
+ }
301
+ export interface TerminalKillParams {
302
+ sessionId: string;
303
+ terminalId: string;
304
+ }
305
+ export interface TerminalReleaseParams {
306
+ sessionId: string;
307
+ terminalId: string;
308
+ }
@@ -73,5 +73,11 @@ export const ServerRequestMethod = {
73
73
  REQUEST_PERMISSION: 'session/request_permission',
74
74
  /** Kimi AskUserQuestion elicitation form(客户端广告 elicitation.form 时启用)。 */
75
75
  ELICITATION_CREATE: 'elicitation/create',
76
+ /** kimi terminal 工具 reverse RPC:Bash 执行下放客户端(acp-terminal)。 */
77
+ TERMINAL_CREATE: 'terminal/create',
78
+ TERMINAL_OUTPUT: 'terminal/output',
79
+ TERMINAL_WAIT_FOR_EXIT: 'terminal/wait_for_exit',
80
+ TERMINAL_KILL: 'terminal/kill',
81
+ TERMINAL_RELEASE: 'terminal/release',
76
82
  };
77
83
  //# sourceMappingURL=protocol-types.js.map
@@ -15,6 +15,6 @@ export { KimiAcpRunner } from './kimi/index.js';
15
15
  export { DshRunner } from './dsh/index.js';
16
16
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
17
17
  export { AgentRegistry } from './registry.js';
18
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
19
19
  export { resolveAgentChoices } from './resolve-agent-choices.js';
20
20
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -46,6 +46,21 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
46
46
  private readonly compactPollTimeoutMs;
47
47
  /** Pending approval requests: requestId → kind + view + options. */
48
48
  private pendingApprovals;
49
+ /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
50
+ private terminals;
51
+ /** terminalId 自增序号(term-1, term-2, …)。 */
52
+ private terminalSeq;
53
+ /**
54
+ * kimi 服务端对 Bash 工具的 tool_call/tool_call_update 通知(title='Bash')。
55
+ * Bash 的真实执行走 terminal/* reverse RPC 下放客户端,tool 事件由本 runner
56
+ * 自产(tool_use 带命令、tool_result 带本地输出);这些通知必须过滤,否则
57
+ * 卡片出现双份 Bash 面板(通知版无 rawInput、terminal embed 无 rawOutput,
58
+ * 只能渲染「_无输出_」)。收集到的 toolCallId 用于过滤对应的 update。
59
+ */
60
+ private bashToolCallIds;
61
+ /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
62
+ private activeCwd;
63
+ private readonly processStopper;
49
64
  /** Tracker for the in-flight prompt request — needed for cancellation. */
50
65
  private promptSettled;
51
66
  constructor(opts: KimiAcpRunnerOptions);
@@ -102,5 +117,41 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
102
117
  */
103
118
  private waitForCompactionRecord;
104
119
  private handleNotification;
120
+ /**
121
+ * Filter kimi's own Bash tool notifications so the run card doesn't get a
122
+ * duplicate, output-less Bash panel alongside the runner-produced one.
123
+ *
124
+ * - tool_call title='Bash' → remember the toolCallId, drop the notification
125
+ * (the tool_use comes from terminal/create with the real command).
126
+ * - tool_call_update for a remembered id → drop (streaming args deltas and
127
+ * the terminal embed final update; the tool_result is emitted locally).
128
+ * Returns true when the notification should be swallowed.
129
+ */
130
+ private filterBashNotifications;
105
131
  private handleServerRequest;
132
+ /**
133
+ * Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
134
+ *
135
+ * 契约(kimi-code acp-terminal/acpTerminalRunner.ts):
136
+ * create → 立即回 {terminalId},进程异步跑,不阻塞 RPC;
137
+ * output → 回累计 stdout(非增量,服务端按 emitted 偏移切片);
138
+ * wait_for_exit → 未退出则等 exit 再回 {exitCode, signal};
139
+ * kill/release → 杀进程并回 {}。
140
+ */
141
+ private handleTerminalRequest;
142
+ /**
143
+ * terminal/create: spawn 一次性进程(bash -c <script>),流式累积
144
+ * stdout(+stderr) 到缓冲,立即回 {terminalId}。
145
+ */
146
+ private handleTerminalCreate;
147
+ /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
148
+ private handleTerminalOutput;
149
+ /** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
150
+ private handleTerminalWaitForExit;
151
+ /** terminal/kill: 立即杀进程组,回 {}。 */
152
+ private handleTerminalKill;
153
+ /** terminal/release: 从 map 删除;进程仍在则一并杀,回 {}。 */
154
+ private handleTerminalRelease;
155
+ /** Kill every live terminal (dispose/connection-close 清理钩子)。 */
156
+ private cleanupTerminals;
106
157
  }