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.
- package/CHANGELOG.md +39 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/approval-coordinator.d.ts +2 -0
- package/dist/bridge/approval-coordinator.js +16 -7
- package/dist/bridge/index.d.ts +22 -0
- package/dist/bridge/index.js +138 -56
- package/dist/card/approval-render.js +9 -8
- package/dist/card/card-budget.js +39 -26
- package/dist/card/run-renderer.js +9 -2
- package/dist/card/text-truncate.d.ts +9 -10
- package/dist/card/text-truncate.js +48 -43
- package/dist/cli.js +17 -0
- package/dist/config/codex-config.d.ts +3 -0
- package/dist/config/codex-config.js +4 -1
- package/dist/config/dir.js +6 -6
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.js +52 -13
- package/dist/config/kimi-config.d.ts +4 -1
- package/dist/config/kimi-config.js +5 -2
- package/dist/config/pi-config.d.ts +2 -7
- package/dist/config/pi-config.js +6 -6
- package/dist/config/wizard.js +8 -4
- package/dist/connector/index.d.ts +19 -0
- package/dist/connector/index.js +134 -23
- package/dist/inbound/placeholder.d.ts +3 -2
- package/dist/inbound/placeholder.js +17 -7
- package/dist/index.js +28 -6
- package/dist/instance-lock.d.ts +31 -15
- package/dist/instance-lock.js +64 -62
- package/dist/platform/agent-stopper.d.ts +29 -7
- package/dist/platform/agent-stopper.js +40 -8
- package/dist/platform/identity.d.ts +59 -3
- package/dist/platform/identity.js +105 -30
- package/dist/platform/path.d.ts +0 -7
- package/dist/platform/path.js +1 -51
- package/dist/platform/shell.d.ts +14 -0
- package/dist/platform/shell.js +35 -2
- package/dist/platform/sleep-blocker.d.ts +6 -0
- package/dist/platform/sleep-blocker.js +31 -5
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +1 -1
- package/dist/platform/terminator-posix.d.ts +1 -29
- package/dist/platform/terminator-posix.js +32 -30
- package/dist/platform/terminator-win32.js +4 -2
- package/dist/platform/terminator.d.ts +2 -2
- package/dist/platform/terminator.js +17 -2
- package/dist/platform/types.d.ts +3 -0
- package/dist/router/card-action-feedback.d.ts +17 -0
- package/dist/router/card-action-feedback.js +31 -0
- package/dist/router/card-helpers.d.ts +38 -2
- package/dist/router/card-helpers.js +79 -2
- package/dist/router/config/claude.js +8 -5
- package/dist/router/index.d.ts +41 -6
- package/dist/router/index.js +254 -84
- package/dist/runner/claude/runner.js +1 -1
- package/dist/runner/claude/session.d.ts +2 -6
- package/dist/runner/claude/session.js +7 -8
- package/dist/runner/codex/app-server/protocol-types.d.ts +4 -0
- package/dist/runner/codex/app-server/protocol-types.js +4 -0
- package/dist/runner/codex/app-server/runner.js +3 -0
- package/dist/runner/common/acp/base-acp-runner.js +7 -0
- package/dist/runner/common/connection-based-runner.d.ts +18 -0
- package/dist/runner/common/connection-based-runner.js +31 -0
- package/dist/runner/common/jsonrpc/client.js +6 -1
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +20 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +27 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +29 -1
- package/dist/runner/common/jsonrpc/transport.js +79 -10
- package/dist/runner/common/spawning-runner.d.ts +49 -19
- package/dist/runner/common/spawning-runner.js +94 -54
- package/dist/runner/dsh/client.d.ts +1 -3
- package/dist/runner/dsh/client.js +0 -4
- package/dist/runner/dsh/types.d.ts +0 -36
- package/dist/runner/kimi/acp/runner.d.ts +18 -1
- package/dist/runner/kimi/acp/runner.js +100 -42
- package/dist/runner/kimi/acp/translator.d.ts +0 -1
- package/dist/runner/opencode/acp/translator.d.ts +0 -1
- package/dist/runner/pi/rpc/protocol-types.d.ts +4 -0
- package/dist/runner/pi/rpc/runner.js +3 -0
- package/dist/runner/resolve-agent-choices.js +0 -5
- package/package.json +2 -2
- package/dist/runner/common/process-stopper.d.ts +0 -7
- package/dist/runner/common/process-stopper.js +0 -8
- package/dist/runner/dsh/fake-dsh-server.d.ts +0 -60
- package/dist/runner/dsh/fake-dsh-server.js +0 -156
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
2
|
import type { Readable } from 'node:stream';
|
|
3
|
-
import {
|
|
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
|
|
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 (
|
|
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
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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:
|
|
143
|
-
*
|
|
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
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* (
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
|
237
|
-
*
|
|
238
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
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,
|
|
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
|
-
|
|
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;
|