lark-remote 0.1.12 → 0.1.13

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 (62) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/bridge/inbound-media.d.ts +0 -3
  3. package/dist/bridge/inbound-media.js +0 -1
  4. package/dist/bridge/index.d.ts +7 -0
  5. package/dist/bridge/index.js +79 -70
  6. package/dist/card/card-shared.d.ts +1 -4
  7. package/dist/card/card-shared.js +2 -4
  8. package/dist/common/truncate.d.ts +0 -6
  9. package/dist/common/truncate.js +1 -1
  10. package/dist/common/ttl-cache.d.ts +24 -0
  11. package/dist/common/ttl-cache.js +53 -0
  12. package/dist/config/codex-config.js +12 -11
  13. package/dist/config/index.d.ts +0 -3
  14. package/dist/config/index.js +0 -5
  15. package/dist/config/kimi-config.d.ts +1 -1
  16. package/dist/config/kimi-config.js +1 -1
  17. package/dist/config/opencode-config.d.ts +1 -1
  18. package/dist/config/opencode-config.js +3 -8
  19. package/dist/router/index.d.ts +7 -0
  20. package/dist/router/index.js +217 -279
  21. package/dist/runner/claude/session.d.ts +0 -3
  22. package/dist/runner/claude/session.js +0 -5
  23. package/dist/runner/codex/app-server/runner.d.ts +1 -16
  24. package/dist/runner/codex/app-server/runner.js +2 -2
  25. package/dist/runner/common/acp/protocol-helpers.d.ts +31 -0
  26. package/dist/runner/common/acp/protocol-helpers.js +48 -0
  27. package/dist/runner/common/connection-based-runner.d.ts +0 -2
  28. package/dist/runner/common/connection-based-runner.js +1 -1
  29. package/dist/runner/dsh/index.d.ts +0 -2
  30. package/dist/runner/dsh/index.js +0 -2
  31. package/dist/runner/dsh/runner.d.ts +0 -2
  32. package/dist/runner/dsh/runner.js +1 -1
  33. package/dist/runner/kimi/acp/runner.d.ts +18 -11
  34. package/dist/runner/kimi/acp/runner.js +95 -87
  35. package/dist/runner/kimi/acp/translator.d.ts +0 -1
  36. package/dist/runner/kimi/acp/translator.js +5 -13
  37. package/dist/runner/opencode/acp/runner.js +2 -42
  38. package/dist/runner/opencode/acp/translator.d.ts +0 -1
  39. package/dist/runner/opencode/acp/translator.js +3 -13
  40. package/dist/runner/pi/rpc/index.d.ts +0 -2
  41. package/dist/runner/pi/rpc/index.js +0 -2
  42. package/dist/runner/resolve-agent-choices.js +6 -2
  43. package/dist/runner/sync-agent-choices.js +1 -1
  44. package/dist/session/claude/session-index.js +3 -1
  45. package/dist/session/claude/sessions.js +3 -7
  46. package/dist/session/codex/rollout-reader.d.ts +0 -14
  47. package/dist/session/codex/rollout-reader.js +3 -7
  48. package/dist/session/common/pagination.d.ts +0 -2
  49. package/dist/session/common/pagination.js +1 -1
  50. package/dist/session/common/recency.d.ts +7 -0
  51. package/dist/session/common/recency.js +10 -0
  52. package/dist/session/common/usage-accumulator.d.ts +14 -0
  53. package/dist/session/common/usage-accumulator.js +15 -0
  54. package/dist/session/dsh/sessions.js +9 -6
  55. package/dist/session/kimi/sessions.d.ts +18 -4
  56. package/dist/session/kimi/sessions.js +49 -30
  57. package/dist/session/opencode/sessions.d.ts +2 -14
  58. package/dist/session/opencode/sessions.js +9 -7
  59. package/dist/session/pi/sessions.js +10 -22
  60. package/dist/update/index.d.ts +2 -2
  61. package/dist/update/index.js +1 -1
  62. package/package.json +1 -2
@@ -142,9 +142,6 @@ export declare class ClaudeSession extends SpawningRunner {
142
142
  * streamEnded 唤醒等待中的 turn 循环。
143
143
  */
144
144
  private startStreamReader;
145
- /** 最近一次进程退出的 code/signal(流结束错误结果构造用)。 */
146
- private exitCode;
147
- private exitSignal;
148
145
  /**
149
146
  * 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
150
147
  * 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(P1-11 语义)。
@@ -337,8 +337,6 @@ export class ClaudeSession extends SpawningRunner {
337
337
  this.processExitPromise = new Promise((resolve) => {
338
338
  proc.once('close', (code, signal) => {
339
339
  this.spawnHeartbeat.clear();
340
- this.exitCode = code;
341
- this.exitSignal = signal;
342
340
  if (code !== null && code !== 0 && !this.stoppedByUser) {
343
341
  getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${this.spawnStderr.slice(-500)}`);
344
342
  }
@@ -379,9 +377,6 @@ export class ClaudeSession extends SpawningRunner {
379
377
  }
380
378
  })();
381
379
  }
382
- /** 最近一次进程退出的 code/signal(流结束错误结果构造用)。 */
383
- exitCode = null;
384
- exitSignal = null;
385
380
  // =========================================================================
386
381
  // Internal: turn consumption
387
382
  // =========================================================================
@@ -13,7 +13,7 @@
13
13
  import type { AgentKind, AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
14
14
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
15
15
  import { type TranslatorEvent } from './translator.js';
16
- import { type AskForApproval, type SandboxMode, type SandboxPolicy, type InitializeResult } from './protocol-types.js';
16
+ import { type AskForApproval, type SandboxMode, type InitializeResult } from './protocol-types.js';
17
17
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
18
18
  export interface CodexAppServerRunnerOptions {
19
19
  kind: AgentKind;
@@ -38,21 +38,6 @@ export interface CodexAppServerRunnerOptions {
38
38
  /** Configured approval policy (Codex 官方 AskForApproval 标准值). */
39
39
  approvalPolicy?: AskForApproval;
40
40
  }
41
- /**
42
- * Map the client-side SandboxMode enum to the response-side SandboxPolicy
43
- * object used by `thread/settings/update` (`sandboxPolicy`).
44
- */
45
- export declare function sandboxModeToSandboxPolicy(mode: SandboxMode): SandboxPolicy;
46
- /**
47
- * Codex 官方 feature override(thread/start·resume 的 `config` 字段):
48
- * 让 `request_user_input` 在 Default 协作模式下也可用,无需改 codex
49
- * config.toml。协议键与 openai/codex 的 feature registry 一致
50
- * (key `default_mode_request_user_input`),app-server 官方测试
51
- * (turn_start.rs)验证同一路径。
52
- */
53
- export declare const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT: {
54
- readonly 'features.default_mode_request_user_input': true;
55
- };
56
41
  export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcClient<InitializeResult>, TranslatorEvent> {
57
42
  private connectionManager;
58
43
  private currentTranslator;
@@ -72,7 +72,7 @@ function buildRequestUserInputResponse(action, questions, response) {
72
72
  * Map the client-side SandboxMode enum to the response-side SandboxPolicy
73
73
  * object used by `thread/settings/update` (`sandboxPolicy`).
74
74
  */
75
- export function sandboxModeToSandboxPolicy(mode) {
75
+ function sandboxModeToSandboxPolicy(mode) {
76
76
  switch (mode) {
77
77
  case 'read-only':
78
78
  return { type: 'readOnly', networkAccess: false };
@@ -95,7 +95,7 @@ export function sandboxModeToSandboxPolicy(mode) {
95
95
  * (key `default_mode_request_user_input`),app-server 官方测试
96
96
  * (turn_start.rs)验证同一路径。
97
97
  */
98
- export const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT = {
98
+ const THREAD_CONFIG_DEFAULT_MODE_REQUEST_USER_INPUT = {
99
99
  'features.default_mode_request_user_input': true,
100
100
  };
101
101
  export class CodexAppServerRunner extends ConnectionBasedRunner {
@@ -12,3 +12,34 @@ import type { PermissionOption } from './protocol-types.js';
12
12
  * the documented names and the observed real names (§2.3).
13
13
  */
14
14
  export declare function findOptionIdByKind(options: PermissionOption[], candidateKinds: string[]): string | undefined;
15
+ /** 各 ACP 服务端的 option kind 候选表(kimi/opencode 观测名不同,语义对齐)。 */
16
+ export interface AcpApprovalKinds {
17
+ accept: string[];
18
+ acceptForSession: string[];
19
+ decline: string[];
20
+ }
21
+ /** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
22
+ export declare const KIMI_APPROVAL_KINDS: AcpApprovalKinds;
23
+ /** opencode kind(permission.ts:219-223:once/always → approve,其余 → reject)。 */
24
+ export declare const OPENCODE_APPROVAL_KINDS: AcpApprovalKinds;
25
+ /**
26
+ * 把 bridge 审批动作(accept/accept_for_session/decline/cancel)映射为 ACP
27
+ * request_permission 响应:按 kind 候选表查 optionId 回显;找不到一律
28
+ * cancelled(安全兜底——服务端视为拒绝,不会悬挂)。
29
+ */
30
+ export declare function buildAcpPermissionOutcome(action: string, options: PermissionOption[], kinds: AcpApprovalKinds): {
31
+ outcome: {
32
+ outcome: 'cancelled';
33
+ } | {
34
+ outcome: 'selected';
35
+ optionId: string;
36
+ };
37
+ };
38
+ /**
39
+ * 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
40
+ * 带 always 类 option(kimi approve_always / opencode allow_always)才提供
41
+ * acceptForSession(§P4「本会话总是允许」)。kimi/opencode 两侧共用。
42
+ */
43
+ export declare function deriveAcpAvailableDecisions(options: PermissionOption[]): string[];
44
+ /** 截断长文本并追加省略号(translator 审批 reason 用,原先两份相同拷贝)。 */
45
+ export declare function truncateWithEllipsis(text: string, maxLen: number): string;
@@ -18,4 +18,52 @@ export function findOptionIdByKind(options, candidateKinds) {
18
18
  }
19
19
  return undefined;
20
20
  }
21
+ /** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
22
+ export const KIMI_APPROVAL_KINDS = {
23
+ accept: ['approve_once', 'approve_always', 'allow_once'],
24
+ acceptForSession: ['approve_always', 'allow_always'],
25
+ decline: ['reject', 'reject_once'],
26
+ };
27
+ /** opencode kind(permission.ts:219-223:once/always → approve,其余 → reject)。 */
28
+ export const OPENCODE_APPROVAL_KINDS = {
29
+ accept: ['allow_once', 'allow_always'],
30
+ acceptForSession: ['allow_always', 'approve_always'],
31
+ decline: ['reject_once', 'reject'],
32
+ };
33
+ /**
34
+ * 把 bridge 审批动作(accept/accept_for_session/decline/cancel)映射为 ACP
35
+ * request_permission 响应:按 kind 候选表查 optionId 回显;找不到一律
36
+ * cancelled(安全兜底——服务端视为拒绝,不会悬挂)。
37
+ */
38
+ export function buildAcpPermissionOutcome(action, options, kinds) {
39
+ if (action === 'cancel') {
40
+ return { outcome: { outcome: 'cancelled' } };
41
+ }
42
+ const candidates = action === 'accept'
43
+ ? kinds.accept
44
+ : action === 'accept_for_session'
45
+ ? kinds.acceptForSession
46
+ : kinds.decline;
47
+ const optionId = findOptionIdByKind(options, candidates);
48
+ return optionId
49
+ ? { outcome: { outcome: 'selected', optionId } }
50
+ : { outcome: { outcome: 'cancelled' } };
51
+ }
52
+ /**
53
+ * 从服务端 options kind 派生审批决定列表:accept/decline/cancel 恒有;
54
+ * 带 always 类 option(kimi approve_always / opencode allow_always)才提供
55
+ * acceptForSession(§P4「本会话总是允许」)。kimi/opencode 两侧共用。
56
+ */
57
+ export function deriveAcpAvailableDecisions(options) {
58
+ const decisions = ['accept', 'decline', 'cancel'];
59
+ const hasAlwaysOption = options.some((opt) => opt.kind === 'allow_always' || opt.kind === 'approve_always');
60
+ if (hasAlwaysOption) {
61
+ decisions.push('acceptForSession');
62
+ }
63
+ return decisions;
64
+ }
65
+ /** 截断长文本并追加省略号(translator 审批 reason 用,原先两份相同拷贝)。 */
66
+ export function truncateWithEllipsis(text, maxLen) {
67
+ return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
68
+ }
21
69
  //# sourceMappingURL=protocol-helpers.js.map
@@ -15,8 +15,6 @@
15
15
  * updateApprovalMode, getStatusInfo) and provide the small abstract hooks below.
16
16
  */
17
17
  import type { AgentKind, AgentSessionReader, AgentEvent, AgentRunner, AgentStatusInfo, SpawnOptions } from '../types.js';
18
- /** How long to wait for turn output notifications before failing. */
19
- export declare const TURN_IDLE_TIMEOUT_MS: number;
20
18
  export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent> implements AgentRunner {
21
19
  readonly kind: AgentKind;
22
20
  readonly sessionReader: AgentSessionReader;
@@ -2,7 +2,7 @@ import { ConnectionLostError } from './jsonrpc/client.js';
2
2
  import { getLogger } from '../../logger/index.js';
3
3
  import { syntheticInitEvent } from './runner-utils.js';
4
4
  /** How long to wait for turn output notifications before failing. */
5
- export const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
5
+ const TURN_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
6
6
  export class ConnectionBasedRunner {
7
7
  kind;
8
8
  sessionReader;
@@ -1,3 +1 @@
1
1
  export { DshRunner, type DshRunnerOptions } from './runner.js';
2
- export { DshClient, DshError } from './client.js';
3
- export { DshTranslator, mapUsage } from './translator.js';
@@ -1,4 +1,2 @@
1
1
  export { DshRunner } from './runner.js';
2
- export { DshClient, DshError } from './client.js';
3
- export { DshTranslator, mapUsage } from './translator.js';
4
2
  //# sourceMappingURL=index.js.map
@@ -15,8 +15,6 @@
15
15
  import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../types.js';
16
16
  import type { AgentRunner } from '../types.js';
17
17
  import { DshClient, type WebSocketFactory } from './client.js';
18
- /** DSH selectModel provider(deepseek-official 是 rc.7 模型目录的 provider)。 */
19
- export declare const DSH_MODEL_PROVIDER = "deepseek-official";
20
18
  export interface DshRunnerOptions {
21
19
  kind: 'dsh';
22
20
  sessionReader: AgentSessionReader;
@@ -16,7 +16,7 @@ import { DshClient } from './client.js';
16
16
  import { DshTranslator } from './translator.js';
17
17
  import { getLogger } from '../../logger/index.js';
18
18
  /** DSH selectModel provider(deepseek-official 是 rc.7 模型目录的 provider)。 */
19
- export const DSH_MODEL_PROVIDER = 'deepseek-official';
19
+ const DSH_MODEL_PROVIDER = 'deepseek-official';
20
20
  export class DshRunner {
21
21
  kind = 'dsh';
22
22
  sessionReader;
@@ -30,9 +30,11 @@ export interface KimiAcpRunnerOptions {
30
30
  idleTtlMs?: number;
31
31
  /** How long to wait for turn output before failing. Defaults to 10 min. */
32
32
  turnIdleTimeoutMs?: number;
33
- /** R2: how long to poll wire.jsonl for the compaction record after the
34
- * /compact prompt settles. Defaults to 30s; WARN (not fail) on timeout. */
35
- compactPollTimeoutMs?: number;
33
+ /** §5.2: compaction 等待的沉默超时——距上次观察到任何新 compaction 记录
34
+ * (begin/terminal)超过该时长仍未终态才放弃(outcome unknown,卡片 error,
35
+ * 不再「超时=成功」)。默认 10 分钟(对齐 turnIdleTimeoutMs)。压缩是完整
36
+ * LLM 请求(引擎带重试),时长无上限,旧的固定 30s 轮询超时已删除。 */
37
+ compactIdleTimeoutMs?: number;
36
38
  model?: string;
37
39
  /** Kimi permission mode (user-facing: manual/auto/yolo). */
38
40
  permissionMode?: 'manual' | 'auto' | 'yolo';
@@ -43,7 +45,7 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
43
45
  private activeSessionId;
44
46
  private model?;
45
47
  private permissionMode;
46
- private readonly compactPollTimeoutMs;
48
+ private readonly compactIdleTimeoutMs;
47
49
  /** Pending approval requests: requestId → kind + view + options. */
48
50
  private pendingApprovals;
49
51
  /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
@@ -107,15 +109,20 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
107
109
  */
108
110
  protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
109
111
  /**
110
- * R2: poll wire.jsonl until a NEW compaction record
111
- * (context.apply_compaction / full_compaction.complete) appears.
112
+ * §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
112
113
  *
113
- * The /compact prompt settles before the background compaction lands its
114
- * record; returning early would let the caller dispose the connection and
115
- * kill the background job. Timeout is WARN-only (the compact may have been
116
- * a no-op) — never fails the turn. Aborts early on stop/forceFinish.
114
+ * 压缩是完整 LLM 请求(引擎 ≤5 次重试 + overflow 收缩重试),时长无上限;
115
+ * 旧「固定 30s 超时 = 成功」已删除。出口:
116
+ * completed — 观察到 full_compaction.complete / context.apply_compaction;
117
+ * cancelled — 观察到 full_compaction.cancel(API 失败/abort 与用户取消共用,
118
+ * 绝不当成功);
119
+ * stopped — 等待中 stop()/forceFinish(consumeTurn 产 interrupted 兜底);
120
+ * unknown — 距上次观察到任何新 compaction 记录(begin/terminal)超过
121
+ * compactIdleTimeoutMs(沉默超时,压缩可能仍在后台进行)。
122
+ * 观察到新记录即重置沉默时钟,并顺带重置 turn idle 时钟(lastEventAt +
123
+ * wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
117
124
  */
118
- private waitForCompactionRecord;
125
+ private waitForCompactionTerminal;
119
126
  private handleNotification;
120
127
  /**
121
128
  * Filter kimi's own Bash tool notifications so the run card doesn't get a
@@ -16,15 +16,13 @@ import { StringDecoder } from 'node:string_decoder';
16
16
  import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
17
17
  import { KimiAcpTranslator } from './translator.js';
18
18
  import { NotificationMethod, RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
19
- import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
19
+ import { KIMI_APPROVAL_KINDS, buildAcpPermissionOutcome, } from '../../common/acp/protocol-helpers.js';
20
20
  import { mapAnswersByIndex } from '../../question-common.js';
21
21
  import { getLogger } from '../../../logger/index.js';
22
22
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
23
23
  import { ProcessStopper } from '../../common/process-stopper.js';
24
- /** R2: how long to wait for the background compaction record after the
25
- * /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
26
- * context.apply_compaction ~8s after end_turn. */
27
- const COMPACT_POLL_TIMEOUT_MS = 30_000;
24
+ /** §5.2: compaction 等待的默认沉默超时(对齐 turnIdleTimeoutMs 默认值)。 */
25
+ const COMPACT_IDLE_TIMEOUT_MS = 10 * 60_000;
28
26
  /** Poll interval for the wire.jsonl compaction record. */
29
27
  const COMPACT_POLL_INTERVAL_MS = 1_000;
30
28
  // =============================================================================
@@ -36,6 +34,16 @@ const COMPACT_POLL_INTERVAL_MS = 1_000;
36
34
  // =============================================================================
37
35
  /** terminal/create 未显式给 outputByteLimit 时的缓冲上限(对齐服务端默认 4MB)。 */
38
36
  const TERMINAL_DEFAULT_OUTPUT_LIMIT = 4 * 1024 * 1024;
37
+ /** 压缩终态记录:complete / apply_compaction 完成,cancel 为失败或取消(共用)。 */
38
+ function isCompactionTerminalLike(record) {
39
+ return (record.type === 'full_compaction.complete' ||
40
+ record.type === 'context.apply_compaction' ||
41
+ record.type === 'full_compaction.cancel');
42
+ }
43
+ /** 沉默窗口的展示文案(测试用小窗口显示秒,生产默认显示分钟)。 */
44
+ function formatIdleWindow(ms) {
45
+ return ms >= 60_000 ? `${Math.round(ms / 60_000)} 分钟` : `${Math.round(ms / 1000)} 秒`;
46
+ }
39
47
  /**
40
48
  * Map user-facing permission mode to ACP mode id.
41
49
  *
@@ -58,53 +66,6 @@ function toAcpMode(mode) {
58
66
  return 'yolo';
59
67
  }
60
68
  }
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
69
  /**
109
70
  * AskUserQuestion 回编(kind === 'question'):
110
71
  * - elicitation form:answer → {action:'accept', content:{q0..qn}}(多选数组/
@@ -171,7 +132,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
171
132
  activeSessionId = null;
172
133
  model;
173
134
  permissionMode;
174
- compactPollTimeoutMs;
135
+ compactIdleTimeoutMs;
175
136
  /** Pending approval requests: requestId → kind + view + options. */
176
137
  pendingApprovals = new Map();
177
138
  /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
@@ -195,7 +156,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
195
156
  super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
196
157
  this.model = opts.model;
197
158
  this.permissionMode = opts.permissionMode ?? 'manual';
198
- this.compactPollTimeoutMs = opts.compactPollTimeoutMs ?? COMPACT_POLL_TIMEOUT_MS;
159
+ this.compactIdleTimeoutMs = opts.compactIdleTimeoutMs ?? COMPACT_IDLE_TIMEOUT_MS;
199
160
  const managerOpts = {
200
161
  binary: opts.binary ?? 'kimi',
201
162
  args: opts.acpArgs ?? ['acp'],
@@ -316,17 +277,21 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
316
277
  sessionId,
317
278
  prompt: [{ type: 'text', text: '/compact' }],
318
279
  };
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 打回复现).
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 事故).
326
289
  const compactionReader = this.sessionReader.readCompactionRecords;
327
- const baselineCount = compactionReader
328
- ? compactionReader.call(this.sessionReader, sessionId, opts.cwd).length
329
- : 0;
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;
330
295
  this.promptSettled = false;
331
296
  // ACP holds the session/prompt response for the ENTIRE turn (unlike
332
297
  // codex turn/start), so the RPC-level timeout must not apply here —
@@ -337,13 +302,37 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
337
302
  if (this.stopRequested)
338
303
  return; // already cancelled
339
304
  if (compactionReader) {
340
- await this.waitForCompactionRecord(compactionReader, opts, baselineCount);
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 兜底。
331
+ }
332
+ else {
333
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
334
+ this.pushEvents([resultEvent]);
341
335
  }
342
- if (this.forceFinish)
343
- return; // stopped while polling
344
- // Translate prompt result into a result event
345
- const resultEvent = translator.handlePromptResponse(sessionId, result);
346
- this.pushEvents([resultEvent]);
347
336
  }, (err) => {
348
337
  this.promptSettled = true;
349
338
  if (this.stopRequested) {
@@ -367,7 +356,7 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
367
356
  const action = response?.action ?? 'decline';
368
357
  const acpResponse = pending.kind === 'question'
369
358
  ? buildQuestionResponse(action, pending, response)
370
- : buildApprovalResponse(action, pending);
359
+ : buildAcpPermissionOutcome(action, pending.options, KIMI_APPROVAL_KINDS);
371
360
  client.respond(requestId, acpResponse);
372
361
  this.pendingApprovals.delete(requestId);
373
362
  getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
@@ -447,8 +436,8 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
447
436
  }
448
437
  this.activeSessionId = sessionId;
449
438
  // CC-07: 下发配置的模型(provider/model)。session/new|resume 只带 cwd/mcpServers,
450
- // 不带 model;不主动下发则实际跑 kimi 服务端默认模型(旧 KimiRunner 通过 -m 传模型,
451
- // 迁移到纯 ACP 后丢失)。仿 opencode 用 session/set_config_option。失败仅告警不阻断。
439
+ // 不带 model;不主动下发则实际跑 kimi 服务端默认模型。
440
+ // 仿 opencode 用 session/set_config_option。失败仅告警不阻断。
452
441
  if (this.model) {
453
442
  try {
454
443
  await client.request('session/set_config_option', {
@@ -511,28 +500,47 @@ export class KimiAcpRunner extends ConnectionBasedRunner {
511
500
  });
512
501
  }
513
502
  /**
514
- * R2: poll wire.jsonl until a NEW compaction record
515
- * (context.apply_compaction / full_compaction.complete) appears.
503
+ * §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
516
504
  *
517
- * The /compact prompt settles before the background compaction lands its
518
- * record; returning early would let the caller dispose the connection and
519
- * kill the background job. Timeout is WARN-only (the compact may have been
520
- * a no-op) — never fails the turn. Aborts early on stop/forceFinish.
505
+ * 压缩是完整 LLM 请求(引擎 ≤5 次重试 + overflow 收缩重试),时长无上限;
506
+ * 旧「固定 30s 超时 = 成功」已删除。出口:
507
+ * completed — 观察到 full_compaction.complete / context.apply_compaction;
508
+ * cancelled — 观察到 full_compaction.cancel(API 失败/abort 与用户取消共用,
509
+ * 绝不当成功);
510
+ * stopped — 等待中 stop()/forceFinish(consumeTurn 产 interrupted 兜底);
511
+ * unknown — 距上次观察到任何新 compaction 记录(begin/terminal)超过
512
+ * compactIdleTimeoutMs(沉默超时,压缩可能仍在后台进行)。
513
+ * 观察到新记录即重置沉默时钟,并顺带重置 turn idle 时钟(lastEventAt +
514
+ * wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
521
515
  */
522
- async waitForCompactionRecord(reader, opts, baselineCount) {
516
+ async waitForCompactionTerminal(reader, opts, baselineTerminalCount) {
523
517
  const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
524
- const deadline = Date.now() + this.compactPollTimeoutMs;
525
- while (Date.now() < deadline) {
518
+ let lastActivityAt = Date.now();
519
+ let lastTotalCount = reader.call(this.sessionReader, sessionId, opts.cwd).length;
520
+ while (true) {
526
521
  if (this.forceFinish || this.stopRequested)
527
- return;
522
+ return 'stopped';
528
523
  const records = reader.call(this.sessionReader, sessionId, opts.cwd);
529
- if (records.length > baselineCount) {
530
- getLogger().info(`[${this.logTag}] compaction record observed (${records.length} > baseline ${baselineCount})`);
531
- return;
524
+ if (records.length !== lastTotalCount) {
525
+ lastTotalCount = records.length;
526
+ lastActivityAt = Date.now();
527
+ // 压缩仍在活动(新 begin / 重试 / terminal):重置 turn idle 时钟,
528
+ // 让 consumeTurn 重新计算滚动截止时间。
529
+ this.lastEventAt = Date.now();
530
+ this.wakeWaiters();
531
+ }
532
+ const terminalRecords = records.filter(isCompactionTerminalLike);
533
+ if (terminalRecords.length > baselineTerminalCount) {
534
+ const newest = terminalRecords[terminalRecords.length - 1];
535
+ getLogger().info(`[${this.logTag}] compaction terminal observed (${newest.type}, ${terminalRecords.length} > baseline ${baselineTerminalCount})`);
536
+ return newest.type === 'full_compaction.cancel' ? 'cancelled' : 'completed';
537
+ }
538
+ if (Date.now() - lastActivityAt >= this.compactIdleTimeoutMs) {
539
+ getLogger().warn(`[${this.logTag}] no compaction terminal within ${this.compactIdleTimeoutMs}ms of last activity (baselineTerminal=${baselineTerminalCount}); reporting unknown`);
540
+ return 'unknown';
532
541
  }
533
542
  await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
534
543
  }
535
- getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
536
544
  }
537
545
  handleNotification(method, params) {
538
546
  // Terminal-backed Bash 的通知走 runner 自产事件(见 bashToolCallIds),
@@ -127,7 +127,6 @@ export declare class KimiAcpTranslator {
127
127
  * accept/decline/cancel are universal; acceptForSession requires an
128
128
  * always-class option (kimi `approve_always`, opencode `allow_always`).
129
129
  */
130
- private deriveAvailableDecisions;
131
130
  /**
132
131
  * ResultEvent.usage requires input_tokens/output_tokens: number, but the
133
132
  * ACP wire has no such split — the live snapshot is context occupancy only.
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { makeQuestionApprovalEvent } from '../../question-common.js';
32
32
  import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
33
+ import { deriveAcpAvailableDecisions, truncateWithEllipsis, } from '../../common/acp/protocol-helpers.js';
33
34
  import { getLogger } from '../../../logger/index.js';
34
35
  /**
35
36
  * Fixed snapshot item ids: ACP has exactly one text stream and one thinking
@@ -328,10 +329,12 @@ export class KimiAcpTranslator {
328
329
  requestId,
329
330
  kind: 'command',
330
331
  command: params.toolCall?.title ?? undefined,
331
- reason: params.toolCall?.rawInput ? truncate(params.toolCall.rawInput, 200) : undefined,
332
+ reason: params.toolCall?.rawInput
333
+ ? truncateWithEllipsis(params.toolCall.rawInput, 200)
334
+ : undefined,
332
335
  // §P4: 从服务端 options kind 派生——带 approve_always(或 allow_always)
333
336
  // 才提供「本会话总是允许」(acceptForSession);否则与旧行为一致。
334
- availableDecisions: this.deriveAvailableDecisions(params),
337
+ availableDecisions: deriveAcpAvailableDecisions(params.options ?? []),
335
338
  };
336
339
  return [
337
340
  {
@@ -372,14 +375,6 @@ export class KimiAcpTranslator {
372
375
  * accept/decline/cancel are universal; acceptForSession requires an
373
376
  * always-class option (kimi `approve_always`, opencode `allow_always`).
374
377
  */
375
- deriveAvailableDecisions(params) {
376
- const decisions = ['accept', 'decline', 'cancel'];
377
- const hasAlwaysOption = (params.options ?? []).some((opt) => opt.kind === 'approve_always' || opt.kind === 'allow_always');
378
- if (hasAlwaysOption) {
379
- decisions.push('acceptForSession');
380
- }
381
- return decisions;
382
- }
383
378
  // =========================================================================
384
379
  // Private helpers
385
380
  // =========================================================================
@@ -396,9 +391,6 @@ export class KimiAcpTranslator {
396
391
  // =============================================================================
397
392
  // Helpers
398
393
  // =============================================================================
399
- function truncate(text, maxLen) {
400
- return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
401
- }
402
394
  /** elicitation schema property → UserQuestion(无法解析返回 null)。 */
403
395
  function elicitationPropertyToQuestion(prop) {
404
396
  const title = prop.title?.trim();