lark-remote 0.5.13 → 0.5.14

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 (126) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/agent-kinds.d.ts +27 -0
  3. package/dist/agent-kinds.js +38 -0
  4. package/dist/bridge/approval-actions.d.ts +95 -0
  5. package/dist/bridge/approval-actions.js +104 -0
  6. package/dist/bridge/final-usage.d.ts +8 -17
  7. package/dist/bridge/final-usage.js +1 -18
  8. package/dist/bridge/index.d.ts +46 -39
  9. package/dist/bridge/index.js +201 -229
  10. package/dist/bridge/queue-manager.d.ts +25 -1
  11. package/dist/bridge/queue-manager.js +179 -160
  12. package/dist/bridge/run-approval.d.ts +2 -1
  13. package/dist/bridge/run-usage.d.ts +16 -30
  14. package/dist/bridge/run-usage.js +41 -50
  15. package/dist/bridge/session-writeback.d.ts +3 -2
  16. package/dist/card/approval-render.d.ts +3 -2
  17. package/dist/card/approval-render.js +3 -2
  18. package/dist/{router → card}/card-helpers.d.ts +7 -7
  19. package/dist/{router → card}/card-helpers.js +8 -8
  20. package/dist/card/card-session.d.ts +4 -2
  21. package/dist/card/card-session.js +8 -6
  22. package/dist/card/card-shared.d.ts +1 -1
  23. package/dist/card/card-shared.js +1 -1
  24. package/dist/card/final-answer-overflow.d.ts +0 -6
  25. package/dist/card/final-answer-overflow.js +1 -1
  26. package/dist/card/run-card-session.d.ts +2 -2
  27. package/dist/card/run-card-session.js +2 -2
  28. package/dist/card/run-renderer.js +7 -22
  29. package/dist/card/run-state.d.ts +28 -50
  30. package/dist/card/run-state.js +23 -0
  31. package/dist/card/usage-format.d.ts +30 -0
  32. package/dist/{router → card}/usage-format.js +3 -10
  33. package/dist/clone.js +6 -1
  34. package/dist/config/index.d.ts +16 -20
  35. package/dist/config/index.js +0 -16
  36. package/dist/config/pi-config.js +6 -4
  37. package/dist/connector/index.d.ts +2 -4
  38. package/dist/connector/index.js +7 -11
  39. package/dist/{runner/dsh → dsh}/auth.js +1 -1
  40. package/dist/dsh/protocol.d.ts +9 -0
  41. package/dist/dsh/protocol.js +61 -0
  42. package/dist/index.js +10 -5
  43. package/dist/model-catalog.d.ts +14 -0
  44. package/dist/model-catalog.js +25 -0
  45. package/dist/platform/path.d.ts +9 -0
  46. package/dist/platform/path.js +18 -0
  47. package/dist/router/active-run-usage.d.ts +4 -0
  48. package/dist/router/active-run-usage.js +12 -0
  49. package/dist/router/commands/ls.js +1 -1
  50. package/dist/router/commands/order.js +1 -1
  51. package/dist/router/commands/resume.d.ts +5 -4
  52. package/dist/router/commands/resume.js +4 -4
  53. package/dist/router/commands/ws.js +1 -1
  54. package/dist/router/config/claude.js +2 -1
  55. package/dist/router/config/common/render.d.ts +5 -1
  56. package/dist/router/config/common/render.js +6 -13
  57. package/dist/router/config/dsh.d.ts +1 -1
  58. package/dist/router/config/index.d.ts +1 -1
  59. package/dist/router/config/types.d.ts +11 -3
  60. package/dist/router/index.d.ts +41 -11
  61. package/dist/router/index.js +165 -226
  62. package/dist/runner/agent-choices-common.d.ts +1 -1
  63. package/dist/runner/claude/runner.d.ts +20 -2
  64. package/dist/runner/claude/runner.js +2 -15
  65. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  66. package/dist/runner/codex/app-server/translator.d.ts +11 -0
  67. package/dist/runner/codex/app-server/translator.js +56 -149
  68. package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
  69. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  70. package/dist/runner/common/jsonrpc/transport.js +17 -7
  71. package/dist/runner/dsh/client.d.ts +1 -4
  72. package/dist/runner/dsh/client.js +2 -55
  73. package/dist/runner/dsh/daemon.js +9 -2
  74. package/dist/runner/dsh/exit-cancel.js +1 -1
  75. package/dist/runner/dsh/mux-stream.js +21 -2
  76. package/dist/runner/dsh/runner.d.ts +21 -1
  77. package/dist/runner/dsh/runner.js +76 -73
  78. package/dist/runner/dsh/translator.d.ts +1 -1
  79. package/dist/runner/index.d.ts +1 -1
  80. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  81. package/dist/runner/kimi/acp/runner.js +83 -60
  82. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  83. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  84. package/dist/runner/opencode/acp/runner.d.ts +3 -1
  85. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  86. package/dist/runner/probe.d.ts +1 -1
  87. package/dist/runner/probe.js +10 -9
  88. package/dist/runner/registry.d.ts +2 -1
  89. package/dist/runner/types.d.ts +20 -154
  90. package/dist/runner/types.js +4 -24
  91. package/dist/session/claude/session-index.d.ts +11 -4
  92. package/dist/session/claude/session-index.js +30 -8
  93. package/dist/session/claude/session-reader.d.ts +1 -1
  94. package/dist/session/claude/sessions.d.ts +2 -2
  95. package/dist/session/claude/sessions.js +41 -26
  96. package/dist/session/codex/rollout-reader.d.ts +1 -1
  97. package/dist/session/codex/rollout-reader.js +14 -13
  98. package/dist/session/codex/sessions.d.ts +1 -1
  99. package/dist/session/codex/sessions.js +4 -2
  100. package/dist/session/common/jsonl.js +7 -1
  101. package/dist/session/common/pagination.d.ts +2 -0
  102. package/dist/session/common/pagination.js +1 -1
  103. package/dist/session/common/recency.d.ts +6 -0
  104. package/dist/session/common/recency.js +7 -5
  105. package/dist/session/common/two-pass.d.ts +1 -1
  106. package/dist/session/common/usage-accumulator.d.ts +1 -1
  107. package/dist/session/dsh/sessions.d.ts +1 -1
  108. package/dist/session/dsh/sessions.js +37 -12
  109. package/dist/session/kimi/sessions.d.ts +1 -1
  110. package/dist/session/kimi/sessions.js +35 -8
  111. package/dist/session/opencode/sessions.d.ts +1 -1
  112. package/dist/session/opencode/sessions.js +51 -47
  113. package/dist/session/pi/sessions.d.ts +1 -1
  114. package/dist/session/pi/sessions.js +20 -7
  115. package/dist/session/registry.d.ts +2 -1
  116. package/dist/session/session-store.d.ts +1 -2
  117. package/dist/session/session-store.js +14 -7
  118. package/dist/session-contract.d.ts +178 -0
  119. package/dist/session-contract.js +9 -0
  120. package/package.json +1 -1
  121. package/dist/router/usage-format.d.ts +0 -60
  122. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  123. /package/dist/{runner/dsh → dsh}/home.d.ts +0 -0
  124. /package/dist/{runner/dsh → dsh}/home.js +0 -0
  125. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  126. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -1,5 +1,23 @@
1
1
  import { getLogger } from '../../logger/index.js';
2
2
  import { DshError } from './errors.js';
3
+ /**
4
+ * onerror 的事件对象在 ws 实现之间形态不一({message} / {error} / {type})。
5
+ * 尽力取出可读描述;取不到就不追加,别拿 '[object Object]' 充数。
6
+ */
7
+ function describeSocketError(event) {
8
+ if (!event || typeof event !== 'object')
9
+ return undefined;
10
+ const e = event;
11
+ if (typeof e.message === 'string' && e.message)
12
+ return e.message;
13
+ if (e.error instanceof Error && e.error.message)
14
+ return e.error.message;
15
+ if (typeof e.error === 'string' && e.error)
16
+ return e.error;
17
+ if (typeof e.type === 'string' && e.type)
18
+ return e.type;
19
+ return undefined;
20
+ }
3
21
  export async function* openMuxStream(opts) {
4
22
  const cookie = await opts.authCookie();
5
23
  const socket = opts.webSocketFactory(`${opts.baseUrl.replace(/^http/, 'ws')}/api/remote.mux`, cookie ? { headers: { cookie } } : undefined);
@@ -87,10 +105,11 @@ export async function* openMuxStream(opts) {
87
105
  settle(undefined);
88
106
  }
89
107
  };
90
- socket.onerror = () => {
108
+ socket.onerror = (event) => {
91
109
  if (ended)
92
110
  return;
93
- failure = new DshError('transport', 'DSH remote mux WebSocket error');
111
+ const detail = describeSocketError(event);
112
+ failure = new DshError('transport', `DSH remote mux WebSocket error${detail ? `: ${detail}` : ''}`);
94
113
  ended = true;
95
114
  settle(undefined);
96
115
  };
@@ -17,7 +17,8 @@
17
17
  * approval on the same card. If `$events` is unavailable, the runner falls
18
18
  * back to the Web UI prompt.
19
19
  */
20
- import type { AgentEvent, AgentSessionReader, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
20
+ import type { AgentSessionReader } from '../../session-contract.js';
21
+ import type { AgentEvent, AgentStatusInfo, SpawnOptions, StopOutcome } from '../types.js';
21
22
  import type { AgentRunner } from '../types.js';
22
23
  import { DshClient, type WebSocketFactory } from './client.js';
23
24
  import type { DshDaemon } from './daemon.js';
@@ -134,6 +135,25 @@ export declare class DshRunner implements AgentRunner {
134
135
  * the live catalog for that provider's default (or first) model.
135
136
  */
136
137
  private resolveModelSelection;
138
+ /**
139
+ * 新建 DSH session 并取回 sessionId;run 在挂起期间被 stop 时返回 undefined。
140
+ *
141
+ * 红线:不把 run 的 abort signal 直接交给 create —— abort 掉请求会丢掉刚创建的
142
+ * sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。改为 abort
143
+ * 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
144
+ */
145
+ private acquireSessionId;
146
+ /**
147
+ * 把本轮消息交给 DSH 的排队收件箱,返回本轮的身份与起始水位。
148
+ *
149
+ * 三条不变量(都关系到「迟到/抢先的 stop 能否被正确撤销」):
150
+ * - 起始水位取 prompt **之前**的 latestEventSeq:session/page 返回整个会话,
151
+ * 从 0 开始会把首轮已完成 turn 在每次后续消息时重放一遍。
152
+ * - 身份(requestId)在请求发出**之前**就公布:stop 可能落在 session/prompt 的
153
+ * 在途窗口里,那时它必须知道宿主受理后该撤销哪个收件项。
154
+ * - `promptAdmitted` 只在宿主接受后置真:撤销路径据此区分「还没进去」与「已排队」。
155
+ */
156
+ private submitPrompt;
137
157
  run(message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
138
158
  /**
139
159
  * Consume one turn from both transports. `session/follow` owns transcript
@@ -178,6 +178,20 @@ function parseToolArguments(raw) {
178
178
  return {};
179
179
  }
180
180
  }
181
+ /**
182
+ * 用户 stop 的终态结果(run() 的 5 个中止点各 yield 同一形状)。
183
+ *
184
+ * 必须 yield result 而不是裸 return:调用方靠 result 收尾 run 卡,否则卡片
185
+ * 停留在「运行中」(见 run() 内 startup abort 注释)。
186
+ */
187
+ function interruptedResult(sessionId) {
188
+ return {
189
+ type: 'result',
190
+ subtype: 'interrupted',
191
+ session_id: sessionId,
192
+ errorMessage: 'run stopped',
193
+ };
194
+ }
181
195
  export class DshRunner {
182
196
  kind = 'dsh';
183
197
  /**
@@ -323,6 +337,57 @@ export class DshRunner {
323
337
  }
324
338
  return { provider, model, ...(reasoningEffort ? { reasoningEffort } : {}) };
325
339
  }
340
+ /**
341
+ * 新建 DSH session 并取回 sessionId;run 在挂起期间被 stop 时返回 undefined。
342
+ *
343
+ * 红线:不把 run 的 abort signal 直接交给 create —— abort 掉请求会丢掉刚创建的
344
+ * sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。改为 abort
345
+ * 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
346
+ */
347
+ async acquireSessionId(cwd, signal) {
348
+ const createPromise = this.client.createSession({
349
+ cwd,
350
+ ...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
351
+ });
352
+ const created = await this.awaitOrAbort(createPromise, signal);
353
+ if (created)
354
+ return created.sessionId;
355
+ void createPromise.then((c) => this.cancelSessionQuietly(c.sessionId), () => {
356
+ /* create failed after stop — nothing to cancel */
357
+ });
358
+ getLogger().info('[dsh] run stopped during session/create; will cancel if it settles');
359
+ return undefined;
360
+ }
361
+ /**
362
+ * 把本轮消息交给 DSH 的排队收件箱,返回本轮的身份与起始水位。
363
+ *
364
+ * 三条不变量(都关系到「迟到/抢先的 stop 能否被正确撤销」):
365
+ * - 起始水位取 prompt **之前**的 latestEventSeq:session/page 返回整个会话,
366
+ * 从 0 开始会把首轮已完成 turn 在每次后续消息时重放一遍。
367
+ * - 身份(requestId)在请求发出**之前**就公布:stop 可能落在 session/prompt 的
368
+ * 在途窗口里,那时它必须知道宿主受理后该撤销哪个收件项。
369
+ * - `promptAdmitted` 只在宿主接受后置真:撤销路径据此区分「还没进去」与「已排队」。
370
+ */
371
+ async submitPrompt(sessionId, cwd, message, signal) {
372
+ const prePromptSeq = await this.client.latestEventSeq(sessionId, { signal });
373
+ const promptRequestId = newPromptRequestId();
374
+ this.activePromptRequestId = promptRequestId;
375
+ this.promptAdmitted = false;
376
+ this.inflight?.record({
377
+ sessionId,
378
+ promptRequestId,
379
+ baseUrl: this.client.baseUrl,
380
+ cwd,
381
+ });
382
+ await this.client.unary('session/prompt', {
383
+ requestId: promptRequestId,
384
+ sessionId,
385
+ mode: 'queue',
386
+ content: [{ type: 'text', text: message }],
387
+ }, { signal });
388
+ this.promptAdmitted = true;
389
+ return { prePromptSeq, promptRequestId };
390
+ }
326
391
  async *run(message, opts) {
327
392
  if (this.running) {
328
393
  throw new Error('DshRunner is already running');
@@ -346,29 +411,13 @@ export class DshRunner {
346
411
  // session/create 必须包进同一 try/finally,否则创建失败(服务端不可达/报错)
347
412
  // 会在进入旧 try 之前抛异常,finally 不执行 → running 恒 true、activeAbort 残留。
348
413
  if (!sessionId) {
349
- const createPromise = this.client.createSession({
350
- cwd: opts.cwd,
351
- ...(this.agentPreset ? { agentPreset: this.agentPreset } : {}),
352
- });
353
- // 不把 run 的 abort signal 直接交给 create:abort 掉请求会丢掉刚创建
354
- // 的 sessionId,服务端若已完成创建就留下一个无法 cancel 的孤儿 session。
355
- // 改为 abort 时立即放行 run(队列不被挂起),create 后台落定后再补 cancel。
356
- const created = await this.awaitOrAbort(createPromise, abort.signal);
357
- if (!created) {
358
- void createPromise.then((c) => this.cancelSessionQuietly(c.sessionId), () => {
359
- /* create failed after stop — nothing to cancel */
360
- });
361
- getLogger().info('[dsh] run stopped during session/create; will cancel if it settles');
414
+ const created = await this.acquireSessionId(opts.cwd, abort.signal);
415
+ if (created === undefined) {
362
416
  terminalEmitted = true;
363
- yield {
364
- type: 'result',
365
- subtype: 'interrupted',
366
- session_id: '',
367
- errorMessage: 'run stopped',
368
- };
417
+ yield interruptedResult('');
369
418
  return;
370
419
  }
371
- sessionId = created.sessionId;
420
+ sessionId = created;
372
421
  }
373
422
  this.activeSessionId = sessionId;
374
423
  // Startup abort:createSession 挂起期间 stop() 已 abort(此时 sessionId 尚
@@ -380,12 +429,7 @@ export class DshRunner {
380
429
  await this.cancelSessionQuietly(sessionId);
381
430
  getLogger().info(`[dsh] run aborted during startup, cancelled session ${sessionId}`);
382
431
  terminalEmitted = true;
383
- yield {
384
- type: 'result',
385
- subtype: 'interrupted',
386
- session_id: sessionId,
387
- errorMessage: 'run stopped',
388
- };
432
+ yield interruptedResult(sessionId);
389
433
  return;
390
434
  }
391
435
  // provider/模型/推理强度对齐:会话首次 run 时 selectModel 一次
@@ -403,12 +447,7 @@ export class DshRunner {
403
447
  if (abort.signal.aborted) {
404
448
  getLogger().info(`[dsh] run stopped during selectModel session=${sessionId}`);
405
449
  terminalEmitted = true;
406
- yield {
407
- type: 'result',
408
- subtype: 'interrupted',
409
- session_id: sessionId,
410
- errorMessage: 'run stopped',
411
- };
450
+ yield interruptedResult(sessionId);
412
451
  return;
413
452
  }
414
453
  if (this.configuredProvider) {
@@ -425,12 +464,7 @@ export class DshRunner {
425
464
  // Abort 可能在上述 unary 完成与检查之间到达:不进入 prompt/turn。
426
465
  if (abort.signal.aborted) {
427
466
  terminalEmitted = true;
428
- yield {
429
- type: 'result',
430
- subtype: 'interrupted',
431
- session_id: sessionId,
432
- errorMessage: 'run stopped',
433
- };
467
+ yield interruptedResult(sessionId);
434
468
  return;
435
469
  }
436
470
  yield {
@@ -453,48 +487,17 @@ export class DshRunner {
453
487
  if (seedGoal.known && seedGoal.goal)
454
488
  yield goalStateEvent(seedGoal.goal, true);
455
489
  try {
456
- // Snapshot the pre-prompt high-water mark before mutating the session.
457
- // DSH session/page returns the whole session, so starting from seq 0
458
- // would replay the first completed turn on every follow-up prompt.
459
- const prePromptSeq = await this.client.latestEventSeq(sessionId, {
460
- signal: abort.signal,
461
- });
462
- // Keep the prompt identity: DSH echoes it into the durable user/message
463
- // source.rpcId, which is how runTurn tells this run's turn from a turn
464
- // it is merely queued behind.
465
- promptRequestId = newPromptRequestId();
466
- // Publish the identity before the request leaves: a stop that arrives
467
- // while `session/prompt` is in flight must still know which inbox item
468
- // (if any) to withdraw once the host admits it.
469
- this.activePromptRequestId = promptRequestId;
470
- this.promptAdmitted = false;
471
- this.inflight?.record({
472
- sessionId,
473
- promptRequestId,
474
- baseUrl: this.client.baseUrl,
475
- cwd: opts.cwd,
476
- });
477
- await this.client.unary('session/prompt', {
478
- requestId: promptRequestId,
479
- sessionId,
480
- mode: 'queue',
481
- content: [{ type: 'text', text: message }],
482
- }, { signal: abort.signal });
483
- this.promptAdmitted = true;
490
+ const submitted = await this.submitPrompt(sessionId, opts.cwd, message, abort.signal);
491
+ promptRequestId = submitted.promptRequestId;
484
492
  if (abort.signal.aborted) {
485
493
  // stop() 在 prompt 已受理后到达:cancel 由 stop() 发出,这里直接收尾,
486
494
  // 不再打开 mux(否则会看到 cancel 产生的 turn/end 事件)。
487
495
  terminalEmitted = true;
488
- yield {
489
- type: 'result',
490
- subtype: 'interrupted',
491
- session_id: sessionId,
492
- errorMessage: 'run stopped',
493
- };
496
+ yield interruptedResult(sessionId);
494
497
  return;
495
498
  }
496
499
  try {
497
- for await (const ev of this.runTurn(sessionId, opts.cwd, prePromptSeq, promptRequestId, abort, { last: lastGoal })) {
500
+ for await (const ev of this.runTurn(sessionId, opts.cwd, submitted.prePromptSeq, promptRequestId, abort, { last: lastGoal })) {
498
501
  yield ev;
499
502
  if (ev.type === 'result') {
500
503
  terminalEmitted = true;
@@ -24,7 +24,7 @@
24
24
  * cache_creation_tokens (cacheWrite = cache creation) + total_tokens sum.
25
25
  */
26
26
  import type { AgentEvent, ResultEvent } from '../types.js';
27
- import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from './types.js';
27
+ import type { DshAssistantStreamFrame, DshSessionEvent, DshTokenUsage } from '../../dsh/types.js';
28
28
  export declare class DshTranslator {
29
29
  /** 本 turn 累计用量:DSH 每次模型调用(step)一条 usage,工具往返 = 多条。 */
30
30
  private turnUsage;
@@ -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, FinalAnswerEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentRunner, TurnStartedEvent, TurnDiffEvent, FinalAnswerEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
19
19
  export { resolveAgentChoices } from './resolve-agent-choices.js';
20
20
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -17,7 +17,9 @@
17
17
  * - AskUserQuestion flows through elicitation forms + a request_permission
18
18
  * question bridge (buildQuestionResponse)
19
19
  */
20
- import type { AgentKind, AgentSessionReader, AgentStatusInfo, SpawnOptions } from '../../types.js';
20
+ import type { AgentKind } from '../../../agent-kinds.js';
21
+ import type { AgentSessionReader } from '../../../session-contract.js';
22
+ import type { AgentStatusInfo, SpawnOptions } from '../../types.js';
21
23
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
22
24
  import { KimiAcpTranslator } from './translator.js';
23
25
  import { type SessionPromptResult } from '../../common/acp/protocol-types.js';
@@ -167,6 +169,25 @@ export declare class KimiAcpRunner extends BaseAcpRunner<KimiAcpTranslator> {
167
169
  * stdout(+stderr) 到缓冲,立即回 {terminalId}。
168
170
  */
169
171
  private handleTerminalCreate;
172
+ /**
173
+ * 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」收口——win32 上
174
+ * Git Bash 缺失时在这里抛 ShellUnavailableError,由 handleTerminalRequest 转成
175
+ * 明确的 Terminal request failed 回执,而不是一个语焉不详的 spawn ENOENT。
176
+ */
177
+ private spawnTerminalProcess;
178
+ /**
179
+ * 有界输出缓冲:每个流独立 decoder(多字节字符跨 chunk 拆分时由 decoder 保留
180
+ * 不完整序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符)。
181
+ */
182
+ private attachTerminalOutputBuffer;
183
+ /**
184
+ * 退出/错误收尾管线:tool_result 必须先入队,再放行 `wait_for_exit`。
185
+ */
186
+ private wireTerminalExit;
187
+ /** Bash 面板的 tool_result 帧(输出全量 + 是否失败)。 */
188
+ private pushTerminalToolResult;
189
+ /** Bash 面板的 tool_use 帧(真实命令 + cwd,create 时立刻可见)。 */
190
+ private pushTerminalToolUse;
170
191
  /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
171
192
  private handleTerminalOutput;
172
193
  /** terminal/wait_for_exit: 未退出则等 exit promise 再回 {exitCode, signal}。 */
@@ -582,11 +582,55 @@ export class KimiAcpRunner extends BaseAcpRunner {
582
582
  : undefined;
583
583
  const cwd = params.cwd ?? this.activeCwd ?? undefined;
584
584
  const outputLimit = params.outputByteLimit ?? TERMINAL_DEFAULT_OUTPUT_LIMIT;
585
- // 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」
586
- // 收口——win32 上 Git Bash 缺失时在这里抛 ShellUnavailableError,由
587
- // handleTerminalRequest 转成明确的 Terminal request failed 回执,而不是一个
588
- // 语焉不详的 spawn ENOENT。
589
- const proc = this.shell.spawnArgv(params.command, params.args ?? [], {
585
+ const proc = this.spawnTerminalProcess(params, cwd, env);
586
+ // spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
587
+ // wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
588
+ // 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
589
+ let resolveExit;
590
+ const exitPromise = new Promise((resolve) => {
591
+ resolveExit = resolve;
592
+ });
593
+ // 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
594
+ // 后赋值,exit/error 回调执行时经 emitResult 间接调用。
595
+ let emitTerminalResult = undefined;
596
+ const emitResult = () => {
597
+ emitTerminalResult?.();
598
+ };
599
+ const handle = {
600
+ proc,
601
+ output: '',
602
+ outputBytes: 0,
603
+ outputLimit,
604
+ truncated: false,
605
+ exitCode: null,
606
+ signal: null,
607
+ exitPromise,
608
+ };
609
+ this.attachTerminalOutputBuffer(proc, handle);
610
+ this.wireTerminalExit(proc, handle, emitResult, () => resolveExit?.());
611
+ const terminalId = `term-${++this.terminalSeq}`;
612
+ this.terminals.set(terminalId, handle);
613
+ // Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
614
+ // 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
615
+ const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
616
+ let resultEmitted = false;
617
+ emitTerminalResult = () => {
618
+ if (resultEmitted)
619
+ return;
620
+ resultEmitted = true;
621
+ this.pushTerminalToolResult(terminalId, handle.output, handle.exitCode);
622
+ };
623
+ this.pushTerminalToolUse(terminalId, script, cwd);
624
+ getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
625
+ this.currentClient?.respond(id, { terminalId });
626
+ }
627
+ /**
628
+ * 走 shell seam:argv 逐 token 保留,只把「可执行文件在哪」收口——win32 上
629
+ * Git Bash 缺失时在这里抛 ShellUnavailableError,由 handleTerminalRequest 转成
630
+ * 明确的 Terminal request failed 回执,而不是一个语焉不详的 spawn ENOENT。
631
+ */
632
+ spawnTerminalProcess(params, cwd, env) {
633
+ return this.shell.spawnArgv(params.command, params.args ?? [], {
590
634
  cwd,
591
635
  options: {
592
636
  // env 覆盖大小写不敏感合并(win32 PATH/Path 双键防护)
@@ -600,8 +644,12 @@ export class KimiAcpRunner extends BaseAcpRunner {
600
644
  windowsHide: true,
601
645
  },
602
646
  });
603
- // 每个流独立 decoder:多字节字符跨 chunk 拆分时由 decoder 保留不完整
604
- // 序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符。
647
+ }
648
+ /**
649
+ * 有界输出缓冲:每个流独立 decoder(多字节字符跨 chunk 拆分时由 decoder 保留
650
+ * 不完整序列,避免按 chunk 单独 toString 产生 U+FFFD 替换符)。
651
+ */
652
+ attachTerminalOutputBuffer(proc, handle) {
605
653
  const stdoutDecoder = new StringDecoder('utf8');
606
654
  const stderrDecoder = new StringDecoder('utf8');
607
655
  const append = (chunk, decoder) => {
@@ -624,29 +672,11 @@ export class KimiAcpRunner extends BaseAcpRunner {
624
672
  };
625
673
  proc.stdout?.on('data', (chunk) => append(chunk, stdoutDecoder));
626
674
  proc.stderr?.on('data', (chunk) => append(chunk, stderrDecoder));
627
- // spawn 失败(如 ENOENT)只发 'error' 不发 'exit'——一并 resolve,防止
628
- // wait_for_exit 永久挂起(runner 层 ENOENT 兜底红线)。
629
- // 用 resolveExit 承接,避免回调在 handle 初始化前引用它(TDZ)。
630
- let resolveExit;
631
- const exitPromise = new Promise((resolve) => {
632
- resolveExit = resolve;
633
- });
634
- // 与 resolveExit 同款 TDZ 规避:tool_result 发射闭包在 terminalId 初始化
635
- // 后赋值,exit/error 回调执行时经 emitResult 间接调用。
636
- let emitTerminalResult = undefined;
637
- const emitResult = () => {
638
- emitTerminalResult?.();
639
- };
640
- const handle = {
641
- proc,
642
- output: '',
643
- outputBytes: 0,
644
- outputLimit,
645
- truncated: false,
646
- exitCode: null,
647
- signal: null,
648
- exitPromise,
649
- };
675
+ }
676
+ /**
677
+ * 退出/错误收尾管线:tool_result 必须先入队,再放行 `wait_for_exit`。
678
+ */
679
+ wireTerminalExit(proc, handle, emitResult, resolveExit) {
650
680
  proc.once('exit', (code, signal) => {
651
681
  handle.exitCode = code;
652
682
  handle.signal = signal;
@@ -661,7 +691,7 @@ export class KimiAcpRunner extends BaseAcpRunner {
661
691
  const flushThenEmit = () => {
662
692
  clearTimeout(emitTimer);
663
693
  emitResult();
664
- resolveExit?.();
694
+ resolveExit();
665
695
  };
666
696
  const emitTimer = setTimeout(flushThenEmit, 200);
667
697
  emitTimer.unref?.();
@@ -680,35 +710,30 @@ export class KimiAcpRunner extends BaseAcpRunner {
680
710
  }
681
711
  // 同 exit:tool_result 先入队,再放行 wait_for_exit。
682
712
  emitResult();
683
- resolveExit?.();
713
+ resolveExit();
684
714
  });
685
- const terminalId = `term-${++this.terminalSeq}`;
686
- this.terminals.set(terminalId, handle);
687
- // Bash 工具事件由本 runner 自产(kimi 通知已过滤):create 即推 tool_use
688
- // 带真实命令,让 run 卡立刻出现可识别的 Bash 面板。
689
- const script = Array.isArray(params.args) && params.args.length > 1 ? params.args[1] : params.command;
690
- let resultEmitted = false;
691
- emitTerminalResult = () => {
692
- if (resultEmitted)
693
- return;
694
- resultEmitted = true;
695
- this.pushEvents([
696
- {
697
- type: 'user',
698
- message: {
699
- content: [
700
- {
701
- type: 'tool_result',
702
- tool_use_id: terminalId,
703
- content: handle.output,
704
- is_error: handle.exitCode !== 0,
705
- },
706
- ],
707
- },
708
- timestamp: new Date().toISOString(),
715
+ }
716
+ /** Bash 面板的 tool_result 帧(输出全量 + 是否失败)。 */
717
+ pushTerminalToolResult(terminalId, output, exitCode) {
718
+ this.pushEvents([
719
+ {
720
+ type: 'user',
721
+ message: {
722
+ content: [
723
+ {
724
+ type: 'tool_result',
725
+ tool_use_id: terminalId,
726
+ content: output,
727
+ is_error: exitCode !== 0,
728
+ },
729
+ ],
709
730
  },
710
- ]);
711
- };
731
+ timestamp: new Date().toISOString(),
732
+ },
733
+ ]);
734
+ }
735
+ /** Bash 面板的 tool_use 帧(真实命令 + cwd,create 时立刻可见)。 */
736
+ pushTerminalToolUse(terminalId, script, cwd) {
712
737
  this.pushEvents([
713
738
  {
714
739
  type: 'assistant',
@@ -725,8 +750,6 @@ export class KimiAcpRunner extends BaseAcpRunner {
725
750
  timestamp: new Date().toISOString(),
726
751
  },
727
752
  ]);
728
- getLogger().info(`[${this.logTag}] terminal created id=${terminalId} command=${params.command} args=${JSON.stringify(params.args ?? [])}`);
729
- this.currentClient?.respond(id, { terminalId });
730
753
  }
731
754
  /** terminal/output: 回累计全量输出 + truncated + 当前退出状态(可未退出)。 */
732
755
  handleTerminalOutput(id, params) {
@@ -45,6 +45,12 @@ export declare class OpencodeLogErrorMonitor {
45
45
  private timer;
46
46
  private sessionId;
47
47
  private onError;
48
+ /**
49
+ * IO 故障留痕的节流状态:poll 每 2s 一次,同一条 EACCES 不得刷满日志,
50
+ * 但也不能像以前那样完全静默(监视器自己坏了没人知道)。
51
+ */
52
+ private lastIoError;
53
+ private lastIoErrorAt;
48
54
  constructor(opts: OpencodeLogErrorMonitorOptions);
49
55
  /**
50
56
  * Start monitoring the given session. Replaces any previous monitor state;
@@ -54,6 +60,11 @@ export declare class OpencodeLogErrorMonitor {
54
60
  /** Stop monitoring and clear all state. Safe to call multiple times. */
55
61
  stop(): void;
56
62
  private currentSize;
63
+ /**
64
+ * 监视器自身读不到日志时留痕(同一条最多每分钟一次)。ENOENT 通常是日志
65
+ * 还没生成,属正常空态,不记。
66
+ */
67
+ private noteIoError;
57
68
  private poll;
58
69
  /** Match a main-turn stream error line for the monitored session with a
59
70
  * limit/quota reason; returns the extracted error text or null. */
@@ -27,9 +27,12 @@
27
27
  import { closeSync, openSync, readSync, statSync } from 'node:fs';
28
28
  import { homedir } from 'node:os';
29
29
  import { join } from 'node:path';
30
+ import { getLogger } from '../../../logger/index.js';
30
31
  /** Error text we fail fast on: quota/limit conditions opencode backs off on. */
31
32
  const LIMIT_ERROR_RE = /usage limit|quota|rate limit|rate_limit|too many requests|insufficient balance|free usage/i;
32
33
  const STREAM_ERROR_RE = /message="stream error"/;
34
+ /** 同一条 IO 故障的日志节流窗口。 */
35
+ const IO_ERROR_LOG_INTERVAL_MS = 60_000;
33
36
  const MAIN_TURN_RE = /small=false/;
34
37
  const ERROR_MESSAGE_RE = /error\.error="((?:[^"\\]|\\.)*)"/;
35
38
  /**
@@ -50,6 +53,12 @@ export class OpencodeLogErrorMonitor {
50
53
  timer = null;
51
54
  sessionId = null;
52
55
  onError = null;
56
+ /**
57
+ * IO 故障留痕的节流状态:poll 每 2s 一次,同一条 EACCES 不得刷满日志,
58
+ * 但也不能像以前那样完全静默(监视器自己坏了没人知道)。
59
+ */
60
+ lastIoError = '';
61
+ lastIoErrorAt = 0;
53
62
  constructor(opts) {
54
63
  this.logPath = opts.logPath;
55
64
  this.pollIntervalMs = opts.pollIntervalMs ?? 2000;
@@ -81,10 +90,28 @@ export class OpencodeLogErrorMonitor {
81
90
  try {
82
91
  return statSync(this.logPath).size;
83
92
  }
84
- catch {
93
+ catch (err) {
94
+ this.noteIoError(err);
85
95
  return 0;
86
96
  }
87
97
  }
98
+ /**
99
+ * 监视器自身读不到日志时留痕(同一条最多每分钟一次)。ENOENT 通常是日志
100
+ * 还没生成,属正常空态,不记。
101
+ */
102
+ noteIoError(err) {
103
+ if (err.code === 'ENOENT')
104
+ return;
105
+ const message = err?.message ?? String(err);
106
+ const now = Date.now();
107
+ if (message === this.lastIoError && now - this.lastIoErrorAt < IO_ERROR_LOG_INTERVAL_MS) {
108
+ return;
109
+ }
110
+ this.lastIoError = message;
111
+ this.lastIoErrorAt = now;
112
+ getLogger().warn(`[opencode-error-monitor] cannot read ${this.logPath}: ${message}; ` +
113
+ 'limit errors will fall back to the idle watchdog');
114
+ }
88
115
  poll() {
89
116
  if (this.sessionId === null || this.onError === null)
90
117
  return;
@@ -107,9 +134,11 @@ export class OpencodeLogErrorMonitor {
107
134
  closeSync(fd);
108
135
  }
109
136
  }
110
- catch {
137
+ catch (err) {
138
+ this.noteIoError(err);
111
139
  return;
112
140
  }
141
+ this.lastIoError = '';
113
142
  this.offset = size;
114
143
  this.pendingTail += chunk;
115
144
  }
@@ -19,7 +19,9 @@
19
19
  * - OpencodeLogErrorMonitor tails opencode's own log for LLM stream errors
20
20
  * the ACP protocol never forwards
21
21
  */
22
- import type { AgentKind, AgentSessionReader, AgentStatusInfo } from '../../types.js';
22
+ import type { AgentKind } from '../../../agent-kinds.js';
23
+ import type { AgentSessionReader } from '../../../session-contract.js';
24
+ import type { AgentStatusInfo } from '../../types.js';
23
25
  import { JsonRpcClient } from '../../common/jsonrpc/client.js';
24
26
  import { OpencodeAcpTranslator } from './translator.js';
25
27
  import { type SessionNewResult, type SessionResumeResult, type SessionPromptResult } from '../../common/acp/protocol-types.js';
@@ -17,7 +17,8 @@
17
17
  * - pi,极简风格,没有 Ask User question。
18
18
  * - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
19
19
  */
20
- import type { AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
20
+ import type { AgentSessionReader } from '../../../session-contract.js';
21
+ import type { AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
21
22
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
22
23
  import { PiRpcClient } from './client.js';
23
24
  import { type PiRpcTranslatorEvent } from './translator.js';
@@ -17,7 +17,7 @@
17
17
  * does PATH + X_OK on posix (same semantics) and PATH × PATHEXT on win32,
18
18
  * with no child process at all.
19
19
  */
20
- import type { AgentKind } from './types.js';
20
+ import type { AgentKind } from '../agent-kinds.js';
21
21
  /**
22
22
  * Probe a single agent, using cache when fresh.
23
23
  * Updates cache on completion.