lark-remote 0.4.1 → 0.4.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/approval-coordinator.d.ts +2 -0
  5. package/dist/bridge/approval-coordinator.js +16 -7
  6. package/dist/bridge/index.d.ts +22 -0
  7. package/dist/bridge/index.js +138 -56
  8. package/dist/card/approval-render.js +9 -8
  9. package/dist/card/card-budget.js +39 -26
  10. package/dist/card/run-renderer.js +9 -2
  11. package/dist/card/text-truncate.d.ts +9 -10
  12. package/dist/card/text-truncate.js +48 -43
  13. package/dist/cli.js +17 -0
  14. package/dist/config/codex-config.d.ts +3 -0
  15. package/dist/config/codex-config.js +4 -1
  16. package/dist/config/dir.js +6 -6
  17. package/dist/config/index.d.ts +7 -5
  18. package/dist/config/index.js +52 -13
  19. package/dist/config/kimi-config.d.ts +4 -1
  20. package/dist/config/kimi-config.js +5 -2
  21. package/dist/config/pi-config.d.ts +2 -7
  22. package/dist/config/pi-config.js +6 -6
  23. package/dist/config/wizard.js +8 -4
  24. package/dist/connector/index.d.ts +19 -0
  25. package/dist/connector/index.js +134 -23
  26. package/dist/inbound/placeholder.d.ts +3 -2
  27. package/dist/inbound/placeholder.js +17 -7
  28. package/dist/index.js +28 -6
  29. package/dist/instance-lock.d.ts +31 -15
  30. package/dist/instance-lock.js +64 -62
  31. package/dist/platform/agent-stopper.d.ts +29 -7
  32. package/dist/platform/agent-stopper.js +40 -8
  33. package/dist/platform/identity.d.ts +59 -3
  34. package/dist/platform/identity.js +105 -30
  35. package/dist/platform/path.d.ts +0 -7
  36. package/dist/platform/path.js +1 -51
  37. package/dist/platform/shell.d.ts +14 -0
  38. package/dist/platform/shell.js +35 -2
  39. package/dist/platform/sleep-blocker.d.ts +6 -0
  40. package/dist/platform/sleep-blocker.js +31 -5
  41. package/dist/platform/spawn.d.ts +1 -1
  42. package/dist/platform/spawn.js +1 -1
  43. package/dist/platform/terminator-posix.d.ts +1 -29
  44. package/dist/platform/terminator-posix.js +32 -30
  45. package/dist/platform/terminator-win32.js +4 -2
  46. package/dist/platform/terminator.d.ts +2 -2
  47. package/dist/platform/terminator.js +17 -2
  48. package/dist/platform/types.d.ts +3 -0
  49. package/dist/router/card-action-feedback.d.ts +17 -0
  50. package/dist/router/card-action-feedback.js +31 -0
  51. package/dist/router/card-helpers.d.ts +38 -2
  52. package/dist/router/card-helpers.js +79 -2
  53. package/dist/router/config/claude.js +8 -5
  54. package/dist/router/index.d.ts +41 -6
  55. package/dist/router/index.js +254 -84
  56. package/dist/runner/claude/runner.js +1 -1
  57. package/dist/runner/claude/session.d.ts +2 -6
  58. package/dist/runner/claude/session.js +7 -8
  59. package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
  60. package/dist/runner/codex/app-server/protocol-types.js +4 -0
  61. package/dist/runner/codex/app-server/runner.js +3 -0
  62. package/dist/runner/common/acp/base-acp-runner.js +7 -0
  63. package/dist/runner/common/connection-based-runner.d.ts +18 -0
  64. package/dist/runner/common/connection-based-runner.js +31 -0
  65. package/dist/runner/common/jsonrpc/client.js +6 -1
  66. package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
  67. package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
  68. package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
  69. package/dist/runner/common/jsonrpc/transport.js +79 -10
  70. package/dist/runner/common/spawning-runner.d.ts +49 -19
  71. package/dist/runner/common/spawning-runner.js +94 -54
  72. package/dist/runner/dsh/client.d.ts +1 -3
  73. package/dist/runner/dsh/client.js +0 -4
  74. package/dist/runner/dsh/types.d.ts +0 -36
  75. package/dist/runner/kimi/acp/runner.d.ts +18 -1
  76. package/dist/runner/kimi/acp/runner.js +100 -42
  77. package/dist/runner/kimi/acp/translator.d.ts +0 -1
  78. package/dist/runner/opencode/acp/translator.d.ts +0 -1
  79. package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
  80. package/dist/runner/pi/rpc/runner.js +3 -0
  81. package/dist/runner/resolve-agent-choices.js +0 -5
  82. package/package.json +2 -2
  83. package/dist/runner/common/process-stopper.d.ts +0 -7
  84. package/dist/runner/common/process-stopper.js +0 -8
  85. package/dist/runner/dsh/fake-dsh-server.d.ts +0 -60
  86. package/dist/runner/dsh/fake-dsh-server.js +0 -156
@@ -1,6 +1,6 @@
1
- import { type ChildProcess } from 'node:child_process';
1
+ import type { ChildProcess } from 'node:child_process';
2
2
  import type { Readable } from 'node:stream';
3
- import { ProcessStopper } from './process-stopper.js';
3
+ import { type Terminator } from '../../platform/terminator.js';
4
4
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
5
5
  import type { SpawnOptions } from '../types.js';
6
6
  /**
@@ -50,7 +50,7 @@ export declare function unregisterExitCleanup(handler: ExitCleanupHandler): void
50
50
  export declare abstract class SpawningRunner {
51
51
  protected currentProcess: ChildProcess | null;
52
52
  protected readonly pidFilePath: string;
53
- protected readonly stopper: ProcessStopper;
53
+ protected readonly terminator: Terminator;
54
54
  protected readonly spawnHeartbeat: SpawnHeartbeat;
55
55
  protected binary: string;
56
56
  protected stopGraceMs: number;
@@ -77,6 +77,13 @@ export declare abstract class SpawningRunner {
77
77
  * 非零退出」才判 command-not-found;真挂起由既有 grace/超时兜底。
78
78
  */
79
79
  protected commandNotFoundSeen: boolean;
80
+ /**
81
+ * 本 runner 的 agent key('claude'/…):win32 上既是 Terminator 查协议停止
82
+ * 通道的键,也是这里登记通道的键。undefined = 无通道(如 `!` bash)。
83
+ */
84
+ protected readonly agent?: string;
85
+ /** 已登记的协议停止通道(agent+pid 键);null = 未登记。 */
86
+ private stopperRegistration;
80
87
  constructor(opts: {
81
88
  pidDir?: string;
82
89
  workspace: string;
@@ -96,6 +103,13 @@ export declare abstract class SpawningRunner {
96
103
  * spawn-stage-stalled WARN identifies the right agent.
97
104
  */
98
105
  logTag?: string;
106
+ /**
107
+ * 当前 run 所属 agent('claude'/'kimi'/…):win32 上按这个 key 查协议停止
108
+ * 通道。缺省 undefined = 无通道(Terminator 会显式记录后走树杀,不空等)。
109
+ */
110
+ agent?: string;
111
+ /** 终止器注入(测试用);默认按平台建 POSIX/win32 实现 */
112
+ terminator?: Terminator;
99
113
  });
100
114
  get isRunning(): boolean;
101
115
  /**
@@ -114,7 +128,7 @@ export declare abstract class SpawningRunner {
114
128
  * 'error' for ENOENT/EACCES, but some binaries fail silently without ever
115
129
  * emitting it. Without the race, the spawn lead-in hangs forever → the
116
130
  * workspace serial queue never settles → permanent deadlock, and /stop
117
- * can't recover (ProcessStopper returns early when pid === undefined).
131
+ * can't recover (Terminator.stop returns early when pid === undefined).
118
132
  * The timeout keeps the deadlock bounded.
119
133
  */
120
134
  protected awaitSpawnError(proc: ChildProcess): Promise<Error | undefined>;
@@ -128,30 +142,46 @@ export declare abstract class SpawningRunner {
128
142
  * On success returns the spawned child.
129
143
  */
130
144
  protected spawnChild(opts: SpawnOptions): Promise<ChildProcess>;
145
+ /**
146
+ * 登记本进程的协议停止通道(design §3.3 表):win32 上没有可拦截的跨进程
147
+ * SIGTERM,「优雅停止」只能经 agent 自有通道请求对方自行退出。stdio 型 agent
148
+ * CLI 的通道就是关 stdin(cc-connect 已验证 claude:关 stdin → 干净退出并跑
149
+ * Stop hooks)。
150
+ *
151
+ * 只在子进程真的挂了 stdin 管道时登记:stdin 为 'ignore' 的子类若照登一条空
152
+ * 通道,优雅段会从「无通道 → 显式跳过 + 直接树杀」退化成「空转满 grace 再
153
+ * 树杀」——照样能停,但白等一个不会被消费的请求。
154
+ */
155
+ protected registerStopper(proc: ChildProcess): void;
156
+ /** 注销本 runner 登记的通道(幂等;未登记时无操作)。 */
157
+ protected unregisterStopper(): void;
131
158
  /**
132
159
  * Stop the current process if one is running. Delegates the actual
133
- * SIGTERM → grace → SIGKILL sequence to `this.stopper.stop(proc, opts)`
134
- * (see `src/runner/common/process-stopper.ts`), forwarding the `immediate`
135
- * flag verbatim so all subclasses inherit identical stop semantics.
160
+ * posix SIGTERM → grace → SIGKILL / win32 协议通道 → grace → taskkill 序列
161
+ * 给 `this.terminator.stop(proc, { immediate })`(见
162
+ * `src/platform/terminator.ts`),`immediate` 在这里收敛成显式 boolean,
163
+ * 所有子类继承同一套停止语义。
136
164
  */
137
165
  stop(opts?: {
138
166
  immediate?: boolean;
139
167
  }): Promise<void>;
140
- killOrphan(): void;
141
168
  /**
142
- * P1-10: verify the process behind a stale pid file is actually ours before
143
- * sending any signal. `ps -o command=` (not `comm=`) is used because agent
144
- * binaries may be bash-wrapped scripts whose comm is the interpreter name
145
- * (`/bin/bash`); the full command line contains the binary path/basename.
169
+ * P1-10: 杀掉上一次崩溃留下的孤儿 agent 进程。**身份校验是动手的前置条件**:
170
+ * pid 文件里的 pid 可能已被系统回收给无关进程,`kill(pid, 0)` 存活探测分不出来。
146
171
  *
147
- * Returns:
148
- * 'match' — ps succeeded and the command line contains this.binary
149
- * (absolute path) or its basename (PATH lookup).
150
- * 'mismatch' — ps succeeded but the command line is clearly a different
151
- * process (pid was recycled to an unrelated process).
152
- * 'unknown' — ps failed / process gone / output unreadable: cannot tell.
172
+ * 判定单源 = `platform/identity`(posix `ps -o command=` / win32 CIM),三态:
173
+ * - match → 杀整个进程组(detached:true 下 agent 是组长,与 Terminator
174
+ * posix 路径的 kill(-pid) 同语义,P1-12 子进程不孤儿化);
175
+ * - mismatch → pid 已被无关进程占用:绝不杀,文件是陈旧垃圾,清除自愈;
176
+ * - unknown → 查不到身份(ps 不可用/超时/CIM 被拒):同样不杀,清文件。
177
+ * 统一原则:只有身份匹配才杀,验证失败一律不杀(fail-closed)。
178
+ *
179
+ * 同步实现是有意为之:调用方(Bridge.getRunner)是同步方法,改成异步会级联
180
+ * 整个 runner 接口。posix 上的成本与迁移前逐字相同(一次 execFileSync ps);
181
+ * **win32 上同步路径恒返回 `unknown`**(CIM 必须经 PowerShell 收 stdout,没有同步
182
+ * 形态)→ 落到 fail-closed 分支「不杀 + 清文件」,语义正确而不是能力缺失。
153
183
  */
154
- private matchPidToBinary;
184
+ killOrphan(): void;
155
185
  /**
156
186
  * Register this runner with the process-level exit dispatcher (singleton).
157
187
  * Process listeners are installed at most once; subsequent calls only add
@@ -1,11 +1,12 @@
1
- import { execFileSync } from 'node:child_process';
2
1
  import { spawnProcess, isWindowsCommandNotFoundLine, useDetachedProcessGroup, } from '../../platform/spawn.js';
3
2
  import path from 'node:path';
4
3
  import os from 'node:os';
5
4
  import fs from 'node:fs';
6
5
  import { silentlyUnlink } from '../../common/fs.js';
7
6
  import { getLogger } from '../../logger/index.js';
8
- import { ProcessStopper } from './process-stopper.js';
7
+ import { binaryName, verifyPidIdentityVerdictSync } from '../../platform/identity.js';
8
+ import { createTerminator } from '../../platform/terminator.js';
9
+ import { agentStopperRegistry } from '../../platform/agent-stopper.js';
9
10
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
10
11
  import { createJSONLStream } from './jsonl-stream.js';
11
12
  import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
@@ -78,7 +79,7 @@ export function unregisterExitCleanup(handler) {
78
79
  export class SpawningRunner {
79
80
  currentProcess = null;
80
81
  pidFilePath;
81
- stopper;
82
+ terminator;
82
83
  spawnHeartbeat;
83
84
  binary;
84
85
  stopGraceMs;
@@ -105,16 +106,25 @@ export class SpawningRunner {
105
106
  * 非零退出」才判 command-not-found;真挂起由既有 grace/超时兜底。
106
107
  */
107
108
  commandNotFoundSeen = false;
109
+ /**
110
+ * 本 runner 的 agent key('claude'/…):win32 上既是 Terminator 查协议停止
111
+ * 通道的键,也是这里登记通道的键。undefined = 无通道(如 `!` bash)。
112
+ */
113
+ agent;
114
+ /** 已登记的协议停止通道(agent+pid 键);null = 未登记。 */
115
+ stopperRegistration = null;
108
116
  constructor(opts) {
109
117
  // Subclasses set this.binary after super()
110
118
  this.binary = '';
111
119
  this.stopGraceMs = opts.stopGraceMs ?? DEFAULT_STOP_GRACE_MS;
112
120
  this.logTag = opts.logTag ?? 'spawning-runner';
121
+ this.agent = opts.agent;
113
122
  const pidDir = opts.pidDir ?? path.join(os.homedir(), '.lark-remote');
114
123
  const workspaceSuffix = `-${opts.workspace.replace(/[^a-zA-Z0-9]/g, '_')}`;
115
124
  const pidFilePrefix = opts.pidFilePrefix ?? 'spawning';
116
125
  this.pidFilePath = path.join(pidDir, `${pidFilePrefix}${workspaceSuffix}.pid`);
117
- this.stopper = new ProcessStopper({ graceMs: this.stopGraceMs });
126
+ this.terminator =
127
+ opts.terminator ?? createTerminator({ graceMs: this.stopGraceMs, agent: opts.agent });
118
128
  this.spawnHeartbeat = new SpawnHeartbeat(opts.spawnHeartbeatMs ?? 30_000, this.logTag);
119
129
  }
120
130
  get isRunning() {
@@ -149,7 +159,7 @@ export class SpawningRunner {
149
159
  * 'error' for ENOENT/EACCES, but some binaries fail silently without ever
150
160
  * emitting it. Without the race, the spawn lead-in hangs forever → the
151
161
  * workspace serial queue never settles → permanent deadlock, and /stop
152
- * can't recover (ProcessStopper returns early when pid === undefined).
162
+ * can't recover (Terminator.stop returns early when pid === undefined).
153
163
  * The timeout keeps the deadlock bounded.
154
164
  */
155
165
  awaitSpawnError(proc) {
@@ -209,6 +219,9 @@ export class SpawningRunner {
209
219
  proc.stdout?.once('data', () => {
210
220
  this.spawnHeartbeat.notifyStdout();
211
221
  });
222
+ // 协议停止通道登记(design §3.3):子进程起来后立刻登记,win32 上
223
+ // Terminator 停这个 pid 时才能取到通道;进程退出/被停后注销。
224
+ this.registerStopper(proc);
212
225
  this.spawnStderr = '';
213
226
  proc.stderr?.on('data', (chunk) => {
214
227
  const text = chunk.toString('utf-8').trim();
@@ -231,25 +244,79 @@ export class SpawningRunner {
231
244
  });
232
245
  return proc;
233
246
  }
247
+ /**
248
+ * 登记本进程的协议停止通道(design §3.3 表):win32 上没有可拦截的跨进程
249
+ * SIGTERM,「优雅停止」只能经 agent 自有通道请求对方自行退出。stdio 型 agent
250
+ * CLI 的通道就是关 stdin(cc-connect 已验证 claude:关 stdin → 干净退出并跑
251
+ * Stop hooks)。
252
+ *
253
+ * 只在子进程真的挂了 stdin 管道时登记:stdin 为 'ignore' 的子类若照登一条空
254
+ * 通道,优雅段会从「无通道 → 显式跳过 + 直接树杀」退化成「空转满 grace 再
255
+ * 树杀」——照样能停,但白等一个不会被消费的请求。
256
+ */
257
+ registerStopper(proc) {
258
+ const pid = proc.pid;
259
+ if (this.agent === undefined || pid === undefined || !proc.stdin)
260
+ return;
261
+ const stopper = (target) => {
262
+ target.stdin?.end();
263
+ };
264
+ // 上一次 spawn 的登记先撤掉:进程换代后旧 pid 的条目会一直挂在注册表里
265
+ // (pid 被系统复用时可能指向无关进程)。
266
+ this.unregisterStopper();
267
+ agentStopperRegistry.register(this.agent, pid, stopper);
268
+ this.stopperRegistration = { pid, stopper };
269
+ }
270
+ /** 注销本 runner 登记的通道(幂等;未登记时无操作)。 */
271
+ unregisterStopper() {
272
+ const reg = this.stopperRegistration;
273
+ if (!reg || this.agent === undefined)
274
+ return;
275
+ this.stopperRegistration = null;
276
+ agentStopperRegistry.unregister(this.agent, reg.pid, reg.stopper);
277
+ }
234
278
  /**
235
279
  * Stop the current process if one is running. Delegates the actual
236
- * SIGTERM → grace → SIGKILL sequence to `this.stopper.stop(proc, opts)`
237
- * (see `src/runner/common/process-stopper.ts`), forwarding the `immediate`
238
- * flag verbatim so all subclasses inherit identical stop semantics.
280
+ * posix SIGTERM → grace → SIGKILL / win32 协议通道 → grace → taskkill 序列
281
+ * 给 `this.terminator.stop(proc, { immediate })`(见
282
+ * `src/platform/terminator.ts`),`immediate` 在这里收敛成显式 boolean,
283
+ * 所有子类继承同一套停止语义。
239
284
  */
240
285
  async stop(opts) {
241
286
  const proc = this.currentProcess;
242
287
  if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
243
288
  this.spawnHeartbeat.clear();
244
289
  this.currentProcess = null;
290
+ this.unregisterStopper();
245
291
  return;
246
292
  }
247
293
  this.stoppedByUser = true;
248
- await this.stopper.stop(proc, { immediate: opts?.immediate });
294
+ // `immediate: opts?.immediate === true`:Terminator 契约要求显式 boolean,
295
+ // 旧 ProcessStopper 的 `immediate?: boolean` 会静默接受 undefined。
296
+ await this.terminator.stop(proc, { immediate: opts?.immediate === true });
297
+ // 注销必须在 stop() 之后:Terminator 就是在上面那次调用里查表取通道的
298
+ // (immediate 路径不查表,但一并注销同样正确——进程已进入终止流程)。
299
+ this.unregisterStopper();
249
300
  this.currentProcess = null;
250
301
  silentlyUnlink(this.pidFilePath);
251
302
  getLogger().debug(`[${this.logTag}] cleaned pid file ${this.pidFilePath}`);
252
303
  }
304
+ /**
305
+ * P1-10: 杀掉上一次崩溃留下的孤儿 agent 进程。**身份校验是动手的前置条件**:
306
+ * pid 文件里的 pid 可能已被系统回收给无关进程,`kill(pid, 0)` 存活探测分不出来。
307
+ *
308
+ * 判定单源 = `platform/identity`(posix `ps -o command=` / win32 CIM),三态:
309
+ * - match → 杀整个进程组(detached:true 下 agent 是组长,与 Terminator
310
+ * posix 路径的 kill(-pid) 同语义,P1-12 子进程不孤儿化);
311
+ * - mismatch → pid 已被无关进程占用:绝不杀,文件是陈旧垃圾,清除自愈;
312
+ * - unknown → 查不到身份(ps 不可用/超时/CIM 被拒):同样不杀,清文件。
313
+ * 统一原则:只有身份匹配才杀,验证失败一律不杀(fail-closed)。
314
+ *
315
+ * 同步实现是有意为之:调用方(Bridge.getRunner)是同步方法,改成异步会级联
316
+ * 整个 runner 接口。posix 上的成本与迁移前逐字相同(一次 execFileSync ps);
317
+ * **win32 上同步路径恒返回 `unknown`**(CIM 必须经 PowerShell 收 stdout,没有同步
318
+ * 形态)→ 落到 fail-closed 分支「不杀 + 清文件」,语义正确而不是能力缺失。
319
+ */
253
320
  killOrphan() {
254
321
  if (!fs.existsSync(this.pidFilePath))
255
322
  return;
@@ -260,21 +327,23 @@ export class SpawningRunner {
260
327
  silentlyUnlink(this.pidFilePath);
261
328
  return;
262
329
  }
263
- // P1-10: pid 复用防护。pid 文件里的 pid 可能是 bridge 崩溃后系统回收复用
264
- // 给了无关进程——kill(pid, 0) 存活探测无法区分,必须先验证进程身份。
265
- const match = this.matchPidToBinary(pid);
266
- if (match !== 'match') {
267
- // 'gone':ps 查不到/报错 → 进程已消失,陈旧追踪物自愈清除(不杀)。
268
- // 'mismatch':身份不匹配 → pid 已被无关进程占用,绝不能杀;文件是
269
- // 陈旧垃圾,一并清除。
270
- // 统一原则:只有身份匹配才杀;验证失败一律不杀。
271
- getLogger().warn(`[${this.logTag}] killOrphan: pid ${pid} not confirmed as ${this.binary} ` +
272
- `(${match}), skipping kill`);
330
+ const expectedBinary = binaryName(this.binary);
331
+ // matchMode 'agent-invocation'(不是默认的 'executable'):agent CLI 普遍是
332
+ // `#!/usr/bin/env node` 脚本,ps 看到的是 `node <安装路径>/…/cli.js`,名字只在
333
+ // 路径段里(pi → @earendil-works/pi-coding-agent/…)。用默认档会恒判 mismatch
334
+ // → 这些 agent 的孤儿永远回收不掉(2026-09-20 实测安装形态)。
335
+ const verdict = verifyPidIdentityVerdictSync(pid, {
336
+ expectedBinary,
337
+ matchMode: 'agent-invocation',
338
+ });
339
+ if (verdict !== 'match') {
340
+ getLogger().warn(`[${this.logTag}] killOrphan: pid ${pid} not confirmed as ${expectedBinary} ` +
341
+ `(${verdict}), skipping kill`);
273
342
  silentlyUnlink(this.pidFilePath);
274
343
  return;
275
344
  }
276
345
  // 身份匹配:杀整个进程组(detached:true 下 agent 是组长),与
277
- // ProcessStopper 的 kill(-pid) 语义对齐,子进程不会孤儿化(P1-12)。
346
+ // Terminator 的 kill(-pid) 语义对齐,子进程不会孤儿化(P1-12)。
278
347
  getLogger().info(`[${this.logTag}] killing orphan process group ${-pid}`);
279
348
  try {
280
349
  process.kill(-pid, 'SIGTERM');
@@ -288,37 +357,6 @@ export class SpawningRunner {
288
357
  // ignore
289
358
  }
290
359
  }
291
- /**
292
- * P1-10: verify the process behind a stale pid file is actually ours before
293
- * sending any signal. `ps -o command=` (not `comm=`) is used because agent
294
- * binaries may be bash-wrapped scripts whose comm is the interpreter name
295
- * (`/bin/bash`); the full command line contains the binary path/basename.
296
- *
297
- * Returns:
298
- * 'match' — ps succeeded and the command line contains this.binary
299
- * (absolute path) or its basename (PATH lookup).
300
- * 'mismatch' — ps succeeded but the command line is clearly a different
301
- * process (pid was recycled to an unrelated process).
302
- * 'unknown' — ps failed / process gone / output unreadable: cannot tell.
303
- */
304
- matchPidToBinary(pid) {
305
- try {
306
- const out = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
307
- encoding: 'utf-8',
308
- }).trim();
309
- if (!out || out.includes('<defunct>'))
310
- return 'gone';
311
- const binary = this.binary;
312
- if (!binary)
313
- return 'gone';
314
- const needle = binary.includes('/') ? binary : path.basename(binary);
315
- return out.includes(needle) ? 'match' : 'mismatch';
316
- }
317
- catch {
318
- // ps 失败(进程不存在/权限)→ 视作 gone:不杀,让陈旧文件自愈
319
- return 'gone';
320
- }
321
- }
322
360
  /**
323
361
  * Register this runner with the process-level exit dispatcher (singleton).
324
362
  * Process listeners are installed at most once; subsequent calls only add
@@ -355,12 +393,14 @@ export class SpawningRunner {
355
393
  if (this.currentProcess && this.currentProcess.exitCode === null) {
356
394
  try {
357
395
  // P1-12: 只杀组长会让组内子进程(工具调用起的后台进程)reparent 成孤儿。
358
- // 复用 ProcessStopper 的 immediate 组杀(SIGTERM+SIGKILL 同步发出,
359
- // 进程退出路径 fire-and-forget 足够;正常进程退出时是 no-op)。
360
- void this.stopper.stop(this.currentProcess, { immediate: true });
396
+ // Terminator.cleanupOnExit 在两种平台上都是「fire-and-forget 强杀整个
397
+ // 进程组/进程树」(posix kill(-pgid, SIGKILL) / win32 taskkill /T /F),
398
+ // 退出路径上无法等待,也不需要优雅段。
399
+ this.terminator.cleanupOnExit(this.currentProcess);
361
400
  }
362
401
  catch { }
363
402
  }
403
+ this.unregisterStopper();
364
404
  silentlyUnlink(this.pidFilePath);
365
405
  }
366
406
  }
@@ -12,7 +12,7 @@
12
12
  * (DSH v1 has no `since` resume — reconnection = reopen stream + refetch
13
13
  * history, dedup by event seq).
14
14
  */
15
- import type { DshModelCatalogValue, DshModelSelection, DshPresetListValue, DshServerRequest, DshSessionModelsValue, DshStreamItem, DshTokenUsage } from './types.js';
15
+ import type { DshModelCatalogValue, DshModelSelection, DshPresetListValue, DshServerRequest, DshStreamItem, DshTokenUsage } from './types.js';
16
16
  interface WebSocketMessageEvent {
17
17
  data: unknown;
18
18
  }
@@ -49,8 +49,6 @@ export declare class DshClient {
49
49
  selectModel(payload: DshModelSelection & {
50
50
  sessionId: string;
51
51
  }): Promise<DshModelSelection>;
52
- /** Read the session's current model selection + full model catalog. */
53
- sessionModels(sessionId: string): Promise<DshSessionModelsValue>;
54
52
  /** Read the model catalog (session-independent, llm.models). */
55
53
  listModels(): Promise<DshModelCatalogValue>;
56
54
  /** Read the preset catalog (agentPreset.list). */
@@ -81,10 +81,6 @@ export class DshClient {
81
81
  const value = (await this.unary('session.selectModel', payload));
82
82
  return value.selected;
83
83
  }
84
- /** Read the session's current model selection + full model catalog. */
85
- async sessionModels(sessionId) {
86
- return (await this.unary('session.models', { sessionId }));
87
- }
88
84
  /** Read the model catalog (session-independent, llm.models). */
89
85
  async listModels() {
90
86
  return (await this.unary('llm.models', {}));
@@ -17,12 +17,6 @@ export interface DshSessionEvent {
17
17
  time: number;
18
18
  data: Record<string, unknown>;
19
19
  }
20
- /** assistant/chunk chunk payload (StreamChunk). */
21
- export interface DshChunk {
22
- type: string;
23
- index?: number;
24
- text?: string;
25
- }
26
20
  /** TokenUsage from assistant/message.usage (dsh-llm TokenUsage). */
27
21
  export interface DshTokenUsage {
28
22
  inputTokens: number;
@@ -89,17 +83,6 @@ export interface DshModelSelection {
89
83
  model: string;
90
84
  reasoningEffort?: string;
91
85
  }
92
- /** session.models → value. */
93
- export interface DshSessionModelsValue {
94
- current: DshModelSelection;
95
- routable: boolean;
96
- groups: DshModelGroup[];
97
- failures?: Array<{
98
- id: string;
99
- name: string;
100
- message: string;
101
- }>;
102
- }
103
86
  /** One preset row of agentPreset.list. */
104
87
  export interface DshPresetEntry {
105
88
  id: string;
@@ -115,25 +98,6 @@ export interface DshPresetListValue {
115
98
  authorable: boolean;
116
99
  hasDocument: boolean;
117
100
  }
118
- /** Mux frame payloads we care about (subset of MuxFrame). */
119
- export type DshMuxFrame = {
120
- type: 'session/event';
121
- sessionId: string;
122
- event: DshSessionEvent;
123
- } | {
124
- type: 'approval/requested';
125
- sessionId: string;
126
- approvalId: string;
127
- toolName: string;
128
- callId?: string;
129
- reason?: string;
130
- } | {
131
- type: 'stream/error';
132
- error: {
133
- code: string;
134
- message: string;
135
- };
136
- };
137
101
  /** ServerRequest envelope (frame.method === frame.payload.type). */
138
102
  export interface DshServerRequest {
139
103
  type: 'server-request';
@@ -44,11 +44,18 @@ export interface KimiAcpRunnerOptions {
44
44
  * LLM 请求(引擎带重试),时长无上限,旧的固定 30s 轮询超时已删除。 */
45
45
  compactIdleTimeoutMs?: number;
46
46
  model?: string;
47
+ /**
48
+ * 思考强度(`low`|`high`|`max`,取值由 `KIMI_THINKING_EFFORTS` 校验)。
49
+ * 经 `session/set_config_option {configId:'thinking'}` 下发;不下发则跑
50
+ * kimi 服务端默认档(配置卡片存了也不生效)。
51
+ */
52
+ thinkingEffort?: string;
47
53
  /** Kimi permission mode (user-facing: manual/auto/yolo). */
48
54
  permissionMode?: 'manual' | 'auto' | 'yolo';
49
55
  }
50
56
  export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
51
57
  private permissionMode;
58
+ private thinkingEffort?;
52
59
  private readonly compactIdleTimeoutMs;
53
60
  /**
54
61
  * §5.2 compaction baseline: wire.jsonl records sampled by compactBaseline()
@@ -70,7 +77,17 @@ export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
70
77
  private bashToolCallIds;
71
78
  /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
72
79
  private activeCwd;
73
- private readonly processStopper;
80
+ /**
81
+ * 终端子进程终止器(terminal/kill|release 与 dispose 清理用)。agent 必须是
82
+ * 'kimi':win32 上优雅段靠协议停止通道,查错 key 会静默退化成直接树杀。
83
+ */
84
+ private readonly terminator;
85
+ /**
86
+ * shell seam:terminal/create 的可执行解析走这里(design.md §7.2)。kimi 的
87
+ * ACP 服务端固定要 `bash -c`,win32 上没有 Git Bash 时必须在 seam 里变成
88
+ * 明确错误,而不是一个语焉不详的 spawn ENOENT。
89
+ */
90
+ private readonly shell;
74
91
  constructor(opts: KimiAcpRunnerOptions);
75
92
  protected get logTag(): string;
76
93
  protected get turnTimeoutErrorMessage(): string;