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
@@ -2,6 +2,7 @@ import { spawn, execFileSync } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import fs from 'node:fs';
5
+ import { silentlyUnlink } from '../../common/fs.js';
5
6
  import { getLogger } from '../../logger/index.js';
6
7
  import { ProcessStopper } from './process-stopper.js';
7
8
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
@@ -17,6 +18,9 @@ import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
17
18
  const STDERR_TAIL_BYTES = 4000;
18
19
  /** Max bytes of stderr surfaced in the non-zero-exit log line. */
19
20
  const STDERR_LOG_TAIL = 500;
21
+ /** Error thrown by `spawnChild` when the child process fails to spawn. */
22
+ class SpawnChildError extends Error {
23
+ }
20
24
  /**
21
25
  * Timeout (ms) for awaiting the spawn 'error' event when proc.pid === undefined
22
26
  * (review P2-11). Node guarantees 'error' for ENOENT/EACCES, but some binaries
@@ -58,26 +62,21 @@ export function unregisterExitCleanup(handler) {
58
62
  * Abstract base class for runners that spawn a child process per turn.
59
63
  *
60
64
  * Encapsulates the spawn + completion + cleanup orchestration shared by
61
- * Claude/Codex/OpenCode/Pi/Kimi runners. Subclasses override:
65
+ * Claude/Pi runners. Subclasses override:
62
66
  * - buildArgv(opts) → agent-specific CLI flags (required)
63
- * - createTranslator(opts) → stateful translator (codex/opencode/pi/kimi)
67
+ * - createTranslator(opts) → stateful translator (pi)
64
68
  * OR translate(raw, ctx) → stateless passthrough (claude)
65
69
  *
66
70
  * Optional hooks (with sensible defaults):
67
71
  * - getStdio() → stdio config (default ['ignore','pipe','pipe'])
68
- * - getSpawnEnv(opts) → env vars (default process.env)
69
- * - setupStdin(proc, message) → write prompt to stdin (default no-op)
70
72
  * - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
71
- * - validateBeforeRun(opts) → pre-spawn validation (default no-op)
72
- * - awaitCompletion(proc, p) → wait for exit (default return p; kimi adds 5s race)
73
- * - awaitSpawnError(proc) → wait for spawn error (default once('error'); kimi adds 5s race)
74
- * - resolveTranslatorError(t, c) → translatorError precedence (default honors agent errors)
73
+ * - awaitSpawnError(proc) → wait for spawn error (default once('error'))
75
74
  *
76
75
  * The base class always emits a unified result event at the end of `run()`
77
76
  * via `buildResultEvent(...)` — never throws on non-zero exit. This matches
78
- * the contract pinned by codex/opencode/pi/kimi subclass anchors and the
79
- * bridge's `runAgentStreamToEnd` error handling (which accepts both thrown
80
- * errors and yielded error-result events).
77
+ * the contract pinned by the agent subclass anchors and the bridge's
78
+ * `runAgentStreamToEnd` error handling (which accepts both thrown errors and
79
+ * yielded error-result events).
81
80
  */
82
81
  export class SpawningRunner {
83
82
  currentProcess = null;
@@ -86,6 +85,8 @@ export class SpawningRunner {
86
85
  spawnHeartbeat;
87
86
  binary;
88
87
  stopGraceMs;
88
+ /** Accumulated stderr tail for the current run (filled by spawnChild). */
89
+ spawnStderr = '';
89
90
  /**
90
91
  * Log prefix used in all operational log lines emitted by this runner
91
92
  * (spawn, pid file, non-zero exit, stderr, killOrphan, stop cleanup) and
@@ -130,72 +131,31 @@ export class SpawningRunner {
130
131
  // --- Hooks (subclasses override as needed) ---
131
132
  /**
132
133
  * stdio config for spawn. Default: stdin ignored (claude/kimi/pi pass the
133
- * prompt via argv). codex/opencode override to ['pipe','pipe','pipe'] so
134
- * they can write the prompt to stdin via `setupStdin`.
134
+ * prompt via argv).
135
135
  */
136
136
  getStdio() {
137
137
  return ['ignore', 'pipe', 'pipe'];
138
138
  }
139
- /**
140
- * Env vars for spawn. Default: process.env. opencode overrides to sync
141
- * PWD=opts.cwd (opencode's directory detection reads PWD, not cwd).
142
- */
143
- getSpawnEnv(_opts) {
144
- return process.env;
145
- }
146
- /**
147
- * Write the prompt to the child's stdin (called after spawn succeeds).
148
- * Default: no-op. codex/opencode override to `proc.stdin.end(message)`.
149
- */
150
- setupStdin(_proc, _message) {
151
- // no-op by default
152
- }
153
139
  /**
154
140
  * Create an async iterator over the child's stdout. Default: createJSONLStream
155
141
  * (claude/kimi/pi) with P1-4 backpressure enabled (pauseThreshold=100).
156
- * Subclasses that need to silently skip non-JSON lines should override
157
- * `handleBadStreamLine` instead of this method.
158
142
  */
159
143
  createStreamReader(stdout) {
160
144
  return createJSONLStream(stdout, {
161
- onParseError: (line) => this.handleBadStreamLine(line),
145
+ onParseError: (line) => {
146
+ getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
147
+ },
162
148
  pauseThreshold: 100,
163
149
  resumeThreshold: 50,
164
150
  });
165
151
  }
166
- /**
167
- * Hook called when createJSONLStream encounters a line that cannot be parsed
168
- * as JSON. Default: log a warning via getLogger().warn (same format as
169
- * jsonl-stream's built-in warning). Subclasses override to change behavior
170
- * (e.g. codex/opencode silently skip by providing an empty override).
171
- */
172
- handleBadStreamLine(line) {
173
- getLogger().warn(`[jsonl-stream] failed to parse JSONL: ${line.slice(0, 100)}`);
174
- }
175
152
  /**
176
153
  * Create a stateful translator for this run. Default: null (uses the
177
- * stateless `translate(raw, ctx)` hook). codex/opencode/pi/kimi override to
178
- * return their translator instance.
154
+ * stateless passthrough). pi overrides to return its translator instance.
179
155
  */
180
156
  createTranslator(_opts) {
181
157
  return null;
182
158
  }
183
- /**
184
- * Pre-spawn validation. Return an AgentEvent to yield it and abort run()
185
- * (e.g. codex's API key check yields an error result and returns).
186
- * Default: null (no validation).
187
- */
188
- validateBeforeRun(_opts) {
189
- return null;
190
- }
191
- /**
192
- * Await process completion. Default: return the completion promise as-is.
193
- * kimi overrides to add a 5s timeout race + SIGKILL fallback (kimi
194
- * sometimes hangs after stdout closes).
195
- */
196
- awaitCompletion(_proc, completion) {
197
- return completion;
198
- }
199
159
  /**
200
160
  * Await the spawn 'error' event when proc.pid === undefined. Races a
201
161
  * finite timeout (review P2-11) against the 'error' event: Node guarantees
@@ -214,60 +174,24 @@ export class SpawningRunner {
214
174
  ]);
215
175
  }
216
176
  /**
217
- * Resolve the translatorError to pass to buildResultEvent. Default
218
- * precedence (most-specific first):
219
- * - agent-reported terminal error (translator.hasAgentTerminalError()):
220
- * always wins — the agent error is the root cause, signal/code is a
221
- * consequence. E.g. codex `turn.failed` then non-zero exit.
222
- * - stream-ended-early symptom (translator.getTerminalError() set by
223
- * finish('failed')): ONLY surfaces when there is no external cause
224
- * (signal / non-zero code / user stop). The signal/code is the root
225
- * cause; the early stream-end is a symptom.
226
- * - otherwise: undefined (signal/code/stoppedByUser drives the message).
177
+ * Spawn the agent binary with the subclass's argv and run the shared spawn
178
+ * lead-in: pid-undefined check, pid-file write, heartbeat start, and stderr
179
+ * accumulation (tail-capped).
227
180
  *
228
- * Subclasses can override for custom precedence but the default matches
229
- * the contract pinned by codex/opencode subclass anchors.
230
- */
231
- resolveTranslatorError(translator, ctx) {
232
- if (!translator)
233
- return undefined;
234
- const agentError = translator.hasAgentTerminalError();
235
- const hasExternalCause = this.stoppedByUser || ctx.signal !== null || (ctx.code !== null && ctx.code !== 0);
236
- return agentError || !hasExternalCause ? translator.getTerminalError() : undefined;
237
- }
238
- /**
239
- * Spawn the agent binary with argv built by buildArgv(opts), iterate its
240
- * stdout stream, and yield translated events. Always emits a unified
241
- * result event at the end via `buildResultEvent(...)` — never throws on
242
- * non-zero exit (the bridge accepts both thrown errors and yielded
243
- * error-result events, and the latter carries richer diagnostic info).
181
+ * Throws a `SpawnChildError` (carrying the user-facing spawn failure
182
+ * message) when the process fails to spawn (binary missing / pid undefined).
183
+ * On success returns the spawned child plus the accumulated stderr tail.
244
184
  *
245
- * Spawn failures (binary missing, pid undefined) yield an `authErrorEvent`
246
- * instead of throwing, so the user sees a friendly error card.
185
+ * Shared by the base `run()` (yields an authErrorEvent on failure) and
186
+ * ClaudeSession's `doStartProcess` (returns the error message), which would
187
+ * otherwise duplicate ~60 lines of spawn lead-in with two copies of the
188
+ * STDERR_TAIL_BYTES cap.
247
189
  */
248
- async *run(message, opts) {
249
- if (this.isRunning) {
250
- getLogger().warn(`[${this.logTag}] run() called while already running, refusing`);
251
- throw new Error(`${this.binary} process already running`);
252
- }
253
- this.stoppedByUser = false;
254
- this.currentMessage = message;
255
- // Pre-spawn validation hook (e.g. codex API key check)
256
- const validationError = this.validateBeforeRun(opts);
257
- if (validationError) {
258
- // §9.22: yield synthetic init so the bridge's pre-init result guard and
259
- // the run-state reducer's sessionId check don't silently drop the error
260
- // result. Without init, the card would show a generic "输出流已结束"
261
- // instead of the specific validation error message.
262
- yield syntheticInitEvent(opts.sessionId);
263
- yield validationError;
264
- return;
265
- }
266
- const args = this.buildArgv(opts);
267
- const proc = spawn(this.binary, args, {
190
+ async spawnChild(opts) {
191
+ const proc = spawn(this.binary, this.buildArgv(opts), {
268
192
  cwd: opts.cwd,
269
193
  stdio: this.getStdio(),
270
- env: this.getSpawnEnv(opts),
194
+ env: process.env,
271
195
  // Spawn in a new process group so we can kill the entire group
272
196
  // when stopping. This ensures child processes (shell wrappers,
273
197
  // sub-processes) are also terminated.
@@ -292,10 +216,56 @@ export class SpawningRunner {
292
216
  // so the user is not misdiagnosed into reinstalling the binary.
293
217
  const baseMsg = `${this.binary} 命令不可用(未找到或不可执行),请检查是否已安装或在 PATH 中`;
294
218
  const msg = spawnErr ? `${baseMsg}:${spawnErr.message}` : baseMsg;
219
+ throw new SpawnChildError(msg);
220
+ }
221
+ const pidDir = path.dirname(this.pidFilePath);
222
+ fs.mkdirSync(pidDir, { recursive: true });
223
+ fs.writeFileSync(this.pidFilePath, String(proc.pid), 'utf-8');
224
+ getLogger().info(`[${this.logTag}] wrote pid file ${this.pidFilePath}=${proc.pid}`);
225
+ this.spawnHeartbeat.start({ pid: proc.pid, binary: this.binary, cwd: opts.cwd });
226
+ proc.stdout?.once('data', () => {
227
+ this.spawnHeartbeat.notifyStdout();
228
+ });
229
+ this.spawnStderr = '';
230
+ proc.stderr?.on('data', (chunk) => {
231
+ const text = chunk.toString('utf-8').trim();
232
+ if (text) {
233
+ this.spawnStderr = (this.spawnStderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
234
+ // P2-16: most agent CLIs emit progress/warnings/deprecation notices on
235
+ // stderr, not real errors. Logging each chunk at error level drowns out
236
+ // genuine errors. Downgrade to warn — the accumulated stderr is already
237
+ // surfaced at error level in the non-zero-exit result path.
238
+ getLogger().warn(`[${this.logTag} stderr] ${text}`);
239
+ }
240
+ });
241
+ return proc;
242
+ }
243
+ /**
244
+ * Spawn the agent binary with argv built by buildArgv(opts), iterate its
245
+ * stdout stream, and yield translated events. Always emits a unified
246
+ * result event at the end via `buildResultEvent(...)` — never throws on
247
+ * non-zero exit (the bridge accepts both thrown errors and yielded
248
+ * error-result events, and the latter carries richer diagnostic info).
249
+ *
250
+ * Spawn failures (binary missing, pid undefined) yield an `authErrorEvent`
251
+ * instead of throwing, so the user sees a friendly error card.
252
+ */
253
+ async *run(message, opts) {
254
+ if (this.isRunning) {
255
+ getLogger().warn(`[${this.logTag}] run() called while already running, refusing`);
256
+ throw new Error(`${this.binary} process already running`);
257
+ }
258
+ this.stoppedByUser = false;
259
+ this.currentMessage = message;
260
+ let proc;
261
+ try {
262
+ proc = await this.spawnChild(opts);
263
+ }
264
+ catch (err) {
295
265
  // §9.22: yield synthetic init before the error result so bridge/run-state
296
- // guards don't silently drop it (same rationale as validateBeforeRun).
266
+ // guards don't silently drop it.
297
267
  yield syntheticInitEvent(opts.sessionId);
298
- yield authErrorEvent(msg);
268
+ yield authErrorEvent(err.message);
299
269
  return;
300
270
  }
301
271
  const completion = new Promise((resolve) => {
@@ -317,29 +287,7 @@ export class SpawningRunner {
317
287
  resolve({ code, signal });
318
288
  });
319
289
  });
320
- const pidDir = path.dirname(this.pidFilePath);
321
- fs.mkdirSync(pidDir, { recursive: true });
322
- fs.writeFileSync(this.pidFilePath, String(proc.pid), 'utf-8');
323
- getLogger().info(`[${this.logTag}] wrote pid file ${this.pidFilePath}=${proc.pid}`);
324
- this.spawnHeartbeat.start({ pid: proc.pid, binary: this.binary, cwd: opts.cwd });
325
- proc.stdout?.once('data', () => {
326
- this.spawnHeartbeat.notifyStdout();
327
- });
328
- let stderr = '';
329
- proc.stderr?.on('data', (chunk) => {
330
- const text = chunk.toString('utf-8').trim();
331
- if (text) {
332
- stderr = (stderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
333
- // P2-16: most agent CLIs emit progress/warnings/deprecation notices on
334
- // stderr, not real errors. Logging each chunk at error level drowns out
335
- // genuine errors. Downgrade to warn — the accumulated stderr is already
336
- // surfaced at error level in the non-zero-exit result path.
337
- getLogger().warn(`[${this.logTag} stderr] ${text}`);
338
- }
339
- });
340
- // Hook: write prompt to stdin (codex/opencode)
341
- this.setupStdin(proc, message);
342
- // Create translator (per-run state) — null for stateless passthrough (claude)
290
+ // Create translator (per-run state) — null for stateless passthrough
343
291
  const translator = this.createTranslator(opts);
344
292
  // Create stream reader (jsonl or readline)
345
293
  const stream = this.createStreamReader(proc.stdout);
@@ -380,7 +328,7 @@ export class SpawningRunner {
380
328
  catch (error) {
381
329
  getLogger().error(`[${this.logTag}] stream error: ${error}`);
382
330
  }
383
- const { code, signal } = await this.awaitCompletion(proc, completion);
331
+ const { code, signal } = await completion;
384
332
  // If the stream ended before a terminal event, let the translator
385
333
  // record its terminal state (e.g. codex `finish('failed')` stashes
386
334
  // "stream ended before a terminal event"). The runner folds this
@@ -389,18 +337,27 @@ export class SpawningRunner {
389
337
  const reason = this.stoppedByUser ? 'interrupted' : 'failed';
390
338
  translator.finish(reason);
391
339
  }
392
- const translatorError = this.resolveTranslatorError(translator, { code, signal });
340
+ // Resolve translatorError precedence (most-specific first):
341
+ // - agent-reported terminal error always wins
342
+ // - stream-ended-early symptom only surfaces with no external cause
343
+ let translatorError;
344
+ if (translator) {
345
+ const agentError = translator.hasAgentTerminalError();
346
+ const hasExternalCause = this.stoppedByUser || signal !== null || (code !== null && code !== 0);
347
+ translatorError =
348
+ agentError || !hasExternalCause ? translator.getTerminalError() : undefined;
349
+ }
393
350
  const nonUserError = !this.stoppedByUser && ((code !== null && code !== 0) || signal !== null);
394
351
  yield this.buildResultEvent({
395
352
  code,
396
353
  signal,
397
- stderr,
354
+ stderr: this.spawnStderr,
398
355
  sessionId: translator?.getSessionId?.() ?? '',
399
356
  usage: translator?.getLastUsage?.(),
400
357
  translatorError,
401
358
  });
402
359
  if (nonUserError) {
403
- getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${stderr.slice(-STDERR_LOG_TAIL)}`);
360
+ getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${this.spawnStderr.slice(-STDERR_LOG_TAIL)}`);
404
361
  }
405
362
  }
406
363
  finally {
@@ -420,12 +377,7 @@ export class SpawningRunner {
420
377
  }
421
378
  }
422
379
  this.currentProcess = null;
423
- try {
424
- fs.unlinkSync(this.pidFilePath);
425
- }
426
- catch {
427
- /* ignore — file may not exist if spawn failed before write */
428
- }
380
+ silentlyUnlink(this.pidFilePath);
429
381
  }
430
382
  }
431
383
  /**
@@ -444,10 +396,7 @@ export class SpawningRunner {
444
396
  this.stoppedByUser = true;
445
397
  await this.stopper.stop(proc, { immediate: opts?.immediate });
446
398
  this.currentProcess = null;
447
- try {
448
- fs.unlinkSync(this.pidFilePath);
449
- }
450
- catch { }
399
+ silentlyUnlink(this.pidFilePath);
451
400
  getLogger().debug(`[${this.logTag}] cleaned pid file ${this.pidFilePath}`);
452
401
  }
453
402
  killOrphan() {
@@ -457,7 +406,7 @@ export class SpawningRunner {
457
406
  const pidStr = fs.readFileSync(this.pidFilePath, 'utf-8').trim();
458
407
  const pid = Number(pidStr);
459
408
  if (isNaN(pid) || pid <= 0) {
460
- fs.unlinkSync(this.pidFilePath);
409
+ silentlyUnlink(this.pidFilePath);
461
410
  return;
462
411
  }
463
412
  // P1-10: pid 复用防护。pid 文件里的 pid 可能是 bridge 崩溃后系统回收复用
@@ -470,7 +419,7 @@ export class SpawningRunner {
470
419
  // 统一原则:只有身份匹配才杀;验证失败一律不杀。
471
420
  getLogger().warn(`[${this.logTag}] killOrphan: pid ${pid} not confirmed as ${this.binary} ` +
472
421
  `(${match}), skipping kill`);
473
- fs.unlinkSync(this.pidFilePath);
422
+ silentlyUnlink(this.pidFilePath);
474
423
  return;
475
424
  }
476
425
  // 身份匹配:杀整个进程组(detached:true 下 agent 是组长),与
@@ -482,7 +431,7 @@ export class SpawningRunner {
482
431
  catch {
483
432
  // process may have exited
484
433
  }
485
- fs.unlinkSync(this.pidFilePath);
434
+ silentlyUnlink(this.pidFilePath);
486
435
  }
487
436
  catch {
488
437
  // ignore
@@ -561,10 +510,7 @@ export class SpawningRunner {
561
510
  }
562
511
  catch { }
563
512
  }
564
- try {
565
- fs.unlinkSync(this.pidFilePath);
566
- }
567
- catch { }
513
+ silentlyUnlink(this.pidFilePath);
568
514
  }
569
515
  /**
570
516
  * Build a unified result event from process exit state. Centralizes the
@@ -8,10 +8,10 @@
8
8
  * (共享工具由消费方直接 import `common/*` 具体模块,不走 barrel)
9
9
  */
10
10
  export { ClaudeRunner } from './claude/index.js';
11
- export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } 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
17
  export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
@@ -9,10 +9,10 @@
9
9
  */
10
10
  // Re-export AgentRunner 实现(从各 agent 目录)
11
11
  export { ClaudeRunner } from './claude/index.js';
12
- export { CodexExecRunner, CodexAppServerRunner, createCodexRunner } 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
+ }