lark-remote 0.1.13 → 0.2.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/CHANGELOG.md +25 -0
- package/README.en.md +2 -6
- package/README.md +3 -7
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/index.d.ts +0 -7
- package/dist/bridge/index.js +21 -63
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +13 -1
- package/dist/card/card-shared.js +21 -30
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/config/codex-config.js +2 -0
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.js +0 -11
- package/dist/config/kimi-config.js +1 -0
- package/dist/config/opencode-config.js +13 -2
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -3
- package/dist/index.js +14 -41
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +66 -6
- package/dist/router/index.js +450 -315
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -1
- package/dist/runner/claude/session.js +18 -4
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
- package/dist/runner/common/acp/protocol-helpers.js +34 -0
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +45 -42
- package/dist/runner/kimi/acp/runner.js +140 -293
- package/dist/runner/kimi/acp/translator.d.ts +18 -100
- package/dist/runner/kimi/acp/translator.js +56 -242
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +73 -256
- package/dist/runner/opencode/acp/translator.d.ts +28 -103
- package/dist/runner/opencode/acp/translator.js +51 -236
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/sessions.js +34 -57
- package/dist/session/codex/rollout-reader.d.ts +10 -0
- package/dist/session/codex/rollout-reader.js +123 -41
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/dsh/sessions.js +6 -5
- package/dist/session/kimi/sessions.d.ts +6 -0
- package/dist/session/kimi/sessions.js +50 -73
- package/dist/session/opencode/sessions.js +26 -8
- package/dist/session/pi/sessions.js +54 -73
- package/dist/update/index.d.ts +1 -2
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -6
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
|
@@ -6,20 +6,26 @@
|
|
|
6
6
|
* → synthetic init → session/prompt → consume session/update notifications
|
|
7
7
|
* until prompt settles → result event.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Turn/connection/compact/approval orchestration lives in BaseAcpRunner.
|
|
10
|
+
* Kimi-specific parts:
|
|
11
|
+
* - compaction is a BACKGROUND task: after the compact prompt settles, poll
|
|
12
|
+
* wire.jsonl for a NEW terminal compaction record (§5.2, never fake success)
|
|
13
|
+
* - terminal/* reverse RPC: kimi delegates Bash execution to the client —
|
|
14
|
+
* this runner spawns one-shot local bash processes (handleTerminalRequest)
|
|
15
|
+
* - terminal-backed Bash tool notifications are filtered (bashToolCallIds) so
|
|
16
|
+
* the run card doesn't get duplicate output-less Bash panels
|
|
17
|
+
* - AskUserQuestion flows through elicitation forms + a request_permission
|
|
18
|
+
* question bridge (buildQuestionResponse)
|
|
13
19
|
*/
|
|
14
|
-
import {
|
|
20
|
+
import { spawnProcess, mergeProcessEnv, useDetachedProcessGroup } from '../../../platform/spawn.js';
|
|
15
21
|
import { StringDecoder } from 'node:string_decoder';
|
|
16
|
-
import { ConnectionManager
|
|
22
|
+
import { ConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
|
|
17
23
|
import { KimiAcpTranslator } from './translator.js';
|
|
18
24
|
import { NotificationMethod, RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
|
|
19
25
|
import { KIMI_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/acp/protocol-helpers.js';
|
|
20
26
|
import { mapAnswersByIndex } from '../../question-common.js';
|
|
21
27
|
import { getLogger } from '../../../logger/index.js';
|
|
22
|
-
import {
|
|
28
|
+
import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
|
|
23
29
|
import { ProcessStopper } from '../../common/process-stopper.js';
|
|
24
30
|
/** §5.2: compaction 等待的默认沉默超时(对齐 turnIdleTimeoutMs 默认值)。 */
|
|
25
31
|
const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -126,15 +132,15 @@ function buildQuestionResponse(action, pending, response) {
|
|
|
126
132
|
// =============================================================================
|
|
127
133
|
// Runner
|
|
128
134
|
// =============================================================================
|
|
129
|
-
export class KimiAcpRunner extends
|
|
130
|
-
connectionManager;
|
|
131
|
-
currentTranslator = null;
|
|
132
|
-
activeSessionId = null;
|
|
133
|
-
model;
|
|
135
|
+
export class KimiAcpRunner extends BaseAcpRunner {
|
|
134
136
|
permissionMode;
|
|
135
137
|
compactIdleTimeoutMs;
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
+
/**
|
|
139
|
+
* §5.2 compaction baseline: wire.jsonl records sampled by compactBaseline()
|
|
140
|
+
* BEFORE the compact prompt fires (reset per compact run). onCompactPromptFired
|
|
141
|
+
* derives both the terminal-count baseline and the total-count probe from it.
|
|
142
|
+
*/
|
|
143
|
+
compactBaselineRecords = [];
|
|
138
144
|
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
139
145
|
terminals = new Map();
|
|
140
146
|
/** terminalId 自增序号(term-1, term-2, …)。 */
|
|
@@ -150,13 +156,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
150
156
|
/** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
|
|
151
157
|
activeCwd = null;
|
|
152
158
|
processStopper = new ProcessStopper({ graceMs: 2_000 });
|
|
153
|
-
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
154
|
-
promptSettled = false;
|
|
155
159
|
constructor(opts) {
|
|
156
|
-
super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
|
|
157
|
-
this.model = opts.model;
|
|
158
|
-
this.permissionMode = opts.permissionMode ?? 'manual';
|
|
159
|
-
this.compactIdleTimeoutMs = opts.compactIdleTimeoutMs ?? COMPACT_IDLE_TIMEOUT_MS;
|
|
160
160
|
const managerOpts = {
|
|
161
161
|
binary: opts.binary ?? 'kimi',
|
|
162
162
|
args: opts.acpArgs ?? ['acp'],
|
|
@@ -183,7 +183,14 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
|
-
|
|
186
|
+
super({
|
|
187
|
+
kind: opts.kind,
|
|
188
|
+
sessionReader: opts.sessionReader,
|
|
189
|
+
turnIdleTimeoutMs: opts.turnIdleTimeoutMs,
|
|
190
|
+
}, new ConnectionManager(managerOpts));
|
|
191
|
+
this.model = opts.model;
|
|
192
|
+
this.permissionMode = opts.permissionMode ?? 'manual';
|
|
193
|
+
this.compactIdleTimeoutMs = opts.compactIdleTimeoutMs ?? COMPACT_IDLE_TIMEOUT_MS;
|
|
187
194
|
}
|
|
188
195
|
get logTag() {
|
|
189
196
|
return 'kimi-acp-runner';
|
|
@@ -194,172 +201,113 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
194
201
|
get turnInterruptedErrorMessage() {
|
|
195
202
|
return 'Kimi ACP turn interrupted';
|
|
196
203
|
}
|
|
197
|
-
|
|
198
|
-
return
|
|
204
|
+
get connectionClosedMessage() {
|
|
205
|
+
return 'Kimi ACP connection closed';
|
|
199
206
|
}
|
|
200
207
|
shouldDeferStop() {
|
|
201
208
|
return !this.promptSettled;
|
|
202
209
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
createTranslator() {
|
|
211
|
+
return new KimiAcpTranslator();
|
|
212
|
+
}
|
|
213
|
+
async applyTurnSettings(client, sessionId) {
|
|
214
|
+
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
215
|
+
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型。
|
|
216
|
+
// 仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
217
|
+
if (this.model) {
|
|
218
|
+
try {
|
|
219
|
+
await client.request('session/set_config_option', {
|
|
220
|
+
sessionId,
|
|
221
|
+
configId: 'model',
|
|
222
|
+
value: this.model,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
230
|
+
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
231
|
+
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
232
|
+
// session in manual and tool calls stall on unanswered approvals
|
|
233
|
+
// (live dark-test repro: approval_requested → idle timeout).
|
|
234
|
+
const modeParams = {
|
|
235
|
+
sessionId,
|
|
236
|
+
modeId: toAcpMode(this.permissionMode),
|
|
237
|
+
};
|
|
206
238
|
try {
|
|
207
|
-
|
|
208
|
-
const cancelParams = { sessionId: this.activeSessionId };
|
|
209
|
-
this.currentClient.notify('session/cancel', cancelParams);
|
|
239
|
+
await client.request('session/set_mode', modeParams);
|
|
210
240
|
}
|
|
211
241
|
catch (err) {
|
|
212
|
-
getLogger().warn(`[${this.logTag}] session/
|
|
242
|
+
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
213
243
|
}
|
|
214
244
|
}
|
|
215
|
-
clearTurnState() {
|
|
216
|
-
this.currentTranslator = null;
|
|
217
|
-
this.promptSettled = false;
|
|
218
|
-
this.activeSessionId = null;
|
|
219
|
-
this.activeCwd = null;
|
|
220
|
-
}
|
|
221
|
-
async releaseConnection(cwd) {
|
|
222
|
-
await this.connectionManager.release(cwd);
|
|
223
|
-
}
|
|
224
|
-
notifyIdle(cwd) {
|
|
225
|
-
this.connectionManager.notifyIdle(cwd);
|
|
226
|
-
}
|
|
227
|
-
async disposeConnections() {
|
|
228
|
-
// 关闭前 kill/release 所有存活 terminal,避免孤儿 bash 进程。
|
|
229
|
-
await this.cleanupTerminals();
|
|
230
|
-
await this.connectionManager.disposeAll();
|
|
231
|
-
}
|
|
232
245
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
246
|
+
* §5.2: compaction is a BACKGROUND task — the prompt settles before the
|
|
247
|
+
* wire.jsonl terminal record lands. Baselines are captured BEFORE the
|
|
248
|
+
* compact prompt fires (see compactBaseline override — a begin record
|
|
249
|
+
* landing after the fire must count as new activity, not baseline), then
|
|
250
|
+
* wait for a NEW terminal (complete/apply → completed; cancel → cancelled)
|
|
251
|
+
* before producing the result. There is NO total timeout — silence beyond
|
|
252
|
+
* compactIdleTimeoutMs yields 'unknown' (never a fake success). Keeps the
|
|
253
|
+
* connection alive while the background compaction finishes — otherwise
|
|
254
|
+
* dispose/exit kills it and the record never lands (S5/S6 打回复现 /
|
|
255
|
+
* 2026-08-31 事故).
|
|
238
256
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
sessionId,
|
|
263
|
-
cwd: opts.cwd,
|
|
264
|
-
};
|
|
265
|
-
await client.request('session/resume', resumeParams);
|
|
266
|
-
const translator = new KimiAcpTranslator();
|
|
267
|
-
translator.setOperationKind('compact');
|
|
268
|
-
this.currentTranslator = translator;
|
|
269
|
-
// Emit turn_started with operationKind='compaction' before the prompt
|
|
270
|
-
// (consumeTurn yields it while the background compaction runs).
|
|
271
|
-
const turnId = `compact-${Date.now()}`;
|
|
272
|
-
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
273
|
-
this.currentTurnId = turnId;
|
|
274
|
-
this.pushEvents([turnStarted]);
|
|
275
|
-
// Send /compact as prompt text
|
|
276
|
-
const promptParams = {
|
|
277
|
-
sessionId,
|
|
278
|
-
prompt: [{ type: 'text', text: '/compact' }],
|
|
279
|
-
};
|
|
280
|
-
// §5.2: compaction is a BACKGROUND task — the prompt settles before the
|
|
281
|
-
// wire.jsonl terminal record lands. Baseline the terminal record count,
|
|
282
|
-
// fire the prompt without awaiting (notifications flow through
|
|
283
|
-
// consumeTurn), then wait for a NEW terminal (complete/apply →
|
|
284
|
-
// completed; cancel → cancelled) before producing the result. There is
|
|
285
|
-
// NO total timeout — silence beyond compactIdleTimeoutMs yields
|
|
286
|
-
// 'unknown' (never a fake success). Keeps the connection alive while the
|
|
287
|
-
// background compaction finishes — otherwise dispose/exit kills it and
|
|
288
|
-
// the record never lands (S5/S6 打回复现 / 2026-08-31 事故).
|
|
289
|
-
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
290
|
-
const baselineRecords = compactionReader
|
|
291
|
-
? compactionReader.call(this.sessionReader, sessionId, opts.cwd)
|
|
292
|
-
: [];
|
|
293
|
-
const baselineTerminalCount = baselineRecords.filter(isCompactionTerminalLike).length;
|
|
294
|
-
const baselineTotalCount = baselineRecords.length;
|
|
295
|
-
this.promptSettled = false;
|
|
296
|
-
// ACP holds the session/prompt response for the ENTIRE turn (unlike
|
|
297
|
-
// codex turn/start), so the RPC-level timeout must not apply here —
|
|
298
|
-
// turn liveness is guarded by the rolling idle watchdog instead.
|
|
299
|
-
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
300
|
-
promptPromise.then(async (result) => {
|
|
301
|
-
this.promptSettled = true;
|
|
302
|
-
if (this.stopRequested)
|
|
303
|
-
return; // already cancelled
|
|
304
|
-
if (compactionReader) {
|
|
305
|
-
// §5.4 可选防线(日志探针):prompt settle 后既无新 begin 也无新
|
|
306
|
-
// terminal → compact 未被引擎接受(可能已在跑或被拒绝)。不解析
|
|
307
|
-
// "already running" 文本、不影响卡片。
|
|
308
|
-
const totalNow = compactionReader.call(this.sessionReader, sessionId, opts.cwd).length;
|
|
309
|
-
if (totalNow === baselineTotalCount) {
|
|
310
|
-
getLogger().warn(`[${this.logTag}] compact 未被引擎接受(无新 begin/terminal 记录,可能已在跑或被拒绝)`);
|
|
311
|
-
}
|
|
312
|
-
const outcome = await this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount);
|
|
313
|
-
if (this.forceFinish)
|
|
314
|
-
return; // stopped while polling
|
|
315
|
-
if (outcome === 'completed') {
|
|
316
|
-
// 压缩完成:正常翻译 prompt 结果(subtype success)。
|
|
317
|
-
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
318
|
-
this.pushEvents([resultEvent]);
|
|
319
|
-
}
|
|
320
|
-
else if (outcome === 'cancelled') {
|
|
321
|
-
this.pushEvents([
|
|
322
|
-
translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
|
|
323
|
-
]);
|
|
324
|
-
}
|
|
325
|
-
else if (outcome === 'unknown') {
|
|
326
|
-
this.pushEvents([
|
|
327
|
-
translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
|
|
328
|
-
]);
|
|
329
|
-
}
|
|
330
|
-
// outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
|
|
257
|
+
compactBaseline(sessionId, opts) {
|
|
258
|
+
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
259
|
+
this.compactBaselineRecords = compactionReader
|
|
260
|
+
? compactionReader.call(this.sessionReader, sessionId, opts.cwd)
|
|
261
|
+
: [];
|
|
262
|
+
return this.compactBaselineRecords.length;
|
|
263
|
+
}
|
|
264
|
+
onCompactPromptFired(promptPromise, translator, sessionId, opts, baselineTotalCount) {
|
|
265
|
+
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
266
|
+
// Derived from the pre-fire snapshot captured by compactBaseline() —
|
|
267
|
+
// re-reading here would race the prompt fire and misclassify new records.
|
|
268
|
+
const baselineTerminalCount = this.compactBaselineRecords.filter(isCompactionTerminalLike).length;
|
|
269
|
+
promptPromise.then(async (result) => {
|
|
270
|
+
this.promptSettled = true;
|
|
271
|
+
if (this.stopRequested)
|
|
272
|
+
return; // already cancelled
|
|
273
|
+
if (compactionReader) {
|
|
274
|
+
// §5.4 可选防线(日志探针):prompt settle 后既无新 begin 也无新
|
|
275
|
+
// terminal → compact 未被引擎接受(可能已在跑或被拒绝)。不解析
|
|
276
|
+
// "already running" 文本、不影响卡片。
|
|
277
|
+
const totalNow = compactionReader.call(this.sessionReader, sessionId, opts.cwd).length;
|
|
278
|
+
if (totalNow === baselineTotalCount) {
|
|
279
|
+
getLogger().warn(`[${this.logTag}] compact 未被引擎接受(无新 begin/terminal 记录,可能已在跑或被拒绝)`);
|
|
331
280
|
}
|
|
332
|
-
|
|
281
|
+
const outcome = await this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount);
|
|
282
|
+
if (this.forceFinish)
|
|
283
|
+
return; // stopped while polling
|
|
284
|
+
if (outcome === 'completed') {
|
|
285
|
+
// 压缩完成:正常翻译 prompt 结果(subtype success)。
|
|
333
286
|
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
334
287
|
this.pushEvents([resultEvent]);
|
|
335
288
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
289
|
+
else if (outcome === 'cancelled') {
|
|
290
|
+
this.pushEvents([
|
|
291
|
+
translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
else if (outcome === 'unknown') {
|
|
295
|
+
this.pushEvents([
|
|
296
|
+
translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
|
|
297
|
+
]);
|
|
341
298
|
}
|
|
342
|
-
|
|
299
|
+
// outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
343
303
|
this.pushEvents([resultEvent]);
|
|
344
|
-
}
|
|
345
|
-
});
|
|
304
|
+
}
|
|
305
|
+
}, (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
|
|
346
306
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
* ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
|
|
350
|
-
*/
|
|
351
|
-
async respondApproval(requestId, response) {
|
|
352
|
-
const client = this.currentClient;
|
|
353
|
-
const pending = this.pendingApprovals.get(requestId);
|
|
354
|
-
if (!client || !pending)
|
|
355
|
-
return;
|
|
356
|
-
const action = response?.action ?? 'decline';
|
|
357
|
-
const acpResponse = pending.kind === 'question'
|
|
307
|
+
buildApprovalOutcome(action, pending, response) {
|
|
308
|
+
return pending.kind === 'question'
|
|
358
309
|
? buildQuestionResponse(action, pending, response)
|
|
359
310
|
: buildAcpPermissionOutcome(action, pending.options, KIMI_APPROVAL_KINDS);
|
|
360
|
-
client.respond(requestId, acpResponse);
|
|
361
|
-
this.pendingApprovals.delete(requestId);
|
|
362
|
-
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
363
311
|
}
|
|
364
312
|
getStatusInfo() {
|
|
365
313
|
return {
|
|
@@ -384,121 +332,11 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
384
332
|
if (settings.permissionMode !== undefined) {
|
|
385
333
|
this.permissionMode = settings.permissionMode;
|
|
386
334
|
}
|
|
387
|
-
|
|
388
|
-
return;
|
|
389
|
-
const modeParams = {
|
|
390
|
-
sessionId: this.activeSessionId,
|
|
391
|
-
modeId: toAcpMode(this.permissionMode),
|
|
392
|
-
};
|
|
393
|
-
try {
|
|
394
|
-
await this.currentClient.request('session/set_mode', modeParams);
|
|
395
|
-
getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
|
|
396
|
-
}
|
|
397
|
-
catch (err) {
|
|
398
|
-
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
399
|
-
}
|
|
335
|
+
await this.sendApprovalModeUpdate(toAcpMode(this.permissionMode));
|
|
400
336
|
}
|
|
401
337
|
// =========================================================================
|
|
402
338
|
// Internal
|
|
403
339
|
// =========================================================================
|
|
404
|
-
/**
|
|
405
|
-
* Acquire the connection and set up the turn:
|
|
406
|
-
* session/new (new session) or session/resume (existing sessionId)
|
|
407
|
-
* → set permission mode if needed → session/prompt.
|
|
408
|
-
*/
|
|
409
|
-
async setupTurn(message, opts) {
|
|
410
|
-
const client = await this.connectionManager.acquire(opts.cwd);
|
|
411
|
-
this.connectionManager.notifyActivity(opts.cwd);
|
|
412
|
-
this.currentClient = client;
|
|
413
|
-
this.activeCwd = opts.cwd;
|
|
414
|
-
client.setHooks({
|
|
415
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
416
|
-
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
417
|
-
onClose: () => {
|
|
418
|
-
getLogger().warn(`[${this.logTag}] client connection closed`);
|
|
419
|
-
void this.cleanupTerminals();
|
|
420
|
-
this.failTurn('Kimi ACP connection closed');
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
let sessionId;
|
|
424
|
-
if (opts.sessionId) {
|
|
425
|
-
const resumeParams = {
|
|
426
|
-
sessionId: opts.sessionId,
|
|
427
|
-
cwd: opts.cwd,
|
|
428
|
-
};
|
|
429
|
-
await client.request('session/resume', resumeParams);
|
|
430
|
-
sessionId = opts.sessionId;
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
const newParams = { cwd: opts.cwd, mcpServers: [] };
|
|
434
|
-
const newResult = await client.request('session/new', newParams);
|
|
435
|
-
sessionId = newResult.sessionId;
|
|
436
|
-
}
|
|
437
|
-
this.activeSessionId = sessionId;
|
|
438
|
-
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
439
|
-
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型。
|
|
440
|
-
// 仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
441
|
-
if (this.model) {
|
|
442
|
-
try {
|
|
443
|
-
await client.request('session/set_config_option', {
|
|
444
|
-
sessionId,
|
|
445
|
-
configId: 'model',
|
|
446
|
-
value: this.model,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
catch (err) {
|
|
450
|
-
getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
454
|
-
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
455
|
-
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
456
|
-
// session in manual and tool calls stall on unanswered approvals
|
|
457
|
-
// (live dark-test repro: approval_requested → idle timeout).
|
|
458
|
-
const modeParams = {
|
|
459
|
-
sessionId,
|
|
460
|
-
modeId: toAcpMode(this.permissionMode),
|
|
461
|
-
};
|
|
462
|
-
try {
|
|
463
|
-
await client.request('session/set_mode', modeParams);
|
|
464
|
-
}
|
|
465
|
-
catch (err) {
|
|
466
|
-
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
467
|
-
}
|
|
468
|
-
const translator = new KimiAcpTranslator();
|
|
469
|
-
this.currentTranslator = translator;
|
|
470
|
-
// Emit turn_started before prompt
|
|
471
|
-
const turnId = `turn-${Date.now()}`;
|
|
472
|
-
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
473
|
-
this.currentTurnId = turnId;
|
|
474
|
-
this.pushEvents([turnStarted]);
|
|
475
|
-
// Send the prompt
|
|
476
|
-
const promptParams = {
|
|
477
|
-
sessionId,
|
|
478
|
-
prompt: [{ type: 'text', text: message }],
|
|
479
|
-
};
|
|
480
|
-
// Fire the prompt request; notifications will be buffered in the queue.
|
|
481
|
-
// We process them concurrently.
|
|
482
|
-
this.promptSettled = false;
|
|
483
|
-
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
484
|
-
// Wait for prompt to settle, then push the result event.
|
|
485
|
-
// Notifications are pushed concurrently by the hook handlers.
|
|
486
|
-
promptPromise.then((result) => {
|
|
487
|
-
this.promptSettled = true;
|
|
488
|
-
if (this.stopRequested)
|
|
489
|
-
return; // already cancelled
|
|
490
|
-
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
491
|
-
this.pushEvents([resultEvent]);
|
|
492
|
-
}, (err) => {
|
|
493
|
-
this.promptSettled = true;
|
|
494
|
-
if (this.stopRequested) {
|
|
495
|
-
getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const resultEvent = translator.produceErrorResult(sessionId, err.message);
|
|
499
|
-
this.pushEvents([resultEvent]);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
340
|
/**
|
|
503
341
|
* §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
|
|
504
342
|
*
|
|
@@ -614,17 +452,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
614
452
|
}
|
|
615
453
|
return;
|
|
616
454
|
}
|
|
617
|
-
|
|
618
|
-
if (ev.type === 'approval_requested') {
|
|
619
|
-
this.pendingApprovals.set(ev.requestId, {
|
|
620
|
-
kind: ev.kind,
|
|
621
|
-
view: ev.view,
|
|
622
|
-
options: params.options,
|
|
623
|
-
proto: method === ServerRequestMethod.ELICITATION_CREATE ? 'elicitation' : 'permission',
|
|
624
|
-
});
|
|
625
|
-
getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
455
|
+
this.registerApprovalEvents(events, params.options, method === ServerRequestMethod.ELICITATION_CREATE ? 'elicitation' : 'permission');
|
|
628
456
|
this.pushEvents(events);
|
|
629
457
|
}
|
|
630
458
|
/**
|
|
@@ -673,14 +501,17 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
673
501
|
: undefined;
|
|
674
502
|
const cwd = params.cwd ?? this.activeCwd ?? undefined;
|
|
675
503
|
const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
|
|
676
|
-
const proc =
|
|
504
|
+
const proc = spawnProcess(params.command, params.args ?? [], {
|
|
677
505
|
cwd,
|
|
678
|
-
|
|
506
|
+
// env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护,v2 §8.3)
|
|
507
|
+
env: env ? mergeProcessEnv(process.env, env) : undefined,
|
|
679
508
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
680
509
|
// ProcessStopper 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
|
|
681
510
|
// 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
|
|
682
|
-
// 与 JsonlRpcTransport / spawning-runner
|
|
683
|
-
|
|
511
|
+
// 与 JsonlRpcTransport / spawning-runner 同模式;win32 不 detached
|
|
512
|
+
// (`.cmd` 垫片在 DETACHED_PROCESS 下丢 stdio),树杀由 taskkill /T 负责。
|
|
513
|
+
detached: useDetachedProcessGroup(),
|
|
514
|
+
windowsHide: true,
|
|
684
515
|
});
|
|
685
516
|
// 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
|
|
686
517
|
// 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
|
|
@@ -843,5 +674,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
843
674
|
await this.processStopper.stop(handle.proc, { immediate: true });
|
|
844
675
|
}
|
|
845
676
|
}
|
|
677
|
+
// =========================================================================
|
|
678
|
+
// BaseAcpRunner hooks
|
|
679
|
+
// =========================================================================
|
|
680
|
+
onConnectionAcquired(opts) {
|
|
681
|
+
this.activeCwd = opts.cwd;
|
|
682
|
+
}
|
|
683
|
+
onConnectionClosed() {
|
|
684
|
+
void this.cleanupTerminals();
|
|
685
|
+
}
|
|
686
|
+
onClearTurnState() {
|
|
687
|
+
this.activeCwd = null;
|
|
688
|
+
}
|
|
689
|
+
async beforeDisposeConnections() {
|
|
690
|
+
// 关闭前 kill/release 所有存活 terminal,避免孤儿 bash 进程。
|
|
691
|
+
await this.cleanupTerminals();
|
|
692
|
+
}
|
|
846
693
|
}
|
|
847
694
|
//# sourceMappingURL=runner.js.map
|