lark-remote 0.1.6 → 0.1.8

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 (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +97 -13
  9. package/dist/bridge/index.js +227 -87
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +294 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +7 -16
  20. package/dist/card/card-shared.js +7 -30
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. package/dist/session/common/catch-up.js +0 -31
@@ -0,0 +1,106 @@
1
+ /**
2
+ * KimiAcpRunner: workspace-lifetime runner using the Kimi ACP protocol
3
+ * (`kimi acp`, JSON-RPC over stdio).
4
+ *
5
+ * Flow per run: acquire persistent connection → session/new or session/resume
6
+ * → synthetic init → session/prompt → consume session/update notifications
7
+ * until prompt settles → result event.
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.
13
+ */
14
+ import type { AgentKind, AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
15
+ import { JsonRpcClient as KimiAcpClient } from '../../common/jsonrpc/client.js';
16
+ import { type AcpTranslatorEvent } from './translator.js';
17
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
18
+ export interface KimiAcpRunnerOptions {
19
+ kind: AgentKind;
20
+ sessionReader: AgentSessionReader;
21
+ /** Path to the kimi binary. Defaults to `kimi`. */
22
+ binary?: string;
23
+ /** Environment variables. */
24
+ env?: Record<string, string | undefined>;
25
+ /** Args to spawn the ACP server with. Defaults to `['acp']`. */
26
+ acpArgs?: string[];
27
+ /** Request timeout in milliseconds. */
28
+ requestTimeoutMs?: number;
29
+ /** Idle TTL for connection manager. */
30
+ idleTtlMs?: number;
31
+ /** How long to wait for turn output before failing. Defaults to 10 min. */
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;
36
+ model?: string;
37
+ /** Kimi permission mode (user-facing: manual/auto/yolo). */
38
+ permissionMode?: 'manual' | 'auto' | 'yolo';
39
+ }
40
+ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient, AcpTranslatorEvent> {
41
+ private connectionManager;
42
+ private currentTranslator;
43
+ private activeSessionId;
44
+ private model?;
45
+ private permissionMode;
46
+ private readonly compactPollTimeoutMs;
47
+ /** Pending approval requests: requestId → kind + view + options. */
48
+ private pendingApprovals;
49
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
50
+ private promptSettled;
51
+ constructor(opts: KimiAcpRunnerOptions);
52
+ protected get logTag(): string;
53
+ protected get turnTimeoutErrorMessage(): string;
54
+ protected get turnInterruptedErrorMessage(): string;
55
+ protected currentSessionId(): string | null;
56
+ protected shouldDeferStop(): boolean;
57
+ protected cancelCurrentTurn(): Promise<void>;
58
+ protected clearTurnState(): void;
59
+ protected releaseConnection(cwd: string): Promise<void>;
60
+ protected notifyIdle(cwd: string): void;
61
+ protected disposeConnections(): Promise<void>;
62
+ /**
63
+ * Run a compact operation on the current session.
64
+ *
65
+ * Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
66
+ * text (builtin slash command, host intercepts) → consume notifications until
67
+ * prompt settles.
68
+ */
69
+ runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
70
+ /**
71
+ * Respond to an approval server request. `response` is the bridge
72
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
73
+ */
74
+ respondApproval(requestId: number | string, response: unknown): Promise<void>;
75
+ getStatusInfo(): AgentStatusInfo;
76
+ /**
77
+ * Hot-apply a permission-mode change to the live ACP session (§P5).
78
+ *
79
+ * The local cache is always updated so `getStatusInfo()` (and therefore
80
+ * `/s`) reflects the new mode immediately. When a session is connected,
81
+ * re-sends `session/set_mode` so the running session picks up the new mode
82
+ * without waiting for the runner to be evicted/recreated. Failure is
83
+ * non-fatal: the next setupTurn re-applies the cached mode unconditionally.
84
+ */
85
+ updateApprovalMode(settings: {
86
+ permissionMode?: 'manual' | 'auto' | 'yolo';
87
+ }): Promise<void>;
88
+ /**
89
+ * Acquire the connection and set up the turn:
90
+ * session/new (new session) or session/resume (existing sessionId)
91
+ * → set permission mode if needed → session/prompt.
92
+ */
93
+ protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
94
+ /**
95
+ * R2: poll wire.jsonl until a NEW compaction record
96
+ * (context.apply_compaction / full_compaction.complete) appears.
97
+ *
98
+ * The /compact prompt settles before the background compaction lands its
99
+ * record; returning early would let the caller dispose the connection and
100
+ * kill the background job. Timeout is WARN-only (the compact may have been
101
+ * a no-op) — never fails the turn. Aborts early on stop/forceFinish.
102
+ */
103
+ private waitForCompactionRecord;
104
+ private handleNotification;
105
+ private handleServerRequest;
106
+ }
@@ -0,0 +1,516 @@
1
+ /**
2
+ * KimiAcpRunner: workspace-lifetime runner using the Kimi ACP protocol
3
+ * (`kimi acp`, JSON-RPC over stdio).
4
+ *
5
+ * Flow per run: acquire persistent connection → session/new or session/resume
6
+ * → synthetic init → session/prompt → consume session/update notifications
7
+ * until prompt settles → result event.
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.
13
+ */
14
+ import { ConnectionManager as KimiAcpConnectionManager, } from '../../common/jsonrpc/connection-manager.js';
15
+ import { KimiAcpTranslator } from './translator.js';
16
+ import { RpcErrorCode, ServerRequestMethod, } from '../../common/acp/protocol-types.js';
17
+ import { findOptionIdByKind } from '../../common/acp/protocol-helpers.js';
18
+ import { mapAnswersByIndex } from '../../question-common.js';
19
+ import { getLogger } from '../../../logger/index.js';
20
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
21
+ /** R2: how long to wait for the background compaction record after the
22
+ * /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
23
+ * context.apply_compaction ~8s after end_turn. */
24
+ const COMPACT_POLL_TIMEOUT_MS = 30_000;
25
+ /** Poll interval for the wire.jsonl compaction record. */
26
+ const COMPACT_POLL_INTERVAL_MS = 1_000;
27
+ /**
28
+ * Map user-facing permission mode to ACP mode id.
29
+ *
30
+ * ACP layer mode ids (acp-server/src/modes.ts:49):
31
+ * default → permission manual
32
+ * auto → permission auto
33
+ * yolo → permission yolo
34
+ * plan → not exposed in v1
35
+ *
36
+ * This is not self-invented naming — both are official enums at their
37
+ * respective layers. The mapping happens only at the ACP boundary.
38
+ */
39
+ function toAcpMode(mode) {
40
+ switch (mode) {
41
+ case 'manual':
42
+ return 'default';
43
+ case 'auto':
44
+ return 'auto';
45
+ case 'yolo':
46
+ return 'yolo';
47
+ }
48
+ }
49
+ /**
50
+ * Build the ACP protocol response for a permission approval decision.
51
+ *
52
+ * accept → {outcome:{outcome:'selected', optionId:<approve_once kind>}}
53
+ * accept_for_session → {outcome:{outcome:'selected', optionId:<approve_always kind>}}
54
+ * decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
55
+ * cancel → {outcome:{outcome:'cancelled'}}
56
+ *
57
+ * optionId is looked up from the request's options[] by kind (approval.ts:28-29):
58
+ * approve_once / approve_always / allow_once for accept;
59
+ * approve_always / allow_always for accept_for_session;
60
+ * reject / reject_once for decline. If not found, fall back to cancelled
61
+ * (safe universal default — the server treats it as "user declined").
62
+ *
63
+ * 2026-08-15 live test: approve_once + approve_always + reject observed.
64
+ */
65
+ function buildApprovalResponse(action, pending) {
66
+ if (action === 'cancel') {
67
+ return { outcome: { outcome: 'cancelled' } };
68
+ }
69
+ if (action === 'accept') {
70
+ const optionId = findOptionIdByKind(pending.options, [
71
+ 'approve_once',
72
+ 'approve_always',
73
+ 'allow_once',
74
+ ]);
75
+ if (optionId) {
76
+ return { outcome: { outcome: 'selected', optionId } };
77
+ }
78
+ // Fallback: can't find allow option → cancel is safe
79
+ return { outcome: { outcome: 'cancelled' } };
80
+ }
81
+ if (action === 'accept_for_session') {
82
+ // §P4: 卡片「允许本次会话」→ always 类 optionId(kimi approve_always)
83
+ const optionId = findOptionIdByKind(pending.options, ['approve_always', 'allow_always']);
84
+ if (optionId) {
85
+ return { outcome: { outcome: 'selected', optionId } };
86
+ }
87
+ return { outcome: { outcome: 'cancelled' } };
88
+ }
89
+ // decline
90
+ const optionId = findOptionIdByKind(pending.options, ['reject', 'reject_once']);
91
+ if (optionId) {
92
+ return { outcome: { outcome: 'selected', optionId } };
93
+ }
94
+ return { outcome: { outcome: 'cancelled' } };
95
+ }
96
+ /**
97
+ * AskUserQuestion 回编(kind === 'question'):
98
+ * - elicitation form:answer → {action:'accept', content:{q0..qn}}(多选数组/
99
+ * 单选字符串,key 按问题顺序);decline → {action:'decline'};cancel →
100
+ * {action:'cancel'}。
101
+ * - request_permission 兜底桥:answer → 按 label 回显 q0_opt_{i} optionId;
102
+ * decline → q0_skip optionId(无则 cancelled);cancel → cancelled。
103
+ * 答案以 {问题文本: 值} 回传,先按问题顺序展开再协议编码。
104
+ */
105
+ function buildQuestionResponse(action, pending, response) {
106
+ if (pending.proto === 'elicitation') {
107
+ if (action === 'answer') {
108
+ const answers = response.answers;
109
+ const questions = pending.view.questions ?? [];
110
+ const byIndex = mapAnswersByIndex(questions, answers ?? {});
111
+ const content = {};
112
+ questions.forEach((q, i) => {
113
+ const value = byIndex[i];
114
+ if (value === undefined)
115
+ return;
116
+ if (q.multiSelect) {
117
+ const values = Array.isArray(value) ? value : [value];
118
+ const cleaned = values.filter((v) => typeof v === 'string' && v.length > 0);
119
+ if (cleaned.length > 0)
120
+ content[`q${i}`] = cleaned;
121
+ }
122
+ else {
123
+ const single = Array.isArray(value) ? value[0] : value;
124
+ if (typeof single === 'string' && single.length > 0)
125
+ content[`q${i}`] = single;
126
+ }
127
+ });
128
+ return { action: 'accept', content };
129
+ }
130
+ if (action === 'decline')
131
+ return { action: 'decline' };
132
+ return { action: 'cancel' };
133
+ }
134
+ // request_permission 兜底桥(单题单选)
135
+ if (action === 'answer') {
136
+ const answers = response.answers;
137
+ const first = pending.view.questions?.[0];
138
+ const value = first ? answers?.[first.question] : undefined;
139
+ const label = Array.isArray(value) ? value[0] : value;
140
+ const option = (pending.options ?? []).find((opt) => /^q\d+_opt_\d+$/.test(opt.optionId) && opt.name === label);
141
+ if (option)
142
+ return { outcome: { outcome: 'selected', optionId: option.optionId } };
143
+ return { outcome: { outcome: 'cancelled' } };
144
+ }
145
+ if (action === 'decline') {
146
+ const skip = (pending.options ?? []).find((opt) => /^q\d+_skip$/.test(opt.optionId));
147
+ if (skip)
148
+ return { outcome: { outcome: 'selected', optionId: skip.optionId } };
149
+ return { outcome: { outcome: 'cancelled' } };
150
+ }
151
+ return { outcome: { outcome: 'cancelled' } };
152
+ }
153
+ // =============================================================================
154
+ // Runner
155
+ // =============================================================================
156
+ export class KimiAcpRunner extends ConnectionBasedRunner {
157
+ connectionManager;
158
+ currentTranslator = null;
159
+ activeSessionId = null;
160
+ model;
161
+ permissionMode;
162
+ compactPollTimeoutMs;
163
+ /** Pending approval requests: requestId → kind + view + options. */
164
+ pendingApprovals = new Map();
165
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
166
+ promptSettled = false;
167
+ constructor(opts) {
168
+ super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
169
+ this.model = opts.model;
170
+ this.permissionMode = opts.permissionMode ?? 'manual';
171
+ this.compactPollTimeoutMs = opts.compactPollTimeoutMs ?? COMPACT_POLL_TIMEOUT_MS;
172
+ const managerOpts = {
173
+ binary: opts.binary ?? 'kimi',
174
+ args: opts.acpArgs ?? ['acp'],
175
+ env: opts.env,
176
+ requestTimeoutMs: opts.requestTimeoutMs,
177
+ idleTtlMs: opts.idleTtlMs,
178
+ initializeParams: {
179
+ protocolVersion: 1,
180
+ clientCapabilities: {
181
+ fs: { readTextFile: false, writeTextFile: false },
182
+ terminal: false,
183
+ // AskUserQuestion 走 elicitation form(原生多题+多选;form 失败时
184
+ // kimi 服务端自动回退 request_permission 桥,客户端两者都处理)。
185
+ // 注意:ACP SDK 对 elicitation.form 的 zod schema 是对象
186
+ // (z.record(z.string(), z.any())),布尔 true 会被 kimi 服务端静默
187
+ // 丢弃 → elicitationForm=false → 多选回退成 request_permission 单选
188
+ // 桥(勾一个选项即提交)。必须用空对象 {}。
189
+ elicitation: { form: {} },
190
+ },
191
+ },
192
+ };
193
+ this.connectionManager = new KimiAcpConnectionManager(managerOpts);
194
+ }
195
+ get logTag() {
196
+ return 'kimi-acp-runner';
197
+ }
198
+ get turnTimeoutErrorMessage() {
199
+ return 'Kimi ACP turn timed out';
200
+ }
201
+ get turnInterruptedErrorMessage() {
202
+ return 'Kimi ACP turn interrupted';
203
+ }
204
+ currentSessionId() {
205
+ return this.activeSessionId;
206
+ }
207
+ shouldDeferStop() {
208
+ return !this.promptSettled;
209
+ }
210
+ async cancelCurrentTurn() {
211
+ if (!this.currentClient || !this.activeSessionId)
212
+ return;
213
+ try {
214
+ // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
215
+ const cancelParams = { sessionId: this.activeSessionId };
216
+ this.currentClient.notify('session/cancel', cancelParams);
217
+ }
218
+ catch (err) {
219
+ getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
220
+ }
221
+ }
222
+ clearTurnState() {
223
+ this.currentTranslator = null;
224
+ this.promptSettled = false;
225
+ this.activeSessionId = null;
226
+ }
227
+ async releaseConnection(cwd) {
228
+ await this.connectionManager.release(cwd);
229
+ }
230
+ notifyIdle(cwd) {
231
+ this.connectionManager.notifyIdle(cwd);
232
+ }
233
+ async disposeConnections() {
234
+ await this.connectionManager.disposeAll();
235
+ }
236
+ /**
237
+ * Run a compact operation on the current session.
238
+ *
239
+ * Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
240
+ * text (builtin slash command, host intercepts) → consume notifications until
241
+ * prompt settles.
242
+ */
243
+ async *runCompact(_message, opts) {
244
+ yield* this.executeTurn(opts, async () => {
245
+ const client = await this.connectionManager.acquire(opts.cwd);
246
+ this.connectionManager.notifyActivity(opts.cwd);
247
+ this.currentClient = client;
248
+ client.setHooks({
249
+ onNotification: (method, params) => this.handleNotification(method, params),
250
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
251
+ onClose: () => this.failTurn('Kimi ACP connection closed'),
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
+ // R2: compaction is a BACKGROUND task — the prompt settles before the
281
+ // wire.jsonl compaction record lands (real kimi: ~8s). Baseline the
282
+ // record count, fire the prompt without awaiting (notifications flow
283
+ // through consumeTurn), then poll for a NEW record before producing
284
+ // the result. This keeps the connection alive while the background
285
+ // compaction finishes — otherwise dispose/exit kills it and the record
286
+ // never lands (S5/S6 打回复现).
287
+ const compactionReader = this.sessionReader.readCompactionRecords;
288
+ const baselineCount = compactionReader
289
+ ? compactionReader.call(this.sessionReader, sessionId, opts.cwd).length
290
+ : 0;
291
+ this.promptSettled = false;
292
+ // ACP holds the session/prompt response for the ENTIRE turn (unlike
293
+ // codex turn/start), so the RPC-level timeout must not apply here —
294
+ // turn liveness is guarded by the rolling idle watchdog instead.
295
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
296
+ promptPromise.then(async (result) => {
297
+ this.promptSettled = true;
298
+ if (this.stopRequested)
299
+ return; // already cancelled
300
+ if (compactionReader) {
301
+ await this.waitForCompactionRecord(compactionReader, opts, baselineCount);
302
+ }
303
+ if (this.forceFinish)
304
+ return; // stopped while polling
305
+ // Translate prompt result into a result event
306
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
307
+ this.pushEvents([resultEvent]);
308
+ }, (err) => {
309
+ this.promptSettled = true;
310
+ if (this.stopRequested) {
311
+ getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
312
+ return;
313
+ }
314
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
315
+ this.pushEvents([resultEvent]);
316
+ });
317
+ });
318
+ }
319
+ /**
320
+ * Respond to an approval server request. `response` is the bridge
321
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
322
+ */
323
+ async respondApproval(requestId, response) {
324
+ const client = this.currentClient;
325
+ const pending = this.pendingApprovals.get(requestId);
326
+ if (!client || !pending)
327
+ return;
328
+ const action = response?.action ?? 'decline';
329
+ const acpResponse = pending.kind === 'question'
330
+ ? buildQuestionResponse(action, pending, response)
331
+ : buildApprovalResponse(action, pending);
332
+ client.respond(requestId, acpResponse);
333
+ this.pendingApprovals.delete(requestId);
334
+ getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
335
+ }
336
+ getStatusInfo() {
337
+ return {
338
+ kind: this.kind,
339
+ model: this.model ?? '(kimi-acp)',
340
+ extras: {
341
+ mode: 'acp',
342
+ permissionMode: this.permissionMode,
343
+ },
344
+ };
345
+ }
346
+ /**
347
+ * Hot-apply a permission-mode change to the live ACP session (§P5).
348
+ *
349
+ * The local cache is always updated so `getStatusInfo()` (and therefore
350
+ * `/s`) reflects the new mode immediately. When a session is connected,
351
+ * re-sends `session/set_mode` so the running session picks up the new mode
352
+ * without waiting for the runner to be evicted/recreated. Failure is
353
+ * non-fatal: the next setupTurn re-applies the cached mode unconditionally.
354
+ */
355
+ async updateApprovalMode(settings) {
356
+ if (settings.permissionMode !== undefined) {
357
+ this.permissionMode = settings.permissionMode;
358
+ }
359
+ if (!this.currentClient || !this.activeSessionId)
360
+ return;
361
+ const modeParams = {
362
+ sessionId: this.activeSessionId,
363
+ modeId: toAcpMode(this.permissionMode),
364
+ };
365
+ try {
366
+ await this.currentClient.request('session/set_mode', modeParams);
367
+ getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
368
+ }
369
+ catch (err) {
370
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
371
+ }
372
+ }
373
+ // =========================================================================
374
+ // Internal
375
+ // =========================================================================
376
+ /**
377
+ * Acquire the connection and set up the turn:
378
+ * session/new (new session) or session/resume (existing sessionId)
379
+ * → set permission mode if needed → session/prompt.
380
+ */
381
+ async setupTurn(message, opts) {
382
+ const client = await this.connectionManager.acquire(opts.cwd);
383
+ this.connectionManager.notifyActivity(opts.cwd);
384
+ this.currentClient = client;
385
+ client.setHooks({
386
+ onNotification: (method, params) => this.handleNotification(method, params),
387
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
388
+ onClose: () => {
389
+ getLogger().warn(`[${this.logTag}] client connection closed`);
390
+ this.failTurn('Kimi ACP connection closed');
391
+ },
392
+ });
393
+ let sessionId;
394
+ if (opts.sessionId) {
395
+ const resumeParams = {
396
+ sessionId: opts.sessionId,
397
+ cwd: opts.cwd,
398
+ };
399
+ await client.request('session/resume', resumeParams);
400
+ sessionId = opts.sessionId;
401
+ }
402
+ else {
403
+ const newParams = { cwd: opts.cwd, mcpServers: [] };
404
+ const newResult = await client.request('session/new', newParams);
405
+ sessionId = newResult.sessionId;
406
+ }
407
+ this.activeSessionId = sessionId;
408
+ // Set permission mode (§5.1): EVERY fresh ACP session starts in
409
+ // 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
410
+ // so the mode must be sent unconditionally — skipping yolo leaves the
411
+ // session in manual and tool calls stall on unanswered approvals
412
+ // (live dark-test repro: approval_requested → idle timeout).
413
+ const modeParams = {
414
+ sessionId,
415
+ modeId: toAcpMode(this.permissionMode),
416
+ };
417
+ try {
418
+ await client.request('session/set_mode', modeParams);
419
+ }
420
+ catch (err) {
421
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
422
+ }
423
+ const translator = new KimiAcpTranslator();
424
+ this.currentTranslator = translator;
425
+ // Emit turn_started before prompt
426
+ const turnId = `turn-${Date.now()}`;
427
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
428
+ this.currentTurnId = turnId;
429
+ this.pushEvents([turnStarted]);
430
+ // Send the prompt
431
+ const promptParams = {
432
+ sessionId,
433
+ prompt: [{ type: 'text', text: message }],
434
+ };
435
+ // Fire the prompt request; notifications will be buffered in the queue.
436
+ // We process them concurrently.
437
+ this.promptSettled = false;
438
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
439
+ // Wait for prompt to settle, then push the result event.
440
+ // Notifications are pushed concurrently by the hook handlers.
441
+ promptPromise.then((result) => {
442
+ this.promptSettled = true;
443
+ if (this.stopRequested)
444
+ return; // already cancelled
445
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
446
+ this.pushEvents([resultEvent]);
447
+ }, (err) => {
448
+ this.promptSettled = true;
449
+ if (this.stopRequested) {
450
+ getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
451
+ return;
452
+ }
453
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
454
+ this.pushEvents([resultEvent]);
455
+ });
456
+ }
457
+ /**
458
+ * R2: poll wire.jsonl until a NEW compaction record
459
+ * (context.apply_compaction / full_compaction.complete) appears.
460
+ *
461
+ * The /compact prompt settles before the background compaction lands its
462
+ * record; returning early would let the caller dispose the connection and
463
+ * kill the background job. Timeout is WARN-only (the compact may have been
464
+ * a no-op) — never fails the turn. Aborts early on stop/forceFinish.
465
+ */
466
+ async waitForCompactionRecord(reader, opts, baselineCount) {
467
+ const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
468
+ const deadline = Date.now() + this.compactPollTimeoutMs;
469
+ while (Date.now() < deadline) {
470
+ if (this.forceFinish || this.stopRequested)
471
+ return;
472
+ const records = reader.call(this.sessionReader, sessionId, opts.cwd);
473
+ if (records.length > baselineCount) {
474
+ getLogger().info(`[${this.logTag}] compaction record observed (${records.length} > baseline ${baselineCount})`);
475
+ return;
476
+ }
477
+ await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
478
+ }
479
+ getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
480
+ }
481
+ handleNotification(method, params) {
482
+ const events = this.currentTranslator?.handleNotification(method, params) ?? [];
483
+ this.pushEvents(events);
484
+ }
485
+ handleServerRequest(id, method, params) {
486
+ const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
487
+ if (events.length === 0) {
488
+ // Unhandled/unsupported server request must be explicitly responded to
489
+ // (error = "rejected"), otherwise the server hangs waiting for a
490
+ // response. For question elicitation (§5.4), auto-respond cancelled.
491
+ const rpParams = params;
492
+ if (method === ServerRequestMethod.REQUEST_PERMISSION &&
493
+ (rpParams.isQuestion || !rpParams.toolCall)) {
494
+ this.currentClient?.respond(id, { outcome: { outcome: 'cancelled' } });
495
+ getLogger().info(`[${this.logTag}] auto-responded cancelled to question elicitation requestId=${id}`);
496
+ }
497
+ else {
498
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
499
+ }
500
+ return;
501
+ }
502
+ for (const ev of events) {
503
+ if (ev.type === 'approval_requested') {
504
+ this.pendingApprovals.set(ev.requestId, {
505
+ kind: ev.kind,
506
+ view: ev.view,
507
+ options: params.options,
508
+ proto: method === ServerRequestMethod.ELICITATION_CREATE ? 'elicitation' : 'permission',
509
+ });
510
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
511
+ }
512
+ }
513
+ this.pushEvents(events);
514
+ }
515
+ }
516
+ //# sourceMappingURL=runner.js.map