lark-remote 0.1.13 → 0.2.1

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 (132) hide show
  1. package/CHANGELOG.md +36 -3
  2. package/README.en.md +7 -11
  3. package/README.md +7 -11
  4. package/dist/bridge/final-usage.d.ts +24 -0
  5. package/dist/bridge/final-usage.js +55 -0
  6. package/dist/bridge/index.d.ts +0 -7
  7. package/dist/bridge/index.js +22 -64
  8. package/dist/bridge/queue-manager.js +4 -3
  9. package/dist/card/approval-render.js +39 -94
  10. package/dist/card/bash-renderer.js +12 -21
  11. package/dist/card/card-shared.d.ts +13 -1
  12. package/dist/card/card-shared.js +21 -30
  13. package/dist/card/run-renderer.d.ts +0 -22
  14. package/dist/card/run-renderer.js +206 -153
  15. package/dist/card/run-state.d.ts +29 -1
  16. package/dist/card/run-state.js +172 -84
  17. package/dist/card/text-truncate.d.ts +0 -9
  18. package/dist/card/text-truncate.js +0 -9
  19. package/dist/card/tool-render.d.ts +4 -1
  20. package/dist/card/tool-render.js +76 -32
  21. package/dist/cli.js +2 -2
  22. package/dist/common/fs.d.ts +14 -0
  23. package/dist/common/fs.js +57 -3
  24. package/dist/config/codex-config.js +2 -0
  25. package/dist/config/index.d.ts +0 -5
  26. package/dist/config/index.js +0 -11
  27. package/dist/config/kimi-config.js +1 -0
  28. package/dist/config/opencode-config.js +13 -2
  29. package/dist/config/qr.d.ts +36 -0
  30. package/dist/config/qr.js +124 -0
  31. package/dist/config/wizard.js +69 -27
  32. package/dist/connector/index.d.ts +3 -2
  33. package/dist/connector/index.js +5 -4
  34. package/dist/index.js +15 -42
  35. package/dist/instance-lock.js +7 -0
  36. package/dist/persistence/atomic-write.d.ts +2 -0
  37. package/dist/persistence/atomic-write.js +20 -5
  38. package/dist/persistence/load-json-file.d.ts +1 -2
  39. package/dist/persistence/load-json-file.js +2 -3
  40. package/dist/platform/agent-stopper.d.ts +22 -0
  41. package/dist/platform/agent-stopper.js +19 -0
  42. package/dist/platform/command.d.ts +11 -0
  43. package/dist/platform/command.js +107 -0
  44. package/dist/platform/identity.d.ts +33 -0
  45. package/dist/platform/identity.js +187 -0
  46. package/dist/platform/path.d.ts +24 -0
  47. package/dist/platform/path.js +110 -0
  48. package/dist/platform/select.d.ts +9 -0
  49. package/dist/platform/select.js +12 -0
  50. package/dist/platform/shell.d.ts +51 -0
  51. package/dist/platform/shell.js +75 -0
  52. package/dist/platform/spawn.d.ts +41 -0
  53. package/dist/platform/spawn.js +77 -0
  54. package/dist/platform/terminator-posix.d.ts +38 -0
  55. package/dist/platform/terminator-posix.js +103 -0
  56. package/dist/platform/terminator-win32.d.ts +21 -0
  57. package/dist/platform/terminator-win32.js +110 -0
  58. package/dist/platform/terminator.d.ts +44 -0
  59. package/dist/platform/terminator.js +24 -0
  60. package/dist/platform/types.d.ts +23 -0
  61. package/dist/platform/types.js +5 -0
  62. package/dist/restart.js +4 -2
  63. package/dist/router/card-helpers.d.ts +33 -6
  64. package/dist/router/card-helpers.js +67 -11
  65. package/dist/router/config/common/render.d.ts +1 -1
  66. package/dist/router/config/common/render.js +1 -1
  67. package/dist/router/config-diff.d.ts +4 -0
  68. package/dist/router/config-diff.js +70 -0
  69. package/dist/router/index.d.ts +66 -6
  70. package/dist/router/index.js +451 -316
  71. package/dist/router/utils.d.ts +6 -0
  72. package/dist/router/utils.js +12 -0
  73. package/dist/runner/bash/runner.d.ts +9 -2
  74. package/dist/runner/bash/runner.js +45 -20
  75. package/dist/runner/claude/session.d.ts +4 -1
  76. package/dist/runner/claude/session.js +18 -4
  77. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
  78. package/dist/runner/codex/app-server/translator.d.ts +18 -0
  79. package/dist/runner/codex/app-server/translator.js +146 -0
  80. package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
  81. package/dist/runner/common/acp/base-acp-runner.js +248 -0
  82. package/dist/runner/common/acp/base-translator.d.ts +141 -0
  83. package/dist/runner/common/acp/base-translator.js +271 -0
  84. package/dist/runner/common/acp/protocol-helpers.d.ts +39 -0
  85. package/dist/runner/common/acp/protocol-helpers.js +34 -0
  86. package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
  87. package/dist/runner/common/jsonrpc/transport.js +25 -5
  88. package/dist/runner/common/process-stopper.d.ts +4 -26
  89. package/dist/runner/common/process-stopper.js +4 -76
  90. package/dist/runner/common/runner-utils.d.ts +0 -5
  91. package/dist/runner/common/runner-utils.js +0 -7
  92. package/dist/runner/common/spawning-runner.d.ts +42 -132
  93. package/dist/runner/common/spawning-runner.js +52 -271
  94. package/dist/runner/common/usage.d.ts +2 -0
  95. package/dist/runner/common/usage.js +2 -0
  96. package/dist/runner/index.d.ts +1 -1
  97. package/dist/runner/kimi/acp/runner.d.ts +45 -42
  98. package/dist/runner/kimi/acp/runner.js +140 -293
  99. package/dist/runner/kimi/acp/translator.d.ts +18 -100
  100. package/dist/runner/kimi/acp/translator.js +56 -242
  101. package/dist/runner/opencode/acp/runner.d.ts +22 -46
  102. package/dist/runner/opencode/acp/runner.js +73 -256
  103. package/dist/runner/opencode/acp/translator.d.ts +28 -103
  104. package/dist/runner/opencode/acp/translator.js +51 -236
  105. package/dist/runner/pi/rpc/translator.d.ts +3 -0
  106. package/dist/runner/pi/rpc/translator.js +47 -9
  107. package/dist/runner/probe.d.ts +8 -11
  108. package/dist/runner/probe.js +17 -21
  109. package/dist/runner/registry.d.ts +4 -4
  110. package/dist/runner/registry.js +4 -4
  111. package/dist/runner/types.d.ts +38 -1
  112. package/dist/session/claude/sessions.js +35 -58
  113. package/dist/session/codex/rollout-reader.d.ts +10 -0
  114. package/dist/session/codex/rollout-reader.js +123 -41
  115. package/dist/session/common/constants.d.ts +7 -0
  116. package/dist/session/common/constants.js +9 -0
  117. package/dist/session/common/two-pass.d.ts +22 -0
  118. package/dist/session/common/two-pass.js +70 -0
  119. package/dist/session/dsh/sessions.js +7 -6
  120. package/dist/session/kimi/sessions.d.ts +6 -0
  121. package/dist/session/kimi/sessions.js +50 -73
  122. package/dist/session/opencode/sessions.js +26 -8
  123. package/dist/session/pi/sessions.js +54 -73
  124. package/dist/update/index.d.ts +1 -2
  125. package/dist/update/index.js +1 -2
  126. package/dist/update/install.d.ts +2 -1
  127. package/dist/update/install.js +58 -9
  128. package/dist/update/version-check.d.ts +5 -0
  129. package/dist/update/version-check.js +9 -0
  130. package/package.json +9 -6
  131. package/dist/update/startup-hint.d.ts +0 -5
  132. package/dist/update/startup-hint.js +0 -11
@@ -1,4 +1,5 @@
1
- import { spawn, execFileSync } from 'node:child_process';
1
+ import { execFileSync } from 'node:child_process';
2
+ import { spawnProcess, isWindowsCommandNotFoundLine, useDetachedProcessGroup, } from '../../platform/spawn.js';
2
3
  import path from 'node:path';
3
4
  import os from 'node:os';
4
5
  import fs from 'node:fs';
@@ -7,7 +8,6 @@ import { getLogger } from '../../logger/index.js';
7
8
  import { ProcessStopper } from './process-stopper.js';
8
9
  import { SpawnHeartbeat } from './spawn-heartbeat.js';
9
10
  import { createJSONLStream } from './jsonl-stream.js';
10
- import { authErrorEvent, syntheticInitEvent } from './runner-utils.js';
11
11
  import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
12
12
  /**
13
13
  * Magic-number constants (Clean Code P3-1, G25 Replace Magic Numbers with
@@ -16,16 +16,15 @@ import { DEFAULT_STOP_GRACE_MS } from '../../config/index.js';
16
16
  */
17
17
  /** Max bytes of stderr retained for the result-event error message. */
18
18
  const STDERR_TAIL_BYTES = 4000;
19
- /** Max bytes of stderr surfaced in the non-zero-exit log line. */
20
- const STDERR_LOG_TAIL = 500;
21
19
  /** Error thrown by `spawnChild` when the child process fails to spawn. */
22
20
  class SpawnChildError extends Error {
23
21
  }
24
22
  /**
25
23
  * Timeout (ms) for awaiting the spawn 'error' event when proc.pid === undefined
26
24
  * (review P2-11). Node guarantees 'error' for ENOENT/EACCES, but some binaries
27
- * fail silently; the race keeps a silent failure from hanging run() forever.
28
- * Matches the 5s race kimi's override used before it was hoisted to the base.
25
+ * fail silently; the race keeps a silent failure from hanging the spawn lead-in
26
+ * forever. Matches the 5s race kimi's override used before it was hoisted to
27
+ * the base.
29
28
  */
30
29
  const SPAWN_ERROR_TIMEOUT_MS = 5000;
31
30
  const registeredRunners = new Set();
@@ -59,24 +58,22 @@ export function unregisterExitCleanup(handler) {
59
58
  registeredRunners.delete(handler);
60
59
  }
61
60
  /**
62
- * Abstract base class for runners that spawn a child process per turn.
61
+ * Abstract base class encapsulating the spawn lifecycle shared by agent
62
+ * runners: spawn lead-in (pid-undefined check, pid file write, heartbeat,
63
+ * stderr tail accumulation), user-initiated stop, orphan kill with pid
64
+ * identity verification, and process-exit cleanup registration.
63
65
  *
64
- * Encapsulates the spawn + completion + cleanup orchestration shared by
65
- * Claude/Pi runners. Subclasses override:
66
- * - buildArgv(opts) → agent-specific CLI flags (required)
67
- * - createTranslator(opts) → stateful translator (pi)
68
- * OR translate(raw, ctx) → stateless passthrough (claude)
66
+ * The sole production subclass is ClaudeSession (src/runner/claude/session.ts),
67
+ * a long-lived interactive session that overrides `run()` with its own
68
+ * per-turn consumption loop and reads `spawnStderr` / `stoppedByUser` /
69
+ * `commandNotFoundSeen` to build its terminal result events.
69
70
  *
70
- * Optional hooks (with sensible defaults):
71
- * - getStdio() → stdio config (default ['ignore','pipe','pipe'])
72
- * - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
73
- * - awaitSpawnError(proc) → wait for spawn error (default once('error'))
71
+ * Subclasses override:
72
+ * - buildArgv(opts) → agent-specific CLI flags (required)
74
73
  *
75
- * The base class always emits a unified result event at the end of `run()`
76
- * via `buildResultEvent(...)` — never throws on non-zero exit. This matches
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).
74
+ * Optional hooks (with sensible defaults):
75
+ * - getStdio() → stdio config (default ['ignore','pipe','pipe'])
76
+ * - createStreamReader(stdout) → JSONL or readline parser (default createJSONLStream)
80
77
  */
81
78
  export class SpawningRunner {
82
79
  currentProcess = null;
@@ -91,26 +88,23 @@ export class SpawningRunner {
91
88
  * Log prefix used in all operational log lines emitted by this runner
92
89
  * (spawn, pid file, non-zero exit, stderr, killOrphan, stop cleanup) and
93
90
  * as the SpawnHeartbeat label. Subclasses pass their own tag (e.g.
94
- * 'claude-runner') so operators can grep agent-specific logs; the default
95
- * 'spawning-runner' keeps the base-class anchor tests' neutral identity.
91
+ * 'claude-runner') so operators can grep agent-specific logs.
96
92
  */
97
93
  logTag;
98
- /**
99
- * The `message` argument from the most recent `run(message, opts)` call.
100
- * Set in `run()` before `buildArgv(opts)` is invoked so subclasses can
101
- * embed the message in their agent-specific argv (e.g. claude's `-p
102
- * <message>`). This avoids changing the `buildArgv(opts)` signature,
103
- * which would break existing anchor subclasses.
104
- */
105
- currentMessage = '';
106
94
  /**
107
95
  * Whether the current run was interrupted by user-initiated stop().
108
96
  * Set to true by stop() when a running process is being terminated;
109
- * reset to false at the top of run(). `resolveTranslatorError()` and
110
- * `buildResultEvent()` read this to decide result event subtype
111
- * (error vs success) and the error message precedence.
97
+ * subclasses reset it at the start of a turn and read it when building
98
+ * the terminal result event (interrupted vs error precedence).
112
99
  */
113
100
  stoppedByUser = false;
101
+ /**
102
+ * §4.4 win32 command-not-found 嗅探嫌疑标记:stderr 命中特征行时置位,
103
+ * 但**不**立即杀进程——agent 正常输出可能引用该错误文本(调试 Windows
104
+ * 报错场景),见行即杀会误杀 run。定性走双条件:子类收尾时「标记在 +
105
+ * 非零退出」才判 command-not-found;真挂起由既有 grace/超时兜底。
106
+ */
107
+ commandNotFoundSeen = false;
114
108
  constructor(opts) {
115
109
  // Subclasses set this.binary after super()
116
110
  this.binary = '';
@@ -130,15 +124,15 @@ export class SpawningRunner {
130
124
  }
131
125
  // --- Hooks (subclasses override as needed) ---
132
126
  /**
133
- * stdio config for spawn. Default: stdin ignored (claude/kimi/pi pass the
134
- * prompt via argv).
127
+ * stdio config for spawn. Default: stdin ignored (agents pass the prompt
128
+ * via argv).
135
129
  */
136
130
  getStdio() {
137
131
  return ['ignore', 'pipe', 'pipe'];
138
132
  }
139
133
  /**
140
134
  * Create an async iterator over the child's stdout. Default: createJSONLStream
141
- * (claude/kimi/pi) with P1-4 backpressure enabled (pauseThreshold=100).
135
+ * with P1-4 backpressure enabled (pauseThreshold=100).
142
136
  */
143
137
  createStreamReader(stdout) {
144
138
  return createJSONLStream(stdout, {
@@ -149,21 +143,14 @@ export class SpawningRunner {
149
143
  resumeThreshold: 50,
150
144
  });
151
145
  }
152
- /**
153
- * Create a stateful translator for this run. Default: null (uses the
154
- * stateless passthrough). pi overrides to return its translator instance.
155
- */
156
- createTranslator(_opts) {
157
- return null;
158
- }
159
146
  /**
160
147
  * Await the spawn 'error' event when proc.pid === undefined. Races a
161
148
  * finite timeout (review P2-11) against the 'error' event: Node guarantees
162
149
  * 'error' for ENOENT/EACCES, but some binaries fail silently without ever
163
- * emitting it (kimi's CLI is documented to do so). Without the race, run()
164
- * hangs forever → the workspace serial queue never settles → permanent
165
- * deadlock, and /stop can't recover (ProcessStopper returns early when
166
- * pid === undefined). The timeout keeps the deadlock bounded.
150
+ * emitting it. Without the race, the spawn lead-in hangs forever → the
151
+ * workspace serial queue never settles → permanent deadlock, and /stop
152
+ * can't recover (ProcessStopper returns early when pid === undefined).
153
+ * The timeout keeps the deadlock bounded.
167
154
  */
168
155
  awaitSpawnError(proc) {
169
156
  return Promise.race([
@@ -180,22 +167,18 @@ export class SpawningRunner {
180
167
  *
181
168
  * Throws a `SpawnChildError` (carrying the user-facing spawn failure
182
169
  * message) when the process fails to spawn (binary missing / pid undefined).
183
- * On success returns the spawned child plus the accumulated stderr tail.
184
- *
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.
170
+ * On success returns the spawned child.
189
171
  */
190
172
  async spawnChild(opts) {
191
- const proc = spawn(this.binary, this.buildArgv(opts), {
173
+ const proc = spawnProcess(this.binary, this.buildArgv(opts), {
192
174
  cwd: opts.cwd,
193
175
  stdio: this.getStdio(),
194
176
  env: process.env,
195
- // Spawn in a new process group so we can kill the entire group
196
- // when stopping. This ensures child processes (shell wrappers,
197
- // sub-processes) are also terminated.
198
- detached: true,
177
+ // posix 建新进程组以便负 PID 组杀(壳包装/子进程一并终止);win32 不
178
+ // detached —— `.cmd` 垫片在 DETACHED_PROCESS 下丢 stdio(见
179
+ // useDetachedProcessGroup),树杀由 taskkill /T 负责。
180
+ detached: useDetachedProcessGroup(),
181
+ windowsHide: true,
199
182
  });
200
183
  this.currentProcess = proc;
201
184
  getLogger().info(`[${this.logTag}] spawn pid=${proc.pid ?? '(none)'} binary=${this.binary} ` +
@@ -231,6 +214,14 @@ export class SpawningRunner {
231
214
  const text = chunk.toString('utf-8').trim();
232
215
  if (text) {
233
216
  this.spawnStderr = (this.spawnStderr + '\n' + text).trim().slice(-STDERR_TAIL_BYTES);
217
+ // §4.4 win32 command-not-found 嗅探:经 cmd 垫片启动失败不是 spawn 的
218
+ // ENOENT,而是 stderr 的 "is not recognized..." 行。命中只记嫌疑标记
219
+ // (双条件之一),不立即杀——定性由子类收尾的「标记 + 非零退出」
220
+ // 双条件完成;子进程真挂起由既有 grace/超时看门狗兜底。
221
+ if (isWindowsCommandNotFoundLine(text)) {
222
+ this.commandNotFoundSeen = true;
223
+ getLogger().error(`[${this.logTag}] command not found suspected (win32 shim): ${text}`);
224
+ }
234
225
  // P2-16: most agent CLIs emit progress/warnings/deprecation notices on
235
226
  // stderr, not real errors. Logging each chunk at error level drowns out
236
227
  // genuine errors. Downgrade to warn — the accumulated stderr is already
@@ -240,151 +231,11 @@ export class SpawningRunner {
240
231
  });
241
232
  return proc;
242
233
  }
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) {
265
- // §9.22: yield synthetic init before the error result so bridge/run-state
266
- // guards don't silently drop it.
267
- yield syntheticInitEvent(opts.sessionId);
268
- yield authErrorEvent(err.message);
269
- return;
270
- }
271
- const completion = new Promise((resolve) => {
272
- proc.once('error', (err) => {
273
- this.spawnHeartbeat.clear();
274
- getLogger().error(`[${this.logTag}] spawn failed: ${err.message} binary=${this.binary} cwd=${opts.cwd}`);
275
- // Resolve (not reject) so the unified result event is emitted
276
- // via buildResultEvent instead of throwing past the finally
277
- // into the bridge's catch (which would leak the pid file).
278
- // P2-12: a post-spawn 'error' (e.g. kill failure, broken pipe)
279
- // MUST surface as an error result, not success. Previously
280
- // {code:null,signal:null} was classified as success by
281
- // buildResultEvent; code=1 forces the error branch so the
282
- // failure is never silently swallowed as a successful run.
283
- resolve({ code: 1, signal: null });
284
- });
285
- proc.once('close', (code, signal) => {
286
- this.spawnHeartbeat.clear();
287
- resolve({ code, signal });
288
- });
289
- });
290
- // Create translator (per-run state) — null for stateless passthrough
291
- const translator = this.createTranslator(opts);
292
- // Create stream reader (jsonl or readline)
293
- const stream = this.createStreamReader(proc.stdout);
294
- try {
295
- // Stream loop wrapped in try/catch so a stream error (e.g. stdout
296
- // 'error' event propagated through the async generator) does not
297
- // bypass the completion await and result event emission. Matches
298
- // kimi's long-standing behavior; safe for all runners because the
299
- // completion promise still resolves via proc 'close'.
300
- try {
301
- for await (const rawEvent of stream) {
302
- let translated;
303
- if (translator) {
304
- // Per-event error isolation: a translator throw must not kill
305
- // the stream (pi's accumulator could throw on malformed input).
306
- try {
307
- translated = translator.translate(rawEvent);
308
- }
309
- catch (err) {
310
- getLogger().warn(`[${this.logTag}] translator error: ${err.message}, raw: ${JSON.stringify(rawEvent).slice(0, 100)}`);
311
- continue;
312
- }
313
- }
314
- else {
315
- translated = this.translate(rawEvent, { message, opts });
316
- }
317
- if (translated === null)
318
- continue;
319
- if (Array.isArray(translated)) {
320
- for (const e of translated)
321
- yield e;
322
- }
323
- else {
324
- yield translated;
325
- }
326
- }
327
- }
328
- catch (error) {
329
- getLogger().error(`[${this.logTag}] stream error: ${error}`);
330
- }
331
- const { code, signal } = await completion;
332
- // If the stream ended before a terminal event, let the translator
333
- // record its terminal state (e.g. codex `finish('failed')` stashes
334
- // "stream ended before a terminal event"). The runner folds this
335
- // into buildResultEvent below.
336
- if (translator && !translator.isTerminal()) {
337
- const reason = this.stoppedByUser ? 'interrupted' : 'failed';
338
- translator.finish(reason);
339
- }
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
- }
350
- const nonUserError = !this.stoppedByUser && ((code !== null && code !== 0) || signal !== null);
351
- yield this.buildResultEvent({
352
- code,
353
- signal,
354
- stderr: this.spawnStderr,
355
- sessionId: translator?.getSessionId?.() ?? '',
356
- usage: translator?.getLastUsage?.(),
357
- translatorError,
358
- });
359
- if (nonUserError) {
360
- getLogger().error(`[${this.logTag}] non-zero exit code=${code} signal=${signal} stderr=${this.spawnStderr.slice(-STDERR_LOG_TAIL)}`);
361
- }
362
- }
363
- finally {
364
- this.spawnHeartbeat.clear();
365
- // P1-11: 消费者提前关闭生成器(for-await 循环体抛错/break 触发的 .return())
366
- // 会跳过 completion await 直接进 finally——此时子进程可能仍在运行。若不杀,
367
- // 它会成为 stop()(currentProcess 已置 null)、killOrphan()(pid 文件已删)、
368
- // exit handler(同一字段)都够不到的孤儿黑洞,且 stdout 管道背压会把它永久
369
- // 阻塞。正常完成路径进程已退出,stop 是 no-op,零成本。
370
- const proc = this.currentProcess;
371
- if (proc && proc.exitCode === null && proc.signalCode === null) {
372
- try {
373
- await this.stopper.stop(proc, { immediate: true });
374
- }
375
- catch {
376
- /* ignore */
377
- }
378
- }
379
- this.currentProcess = null;
380
- silentlyUnlink(this.pidFilePath);
381
- }
382
- }
383
234
  /**
384
235
  * Stop the current process if one is running. Delegates the actual
385
236
  * SIGTERM → grace → SIGKILL sequence to `this.stopper.stop(proc, opts)`
386
237
  * (see `src/runner/common/process-stopper.ts`), forwarding the `immediate`
387
- * flag verbatim so all 5 subclasses inherit identical stop semantics.
238
+ * flag verbatim so all subclasses inherit identical stop semantics.
388
239
  */
389
240
  async stop(opts) {
390
241
  const proc = this.currentProcess;
@@ -497,7 +348,7 @@ export class SpawningRunner {
497
348
  /**
498
349
  * Shared process-exit cleanup: SIGTERM the still-running child process and
499
350
  * remove the pid file. Called by the singleton dispatcher for every
500
- * registered runner; previously duplicated verbatim in all 5 runner
351
+ * registered runner; previously duplicated verbatim in the runner
501
352
  * subclasses.
502
353
  */
503
354
  cleanupOnExit() {
@@ -512,75 +363,5 @@ export class SpawningRunner {
512
363
  }
513
364
  silentlyUnlink(this.pidFilePath);
514
365
  }
515
- /**
516
- * Build a unified result event from process exit state. Centralizes the
517
- * result-event semantics specified in Candidate 2:
518
- * - stoppedByUser → error ("interrupted by user")
519
- * - translatorError set → error (translator-supplied message, e.g. codex
520
- * `turn.failed` or "stream ended before a terminal event")
521
- * - code !== 0 && code !== null → error (exit code + stderr tail)
522
- * - signal !== null → error (killed by signal)
523
- * - otherwise → success (with optional usage)
524
- *
525
- * Error message precedence (most-specific first):
526
- * - stoppedByUser: "{binary} interrupted by user"
527
- * - translatorError: verbatim translator-supplied message
528
- * - signal: "{binary} killed by signal {signal}{stderr tail}"
529
- * - non-zero code: "{binary} exited code={code}{stderr tail}"
530
- *
531
- * @param opts.code - Process exit code (null if killed by signal)
532
- * @param opts.signal - Process signal code (null if exited normally)
533
- * @param opts.stderr - Accumulated stderr (tail included in code/signal errors)
534
- * @param opts.sessionId - Session ID to include in the result event
535
- * @param opts.usage - Token usage to include on success
536
- * @param opts.translatorError - Agent-specific terminal error message (e.g.
537
- * codex turn.failed, or "stream ended early")
538
- * @returns AgentEvent with type='result'
539
- */
540
- buildResultEvent(opts) {
541
- const isError = this.stoppedByUser ||
542
- opts.translatorError !== undefined ||
543
- (opts.code !== null && opts.code !== 0) ||
544
- opts.signal !== null;
545
- const sessionId = opts.sessionId ?? '';
546
- if (isError) {
547
- let errorMessage;
548
- const stderrTail = opts.stderr ? opts.stderr.slice(-STDERR_LOG_TAIL) : '';
549
- if (this.stoppedByUser) {
550
- errorMessage = `${this.binary} interrupted by user`;
551
- }
552
- else if (opts.translatorError !== undefined) {
553
- errorMessage = opts.translatorError;
554
- }
555
- else if (opts.signal !== null) {
556
- errorMessage = `${this.binary} killed by signal ${opts.signal}${stderrTail ? `: ${stderrTail}` : ''}`;
557
- }
558
- else {
559
- errorMessage = `${this.binary} exited code=${opts.code}${stderrTail ? `: ${stderrTail}` : ''}`;
560
- }
561
- return {
562
- type: 'result',
563
- subtype: 'error',
564
- session_id: sessionId,
565
- errorMessage,
566
- };
567
- }
568
- return {
569
- type: 'result',
570
- subtype: 'success',
571
- session_id: sessionId,
572
- ...(opts.usage ? { usage: opts.usage } : {}),
573
- };
574
- }
575
- /**
576
- * Stateless passthrough translator hook. Used by ClaudeRunner
577
- * which does not need per-run translation state. Subclasses that return
578
- * a translator from `createTranslator(opts)` leave this as the default
579
- * no-op — the base `run()` loop only calls `translate()` when
580
- * `createTranslator()` returns null.
581
- */
582
- translate(_rawEvent, _ctx) {
583
- return null;
584
- }
585
366
  }
586
367
  //# sourceMappingURL=spawning-runner.js.map
@@ -25,6 +25,8 @@ interface NormalizedResultUsage {
25
25
  totalTokens?: number;
26
26
  /** Model context window limit (codex app-server `modelContextWindow` passthrough). */
27
27
  contextLimit?: number;
28
+ /** Reasoning tokens (pi usage.reasoning). Undefined when not provided. */
29
+ reasoningTokens?: number;
28
30
  /** Reconstructed or agent-declared context length (see JSDoc above). */
29
31
  contextLength: number;
30
32
  }
@@ -15,6 +15,7 @@ export function normalizeResultUsage(usage) {
15
15
  const cacheCreation = usage.cache_creation_tokens ?? usage.cache_creation_input_tokens;
16
16
  const totalTokens = usage.total_tokens;
17
17
  const contextLimit = usage.context_limit;
18
+ const reasoningTokens = usage.reasoning_tokens;
18
19
  // total_tokens (agent-declared total) takes priority; otherwise reconstruct
19
20
  // from the parts: input + cacheRead + cacheCreation + output.
20
21
  const contextLength = totalTokens ?? inputTokens + (cacheRead ?? 0) + (cacheCreation ?? 0) + outputTokens;
@@ -25,6 +26,7 @@ export function normalizeResultUsage(usage) {
25
26
  cacheCreationTokens: cacheCreation,
26
27
  totalTokens,
27
28
  contextLimit,
29
+ reasoningTokens,
28
30
  contextLength,
29
31
  };
30
32
  }
@@ -15,6 +15,6 @@ export { KimiAcpRunner } from './kimi/index.js';
15
15
  export { DshRunner } from './dsh/index.js';
16
16
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
17
17
  export { AgentRegistry } from './registry.js';
18
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
19
19
  export { resolveAgentChoices } from './resolve-agent-choices.js';
20
20
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -6,15 +6,23 @@
6
6
  * → synthetic init → session/prompt → consume session/update notifications
7
7
  * until prompt settles → result event.
8
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.
9
+ * Turn/connection/compact/approval orchestration lives in BaseAcpRunner.
10
+ * Kimi-specific parts:
11
+ * - compaction is a BACKGROUND task: after the compact prompt settles, poll
12
+ * wire.jsonl for a NEW terminal compaction record (§5.2, never fake success)
13
+ * - terminal/* reverse RPC: kimi delegates Bash execution to the client —
14
+ * this runner spawns one-shot local bash processes (handleTerminalRequest)
15
+ * - terminal-backed Bash tool notifications are filtered (bashToolCallIds) so
16
+ * the run card doesn't get duplicate output-less Bash panels
17
+ * - AskUserQuestion flows through elicitation forms + a request_permission
18
+ * question bridge (buildQuestionResponse)
13
19
  */
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';
20
+ import type { AgentKind, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../../types.js';
21
+ import { JsonRpcClient } from '../../common/jsonrpc/client.js';
22
+ import { KimiAcpTranslator } from './translator.js';
23
+ import { type SessionPromptResult } from '../../common/acp/protocol-types.js';
24
+ import { type AcpPendingApproval } from '../../common/acp/protocol-helpers.js';
25
+ import { BaseAcpRunner } from '../../common/acp/base-acp-runner.js';
18
26
  export interface KimiAcpRunnerOptions {
19
27
  kind: AgentKind;
20
28
  sessionReader: AgentSessionReader;
@@ -39,15 +47,15 @@ export interface KimiAcpRunnerOptions {
39
47
  /** Kimi permission mode (user-facing: manual/auto/yolo). */
40
48
  permissionMode?: 'manual' | 'auto' | 'yolo';
41
49
  }
42
- export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient, AcpTranslatorEvent> {
43
- private connectionManager;
44
- private currentTranslator;
45
- private activeSessionId;
46
- private model?;
50
+ export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
47
51
  private permissionMode;
48
52
  private readonly compactIdleTimeoutMs;
49
- /** Pending approval requests: requestId → kind + view + options. */
50
- private pendingApprovals;
53
+ /**
54
+ * §5.2 compaction baseline: wire.jsonl records sampled by compactBaseline()
55
+ * BEFORE the compact prompt fires (reset per compact run). onCompactPromptFired
56
+ * derives both the terminal-count baseline and the total-count probe from it.
57
+ */
58
+ private compactBaselineRecords;
51
59
  /** 存活 terminal(kimi 把 Bash 执行下放客户端):terminalId → 本地句柄。 */
52
60
  private terminals;
53
61
  /** terminalId 自增序号(term-1, term-2, …)。 */
@@ -63,32 +71,29 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
63
71
  /** 当前 run 的 session cwd(terminal/create 未给 cwd 时回退用)。 */
64
72
  private activeCwd;
65
73
  private readonly processStopper;
66
- /** Tracker for the in-flight prompt request — needed for cancellation. */
67
- private promptSettled;
68
74
  constructor(opts: KimiAcpRunnerOptions);
69
75
  protected get logTag(): string;
70
76
  protected get turnTimeoutErrorMessage(): string;
71
77
  protected get turnInterruptedErrorMessage(): string;
72
- protected currentSessionId(): string | null;
78
+ protected get connectionClosedMessage(): string;
73
79
  protected shouldDeferStop(): boolean;
74
- protected cancelCurrentTurn(): Promise<void>;
75
- protected clearTurnState(): void;
76
- protected releaseConnection(cwd: string): Promise<void>;
77
- protected notifyIdle(cwd: string): void;
78
- protected disposeConnections(): Promise<void>;
80
+ protected createTranslator(): KimiAcpTranslator;
81
+ protected applyTurnSettings(client: JsonRpcClient, sessionId: string): Promise<void>;
79
82
  /**
80
- * Run a compact operation on the current session.
81
- *
82
- * Design doc §6.1: acquire → session/resume → session/prompt with "/compact"
83
- * text (builtin slash command, host intercepts) → consume notifications until
84
- * prompt settles.
83
+ * §5.2: compaction is a BACKGROUND task — the prompt settles before the
84
+ * wire.jsonl terminal record lands. Baselines are captured BEFORE the
85
+ * compact prompt fires (see compactBaseline override — a begin record
86
+ * landing after the fire must count as new activity, not baseline), then
87
+ * wait for a NEW terminal (complete/apply → completed; cancel → cancelled)
88
+ * before producing the result. There is NO total timeout — silence beyond
89
+ * compactIdleTimeoutMs yields 'unknown' (never a fake success). Keeps the
90
+ * connection alive while the background compaction finishes — otherwise
91
+ * dispose/exit kills it and the record never lands (S5/S6 打回复现 /
92
+ * 2026-08-31 事故).
85
93
  */
86
- runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
87
- /**
88
- * Respond to an approval server request. `response` is the bridge
89
- * ApprovalAction (`{ action: 'accept' | 'decline' | 'cancel' }`).
90
- */
91
- respondApproval(requestId: number | string, response: unknown): Promise<void>;
94
+ protected compactBaseline(sessionId: string, opts: SpawnOptions): number;
95
+ protected onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: KimiAcpTranslator, sessionId: string, opts: SpawnOptions, baselineTotalCount: number): void;
96
+ protected buildApprovalOutcome(action: string, pending: AcpPendingApproval, response: unknown): unknown;
92
97
  getStatusInfo(): AgentStatusInfo;
93
98
  /**
94
99
  * Hot-apply a permission-mode change to the live ACP session (§P5).
@@ -102,12 +107,6 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
102
107
  updateApprovalMode(settings: {
103
108
  permissionMode?: 'manual' | 'auto' | 'yolo';
104
109
  }): Promise<void>;
105
- /**
106
- * Acquire the connection and set up the turn:
107
- * session/new (new session) or session/resume (existing sessionId)
108
- * → set permission mode if needed → session/prompt.
109
- */
110
- protected setupTurn(message: string, opts: SpawnOptions): Promise<void>;
111
110
  /**
112
111
  * §5.2: 等 wire.jsonl 出现 NEW 压缩终态记录,返回判别结果而非 void。
113
112
  *
@@ -123,7 +122,7 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
123
122
  * wakeWaiters),避免数分钟的压缩被 consumeTurn 滚动空闲看门狗误杀。
124
123
  */
125
124
  private waitForCompactionTerminal;
126
- private handleNotification;
125
+ protected handleNotification(method: string, params: unknown): void;
127
126
  /**
128
127
  * Filter kimi's own Bash tool notifications so the run card doesn't get a
129
128
  * duplicate, output-less Bash panel alongside the runner-produced one.
@@ -135,7 +134,7 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
135
134
  * Returns true when the notification should be swallowed.
136
135
  */
137
136
  private filterBashNotifications;
138
- private handleServerRequest;
137
+ protected handleServerRequest(id: number | string, method: string, params: unknown): void;
139
138
  /**
140
139
  * Handle a kimi terminal/* reverse RPC by spawning a one-shot local process.
141
140
  *
@@ -161,4 +160,8 @@ export declare class KimiAcpRunner extends ConnectionBasedRunner<KimiAcpClient,
161
160
  private handleTerminalRelease;
162
161
  /** Kill every live terminal (dispose/connection-close 清理钩子)。 */
163
162
  private cleanupTerminals;
163
+ protected onConnectionAcquired(opts: SpawnOptions): void;
164
+ protected onConnectionClosed(): void;
165
+ protected onClearTurnState(): void;
166
+ protected beforeDisposeConnections(): Promise<void>;
164
167
  }