lark-remote 0.1.6 → 0.1.7

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 (188) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +57 -34
  5. package/dist/bridge/approval-coordinator.js +191 -40
  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 +78 -7
  9. package/dist/bridge/index.js +202 -79
  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 +152 -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 +1 -12
  20. package/dist/card/card-shared.js +1 -26
  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.js +99 -232
  24. package/dist/card/run-state.d.ts +13 -0
  25. package/dist/card/run-state.js +25 -16
  26. package/dist/card/text-truncate.d.ts +13 -12
  27. package/dist/card/text-truncate.js +13 -63
  28. package/dist/common/fs.d.ts +8 -0
  29. package/dist/common/fs.js +17 -0
  30. package/dist/common/guards.d.ts +0 -20
  31. package/dist/common/guards.js +0 -43
  32. package/dist/common/sleep.d.ts +2 -0
  33. package/dist/common/sleep.js +5 -0
  34. package/dist/common/truncate.d.ts +39 -0
  35. package/dist/common/truncate.js +92 -0
  36. package/dist/config/codex-config.d.ts +1 -1
  37. package/dist/config/codex-config.js +1 -1
  38. package/dist/config/index.d.ts +88 -23
  39. package/dist/config/index.js +105 -34
  40. package/dist/connector/index.d.ts +69 -2
  41. package/dist/connector/index.js +107 -1
  42. package/dist/index.js +123 -105
  43. package/dist/instance-lock.js +4 -12
  44. package/dist/order/alias-resolve.d.ts +13 -0
  45. package/dist/order/alias-resolve.js +22 -0
  46. package/dist/order/alias-store.d.ts +38 -0
  47. package/dist/order/alias-store.js +106 -0
  48. package/dist/persistence/atomic-write.d.ts +8 -2
  49. package/dist/persistence/atomic-write.js +23 -7
  50. package/dist/restart.js +2 -1
  51. package/dist/router/card-helpers.d.ts +63 -0
  52. package/dist/router/card-helpers.js +146 -0
  53. package/dist/router/config/claude.js +25 -1
  54. package/dist/router/config/codex.js +29 -50
  55. package/dist/router/config/common/model-patch.d.ts +22 -0
  56. package/dist/router/config/common/model-patch.js +26 -0
  57. package/dist/router/config/common/render.js +5 -4
  58. package/dist/router/config/index.d.ts +12 -0
  59. package/dist/router/config/index.js +25 -1
  60. package/dist/router/config/kimi.d.ts +4 -1
  61. package/dist/router/config/kimi.js +27 -0
  62. package/dist/router/config/opencode.js +18 -5
  63. package/dist/router/config/pi.js +4 -5
  64. package/dist/router/config/types.d.ts +11 -2
  65. package/dist/router/index.d.ts +56 -21
  66. package/dist/router/index.js +373 -548
  67. package/dist/router/utils.d.ts +2 -0
  68. package/dist/router/utils.js +4 -0
  69. package/dist/runner/agent-choices-common.d.ts +16 -0
  70. package/dist/runner/agent-choices-common.js +31 -0
  71. package/dist/runner/claude/runner.d.ts +47 -10
  72. package/dist/runner/claude/runner.js +117 -46
  73. package/dist/runner/claude/session.d.ts +166 -0
  74. package/dist/runner/claude/session.js +749 -0
  75. package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
  76. package/dist/runner/codex/app-server/protocol-types.js +1 -12
  77. package/dist/runner/codex/app-server/runner.d.ts +42 -59
  78. package/dist/runner/codex/app-server/runner.js +160 -337
  79. package/dist/runner/codex/app-server/translator.d.ts +1 -21
  80. package/dist/runner/codex/app-server/translator.js +0 -18
  81. package/dist/runner/codex/index.d.ts +0 -2
  82. package/dist/runner/codex/index.js +0 -2
  83. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  84. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  85. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  86. package/dist/runner/common/acp/protocol-types.js +75 -0
  87. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  88. package/dist/runner/common/connection-based-runner.js +261 -0
  89. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  90. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  91. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  92. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  93. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  95. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  96. package/dist/runner/common/jsonrpc/types.js +9 -0
  97. package/dist/runner/common/process-stopper.d.ts +2 -1
  98. package/dist/runner/common/process-stopper.js +1 -0
  99. package/dist/runner/common/runner-utils.d.ts +6 -13
  100. package/dist/runner/common/runner-utils.js +6 -47
  101. package/dist/runner/common/spawning-runner.d.ts +24 -70
  102. package/dist/runner/common/spawning-runner.js +98 -152
  103. package/dist/runner/index.d.ts +4 -4
  104. package/dist/runner/index.js +4 -4
  105. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  106. package/dist/runner/kimi/acp/runner.js +448 -0
  107. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  108. package/dist/runner/kimi/acp/translator.js +336 -0
  109. package/dist/runner/kimi/index.d.ts +1 -1
  110. package/dist/runner/kimi/index.js +1 -1
  111. package/dist/runner/opencode/acp/index.d.ts +1 -0
  112. package/dist/runner/opencode/acp/index.js +2 -0
  113. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  114. package/dist/runner/opencode/acp/runner.js +429 -0
  115. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  116. package/dist/runner/opencode/acp/translator.js +345 -0
  117. package/dist/runner/opencode/index.d.ts +1 -1
  118. package/dist/runner/opencode/index.js +1 -1
  119. package/dist/runner/pi/index.d.ts +1 -1
  120. package/dist/runner/pi/index.js +1 -1
  121. package/dist/runner/pi/rpc/client.d.ts +52 -0
  122. package/dist/runner/pi/rpc/client.js +114 -0
  123. package/dist/runner/pi/rpc/index.d.ts +4 -0
  124. package/dist/runner/pi/rpc/index.js +4 -0
  125. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  126. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  127. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  128. package/dist/runner/pi/rpc/runner.js +199 -0
  129. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  130. package/dist/runner/pi/rpc/translator.js +258 -0
  131. package/dist/runner/registry.d.ts +7 -8
  132. package/dist/runner/registry.js +3 -4
  133. package/dist/runner/resolve-agent-choices.js +18 -59
  134. package/dist/runner/sync-agent-choices.js +16 -67
  135. package/dist/runner/types.d.ts +43 -13
  136. package/dist/session/claude/session-index.js +2 -3
  137. package/dist/session/claude/session-reader.js +3 -6
  138. package/dist/session/claude/sessions.js +6 -13
  139. package/dist/session/codex/rollout-reader.d.ts +2 -2
  140. package/dist/session/codex/rollout-reader.js +14 -17
  141. package/dist/session/codex/sessions.d.ts +1 -1
  142. package/dist/session/codex/sessions.js +2 -2
  143. package/dist/session/common/content-blocks.d.ts +7 -0
  144. package/dist/session/common/content-blocks.js +13 -9
  145. package/dist/session/common/jsonl.d.ts +8 -25
  146. package/dist/session/common/jsonl.js +126 -261
  147. package/dist/session/common/pagination.d.ts +28 -0
  148. package/dist/session/common/pagination.js +33 -0
  149. package/dist/session/kimi/sessions.d.ts +34 -1
  150. package/dist/session/kimi/sessions.js +208 -98
  151. package/dist/session/opencode/sessions.d.ts +17 -3
  152. package/dist/session/opencode/sessions.js +29 -29
  153. package/dist/session/pi/sessions.d.ts +5 -1
  154. package/dist/session/pi/sessions.js +12 -16
  155. package/dist/session/session-store.d.ts +11 -1
  156. package/dist/session/session-store.js +75 -178
  157. package/dist/update/index.d.ts +2 -2
  158. package/dist/update/index.js +2 -2
  159. package/dist/update/install.d.ts +20 -2
  160. package/dist/update/install.js +51 -7
  161. package/package.json +1 -1
  162. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  163. package/dist/runner/codex/argv.d.ts +0 -33
  164. package/dist/runner/codex/argv.js +0 -40
  165. package/dist/runner/codex/factory.d.ts +0 -29
  166. package/dist/runner/codex/factory.js +0 -39
  167. package/dist/runner/codex/jsonl.d.ts +0 -37
  168. package/dist/runner/codex/jsonl.js +0 -221
  169. package/dist/runner/codex/runner.d.ts +0 -66
  170. package/dist/runner/codex/runner.js +0 -129
  171. package/dist/runner/common/exec-translator.d.ts +0 -73
  172. package/dist/runner/common/exec-translator.js +0 -105
  173. package/dist/runner/kimi/jsonl.d.ts +0 -28
  174. package/dist/runner/kimi/jsonl.js +0 -143
  175. package/dist/runner/kimi/runner.d.ts +0 -49
  176. package/dist/runner/kimi/runner.js +0 -105
  177. package/dist/runner/opencode/argv.d.ts +0 -32
  178. package/dist/runner/opencode/argv.js +0 -35
  179. package/dist/runner/opencode/jsonl.d.ts +0 -68
  180. package/dist/runner/opencode/jsonl.js +0 -295
  181. package/dist/runner/opencode/runner.d.ts +0 -61
  182. package/dist/runner/opencode/runner.js +0 -93
  183. package/dist/runner/pi/jsonl.d.ts +0 -128
  184. package/dist/runner/pi/jsonl.js +0 -249
  185. package/dist/runner/pi/runner.d.ts +0 -36
  186. package/dist/runner/pi/runner.js +0 -76
  187. package/dist/session/common/catch-up.d.ts +0 -12
  188. package/dist/session/common/catch-up.js +0 -31
@@ -0,0 +1,448 @@
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 { getLogger } from '../../../logger/index.js';
19
+ import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
20
+ /** R2: how long to wait for the background compaction record after the
21
+ * /compact prompt settles (design doc §6.1). Real kimi 0.36.0 lands
22
+ * context.apply_compaction ~8s after end_turn. */
23
+ const COMPACT_POLL_TIMEOUT_MS = 30_000;
24
+ /** Poll interval for the wire.jsonl compaction record. */
25
+ const COMPACT_POLL_INTERVAL_MS = 1_000;
26
+ /**
27
+ * Map user-facing permission mode to ACP mode id.
28
+ *
29
+ * ACP layer mode ids (acp-server/src/modes.ts:49):
30
+ * default → permission manual
31
+ * auto → permission auto
32
+ * yolo → permission yolo
33
+ * plan → not exposed in v1
34
+ *
35
+ * This is not self-invented naming — both are official enums at their
36
+ * respective layers. The mapping happens only at the ACP boundary.
37
+ */
38
+ function toAcpMode(mode) {
39
+ switch (mode) {
40
+ case 'manual':
41
+ return 'default';
42
+ case 'auto':
43
+ return 'auto';
44
+ case 'yolo':
45
+ return 'yolo';
46
+ }
47
+ }
48
+ /**
49
+ * Build the ACP protocol response for a permission approval decision.
50
+ *
51
+ * accept → {outcome:{outcome:'selected', optionId:<approve_once kind>}}
52
+ * accept_for_session → {outcome:{outcome:'selected', optionId:<approve_always kind>}}
53
+ * decline → {outcome:{outcome:'selected', optionId:<reject kind>}}
54
+ * cancel → {outcome:{outcome:'cancelled'}}
55
+ *
56
+ * optionId is looked up from the request's options[] by kind (approval.ts:28-29):
57
+ * approve_once / approve_always / allow_once for accept;
58
+ * approve_always / allow_always for accept_for_session;
59
+ * reject / reject_once for decline. If not found, fall back to cancelled
60
+ * (safe universal default — the server treats it as "user declined").
61
+ *
62
+ * 2026-08-15 live test: approve_once + approve_always + reject observed.
63
+ */
64
+ function buildApprovalResponse(action, pending) {
65
+ if (action === 'cancel') {
66
+ return { outcome: { outcome: 'cancelled' } };
67
+ }
68
+ if (action === 'accept') {
69
+ const optionId = findOptionIdByKind(pending.options, [
70
+ 'approve_once',
71
+ 'approve_always',
72
+ 'allow_once',
73
+ ]);
74
+ if (optionId) {
75
+ return { outcome: { outcome: 'selected', optionId } };
76
+ }
77
+ // Fallback: can't find allow option → cancel is safe
78
+ return { outcome: { outcome: 'cancelled' } };
79
+ }
80
+ if (action === 'accept_for_session') {
81
+ // §P4: 卡片「允许本次会话」→ always 类 optionId(kimi approve_always)
82
+ const optionId = findOptionIdByKind(pending.options, ['approve_always', 'allow_always']);
83
+ if (optionId) {
84
+ return { outcome: { outcome: 'selected', optionId } };
85
+ }
86
+ return { outcome: { outcome: 'cancelled' } };
87
+ }
88
+ // decline
89
+ const optionId = findOptionIdByKind(pending.options, ['reject', 'reject_once']);
90
+ if (optionId) {
91
+ return { outcome: { outcome: 'selected', optionId } };
92
+ }
93
+ return { outcome: { outcome: 'cancelled' } };
94
+ }
95
+ // =============================================================================
96
+ // Runner
97
+ // =============================================================================
98
+ export class KimiAcpRunner extends ConnectionBasedRunner {
99
+ connectionManager;
100
+ currentTranslator = null;
101
+ activeSessionId = null;
102
+ model;
103
+ permissionMode;
104
+ compactPollTimeoutMs;
105
+ /** Pending approval requests: requestId → kind + view + options. */
106
+ pendingApprovals = new Map();
107
+ /** Tracker for the in-flight prompt request — needed for cancellation. */
108
+ promptSettled = false;
109
+ constructor(opts) {
110
+ super({ kind: opts.kind, sessionReader: opts.sessionReader }, opts.turnIdleTimeoutMs);
111
+ this.model = opts.model;
112
+ this.permissionMode = opts.permissionMode ?? 'manual';
113
+ this.compactPollTimeoutMs = opts.compactPollTimeoutMs ?? COMPACT_POLL_TIMEOUT_MS;
114
+ const managerOpts = {
115
+ binary: opts.binary ?? 'kimi',
116
+ args: opts.acpArgs ?? ['acp'],
117
+ env: opts.env,
118
+ requestTimeoutMs: opts.requestTimeoutMs,
119
+ idleTtlMs: opts.idleTtlMs,
120
+ initializeParams: {
121
+ protocolVersion: 1,
122
+ clientCapabilities: {
123
+ fs: { readTextFile: false, writeTextFile: false },
124
+ terminal: false,
125
+ },
126
+ },
127
+ };
128
+ this.connectionManager = new KimiAcpConnectionManager(managerOpts);
129
+ }
130
+ get logTag() {
131
+ return 'kimi-acp-runner';
132
+ }
133
+ get turnTimeoutErrorMessage() {
134
+ return 'Kimi ACP turn timed out';
135
+ }
136
+ get turnInterruptedErrorMessage() {
137
+ return 'Kimi ACP turn interrupted';
138
+ }
139
+ currentSessionId() {
140
+ return this.activeSessionId;
141
+ }
142
+ shouldDeferStop() {
143
+ return !this.promptSettled;
144
+ }
145
+ async cancelCurrentTurn() {
146
+ if (!this.currentClient || !this.activeSessionId)
147
+ return;
148
+ try {
149
+ // session/cancel is a NOTIFICATION (no id, no response) per ACP spec.
150
+ const cancelParams = { sessionId: this.activeSessionId };
151
+ this.currentClient.notify('session/cancel', cancelParams);
152
+ }
153
+ catch (err) {
154
+ getLogger().warn(`[${this.logTag}] session/cancel failed: ${err.message}`);
155
+ }
156
+ }
157
+ clearTurnState() {
158
+ this.currentTranslator = null;
159
+ this.promptSettled = false;
160
+ this.activeSessionId = null;
161
+ }
162
+ async releaseConnection(cwd) {
163
+ await this.connectionManager.release(cwd);
164
+ }
165
+ notifyIdle(cwd) {
166
+ this.connectionManager.notifyIdle(cwd);
167
+ }
168
+ async disposeConnections() {
169
+ await this.connectionManager.disposeAll();
170
+ }
171
+ /**
172
+ * Run a compact operation on the current session.
173
+ *
174
+ * Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
175
+ * text (builtin slash command, host intercepts) → consume notifications until
176
+ * prompt settles.
177
+ */
178
+ async *runCompact(_message, opts) {
179
+ yield* this.executeTurn(opts, async () => {
180
+ const client = await this.connectionManager.acquire(opts.cwd);
181
+ this.connectionManager.notifyActivity(opts.cwd);
182
+ this.currentClient = client;
183
+ client.setHooks({
184
+ onNotification: (method, params) => this.handleNotification(method, params),
185
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
186
+ onClose: () => this.failTurn('Kimi ACP connection closed'),
187
+ });
188
+ const sessionId = opts.sessionId;
189
+ if (!sessionId) {
190
+ throw new Error('compact requires a sessionId');
191
+ }
192
+ this.activeSessionId = sessionId;
193
+ // Cold-connection fallback: session/resume loads the session into memory.
194
+ // On a reused connection the session is already loaded — resume is
195
+ // idempotent and harmless; after connection rebuild it's required.
196
+ const resumeParams = {
197
+ sessionId,
198
+ cwd: opts.cwd,
199
+ };
200
+ await client.request('session/resume', resumeParams);
201
+ const translator = new KimiAcpTranslator();
202
+ translator.setOperationKind('compact');
203
+ this.currentTranslator = translator;
204
+ // Emit turn_started with operationKind='compaction' before the prompt
205
+ // (consumeTurn yields it while the background compaction runs).
206
+ const turnId = `compact-${Date.now()}`;
207
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
208
+ this.currentTurnId = turnId;
209
+ this.pushEvents([turnStarted]);
210
+ // Send /compact as prompt text
211
+ const promptParams = {
212
+ sessionId,
213
+ prompt: [{ type: 'text', text: '/compact' }],
214
+ };
215
+ // R2: compaction is a BACKGROUND task — the prompt settles before the
216
+ // wire.jsonl compaction record lands (real kimi: ~8s). Baseline the
217
+ // record count, fire the prompt without awaiting (notifications flow
218
+ // through consumeTurn), then poll for a NEW record before producing
219
+ // the result. This keeps the connection alive while the background
220
+ // compaction finishes — otherwise dispose/exit kills it and the record
221
+ // never lands (S5/S6 打回复现).
222
+ const compactionReader = this.sessionReader.readCompactionRecords;
223
+ const baselineCount = compactionReader
224
+ ? compactionReader.call(this.sessionReader, sessionId, opts.cwd).length
225
+ : 0;
226
+ this.promptSettled = false;
227
+ // ACP holds the session/prompt response for the ENTIRE turn (unlike
228
+ // codex turn/start), so the RPC-level timeout must not apply here —
229
+ // turn liveness is guarded by the rolling idle watchdog instead.
230
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
231
+ promptPromise.then(async (result) => {
232
+ this.promptSettled = true;
233
+ if (this.stopRequested)
234
+ return; // already cancelled
235
+ if (compactionReader) {
236
+ await this.waitForCompactionRecord(compactionReader, opts, baselineCount);
237
+ }
238
+ if (this.forceFinish)
239
+ return; // stopped while polling
240
+ // Translate prompt result into a result event
241
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
242
+ this.pushEvents([resultEvent]);
243
+ }, (err) => {
244
+ this.promptSettled = true;
245
+ if (this.stopRequested) {
246
+ getLogger().warn(`[${this.logTag}] compact prompt rejected after stopRequested: ${err.message}`);
247
+ return;
248
+ }
249
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
250
+ this.pushEvents([resultEvent]);
251
+ });
252
+ });
253
+ }
254
+ /**
255
+ * Respond to an approval server request. `response` is the bridge
256
+ * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
257
+ */
258
+ async respondApproval(requestId, response) {
259
+ const client = this.currentClient;
260
+ const pending = this.pendingApprovals.get(requestId);
261
+ if (!client || !pending)
262
+ return;
263
+ const action = response?.action ?? 'decline';
264
+ const acpResponse = buildApprovalResponse(action, pending);
265
+ client.respond(requestId, acpResponse);
266
+ this.pendingApprovals.delete(requestId);
267
+ getLogger().info(`[${this.logTag}] approval responded requestId=${requestId} action=${action}`);
268
+ }
269
+ getStatusInfo() {
270
+ return {
271
+ kind: this.kind,
272
+ model: this.model ?? '(kimi-acp)',
273
+ extras: {
274
+ mode: 'acp',
275
+ permissionMode: this.permissionMode,
276
+ },
277
+ };
278
+ }
279
+ /**
280
+ * Hot-apply a permission-mode change to the live ACP session (§P5).
281
+ *
282
+ * The local cache is always updated so `getStatusInfo()` (and therefore
283
+ * `/s`) reflects the new mode immediately. When a session is connected,
284
+ * re-sends `session/set_mode` so the running session picks up the new mode
285
+ * without waiting for the runner to be evicted/recreated. Failure is
286
+ * non-fatal: the next setupTurn re-applies the cached mode unconditionally.
287
+ */
288
+ async updateApprovalMode(settings) {
289
+ if (settings.permissionMode !== undefined) {
290
+ this.permissionMode = settings.permissionMode;
291
+ }
292
+ if (!this.currentClient || !this.activeSessionId)
293
+ return;
294
+ const modeParams = {
295
+ sessionId: this.activeSessionId,
296
+ modeId: toAcpMode(this.permissionMode),
297
+ };
298
+ try {
299
+ await this.currentClient.request('session/set_mode', modeParams);
300
+ getLogger().info(`[${this.logTag}] session/set_mode hot-applied session=${this.activeSessionId} modeId=${modeParams.modeId}`);
301
+ }
302
+ catch (err) {
303
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
304
+ }
305
+ }
306
+ // =========================================================================
307
+ // Internal
308
+ // =========================================================================
309
+ /**
310
+ * Acquire the connection and set up the turn:
311
+ * session/new (new session) or session/resume (existing sessionId)
312
+ * → set permission mode if needed → session/prompt.
313
+ */
314
+ async setupTurn(message, opts) {
315
+ const client = await this.connectionManager.acquire(opts.cwd);
316
+ this.connectionManager.notifyActivity(opts.cwd);
317
+ this.currentClient = client;
318
+ client.setHooks({
319
+ onNotification: (method, params) => this.handleNotification(method, params),
320
+ onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
321
+ onClose: () => {
322
+ getLogger().warn(`[${this.logTag}] client connection closed`);
323
+ this.failTurn('Kimi ACP connection closed');
324
+ },
325
+ });
326
+ let sessionId;
327
+ if (opts.sessionId) {
328
+ const resumeParams = {
329
+ sessionId: opts.sessionId,
330
+ cwd: opts.cwd,
331
+ };
332
+ await client.request('session/resume', resumeParams);
333
+ sessionId = opts.sessionId;
334
+ }
335
+ else {
336
+ const newParams = { cwd: opts.cwd, mcpServers: [] };
337
+ const newResult = await client.request('session/new', newParams);
338
+ sessionId = newResult.sessionId;
339
+ }
340
+ this.activeSessionId = sessionId;
341
+ // Set permission mode (§5.1): EVERY fresh ACP session starts in
342
+ // 'default' (= manual approvals, acp-server modes.ts DEFAULT_MODE_ID),
343
+ // so the mode must be sent unconditionally — skipping yolo leaves the
344
+ // session in manual and tool calls stall on unanswered approvals
345
+ // (live dark-test repro: approval_requested → idle timeout).
346
+ const modeParams = {
347
+ sessionId,
348
+ modeId: toAcpMode(this.permissionMode),
349
+ };
350
+ try {
351
+ await client.request('session/set_mode', modeParams);
352
+ }
353
+ catch (err) {
354
+ getLogger().warn(`[${this.logTag}] session/set_mode failed (non-fatal): ${err.message}`);
355
+ }
356
+ const translator = new KimiAcpTranslator();
357
+ this.currentTranslator = translator;
358
+ // Emit turn_started before prompt
359
+ const turnId = `turn-${Date.now()}`;
360
+ const turnStarted = translator.produceTurnStarted(sessionId, turnId);
361
+ this.currentTurnId = turnId;
362
+ this.pushEvents([turnStarted]);
363
+ // Send the prompt
364
+ const promptParams = {
365
+ sessionId,
366
+ prompt: [{ type: 'text', text: message }],
367
+ };
368
+ // Fire the prompt request; notifications will be buffered in the queue.
369
+ // We process them concurrently.
370
+ this.promptSettled = false;
371
+ const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
372
+ // Wait for prompt to settle, then push the result event.
373
+ // Notifications are pushed concurrently by the hook handlers.
374
+ promptPromise.then((result) => {
375
+ this.promptSettled = true;
376
+ if (this.stopRequested)
377
+ return; // already cancelled
378
+ const resultEvent = translator.handlePromptResponse(sessionId, result);
379
+ this.pushEvents([resultEvent]);
380
+ }, (err) => {
381
+ this.promptSettled = true;
382
+ if (this.stopRequested) {
383
+ getLogger().warn(`[${this.logTag}] prompt rejected after stopRequested: ${err.message}`);
384
+ return;
385
+ }
386
+ const resultEvent = translator.produceErrorResult(sessionId, err.message);
387
+ this.pushEvents([resultEvent]);
388
+ });
389
+ }
390
+ /**
391
+ * R2: poll wire.jsonl until a NEW compaction record
392
+ * (context.apply_compaction / full_compaction.complete) appears.
393
+ *
394
+ * The /compact prompt settles before the background compaction lands its
395
+ * record; returning early would let the caller dispose the connection and
396
+ * kill the background job. Timeout is WARN-only (the compact may have been
397
+ * a no-op) — never fails the turn. Aborts early on stop/forceFinish.
398
+ */
399
+ async waitForCompactionRecord(reader, opts, baselineCount) {
400
+ const sessionId = this.activeSessionId ?? opts.sessionId ?? '';
401
+ const deadline = Date.now() + this.compactPollTimeoutMs;
402
+ while (Date.now() < deadline) {
403
+ if (this.forceFinish || this.stopRequested)
404
+ return;
405
+ const records = reader.call(this.sessionReader, sessionId, opts.cwd);
406
+ if (records.length > baselineCount) {
407
+ getLogger().info(`[${this.logTag}] compaction record observed (${records.length} > baseline ${baselineCount})`);
408
+ return;
409
+ }
410
+ await new Promise((resolve) => setTimeout(resolve, COMPACT_POLL_INTERVAL_MS));
411
+ }
412
+ getLogger().warn(`[${this.logTag}] compaction record not observed within ${this.compactPollTimeoutMs}ms (baseline=${baselineCount}); proceeding without confirmation`);
413
+ }
414
+ handleNotification(method, params) {
415
+ const events = this.currentTranslator?.handleNotification(method, params) ?? [];
416
+ this.pushEvents(events);
417
+ }
418
+ handleServerRequest(id, method, params) {
419
+ const events = this.currentTranslator?.handleServerRequest(id, method, params) ?? [];
420
+ if (events.length === 0) {
421
+ // Unhandled/unsupported server request must be explicitly responded to
422
+ // (error = "rejected"), otherwise the server hangs waiting for a
423
+ // response. For question elicitation (§5.4), auto-respond cancelled.
424
+ const rpParams = params;
425
+ if (method === ServerRequestMethod.REQUEST_PERMISSION &&
426
+ (rpParams.isQuestion || !rpParams.toolCall)) {
427
+ this.currentClient?.respond(id, { outcome: { outcome: 'cancelled' } });
428
+ getLogger().info(`[${this.logTag}] auto-responded cancelled to question elicitation requestId=${id}`);
429
+ }
430
+ else {
431
+ this.currentClient?.respondError(id, RpcErrorCode.METHOD_NOT_FOUND, `Unsupported server request: ${method}`);
432
+ }
433
+ return;
434
+ }
435
+ for (const ev of events) {
436
+ if (ev.type === 'approval_requested') {
437
+ this.pendingApprovals.set(ev.requestId, {
438
+ kind: ev.kind,
439
+ view: ev.view,
440
+ options: params.options,
441
+ });
442
+ getLogger().info(`[${this.logTag}] approval requested requestId=${ev.requestId} kind=${ev.kind}`);
443
+ }
444
+ }
445
+ this.pushEvents(events);
446
+ }
447
+ }
448
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * KimiAcpTranslator: translates ACP session/update notifications and
3
+ * session/request_permission server requests into AgentEvents.
4
+ *
5
+ * Per-turn stateful: accumulates text/thinking deltas, tracks tool calls,
6
+ * and produces turn_started events with operationKind.
7
+ *
8
+ * Wire envelope (R1, source: kimi-code packages/acp-server/src/events-map.ts,
9
+ * verified live 2026-08-15/16 against kimi 0.36.0):
10
+ * session/update params = {sessionId, update: {sessionUpdate: '<kind>', ...}}
11
+ * — the discriminator is `update.sessionUpdate`, NOT a nested `event.type`.
12
+ *
13
+ * Event mapping (design doc §4.2):
14
+ * agent_message_chunk → turn_diff text snapshot (content.text accumulation)
15
+ * agent_thought_chunk → turn_diff reasoning snapshot (content.text accumulation)
16
+ * tool_call → assistant/tool_use (rawInput object passthrough,
17
+ * string → JSON.parse fallback)
18
+ * tool_call_update → user/tool_result (status:'failed' → is_error:true)
19
+ * plan / available_commands_update / session_info_update /
20
+ * current_mode_update / config_option_update → discard
21
+ * usage_update → live context occupancy {used, size} →
22
+ * {total_tokens, context_limit}; NO input/output
23
+ * split exists on the wire — cumulative token stats
24
+ * fall back to wire.jsonl usage.record (dual path)
25
+ * prompt stopReason:'end_turn' → result success
26
+ * prompt stopReason:'cancelled' → result interrupted (独立终态)
27
+ * prompt other stopReason/error → result error
28
+ * turn_started (self-produced) → operationKind 'turn'/'compaction'
29
+ * session/request_permission → approval_requested
30
+ */
31
+ import type { AgentEvent } from '../../types.js';
32
+ import type { ApprovalRequestedEvent } from '../../types.js';
33
+ /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
34
+ export interface AcpTurnStartedEvent {
35
+ type: 'turn_started';
36
+ threadId: string;
37
+ turnId: string;
38
+ operationKind: 'turn' | 'compaction';
39
+ timestamp?: string;
40
+ }
41
+ /**
42
+ * Live usage snapshot from usage_update. The wire only carries context
43
+ * occupancy (`used`) and the model context window (`size`) — there is NO
44
+ * input/output token split (events-map.ts:505-516). input_tokens/output_tokens
45
+ * stay undefined so the bridge falls back to wire.jsonl usage.record for
46
+ * cumulative token stats (R1 dual path).
47
+ */
48
+ export interface AcpLiveUsage {
49
+ total_tokens?: number;
50
+ context_limit?: number;
51
+ input_tokens?: number;
52
+ output_tokens?: number;
53
+ }
54
+ /** Usage update event for live token display. */
55
+ export interface AcpUsageEvent {
56
+ type: 'usage';
57
+ usage: AcpLiveUsage;
58
+ timestamp?: string;
59
+ }
60
+ export type AcpTranslatorEvent = AgentEvent | AcpTurnStartedEvent | AcpUsageEvent | ApprovalRequestedEvent;
61
+ export declare class KimiAcpTranslator {
62
+ /** Accumulated text per tool-call or message (keyed by a logical item id). */
63
+ private textByItem;
64
+ /** Accumulated thinking per item. */
65
+ private thinkingByItem;
66
+ /** Pending tool calls keyed by toolCallId (for matching tool_call_update). */
67
+ private pendingToolCalls;
68
+ /** Current operation kind (turn vs compact). */
69
+ private operationKind;
70
+ /** Live usage snapshot from usage_update events (context occupancy only). */
71
+ private liveUsage;
72
+ /** Whether any usage_update has been seen this turn (no update → no usage). */
73
+ private hasLiveUsage;
74
+ /** Current turn id, set by produceTurnStarted; carried on turn_diff events. */
75
+ private currentTurnId;
76
+ /**
77
+ * Handle a notification from the ACP server and return translated events.
78
+ */
79
+ handleNotification(method: string, params: unknown): AcpTranslatorEvent[];
80
+ /**
81
+ * Handle a server request (reverse RPC from the ACP server).
82
+ */
83
+ handleServerRequest(id: number | string, method: string, params: unknown): AcpTranslatorEvent[];
84
+ /**
85
+ * Produce a turn_started event. Called by the runner when session/prompt
86
+ * is sent (no wire notification for this — self-produced).
87
+ */
88
+ produceTurnStarted(sessionId: string, turnId: string): AcpTurnStartedEvent;
89
+ /**
90
+ * Translate a prompt response into a result event. Carries the live usage
91
+ * snapshot only when a usage_update was seen (otherwise undefined, so the
92
+ * bridge falls back to wire.jsonl usage.record readback).
93
+ */
94
+ handlePromptResponse(sessionId: string, result: {
95
+ stopReason: string;
96
+ }): AgentEvent;
97
+ /**
98
+ * Produce an error result event (for JSON-RPC errors during prompt).
99
+ */
100
+ produceErrorResult(sessionId: string, errorMessage: string): AgentEvent;
101
+ /**
102
+ * Set the operation kind (turn vs compact).
103
+ */
104
+ setOperationKind(kind: 'turn' | 'compact'): void;
105
+ private handleAgentMessageChunk;
106
+ private handleAgentThoughtChunk;
107
+ private handleToolCall;
108
+ private handleToolCallUpdate;
109
+ private handleUsageUpdate;
110
+ private handleRequestPermission;
111
+ /**
112
+ * Derive the approval decision list from the server's offered options.
113
+ * accept/decline/cancel are universal; acceptForSession requires an
114
+ * always-class option (kimi `approve_always`, opencode `allow_always`).
115
+ */
116
+ private deriveAvailableDecisions;
117
+ /**
118
+ * ResultEvent.usage requires input_tokens/output_tokens: number, but the
119
+ * ACP wire has no such split — the live snapshot is context occupancy only.
120
+ * Cast is deliberate: consumers must tolerate undefined input/output (the
121
+ * bridge's kimi path already falls back to wire.jsonl readback).
122
+ */
123
+ private liveUsageSnapshot;
124
+ }