lark-remote 0.1.12 → 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 +37 -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/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -7
- package/dist/bridge/index.js +100 -133
- 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 +14 -5
- package/dist/card/card-shared.js +23 -34
- 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/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.js +14 -11
- package/dist/config/index.d.ts +0 -8
- package/dist/config/index.js +0 -16
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +2 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +16 -10
- 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 +73 -6
- package/dist/router/index.js +663 -590
- 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 -4
- package/dist/runner/claude/session.js +18 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- 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 +70 -0
- package/dist/runner/common/acp/protocol-helpers.js +82 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- 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/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +63 -53
- package/dist/runner/kimi/acp/runner.js +189 -334
- package/dist/runner/kimi/acp/translator.d.ts +18 -101
- package/dist/runner/kimi/acp/translator.js +61 -255
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +74 -297
- package/dist/runner/opencode/acp/translator.d.ts +28 -104
- package/dist/runner/opencode/acp/translator.js +54 -249
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- 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/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +37 -64
- package/dist/session/codex/rollout-reader.d.ts +10 -14
- package/dist/session/codex/rollout-reader.js +126 -48
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +15 -11
- package/dist/session/kimi/sessions.d.ts +24 -4
- package/dist/session/kimi/sessions.js +99 -103
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +35 -15
- package/dist/session/pi/sessions.js +64 -95
- package/dist/update/index.d.ts +2 -3
- 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 -7
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
|
@@ -6,25 +6,29 @@
|
|
|
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
|
-
import {
|
|
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
|
-
/**
|
|
25
|
-
|
|
26
|
-
* context.apply_compaction ~8s after end_turn. */
|
|
27
|
-
const COMPACT_POLL_TIMEOUT_MS = 30_000;
|
|
30
|
+
/** §5.2: compaction 等待的默认沉默超时(对齐 turnIdleTimeoutMs 默认值)。 */
|
|
31
|
+
const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
28
32
|
/** Poll interval for the wire.jsonl compaction record. */
|
|
29
33
|
const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
30
34
|
// =============================================================================
|
|
@@ -36,6 +40,16 @@ const COMPACT_POLL_INTERVAL_MS = 1_000;
|
|
|
36
40
|
// =============================================================================
|
|
37
41
|
/** terminal/create 未显式给 outputByteLimit 时的缓冲上限(对齐服务端默认 4MB)。 */
|
|
38
42
|
const TERMINAL_DEFAULT_OUTPUT_LIMIT = 4 * 1024 * 1024;
|
|
43
|
+
/** 压缩终态记录:complete / apply_compaction 完成,cancel 为失败或取消(共用)。 */
|
|
44
|
+
function isCompactionTerminalLike(record) {
|
|
45
|
+
return (record.type === 'full_compaction.complete' ||
|
|
46
|
+
record.type === 'context.apply_compaction' ||
|
|
47
|
+
record.type === 'full_compaction.cancel');
|
|
48
|
+
}
|
|
49
|
+
/** 沉默窗口的展示文案(测试用小窗口显示秒,生产默认显示分钟)。 */
|
|
50
|
+
function formatIdleWindow(ms) {
|
|
51
|
+
return ms >= 60_000 ? `${Math.round(ms / 60_000)} 分钟` : `${Math.round(ms / 1000)} 秒`;
|
|
52
|
+
}
|
|
39
53
|
/**
|
|
40
54
|
* Map user-facing permission mode to ACP mode id.
|
|
41
55
|
*
|
|
@@ -58,53 +72,6 @@ function toAcpMode(mode) {
|
|
|
58
72
|
return 'yolo';
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the ACP protocol response for a permission approval decision.
|
|
63
|
-
*
|
|
64
|
-
* accept → {outcome:{outcome:'selected', optionId:<approve_once kind>}}
|
|
65
|
-
* accept_for_session → {outcome:{outcome:'selected', optionId:<approve_always kind>}}
|
|
66
|
-
* decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
|
|
67
|
-
* cancel → {outcome:{outcome:'cancelled'}}
|
|
68
|
-
*
|
|
69
|
-
* optionId is looked up from the request's options[] by kind (approval.ts:28-29):
|
|
70
|
-
* approve_once / approve_always / allow_once for accept;
|
|
71
|
-
* approve_always / allow_always for accept_for_session;
|
|
72
|
-
* reject / reject_once for decline. If not found, fall back to cancelled
|
|
73
|
-
* (safe universal default — the server treats it as "user declined").
|
|
74
|
-
*
|
|
75
|
-
* 2026-08-15 live test: approve_once + approve_always + reject observed.
|
|
76
|
-
*/
|
|
77
|
-
function buildApprovalResponse(action, pending) {
|
|
78
|
-
if (action === 'cancel') {
|
|
79
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
80
|
-
}
|
|
81
|
-
if (action === 'accept') {
|
|
82
|
-
const optionId = findOptionIdByKind(pending.options, [
|
|
83
|
-
'approve_once',
|
|
84
|
-
'approve_always',
|
|
85
|
-
'allow_once',
|
|
86
|
-
]);
|
|
87
|
-
if (optionId) {
|
|
88
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
89
|
-
}
|
|
90
|
-
// Fallback: can't find allow option → cancel is safe
|
|
91
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
92
|
-
}
|
|
93
|
-
if (action === 'accept_for_session') {
|
|
94
|
-
// §P4: 卡片「允许本次会话」→ always 类 optionId(kimi approve_always)
|
|
95
|
-
const optionId = findOptionIdByKind(pending.options, ['approve_always', 'allow_always']);
|
|
96
|
-
if (optionId) {
|
|
97
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
98
|
-
}
|
|
99
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
100
|
-
}
|
|
101
|
-
// decline
|
|
102
|
-
const optionId = findOptionIdByKind(pending.options, ['reject', 'reject_once']);
|
|
103
|
-
if (optionId) {
|
|
104
|
-
return { outcome: { outcome: 'selected', optionId } };
|
|
105
|
-
}
|
|
106
|
-
return { outcome: { outcome: 'cancelled' } };
|
|
107
|
-
}
|
|
108
75
|
/**
|
|
109
76
|
* AskUserQuestion 回编(kind === 'question'):
|
|
110
77
|
* - elicitation form:answer → {action:'accept', content:{q0..qn}}(多选数组/
|
|
@@ -165,15 +132,15 @@ function buildQuestionResponse(action, pending, response) {
|
|
|
165
132
|
// =============================================================================
|
|
166
133
|
// Runner
|
|
167
134
|
// =============================================================================
|
|
168
|
-
export class KimiAcpRunner extends
|
|
169
|
-
connectionManager;
|
|
170
|
-
currentTranslator = null;
|
|
171
|
-
activeSessionId = null;
|
|
172
|
-
model;
|
|
135
|
+
export class KimiAcpRunner extends BaseAcpRunner {
|
|
173
136
|
permissionMode;
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
|
|
137
|
+
compactIdleTimeoutMs;
|
|
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 = [];
|
|
177
144
|
/** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
|
|
178
145
|
terminals = new Map();
|
|
179
146
|
/** terminalId 自增序号(term-1, term-2, …)。 */
|
|
@@ -189,13 +156,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
189
156
|
/** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
|
|
190
157
|
activeCwd = null;
|
|
191
158
|
processStopper = new ProcessStopper({ graceMs: 2_000 });
|
|
192
|
-
/** Tracker for the in-flight prompt request — needed for cancellation. */
|
|
193
|
-
promptSettled = false;
|
|
194
159
|
constructor(opts) {
|
|
195
|
-
super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
|
|
196
|
-
this.model = opts.model;
|
|
197
|
-
this.permissionMode = opts.permissionMode ?? 'manual';
|
|
198
|
-
this.compactPollTimeoutMs = opts.compactPollTimeoutMs ?? COMPACT_POLL_TIMEOUT_MS;
|
|
199
160
|
const managerOpts = {
|
|
200
161
|
binary: opts.binary ?? 'kimi',
|
|
201
162
|
args: opts.acpArgs ?? ['acp'],
|
|
@@ -222,7 +183,14 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
222
183
|
},
|
|
223
184
|
},
|
|
224
185
|
};
|
|
225
|
-
|
|
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;
|
|
226
194
|
}
|
|
227
195
|
get logTag() {
|
|
228
196
|
return 'kimi-acp-runner';
|
|
@@ -233,144 +201,113 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
233
201
|
get turnInterruptedErrorMessage() {
|
|
234
202
|
return 'Kimi ACP turn interrupted';
|
|
235
203
|
}
|
|
236
|
-
|
|
237
|
-
return
|
|
204
|
+
get connectionClosedMessage() {
|
|
205
|
+
return 'Kimi ACP connection closed';
|
|
238
206
|
}
|
|
239
207
|
shouldDeferStop() {
|
|
240
208
|
return !this.promptSettled;
|
|
241
209
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
};
|
|
245
238
|
try {
|
|
246
|
-
|
|
247
|
-
const cancelParams = { sessionId: this.activeSessionId };
|
|
248
|
-
this.currentClient.notify('session/cancel', cancelParams);
|
|
239
|
+
await client.request('session/set_mode', modeParams);
|
|
249
240
|
}
|
|
250
241
|
catch (err) {
|
|
251
|
-
getLogger().warn(`[${this.logTag}] session/
|
|
242
|
+
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
252
243
|
}
|
|
253
244
|
}
|
|
254
|
-
clearTurnState() {
|
|
255
|
-
this.currentTranslator = null;
|
|
256
|
-
this.promptSettled = false;
|
|
257
|
-
this.activeSessionId = null;
|
|
258
|
-
this.activeCwd = null;
|
|
259
|
-
}
|
|
260
|
-
async releaseConnection(cwd) {
|
|
261
|
-
await this.connectionManager.release(cwd);
|
|
262
|
-
}
|
|
263
|
-
notifyIdle(cwd) {
|
|
264
|
-
this.connectionManager.notifyIdle(cwd);
|
|
265
|
-
}
|
|
266
|
-
async disposeConnections() {
|
|
267
|
-
// 关闭前 kill/release 所有存活 terminal,避免孤儿 bash 进程。
|
|
268
|
-
await this.cleanupTerminals();
|
|
269
|
-
await this.connectionManager.disposeAll();
|
|
270
|
-
}
|
|
271
245
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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 事故).
|
|
277
256
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
sessionId,
|
|
302
|
-
cwd: opts.cwd,
|
|
303
|
-
};
|
|
304
|
-
await client.request('session/resume', resumeParams);
|
|
305
|
-
const translator = new KimiAcpTranslator();
|
|
306
|
-
translator.setOperationKind('compact');
|
|
307
|
-
this.currentTranslator = translator;
|
|
308
|
-
// Emit turn_started with operationKind='compaction' before the prompt
|
|
309
|
-
// (consumeTurn yields it while the background compaction runs).
|
|
310
|
-
const turnId = `compact-${Date.now()}`;
|
|
311
|
-
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
312
|
-
this.currentTurnId = turnId;
|
|
313
|
-
this.pushEvents([turnStarted]);
|
|
314
|
-
// Send /compact as prompt text
|
|
315
|
-
const promptParams = {
|
|
316
|
-
sessionId,
|
|
317
|
-
prompt: [{ type: 'text', text: '/compact' }],
|
|
318
|
-
};
|
|
319
|
-
// R2: compaction is a BACKGROUND task — the prompt settles before the
|
|
320
|
-
// wire.jsonl compaction record lands (real kimi: ~8s). Baseline the
|
|
321
|
-
// record count, fire the prompt without awaiting (notifications flow
|
|
322
|
-
// through consumeTurn), then poll for a NEW record before producing
|
|
323
|
-
// the result. This keeps the connection alive while the background
|
|
324
|
-
// compaction finishes — otherwise dispose/exit kills it and the record
|
|
325
|
-
// never lands (S5/S6 打回复现).
|
|
326
|
-
const compactionReader = this.sessionReader.readCompactionRecords;
|
|
327
|
-
const baselineCount = compactionReader
|
|
328
|
-
? compactionReader.call(this.sessionReader, sessionId, opts.cwd).length
|
|
329
|
-
: 0;
|
|
330
|
-
this.promptSettled = false;
|
|
331
|
-
// ACP holds the session/prompt response for the ENTIRE turn (unlike
|
|
332
|
-
// codex turn/start), so the RPC-level timeout must not apply here —
|
|
333
|
-
// turn liveness is guarded by the rolling idle watchdog instead.
|
|
334
|
-
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
335
|
-
promptPromise.then(async (result) => {
|
|
336
|
-
this.promptSettled = true;
|
|
337
|
-
if (this.stopRequested)
|
|
338
|
-
return; // already cancelled
|
|
339
|
-
if (compactionReader) {
|
|
340
|
-
await this.waitForCompactionRecord(compactionReader, opts, baselineCount);
|
|
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 记录,可能已在跑或被拒绝)`);
|
|
341
280
|
}
|
|
281
|
+
const outcome = await this.waitForCompactionTerminal(compactionReader, opts, baselineTerminalCount);
|
|
342
282
|
if (this.forceFinish)
|
|
343
283
|
return; // stopped while polling
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
284
|
+
if (outcome === 'completed') {
|
|
285
|
+
// 压缩完成:正常翻译 prompt 结果(subtype success)。
|
|
286
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
287
|
+
this.pushEvents([resultEvent]);
|
|
288
|
+
}
|
|
289
|
+
else if (outcome === 'cancelled') {
|
|
290
|
+
this.pushEvents([
|
|
291
|
+
translator.produceErrorResult(sessionId, '压缩未完成:已被取消或压缩请求失败(可重试)'),
|
|
292
|
+
]);
|
|
352
293
|
}
|
|
353
|
-
|
|
294
|
+
else if (outcome === 'unknown') {
|
|
295
|
+
this.pushEvents([
|
|
296
|
+
translator.produceErrorResult(sessionId, `压缩状态未知:超过 ${formatIdleWindow(this.compactIdleTimeoutMs)} 未观察到完成/取消记录,压缩可能仍在后台进行`),
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
// outcome === 'stopped':不推 result,由 consumeTurn 的 interrupted 兜底。
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
354
303
|
this.pushEvents([resultEvent]);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
304
|
+
}
|
|
305
|
+
}, (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
|
|
357
306
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
* ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
|
|
361
|
-
*/
|
|
362
|
-
async respondApproval(requestId, response) {
|
|
363
|
-
const client = this.currentClient;
|
|
364
|
-
const pending = this.pendingApprovals.get(requestId);
|
|
365
|
-
if (!client || !pending)
|
|
366
|
-
return;
|
|
367
|
-
const action = response?.action ?? 'decline';
|
|
368
|
-
const acpResponse = pending.kind === 'question'
|
|
307
|
+
buildApprovalOutcome(action, pending, response) {
|
|
308
|
+
return pending.kind === 'question'
|
|
369
309
|
? buildQuestionResponse(action, pending, response)
|
|
370
|
-
:
|
|
371
|
-
client.respond(requestId, acpResponse);
|
|
372
|
-
this.pendingApprovals.delete(requestId);
|
|
373
|
-
getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
|
|
310
|
+
: buildAcpPermissionOutcome(action, pending.options, KIMI_APPROVAL_KINDS);
|
|
374
311
|
}
|
|
375
312
|
getStatusInfo() {
|
|
376
313
|
return {
|
|
@@ -395,144 +332,53 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
395
332
|
if (settings.permissionMode !== undefined) {
|
|
396
333
|
this.permissionMode = settings.permissionMode;
|
|
397
334
|
}
|
|
398
|
-
|
|
399
|
-
return;
|
|
400
|
-
const modeParams = {
|
|
401
|
-
sessionId: this.activeSessionId,
|
|
402
|
-
modeId: toAcpMode(this.permissionMode),
|
|
403
|
-
};
|
|
404
|
-
try {
|
|
405
|
-
await this.currentClient.request('session/set_mode', modeParams);
|
|
406
|
-
getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
|
|
407
|
-
}
|
|
408
|
-
catch (err) {
|
|
409
|
-
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
410
|
-
}
|
|
335
|
+
await this.sendApprovalModeUpdate(toAcpMode(this.permissionMode));
|
|
411
336
|
}
|
|
412
337
|
// =========================================================================
|
|
413
338
|
// Internal
|
|
414
339
|
// =========================================================================
|
|
415
340
|
/**
|
|
416
|
-
*
|
|
417
|
-
* session/new (new session) or session/resume (existing sessionId)
|
|
418
|
-
* → set permission mode if needed → session/prompt.
|
|
419
|
-
*/
|
|
420
|
-
async setupTurn(message, opts) {
|
|
421
|
-
const client = await this.connectionManager.acquire(opts.cwd);
|
|
422
|
-
this.connectionManager.notifyActivity(opts.cwd);
|
|
423
|
-
this.currentClient = client;
|
|
424
|
-
this.activeCwd = opts.cwd;
|
|
425
|
-
client.setHooks({
|
|
426
|
-
onNotification: (method, params) => this.handleNotification(method, params),
|
|
427
|
-
onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
|
|
428
|
-
onClose: () => {
|
|
429
|
-
getLogger().warn(`[${this.logTag}] client connection closed`);
|
|
430
|
-
void this.cleanupTerminals();
|
|
431
|
-
this.failTurn('Kimi ACP connection closed');
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
let sessionId;
|
|
435
|
-
if (opts.sessionId) {
|
|
436
|
-
const resumeParams = {
|
|
437
|
-
sessionId: opts.sessionId,
|
|
438
|
-
cwd: opts.cwd,
|
|
439
|
-
};
|
|
440
|
-
await client.request('session/resume', resumeParams);
|
|
441
|
-
sessionId = opts.sessionId;
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
const newParams = { cwd: opts.cwd, mcpServers: [] };
|
|
445
|
-
const newResult = await client.request('session/new', newParams);
|
|
446
|
-
sessionId = newResult.sessionId;
|
|
447
|
-
}
|
|
448
|
-
this.activeSessionId = sessionId;
|
|
449
|
-
// CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
|
|
450
|
-
// 不带 model;不主动下发则实际跑 kimi 服务端默认模型(旧 KimiRunner 通过 -m 传模型,
|
|
451
|
-
// 迁移到纯 ACP 后丢失)。仿 opencode 用 session/set_config_option。失败仅告警不阻断。
|
|
452
|
-
if (this.model) {
|
|
453
|
-
try {
|
|
454
|
-
await client.request('session/set_config_option', {
|
|
455
|
-
sessionId,
|
|
456
|
-
configId: 'model',
|
|
457
|
-
value: this.model,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
getLogger().warn(`[${this.logTag}] set_config_option model failed (non-fatal): ${err.message}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Set permission mode (§5.1): EVERY fresh ACP session starts in
|
|
465
|
-
// 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
|
|
466
|
-
// so the mode must be sent unconditionally — skipping yolo leaves the
|
|
467
|
-
// session in manual and tool calls stall on unanswered approvals
|
|
468
|
-
// (live dark-test repro: approval_requested → idle timeout).
|
|
469
|
-
const modeParams = {
|
|
470
|
-
sessionId,
|
|
471
|
-
modeId: toAcpMode(this.permissionMode),
|
|
472
|
-
};
|
|
473
|
-
try {
|
|
474
|
-
await client.request('session/set_mode', modeParams);
|
|
475
|
-
}
|
|
476
|
-
catch (err) {
|
|
477
|
-
getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
|
|
478
|
-
}
|
|
479
|
-
const translator = new KimiAcpTranslator();
|
|
480
|
-
this.currentTranslator = translator;
|
|
481
|
-
// Emit turn_started before prompt
|
|
482
|
-
const turnId = `turn-${Date.now()}`;
|
|
483
|
-
const turnStarted = translator.produceTurnStarted(sessionId, turnId);
|
|
484
|
-
this.currentTurnId = turnId;
|
|
485
|
-
this.pushEvents([turnStarted]);
|
|
486
|
-
// Send the prompt
|
|
487
|
-
const promptParams = {
|
|
488
|
-
sessionId,
|
|
489
|
-
prompt: [{ type: 'text', text: message }],
|
|
490
|
-
};
|
|
491
|
-
// Fire the prompt request; notifications will be buffered in the queue.
|
|
492
|
-
// We process them concurrently.
|
|
493
|
-
this.promptSettled = false;
|
|
494
|
-
const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
|
|
495
|
-
// Wait for prompt to settle, then push the result event.
|
|
496
|
-
// Notifications are pushed concurrently by the hook handlers.
|
|
497
|
-
promptPromise.then((result) => {
|
|
498
|
-
this.promptSettled = true;
|
|
499
|
-
if (this.stopRequested)
|
|
500
|
-
return; // already cancelled
|
|
501
|
-
const resultEvent = translator.handlePromptResponse(sessionId, result);
|
|
502
|
-
this.pushEvents([resultEvent]);
|
|
503
|
-
}, (err) => {
|
|
504
|
-
this.promptSettled = true;
|
|
505
|
-
if (this.stopRequested) {
|
|
506
|
-
getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
const resultEvent = translator.produceErrorResult(sessionId, err.message);
|
|
510
|
-
this.pushEvents([resultEvent]);
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* R2: poll wire.jsonl until a NEW compaction record
|
|
515
|
-
* (context.apply_compaction / full_compaction.complete) appears.
|
|
341
|
+
* §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
|
|
516
342
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
343
|
+
* 压缩是完整 LLM 请求(引擎 ≤5 次重试 + overflow 收缩重试),时长无上限;
|
|
344
|
+
* 旧「固定 30s 超时 = 成功」已删除。出口:
|
|
345
|
+
* completed — 观察到 full_compaction.complete / context.apply_compaction;
|
|
346
|
+
* cancelled — 观察到 full_compaction.cancel(API 失败/abort 与用户取消共用,
|
|
347
|
+
* 绝不当成功);
|
|
348
|
+
* stopped — 等待中 stop()/forceFinish(consumeTurn 产 interrupted 兜底);
|
|
349
|
+
* unknown — 距上次观察到任何新 compaction 记录(begin/terminal)超过
|
|
350
|
+
* compactIdleTimeoutMs(沉默超时,压缩可能仍在后台进行)。
|
|
351
|
+
* 观察到新记录即重置沉默时钟,并顺带重置 turn idle 时钟(lastEventAt +
|
|
352
|
+
* wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
|
|
521
353
|
*/
|
|
522
|
-
async
|
|
354
|
+
async waitForCompactionTerminal(reader, opts, baselineTerminalCount) {
|
|
523
355
|
const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
|
|
524
|
-
|
|
525
|
-
|
|
356
|
+
let lastActivityAt = Date.now();
|
|
357
|
+
let lastTotalCount = reader.call(this.sessionReader, sessionId, opts.cwd).length;
|
|
358
|
+
while (true) {
|
|
526
359
|
if (this.forceFinish || this.stopRequested)
|
|
527
|
-
return;
|
|
360
|
+
return 'stopped';
|
|
528
361
|
const records = reader.call(this.sessionReader, sessionId, opts.cwd);
|
|
529
|
-
if (records.length
|
|
530
|
-
|
|
531
|
-
|
|
362
|
+
if (records.length !== lastTotalCount) {
|
|
363
|
+
lastTotalCount = records.length;
|
|
364
|
+
lastActivityAt = Date.now();
|
|
365
|
+
// 压缩仍在活动(新 begin / 重试 / terminal):重置 turn idle 时钟,
|
|
366
|
+
// 让 consumeTurn 重新计算滚动截止时间。
|
|
367
|
+
this.lastEventAt = Date.now();
|
|
368
|
+
this.wakeWaiters();
|
|
369
|
+
}
|
|
370
|
+
const terminalRecords = records.filter(isCompactionTerminalLike);
|
|
371
|
+
if (terminalRecords.length > baselineTerminalCount) {
|
|
372
|
+
const newest = terminalRecords[terminalRecords.length - 1];
|
|
373
|
+
getLogger().info(`[${this.logTag}] compaction terminal observed (${newest.type}, ${terminalRecords.length} > baseline ${baselineTerminalCount})`);
|
|
374
|
+
return newest.type === 'full_compaction.cancel' ? 'cancelled' : 'completed';
|
|
375
|
+
}
|
|
376
|
+
if (Date.now() - lastActivityAt >= this.compactIdleTimeoutMs) {
|
|
377
|
+
getLogger().warn(`[${this.logTag}] no compaction terminal within ${this.compactIdleTimeoutMs}ms of last activity (baselineTerminal=${baselineTerminalCount}); reporting unknown`);
|
|
378
|
+
return 'unknown';
|
|
532
379
|
}
|
|
533
380
|
await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
|
|
534
381
|
}
|
|
535
|
-
getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
|
|
536
382
|
}
|
|
537
383
|
handleNotification(method, params) {
|
|
538
384
|
// Terminal-backed Bash 的通知走 runner 自产事件(见 bashToolCallIds),
|
|
@@ -606,17 +452,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
606
452
|
}
|
|
607
453
|
return;
|
|
608
454
|
}
|
|
609
|
-
|
|
610
|
-
if (ev.type === 'approval_requested') {
|
|
611
|
-
this.pendingApprovals.set(ev.requestId, {
|
|
612
|
-
kind: ev.kind,
|
|
613
|
-
view: ev.view,
|
|
614
|
-
options: params.options,
|
|
615
|
-
proto: method === ServerRequestMethod.ELICITATION_CREATE ? 'elicitation' : 'permission',
|
|
616
|
-
});
|
|
617
|
-
getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
455
|
+
this.registerApprovalEvents(events, params.options, method === ServerRequestMethod.ELICITATION_CREATE ? 'elicitation' : 'permission');
|
|
620
456
|
this.pushEvents(events);
|
|
621
457
|
}
|
|
622
458
|
/**
|
|
@@ -665,14 +501,17 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
665
501
|
: undefined;
|
|
666
502
|
const cwd = params.cwd ?? this.activeCwd ?? undefined;
|
|
667
503
|
const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
|
|
668
|
-
const proc =
|
|
504
|
+
const proc = spawnProcess(params.command, params.args ?? [], {
|
|
669
505
|
cwd,
|
|
670
|
-
|
|
506
|
+
// env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护,v2 §8.3)
|
|
507
|
+
env: env ? mergeProcessEnv(process.env, env) : undefined,
|
|
671
508
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
672
509
|
// ProcessStopper 用负 PID 杀进程组(kill(-pgid));子进程必须是组长
|
|
673
510
|
// 才能命中,否则 kill(-pid) 抛 ESRCH 被吞 → kill/release/清理全失效。
|
|
674
|
-
// 与 JsonlRpcTransport / spawning-runner
|
|
675
|
-
|
|
511
|
+
// 与 JsonlRpcTransport / spawning-runner 同模式;win32 不 detached
|
|
512
|
+
// (`.cmd` 垫片在 DETACHED_PROCESS 下丢 stdio),树杀由 taskkill /T 负责。
|
|
513
|
+
detached: useDetachedProcessGroup(),
|
|
514
|
+
windowsHide: true,
|
|
676
515
|
});
|
|
677
516
|
// 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
|
|
678
517
|
// 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
|
|
@@ -835,5 +674,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
|
|
|
835
674
|
await this.processStopper.stop(handle.proc, { immediate: true });
|
|
836
675
|
}
|
|
837
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
|
+
}
|
|
838
693
|
}
|
|
839
694
|
//# sourceMappingURL=runner.js.map
|