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.
Files changed (132) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.en.md +2 -6
  3. package/README.md +3 -7
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/index.d.ts +0 -7
  7. package/dist/bridge/index.js +21 -63
  8. package/dist/bridge/queue-manager.js +4 -3
  9. package/dist/card/approval-render.js +39 -94
  10. package/dist/card/bash-renderer.js +12 -21
  11. package/dist/card/card-shared.d.ts +13 -1
  12. package/dist/card/card-shared.js +21 -30
  13. package/dist/card/run-renderer.d.ts +0 -22
  14. package/dist/card/run-renderer.js +206 -153
  15. package/dist/card/run-state.d.ts +29 -1
  16. package/dist/card/run-state.js +172 -84
  17. package/dist/card/text-truncate.d.ts +0 -9
  18. package/dist/card/text-truncate.js +0 -9
  19. package/dist/card/tool-render.d.ts +4 -1
  20. package/dist/card/tool-render.js +76 -32
  21. package/dist/cli.js +2 -2
  22. package/dist/common/fs.d.ts +14 -0
  23. package/dist/common/fs.js +57 -3
  24. package/dist/config/codex-config.js +2 -0
  25. package/dist/config/index.d.ts +0 -5
  26. package/dist/config/index.js +0 -11
  27. package/dist/config/kimi-config.js +1 -0
  28. package/dist/config/opencode-config.js +13 -2
  29. package/dist/config/qr.d.ts +36 -0
  30. package/dist/config/qr.js +124 -0
  31. package/dist/config/wizard.js +69 -27
  32. package/dist/connector/index.d.ts +3 -2
  33. package/dist/connector/index.js +5 -3
  34. package/dist/index.js +14 -41
  35. package/dist/instance-lock.js +7 -0
  36. package/dist/persistence/atomic-write.d.ts +2 -0
  37. package/dist/persistence/atomic-write.js +20 -5
  38. package/dist/persistence/load-json-file.d.ts +1 -2
  39. package/dist/persistence/load-json-file.js +2 -3
  40. package/dist/platform/agent-stopper.d.ts +22 -0
  41. package/dist/platform/agent-stopper.js +19 -0
  42. package/dist/platform/command.d.ts +11 -0
  43. package/dist/platform/command.js +107 -0
  44. package/dist/platform/identity.d.ts +33 -0
  45. package/dist/platform/identity.js +187 -0
  46. package/dist/platform/path.d.ts +24 -0
  47. package/dist/platform/path.js +110 -0
  48. package/dist/platform/select.d.ts +9 -0
  49. package/dist/platform/select.js +12 -0
  50. package/dist/platform/shell.d.ts +51 -0
  51. package/dist/platform/shell.js +75 -0
  52. package/dist/platform/spawn.d.ts +41 -0
  53. package/dist/platform/spawn.js +77 -0
  54. package/dist/platform/terminator-posix.d.ts +38 -0
  55. package/dist/platform/terminator-posix.js +103 -0
  56. package/dist/platform/terminator-win32.d.ts +21 -0
  57. package/dist/platform/terminator-win32.js +110 -0
  58. package/dist/platform/terminator.d.ts +44 -0
  59. package/dist/platform/terminator.js +24 -0
  60. package/dist/platform/types.d.ts +23 -0
  61. package/dist/platform/types.js +5 -0
  62. package/dist/restart.js +4 -2
  63. package/dist/router/card-helpers.d.ts +33 -6
  64. package/dist/router/card-helpers.js +67 -11
  65. package/dist/router/config/common/render.d.ts +1 -1
  66. package/dist/router/config/common/render.js +1 -1
  67. package/dist/router/config-diff.d.ts +4 -0
  68. package/dist/router/config-diff.js +70 -0
  69. package/dist/router/index.d.ts +66 -6
  70. package/dist/router/index.js +450 -315
  71. package/dist/router/utils.d.ts +6 -0
  72. package/dist/router/utils.js +12 -0
  73. package/dist/runner/bash/runner.d.ts +9 -2
  74. package/dist/runner/bash/runner.js +45 -20
  75. package/dist/runner/claude/session.d.ts +4 -1
  76. package/dist/runner/claude/session.js +18 -4
  77. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  78. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  79. package/dist/runner/codex/app-server/translator.js +146 -0
  80. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  81. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  82. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  83. package/dist/runner/common/acp/base-translator.js +271 -0
  84. package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
  85. package/dist/runner/common/acp/protocol-helpers.js +34 -0
  86. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  87. package/dist/runner/common/jsonrpc/transport.js +25 -5
  88. package/dist/runner/common/process-stopper.d.ts +4 -26
  89. package/dist/runner/common/process-stopper.js +4 -76
  90. package/dist/runner/common/runner-utils.d.ts +0 -5
  91. package/dist/runner/common/runner-utils.js +0 -7
  92. package/dist/runner/common/spawning-runner.d.ts +42 -132
  93. package/dist/runner/common/spawning-runner.js +52 -271
  94. package/dist/runner/common/usage.d.ts +2 -0
  95. package/dist/runner/common/usage.js +2 -0
  96. package/dist/runner/index.d.ts +1 -1
  97. package/dist/runner/kimi/acp/runner.d.ts +45 -42
  98. package/dist/runner/kimi/acp/runner.js +140 -293
  99. package/dist/runner/kimi/acp/translator.d.ts +18 -100
  100. package/dist/runner/kimi/acp/translator.js +56 -242
  101. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  102. package/dist/runner/opencode/acp/runner.js +73 -256
  103. package/dist/runner/opencode/acp/translator.d.ts +28 -103
  104. package/dist/runner/opencode/acp/translator.js +51 -236
  105. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  106. package/dist/runner/pi/rpc/translator.js +47 -9
  107. package/dist/runner/probe.d.ts +8 -11
  108. package/dist/runner/probe.js +17 -21
  109. package/dist/runner/registry.d.ts +4 -4
  110. package/dist/runner/registry.js +4 -4
  111. package/dist/runner/types.d.ts +38 -1
  112. package/dist/session/claude/sessions.js +34 -57
  113. package/dist/session/codex/rollout-reader.d.ts +10 -0
  114. package/dist/session/codex/rollout-reader.js +123 -41
  115. package/dist/session/common/constants.d.ts +7 -0
  116. package/dist/session/common/constants.js +9 -0
  117. package/dist/session/common/two-pass.d.ts +22 -0
  118. package/dist/session/common/two-pass.js +70 -0
  119. package/dist/session/dsh/sessions.js +6 -5
  120. package/dist/session/kimi/sessions.d.ts +6 -0
  121. package/dist/session/kimi/sessions.js +50 -73
  122. package/dist/session/opencode/sessions.js +26 -8
  123. package/dist/session/pi/sessions.js +54 -73
  124. package/dist/update/index.d.ts +1 -2
  125. package/dist/update/index.js +1 -2
  126. package/dist/update/install.d.ts +2 -1
  127. package/dist/update/install.js +58 -9
  128. package/dist/update/version-check.d.ts +5 -0
  129. package/dist/update/version-check.js +9 -0
  130. package/package.json +9 -6
  131. package/dist/update/startup-hint.d.ts +0 -5
  132. 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
- * Structurally aligned with codex AppServerRunner (design doc §3.1) via the
10
- * shared ConnectionBasedRunner base: same notification queue + waitResolve
11
- * pattern, same forceFinish/stopRequested semantics, same turn idle timeout,
12
- * same ConnectionLostError retry.
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 { spawn } from 'node:child_process';
20
+ import { spawnProcess, mergeProcessEnv, useDetachedProcessGroup } from '../../../platform/spawn.js';
15
21
  import { StringDecoder } from 'node:string_decoder';
16
- import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
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 { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
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 ConnectionBasedRunner {
130
- connectionManager;
131
- currentTranslator = null;
132
- activeSessionId = null;
133
- model;
135
+ export class KimiAcpRunner extends BaseAcpRunner {
134
136
  permissionMode;
135
137
  compactIdleTimeoutMs;
136
- /** Pending approval requests: requestId → kind + view + options. */
137
- pendingApprovals = new Map();
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
- this.connectionManager = new KimiAcpConnectionManager(managerOpts);
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
- currentSessionId() {
198
- return this.activeSessionId;
204
+ get connectionClosedMessage() {
205
+ return 'Kimi ACP connection closed';
199
206
  }
200
207
  shouldDeferStop() {
201
208
  return !this.promptSettled;
202
209
  }
203
- async cancelCurrentTurn() {
204
- if (!this.currentClient || !this.activeSessionId)
205
- return;
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
- // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
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/cancel failed: ${err.message}`);
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
- * Run a compact operation on the current session.
234
- *
235
- * Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
236
- * text (builtin slash command, host intercepts) → consume notifications until
237
- * prompt settles.
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
- async *runCompact(_message, opts) {
240
- yield* this.executeTurn(opts, async () => {
241
- const client = await this.connectionManager.acquire(opts.cwd);
242
- this.connectionManager.notifyActivity(opts.cwd);
243
- this.currentClient = client;
244
- this.activeCwd = opts.cwd;
245
- client.setHooks({
246
- onNotification: (method, params) => this.handleNotification(method, params),
247
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
248
- onClose: () => {
249
- void this.cleanupTerminals();
250
- this.failTurn('Kimi ACP connection closed');
251
- },
252
- });
253
- const sessionId = opts.sessionId;
254
- if (!sessionId) {
255
- throw new Error('compact requires a sessionId');
256
- }
257
- this.activeSessionId = sessionId;
258
- // Cold-connection fallback: session/resume loads the session into memory.
259
- // On a reused connection the session is already loaded — resume is
260
- // idempotent and harmless; after connection rebuild it's required.
261
- const resumeParams = {
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
- else {
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
- }, (err) => {
337
- this.promptSettled = true;
338
- if (this.stopRequested) {
339
- getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
340
- return;
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
- const resultEvent = translator.produceErrorResult(sessionId, err.message);
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
- * Respond to an approval server request. `response` is the bridge
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
- if (!this.currentClient || !this.activeSessionId)
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
- for (const ev of events) {
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 = spawn(params.command, params.args ?? [], {
504
+ const proc = spawnProcess(params.command, params.args ?? [], {
677
505
  cwd,
678
- env: env ? { ...process.env, ...env } : undefined,
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 的 detached:true 同模式。
683
- detached: true,
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