lark-remote 0.1.5 → 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 (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  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 +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  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/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  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 +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. package/dist/session/common/catch-up.js +0 -31
@@ -23,6 +23,8 @@ interface NormalizedResultUsage {
23
23
  cacheReadTokens?: number;
24
24
  cacheCreationTokens?: number;
25
25
  totalTokens?: number;
26
+ /** Model context window limit (codex app-server `modelContextWindow` passthrough). */
27
+ contextLimit?: number;
26
28
  /** Reconstructed or agent-declared context length (see JSDoc above). */
27
29
  contextLength: number;
28
30
  }
@@ -14,6 +14,7 @@ export function normalizeResultUsage(usage) {
14
14
  const cacheRead = usage.cache_read_tokens ?? usage.cache_read_input_tokens;
15
15
  const cacheCreation = usage.cache_creation_tokens ?? usage.cache_creation_input_tokens;
16
16
  const totalTokens = usage.total_tokens;
17
+ const contextLimit = usage.context_limit;
17
18
  // total_tokens (agent-declared total) takes priority; otherwise reconstruct
18
19
  // from the parts: input + cacheRead + cacheCreation + output.
19
20
  const contextLength = totalTokens ?? inputTokens + (cacheRead ?? 0) + (cacheCreation ?? 0) + outputTokens;
@@ -23,6 +24,7 @@ export function normalizeResultUsage(usage) {
23
24
  cacheReadTokens: cacheRead,
24
25
  cacheCreationTokens: cacheCreation,
25
26
  totalTokens,
27
+ contextLimit,
26
28
  contextLength,
27
29
  };
28
30
  }
@@ -8,12 +8,12 @@
8
8
  * (共享工具由消费方直接 import `common/*` 具体模块,不走 barrel)
9
9
  */
10
10
  export { ClaudeRunner } from './claude/index.js';
11
- export { CodexExecRunner } from './codex/index.js';
12
- export { OpencodeExecRunner } from './opencode/index.js';
13
- export { PiRunner } from './pi/index.js';
14
- export { KimiRunner } from './kimi/index.js';
11
+ export { CodexAppServerRunner } from './codex/index.js';
12
+ export { OpencodeAcpRunner } from './opencode/index.js';
13
+ export { PiRpcRunner } from './pi/index.js';
14
+ export { KimiAcpRunner } from './kimi/index.js';
15
15
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
16
16
  export { AgentRegistry } from './registry.js';
17
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, } from './types.js';
17
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
18
18
  export { resolveAgentChoices } from './resolve-agent-choices.js';
19
19
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -9,10 +9,10 @@
9
9
  */
10
10
  // Re-export AgentRunner 实现(从各 agent 目录)
11
11
  export { ClaudeRunner } from './claude/index.js';
12
- export { CodexExecRunner } from './codex/index.js';
13
- export { OpencodeExecRunner } from './opencode/index.js';
14
- export { PiRunner } from './pi/index.js';
15
- export { KimiRunner } from './kimi/index.js';
12
+ export { CodexAppServerRunner } from './codex/index.js';
13
+ export { OpencodeAcpRunner } from './opencode/index.js';
14
+ export { PiRpcRunner } from './pi/index.js';
15
+ export { KimiAcpRunner } from './kimi/index.js';
16
16
  export { BashProcessRunner } from './bash/index.js';
17
17
  // Re-export registry
18
18
  export { AgentRegistry } from './registry.js';
@@ -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,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