lark-remote 0.5.19 → 0.5.20

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 (119) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +1 -1
  10. package/dist/bridge/index.d.ts +97 -153
  11. package/dist/bridge/index.js +182 -809
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +109 -97
  18. package/dist/card/card-budget.js +1 -1
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +34 -93
  24. package/dist/card/run-state.d.ts +1 -1
  25. package/dist/card/run-state.js +28 -90
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +1 -1
  28. package/dist/card/usage-format.d.ts +12 -0
  29. package/dist/card/usage-format.js +8 -3
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/config/codex-config.js +130 -106
  33. package/dist/config/index.d.ts +16 -2
  34. package/dist/config/index.js +23 -8
  35. package/dist/connector/index.js +6 -6
  36. package/dist/error-classification.js +3 -2
  37. package/dist/inbound/placeholder.d.ts +1 -1
  38. package/dist/inbound/placeholder.js +1 -1
  39. package/dist/index.js +203 -133
  40. package/dist/logger/index.js +6 -6
  41. package/dist/message-handlers.d.ts +0 -5
  42. package/dist/message-handlers.js +1 -1
  43. package/dist/model-catalog.d.ts +2 -2
  44. package/dist/model-catalog.js +3 -8
  45. package/dist/platform/identity.js +1 -1
  46. package/dist/platform/path.d.ts +1 -1
  47. package/dist/platform/path.js +1 -1
  48. package/dist/platform/sleep-blocker.js +1 -1
  49. package/dist/platform/terminator-win32.js +2 -1
  50. package/dist/router/commands/resume.js +2 -20
  51. package/dist/router/config/dsh.d.ts +4 -8
  52. package/dist/router/config/dsh.js +3 -10
  53. package/dist/router/config/kimi.js +1 -1
  54. package/dist/router/config/types.d.ts +1 -1
  55. package/dist/router/config-diff.js +1 -1
  56. package/dist/router/index.d.ts +56 -11
  57. package/dist/router/index.js +219 -200
  58. package/dist/runner/claude/session.d.ts +1 -1
  59. package/dist/runner/claude/session.js +6 -4
  60. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  61. package/dist/runner/codex/app-server/runner.js +30 -25
  62. package/dist/runner/codex/app-server/translator.d.ts +0 -5
  63. package/dist/runner/codex/app-server/translator.js +4 -17
  64. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  65. package/dist/runner/common/acp/acp-approval.js +1 -1
  66. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  67. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  68. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  69. package/dist/runner/common/acp/base-translator.js +31 -9
  70. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  71. package/dist/runner/common/acp/protocol-types.js +2 -2
  72. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  73. package/dist/runner/common/connection-based-runner.js +32 -2
  74. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  75. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  76. package/dist/runner/common/spawning-runner.d.ts +1 -1
  77. package/dist/runner/common/spawning-runner.js +1 -1
  78. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  79. package/dist/runner/dsh/catalog-probe.js +10 -0
  80. package/dist/runner/dsh/client.d.ts +1 -3
  81. package/dist/runner/dsh/client.js +2 -4
  82. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  83. package/dist/runner/dsh/exit-cancel.js +2 -2
  84. package/dist/runner/dsh/inflight.d.ts +1 -1
  85. package/dist/runner/dsh/inflight.js +1 -1
  86. package/dist/runner/dsh/runner.d.ts +40 -1
  87. package/dist/runner/dsh/runner.js +272 -199
  88. package/dist/runner/dsh/translator.d.ts +1 -1
  89. package/dist/runner/dsh/translator.js +1 -1
  90. package/dist/runner/index.d.ts +1 -1
  91. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  92. package/dist/runner/kimi/acp/runner.js +1 -2
  93. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  94. package/dist/runner/kimi/acp/translator.js +3 -4
  95. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  96. package/dist/runner/opencode/acp/runner.js +4 -4
  97. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  98. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  99. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  100. package/dist/runner/pi/rpc/runner.js +27 -42
  101. package/dist/runner/types.d.ts +2 -2
  102. package/dist/runner/types.js +1 -1
  103. package/dist/session/claude/sessions.d.ts +45 -0
  104. package/dist/session/claude/sessions.js +97 -71
  105. package/dist/session/codex/rollout-reader.js +4 -4
  106. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  107. package/dist/session/common/throw-session-list-error.js +16 -0
  108. package/dist/session/dsh/sessions.js +11 -5
  109. package/dist/session/kimi/sessions.d.ts +40 -1
  110. package/dist/session/kimi/sessions.js +244 -192
  111. package/dist/session/opencode/sessions.d.ts +153 -1
  112. package/dist/session/opencode/sessions.js +354 -321
  113. package/dist/session/pi/sessions.js +4 -5
  114. package/dist/session/session-store.d.ts +1 -1
  115. package/dist/session/session-store.js +1 -1
  116. package/dist/update/version-check.js +2 -1
  117. package/dist/workspace/index.d.ts +1 -1
  118. package/dist/workspace/index.js +1 -1
  119. package/package.json +1 -1
@@ -140,7 +140,7 @@ export declare class ClaudeSession extends SpawningRunner {
140
140
  * 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
141
141
  * 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(语义)。
142
142
  *
143
- * result = turn 终态(2026-08-16 真实 claude 实测:长驻模式下 result 之后
143
+ * result = turn 终态(长驻模式下 result 之后
144
144
  * 不再有 stdout 事件,后台任务完成是静默的;旧 -p 流程「等进程退出再收尾」
145
145
  * 的 post-result drain 在长驻模式下无对应物)。
146
146
  */
@@ -127,7 +127,7 @@ export class ClaudeSession extends SpawningRunner {
127
127
  // 或 --resume 目标会话)。比较进程当前 session(init 报告值)而非 spawn
128
128
  // 请求值:bridge 会把 init 的 session_id 写回 SessionStore,第二条消息
129
129
  // 带着它来;若拿 spawn 请求值('')比较,每条消息都会误判切换、杀进程
130
- // 重启,长驻设计失效(2026-08-16)。
130
+ // 重启,长驻设计失效。
131
131
  if (this.isRunning && this.sessionId !== (opts.sessionId ?? '')) {
132
132
  getLogger().info(`[${this.logTag}] session mismatch: process=${this.sessionId || '(fresh)'} ` +
133
133
  `requested=${opts.sessionId ?? '(fresh)'}, recycling process`);
@@ -151,7 +151,7 @@ export class ClaudeSession extends SpawningRunner {
151
151
  // review:stop()/进程死亡与写 stdin 竞态(EPIPE/ENOTCONN)。用户
152
152
  // stop 已置 stoppedByUser(或进程已死/流已结束)时,写入失败不抛给
153
153
  // 调用方,交由 consumeTurn 按 interrupted/error 产出终态——否则
154
- // stop 在途的 run 会以裸 EPIPE 崩溃(2026-08-16 复现的 flaky)。
154
+ // stop 在途的 run 会以裸 EPIPE 崩溃(flaky)。
155
155
  if (this.stoppedByUser || !this.isRunning || this.streamEnded) {
156
156
  getLogger().warn(`[${this.logTag}] user message write failed, process stopping: ${err instanceof Error ? err.message : String(err)}`);
157
157
  }
@@ -383,7 +383,7 @@ export class ClaudeSession extends SpawningRunner {
383
383
  * 消费事件队列直到本 turn 的 result 事件(非 compact/compaction)。
384
384
  * 消费者提前关闭生成器(bridge 循环体抛错)→ 杀进程防孤儿(语义)。
385
385
  *
386
- * result = turn 终态(2026-08-16 真实 claude 实测:长驻模式下 result 之后
386
+ * result = turn 终态(长驻模式下 result 之后
387
387
  * 不再有 stdout 事件,后台任务完成是静默的;旧 -p 流程「等进程退出再收尾」
388
388
  * 的 post-result drain 在长驻模式下无对应物)。
389
389
  */
@@ -716,7 +716,9 @@ export class ClaudeSession extends SpawningRunner {
716
716
  void this.writeControlResponse(requestId, {
717
717
  behavior: 'deny',
718
718
  message: '无法解析 AskUserQuestion 选项',
719
- }).catch(() => { });
719
+ }).catch((err) => {
720
+ getLogger().warn(`[${this.logTag}] AskUserQuestion deny write failed requestId=${requestId}: ${err.message}`);
721
+ });
720
722
  // 解析失败 deny 后同样释放条目(不经过 respondPermission)。
721
723
  this.pendingToolInputs.delete(requestId);
722
724
  return [];
@@ -62,7 +62,14 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
62
62
  protected cancelCurrentTurn(): Promise<void>;
63
63
  protected clearTurnState(): void;
64
64
  protected releaseConnection(cwd: string): Promise<void>;
65
+ /**
66
+ * Wire the acquired client's hooks into the turn dispatcher (anchor-guarded).
67
+ * 代际锚点与 ACP/pi 同口径:连接按 workspace 缓存复用,旧 turn 迟到的通知 /
68
+ * server request / onClose 不得进入新轮(此前 codex 缺这层守卫,属补齐漂移)。
69
+ */
70
+ private installTurnHooks;
65
71
  protected notifyIdle(cwd: string): void;
72
+ protected notifyConnectionActivity(cwd: string): void;
66
73
  protected disposeConnections(): Promise<void>;
67
74
  /**
68
75
  * Run a compact operation on the current thread.
@@ -100,8 +107,8 @@ export declare class CodexAppServerRunner extends ConnectionBasedRunner<JsonRpcC
100
107
  *
101
108
  * App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
102
109
  * multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
103
- * `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
104
- * (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
110
+ * `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达。
111
+ * 一个 turn 只属于一个线程,因此外来线程的通知一律
105
112
  * 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
106
113
  * 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
107
114
  * "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
@@ -58,8 +58,8 @@ function buildRequestUserInputResponse(action, questions, response) {
58
58
  return;
59
59
  const values = Array.isArray(value) ? value : [value];
60
60
  const cleaned = values.filter((v) => typeof v === 'string' && v.length > 0);
61
- // Codex user_note:补充说明编码为 "user_note: <text>" 条目(2026-08-18
62
- // live 验证 deepseek-v4-flash 明确理解并遵守该格式)。
61
+ // Codex user_note:补充说明编码为 "user_note: <text>" 条目(deepseek-v4-flash
62
+ // 明确理解并遵守该格式)。
63
63
  const note = notes?.[q.question]?.trim();
64
64
  if (note)
65
65
  cleaned.push(`user_note: ${note}`);
@@ -184,9 +184,27 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
184
184
  async releaseConnection(cwd) {
185
185
  await this.connectionManager.release(cwd);
186
186
  }
187
+ /**
188
+ * Wire the acquired client's hooks into the turn dispatcher (anchor-guarded).
189
+ * 代际锚点与 ACP/pi 同口径:连接按 workspace 缓存复用,旧 turn 迟到的通知 /
190
+ * server request / onClose 不得进入新轮(此前 codex 缺这层守卫,属补齐漂移)。
191
+ */
192
+ installTurnHooks(anchor) {
193
+ anchor.client.setHooks({
194
+ onNotification: this.forCurrentTurn(anchor, 'notification', (method, params) => this.handleNotification(method, params)),
195
+ onServerRequest: this.forCurrentTurn(anchor, 'server request', (id, method, params) => this.handleServerRequest(id, method, params)),
196
+ onClose: this.forCurrentTurn(anchor, 'connection close', () => {
197
+ getLogger().warn(`[${this.logTag}] client connection closed`);
198
+ this.failTurn('Codex app-server connection closed');
199
+ }),
200
+ });
201
+ }
187
202
  notifyIdle(cwd) {
188
203
  this.connectionManager.notifyIdle(cwd);
189
204
  }
205
+ notifyConnectionActivity(cwd) {
206
+ this.connectionManager.notifyActivity(cwd);
207
+ }
190
208
  async disposeConnections() {
191
209
  await this.connectionManager.disposeAll();
192
210
  }
@@ -195,14 +213,9 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
195
213
  */
196
214
  async *runCompact(_message, opts) {
197
215
  yield* this.executeTurn(opts, async () => {
198
- const client = await this.connectionManager.acquire(opts.cwd);
199
- this.connectionManager.notifyActivity(opts.cwd);
200
- this.currentClient = client;
201
- client.setHooks({
202
- onNotification: (method, params) => this.handleNotification(method, params),
203
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
204
- onClose: () => this.failTurn('Codex app-server connection closed'),
205
- });
216
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
217
+ this.installTurnHooks(anchor);
218
+ const { client } = anchor;
206
219
  if (!opts.sessionId) {
207
220
  throw new Error('compact requires a sessionId');
208
221
  }
@@ -244,8 +257,8 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
244
257
  else if (pending.kind === 'permissions') {
245
258
  // 权限审批的响应没有 decision 字段(真实协议只回 granted profile +
246
259
  // scope):decline/cancel 必须返回空授权(拒绝全部),否则会把用户已
247
- // 勾选的条目当作授予返回(2026-08-12 :勾选后点「拒绝」实
248
- // 际授予了所选权限)。accept/acceptForSession 才带用户勾选的条目。
260
+ // 勾选的条目当作授予返回(勾选后点「拒绝」实际授予了所选权限)。
261
+ // accept/acceptForSession 才带用户勾选的条目。
249
262
  const denied = action === 'decline' || action === 'cancel';
250
263
  client.respond(requestId, {
251
264
  permissions: denied
@@ -344,17 +357,9 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
344
357
  * thread/resume(按 sessionId 恢复既有线程)→ turn/start。
345
358
  */
346
359
  async setupTurn(message, opts) {
347
- const client = await this.connectionManager.acquire(opts.cwd);
348
- this.connectionManager.notifyActivity(opts.cwd);
349
- this.currentClient = client;
350
- client.setHooks({
351
- onNotification: (method, params) => this.handleNotification(method, params),
352
- onServerRequest: (id, method, params) => this.handleServerRequest(id, method, params),
353
- onClose: () => {
354
- getLogger().warn(`[${this.logTag}] client connection closed`);
355
- this.failTurn('Codex app-server connection closed');
356
- },
357
- });
360
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
361
+ this.installTurnHooks(anchor);
362
+ const { client } = anchor;
358
363
  const threadParams = this.buildThreadParams(opts.cwd);
359
364
  let threadId;
360
365
  if (opts.sessionId) {
@@ -387,8 +392,8 @@ export class CodexAppServerRunner extends ConnectionBasedRunner {
387
392
  *
388
393
  * App-server 把已知线程的通知全部复用同一条 stdio 连接:agent 用
389
394
  * multi-agent v2 派生子 agent 后,子线程的 `turn/started`、`item/*`、
390
- * `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达
391
- * (2026-10-09 实测)。一个 turn 只属于一个线程,因此外来线程的通知一律
395
+ * `turn/completed`、`thread/tokenUsage/updated` 会与本轮通知交错到达。
396
+ * 一个 turn 只属于一个线程,因此外来线程的通知一律
392
397
  * 丢弃——否则子线程的 `turn/started` 会覆盖本轮 threadId,会话写回把子
393
398
  * 线程 id 当成顶层会话指针存盘(冷连接 `thread/resume` 直接报
394
399
  * "cannot resume an unloaded multi-agent v2 sub-agent"),且子线程的
@@ -14,7 +14,6 @@ export type TranslatorEvent = AgentEvent | TurnStartedEvent | TurnDiffEvent | Ap
14
14
  export declare class CodexAppServerTranslator {
15
15
  private turnId;
16
16
  private threadId;
17
- private turnStatus;
18
17
  /** Full agentMessage text per item id (item-scoped snapshot). */
19
18
  private textByItem;
20
19
  /** agentMessage item ids whose protocol phase is `final_answer`. */
@@ -69,10 +68,6 @@ export declare class CodexAppServerTranslator {
69
68
  inputTokens: number;
70
69
  outputTokens: number;
71
70
  };
72
- /**
73
- * Get the current turn status.
74
- */
75
- getTurnStatus(): string;
76
71
  /**
77
72
  * Set the operation kind (exec vs compact).
78
73
  */
@@ -8,6 +8,7 @@
8
8
  * app-server v2 protocol (see protocol-types.ts).
9
9
  */
10
10
  import { makeQuestionApprovalEvent } from '../../question-common.js';
11
+ import { renderPlanMarkdown } from '../../common/acp/base-translator.js';
11
12
  import { NotificationMethod, ServerRequestMethod, UNSUPPORTED_SERVER_REQUEST_METHODS, } from './protocol-types.js';
12
13
  // =============================================================================
13
14
  // Translator
@@ -15,7 +16,6 @@ import { NotificationMethod, ServerRequestMethod, UNSUPPORTED_SERVER_REQUEST_MET
15
16
  export class CodexAppServerTranslator {
16
17
  turnId = '';
17
18
  threadId = '';
18
- turnStatus = 'pending';
19
19
  /** Full agentMessage text per item id (item-scoped snapshot). */
20
20
  textByItem = new Map();
21
21
  /** agentMessage item ids whose protocol phase is `final_answer`. */
@@ -133,7 +133,6 @@ export class CodexAppServerTranslator {
133
133
  */
134
134
  handleTurnCompleted(params) {
135
135
  const turn = params?.turn;
136
- this.turnStatus = turn?.status ?? 'completed';
137
136
  const events = [];
138
137
  // 兜底:turn.items 里权威 agentMessage / plan item 与已流式内容不一致或
139
138
  // 未发过(如 itemsView 摘要只含部分 item)时,补发 item 级快照。已一致则
@@ -204,12 +203,6 @@ export class CodexAppServerTranslator {
204
203
  outputTokens: this.usage.outputTokens,
205
204
  };
206
205
  }
207
- /**
208
- * Get the current turn status.
209
- */
210
- getTurnStatus() {
211
- return this.turnStatus;
212
- }
213
206
  /**
214
207
  * Set the operation kind (exec vs compact).
215
208
  */
@@ -222,7 +215,6 @@ export class CodexAppServerTranslator {
222
215
  handleTurnStarted(params) {
223
216
  this.threadId = params.threadId;
224
217
  this.turnId = params.turn.id;
225
- this.turnStatus = params.turn.status ?? 'inProgress';
226
218
  // 单 turn 用量从零开始累计:turn 内每次采样都会发一条
227
219
  // `thread/tokenUsage/updated`,本 run 的用量是这些增量之和(不是最后一条)。
228
220
  // resume 后服务端重放的上一 turn 用量若在此之后到达,由 handleTokenUsageUpdated
@@ -497,14 +489,9 @@ export class CodexAppServerTranslator {
497
489
  * status 缺省或未知一律按 pending 处理(防御 wire 枚举漂移)。
498
490
  */
499
491
  handleTurnPlanUpdated(params) {
500
- const ICON = { completed: '✅', in_progress: '🔄', pending: '⬜', error: '❌' };
501
- const markdown = params.plan
502
- .map((s) => {
503
- const status = s.status && s.status in ICON ? s.status : 'pending';
504
- return `${ICON[status]} ${s.title}${s.subtitle ? ` — ${s.subtitle}` : ''}`;
505
- })
506
- .join('\n');
507
- return [{ type: 'plan', plan: markdown, timestamp: this.now() }];
492
+ // 图标表 + status 归一与 ACP handlePlan 共用 renderPlanMarkdown
493
+ // (未知/缺省 status 一律按 pending,防 wire 枚举漂移)。
494
+ return [{ type: 'plan', plan: renderPlanMarkdown(params.plan), timestamp: this.now() }];
508
495
  }
509
496
  handleError(params) {
510
497
  if (params.willRetry) {
@@ -13,7 +13,7 @@ export interface AcpApprovalKinds {
13
13
  acceptForSession: string[];
14
14
  decline: string[];
15
15
  }
16
- /** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
16
+ /** kimi 观测 kind(approval.ts:28-29:approve_once/approve_always/reject)。 */
17
17
  export declare const KIMI_APPROVAL_KINDS: AcpApprovalKinds;
18
18
  /** opencode kind(permission.ts:219-223:once/always → approve,其余 → reject)。 */
19
19
  export declare const OPENCODE_APPROVAL_KINDS: AcpApprovalKinds;
@@ -20,7 +20,7 @@ function findOptionIdByKind(options, candidateKinds) {
20
20
  }
21
21
  return undefined;
22
22
  }
23
- /** kimi 观测 kind(approval.ts:28-29 + 2026-08-15 live:approve_once/approve_always/reject)。 */
23
+ /** kimi 观测 kind(approval.ts:28-29:approve_once/approve_always/reject)。 */
24
24
  export const KIMI_APPROVAL_KINDS = {
25
25
  accept: ['approve_once', 'approve_always', 'allow_once'],
26
26
  acceptForSession: ['approve_always', 'allow_always'],
@@ -6,7 +6,7 @@
6
6
  * prompt orchestration, compact orchestration, approval response dispatch,
7
7
  * cancel/clear/release plumbing and the pendingApprovals registry were
8
8
  * line-for-line duplicated with only constants/field names differing
9
- * (2026-09 round-2 simplification). This base absorbs them; what stays in
9
+ * (round-2 simplification). This base absorbs them; what stays in
10
10
  * subclasses:
11
11
  * - applyTurnSettings: mode/model下发 semantics differ (kimi unconditional,
12
12
  * opencode conditional on cached values)
@@ -80,7 +80,7 @@ export declare abstract class BaseAcpRunner<TTranslator extends BaseAcpTranslato
80
80
  * 自身半死:opencode v2 的 location 在每轮拉起的 `opencode acp` 子进程里 boot,
81
81
  * boot 期间后台服务不可用会让它长期停在未激活,后续轮次复用同一连接只会重复
82
82
  * 失败——用户重启 opencode service 也治不好,因为连接缓存在本进程里
83
- * (2026-10-30 现场:4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
83
+ * (4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
84
84
  * 后下一轮重新 spawn,拿到的是新进程的新状态。
85
85
  *
86
86
  * 只对声明为可重试的错误生效:kimi 等沿用默认 `isRetryableSessionSetupError`
@@ -94,6 +94,9 @@ export declare abstract class BaseAcpRunner<TTranslator extends BaseAcpTranslato
94
94
  protected clearTurnState(): void;
95
95
  protected releaseConnection(cwd: string): Promise<void>;
96
96
  protected notifyIdle(cwd: string): void;
97
+ protected notifyConnectionActivity(cwd: string): void;
98
+ /** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
99
+ private installTurnHooks;
97
100
  protected disposeConnections(): Promise<void>;
98
101
  /**
99
102
  * Respond to an approval server request. `response` is the bridge
@@ -65,7 +65,7 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
65
65
  * 自身半死:opencode v2 的 location 在每轮拉起的 `opencode acp` 子进程里 boot,
66
66
  * boot 期间后台服务不可用会让它长期停在未激活,后续轮次复用同一连接只会重复
67
67
  * 失败——用户重启 opencode service 也治不好,因为连接缓存在本进程里
68
- * (2026-10-30 现场:4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
68
+ * (4 次重试全失败,重启服务与 lark-remote 都无效)。丢掉连接
69
69
  * 后下一轮重新 spawn,拿到的是新进程的新状态。
70
70
  *
71
71
  * 只对声明为可重试的错误生效:kimi 等沿用默认 `isRetryableSessionSetupError`
@@ -123,6 +123,21 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
123
123
  notifyIdle(cwd) {
124
124
  this.connectionManager.notifyIdle(cwd);
125
125
  }
126
+ notifyConnectionActivity(cwd) {
127
+ this.connectionManager.notifyActivity(cwd);
128
+ }
129
+ /** Wire the acquired client's hooks into the turn dispatcher (anchor-guarded). */
130
+ installTurnHooks(anchor) {
131
+ anchor.client.setHooks({
132
+ onNotification: this.forCurrentTurn(anchor, 'notification', (method, params) => this.handleNotification(method, params)),
133
+ onServerRequest: this.forCurrentTurn(anchor, 'server request', (id, method, params) => this.handleServerRequest(id, method, params)),
134
+ onClose: this.forCurrentTurn(anchor, 'connection close', () => {
135
+ getLogger().warn(`[${this.logTag}] client connection closed`);
136
+ this.onConnectionClosed();
137
+ this.failTurn(this.connectionClosedMessage);
138
+ }),
139
+ });
140
+ }
126
141
  async disposeConnections() {
127
142
  await this.beforeDisposeConnections();
128
143
  await this.connectionManager.disposeAll();
@@ -201,36 +216,12 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
201
216
  * session/resume (existing sessionId) → applyTurnSettings → session/prompt.
202
217
  */
203
218
  async setupTurn(message, opts) {
204
- // 本轮回调归属锚点:代际 + client 都不可变。旧轮迟到的 prompt
205
- // 结果/通知在新轮开始时 stopRequested 已被重置,只有这两个值能识别它。
206
- const generation = this.turnGeneration;
207
- const client = await this.connectionManager.acquire(opts.cwd);
208
- this.connectionManager.notifyActivity(opts.cwd);
209
- this.currentClient = client;
219
+ // 本轮回调归属锚点(代际 + client)由 acquireForTurn 捕获:旧轮迟到的
220
+ // prompt 结果/通知在新轮开始时 stopRequested 已被重置,只有这对值能识别它。
221
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
210
222
  this.onConnectionAcquired(opts);
211
- client.setHooks({
212
- onNotification: (method, params) => {
213
- if (!this.isCurrentTurn(generation, client)) {
214
- getLogger().debug(`[${this.logTag}] dropped notification from a finished turn`);
215
- return;
216
- }
217
- this.handleNotification(method, params);
218
- },
219
- onServerRequest: (id, method, params) => {
220
- if (!this.isCurrentTurn(generation, client)) {
221
- getLogger().debug(`[${this.logTag}] dropped server request from a finished turn`);
222
- return;
223
- }
224
- this.handleServerRequest(id, method, params);
225
- },
226
- onClose: () => {
227
- if (!this.isCurrentTurn(generation, client))
228
- return;
229
- getLogger().warn(`[${this.logTag}] client connection closed`);
230
- this.onConnectionClosed();
231
- this.failTurn(this.connectionClosedMessage);
232
- },
233
- });
223
+ this.installTurnHooks(anchor);
224
+ const { client } = anchor;
234
225
  let sessionId;
235
226
  if (opts.sessionId) {
236
227
  const resumeParams = {
@@ -267,19 +258,7 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
267
258
  this.promptSettled = false;
268
259
  const promptPromise = client.request('session/prompt', promptParams, Number.POSITIVE_INFINITY);
269
260
  this.promptSent = true;
270
- promptPromise.then((result) => {
271
- if (!this.isCurrentTurn(generation, client)) {
272
- getLogger().debug(`[${this.logTag}] dropped late prompt result from a finished turn`);
273
- return;
274
- }
275
- this.handlePromptResolved(result, translator, sessionId);
276
- }, (err) => {
277
- if (!this.isCurrentTurn(generation, client)) {
278
- getLogger().debug(`[${this.logTag}] dropped late prompt rejection from a finished turn`);
279
- return;
280
- }
281
- this.handlePromptRejected(err, translator, sessionId, '');
282
- });
261
+ promptPromise.then(this.forCurrentTurn(anchor, 'late prompt result', (result) => this.handlePromptResolved(result, translator, sessionId)), this.forCurrentTurn(anchor, 'late prompt rejection', (err) => this.handlePromptRejected(err, translator, sessionId, '')));
283
262
  }
284
263
  /**
285
264
  * Run a compact operation on the current session: resume → '/compact'
@@ -287,29 +266,11 @@ export class BaseAcpRunner extends ConnectionBasedRunner {
287
266
  */
288
267
  async *runCompact(_message, opts) {
289
268
  yield* this.executeTurn(opts, async () => {
290
- const generation = this.turnGeneration;
291
- const client = await this.connectionManager.acquire(opts.cwd);
292
- this.connectionManager.notifyActivity(opts.cwd);
293
- this.currentClient = client;
269
+ // 同 setupTurn:acquireForTurn 捕获代际锚点,installTurnHooks 守卫 dispatch。
270
+ const anchor = await this.acquireForTurn(opts.cwd, () => this.connectionManager.acquire(opts.cwd));
294
271
  this.onConnectionAcquired(opts);
295
- client.setHooks({
296
- onNotification: (method, params) => {
297
- if (!this.isCurrentTurn(generation, client))
298
- return;
299
- this.handleNotification(method, params);
300
- },
301
- onServerRequest: (id, method, params) => {
302
- if (!this.isCurrentTurn(generation, client))
303
- return;
304
- this.handleServerRequest(id, method, params);
305
- },
306
- onClose: () => {
307
- if (!this.isCurrentTurn(generation, client))
308
- return;
309
- this.onConnectionClosed();
310
- this.failTurn(this.connectionClosedMessage);
311
- },
312
- });
272
+ this.installTurnHooks(anchor);
273
+ const { client } = anchor;
313
274
  const sessionId = opts.sessionId;
314
275
  if (!sessionId) {
315
276
  throw new Error('compact requires a sessionId');
@@ -4,7 +4,7 @@
4
4
  * Both agents' session/update notification envelopes, prompt-response
5
5
  * stopReason mapping, tool_call/tool_call_update normalization, usage_update
6
6
  * occupancy bookkeeping and self-produced turn_started events are
7
- * line-for-line identical — extracted here in the 2026-09 round-2
7
+ * line-for-line identical — extracted here in the round-2
8
8
  * simplification. What genuinely diverges is the content channel:
9
9
  * kimi accumulates wire deltas into turn_diff snapshots (snapshot semantics,
10
10
  * see KimiAcpTranslator header), opencode forwards `assistant` deltas
@@ -21,6 +21,16 @@
21
21
  */
22
22
  import type { AgentEvent, ApprovalRequestedEvent, ApprovalView, ResultEvent, TurnStartedEvent } from '../../types.js';
23
23
  import { type AgentMessageChunkEvent, type AgentThoughtChunkEvent, type ToolCallEvent, type ToolCallUpdateEvent, type UsageUpdateEvent, type RequestPermissionParams, type AcpPlanEvent } from './protocol-types.js';
24
+ /**
25
+ * Plan entries → 「图标 + 标题」markdown 清单(每行一条,`\n` join)。
26
+ * ACP 与 codex 双抄收编(R5);subtitle 拼接、trim、空清单语义等差异留在
27
+ * 各自调用点。
28
+ */
29
+ export declare function renderPlanMarkdown(entries: ReadonlyArray<{
30
+ title: string;
31
+ subtitle?: string;
32
+ status?: string;
33
+ }>): string;
24
34
  /** Self-produced turn_started event (no wire equivalent — produced on prompt start). */
25
35
  export type AcpTurnStartedEvent = TurnStartedEvent;
26
36
  /**
@@ -58,7 +68,7 @@ export declare abstract class BaseAcpTranslator {
58
68
  /**
59
69
  * Latest streaming-args text per tool call (keyed by wire toolCallId).
60
70
  * kimi ACP sends the permission request BEFORE tool.call.started (verified
61
- * live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
71
+ * against kimi 0.42.0): the request_permission toolCall
62
72
  * carries only title + a truncated summary in content, NO rawInput — the
63
73
  * full args only ever reach the client as accumulated text in the
64
74
  * tool_call/tool_call_update streaming deltas. Tracking them here lets the
@@ -12,6 +12,32 @@ const KIND_TO_TOOL = {
12
12
  search: 'Grep',
13
13
  fetch: 'WebFetch',
14
14
  };
15
+ /**
16
+ * Plan 条目 status → 渲染图标。未知/缺省 status 一律按 pending 渲染——
17
+ * 两处调用点(ACP handlePlan / codex handleTurnPlanUpdated)注释都点名防
18
+ * wire 枚举漂移,新增枚举值时只改这张表。
19
+ */
20
+ const PLAN_STATUS_ICONS = {
21
+ completed: '✅',
22
+ in_progress: '🔄',
23
+ pending: '⬜',
24
+ error: '❌',
25
+ };
26
+ /**
27
+ * Plan entries → 「图标 + 标题」markdown 清单(每行一条,`\n` join)。
28
+ * ACP 与 codex 双抄收编(R5);subtitle 拼接、trim、空清单语义等差异留在
29
+ * 各自调用点。
30
+ */
31
+ export function renderPlanMarkdown(entries) {
32
+ return entries
33
+ .map((entry) => {
34
+ const status = entry.status && entry.status in PLAN_STATUS_ICONS
35
+ ? entry.status
36
+ : 'pending';
37
+ return `${PLAN_STATUS_ICONS[status]} ${entry.title}${entry.subtitle ? ` — ${entry.subtitle}` : ''}`;
38
+ })
39
+ .join('\n');
40
+ }
15
41
  /**
16
42
  * Join the text payloads of a tool_call(/_update) content array
17
43
  * (`[{type:'content', content:{type:'text', text}}]` entries). Returns
@@ -59,7 +85,7 @@ export class BaseAcpTranslator {
59
85
  /**
60
86
  * Latest streaming-args text per tool call (keyed by wire toolCallId).
61
87
  * kimi ACP sends the permission request BEFORE tool.call.started (verified
62
- * live against kimi 0.42.0, 2026-09-13): the request_permission toolCall
88
+ * against kimi 0.42.0): the request_permission toolCall
63
89
  * carries only title + a truncated summary in content, NO rawInput — the
64
90
  * full args only ever reach the client as accumulated text in the
65
91
  * tool_call/tool_call_update streaming deltas. Tracking them here lets the
@@ -213,16 +239,12 @@ export class BaseAcpTranslator {
213
239
  * completed)。基类具体方法:kimi/opencode wire 结构相同,直接复用。
214
240
  */
215
241
  handlePlan(event) {
216
- const ICON = { completed: '✅', in_progress: '🔄', pending: '⬜', error: '❌' };
217
242
  const entries = Array.isArray(event.entries) ? event.entries : [];
218
- const markdown = entries
219
- .map((e) => {
243
+ const markdown = renderPlanMarkdown(entries.map((e) => {
220
244
  const entry = e;
221
- const status = entry.status && entry.status in ICON ? entry.status : 'pending';
222
- return `${ICON[status]} ${entry.content ?? ''}`;
223
- })
224
- .join('\n')
225
- .trim();
245
+ // ACP 条目的正文字段是 content(codex 是 title),无 subtitle。
246
+ return { title: entry.content ?? '', status: entry.status };
247
+ })).trim();
226
248
  if (!markdown)
227
249
  return [];
228
250
  return [{ type: 'plan', plan: markdown, timestamp: new Date().toISOString() }];
@@ -238,7 +238,7 @@ export interface PermissionOption {
238
238
  export interface RequestPermissionParams {
239
239
  sessionId: string;
240
240
  /**
241
- * Approval target. Divergence by server (2026-09-13 live probes):
241
+ * Approval target. Divergence by server:
242
242
  * - kimi 0.42: {toolCallId, title: toolName, content: [truncated summary
243
243
  * text]} — NO rawInput; the approval gate fires BEFORE tool.call.started,
244
244
  * so the full args only exist in the pre-approval tool_call/_update
@@ -43,7 +43,7 @@ export const RpcErrorCode = {
43
43
  // =============================================================================
44
44
  // Notifications (server → client, via session/update)
45
45
  // Shape source: kimi-code/packages/acp-server/src/events-map.ts
46
- // (each constructor below) + 2026-08-15 live smoke test against kimi 0.36.0
46
+ // (each constructor below) + verified against kimi 0.36.0
47
47
  //
48
48
  // REAL envelope: {sessionId, update: {sessionUpdate: '<kind>', ...}}.
49
49
  // The discriminator field is `update.sessionUpdate` — there is NO nested
@@ -72,7 +72,7 @@ export const SessionEventType = {
72
72
  // Server Requests (approval — reverse RPC)
73
73
  // Shape source: kimi-code/packages/acp-server/src/approval.ts:28-29
74
74
  // options: [{optionId, name, kind}]; optionId is opaque, echo back as-is
75
- // + 2026-08-15 live smoke test: approve_once / approve_always / reject
75
+ // + verified against kimi 0.36.0: approve_once / approve_always / reject
76
76
  // =============================================================================
77
77
  export const ServerRequestMethod = {
78
78
  REQUEST_PERMISSION: 'session/request_permission',
@@ -18,6 +18,18 @@ import type { AgentKind } from '../../agent-kinds.js';
18
18
  import type { AgentSessionReader } from '../../session-contract.js';
19
19
  import type { AgentEvent, AgentRunner, AgentStatusInfo, SpawnOptions } from '../types.js';
20
20
  import type { AgentStopper } from '../../platform/agent-stopper.js';
21
+ /**
22
+ * 本轮回调的归属锚点(generation + client),由 {@link ConnectionBasedRunner.acquireForTurn}
23
+ * 捕获。两个值在整轮内不可变:旧轮迟到的回调(prompt 结果 / 通知 / onClose)在
24
+ * 新轮开始时 `stopRequested` 已被重置,只有这对值能识别它们——pi 的教训是旧 client
25
+ * 上残留的上一轮 onClose 闭包会把假错误推进新 turn 的共享队列(用户消息未发出即
26
+ * 丢失),迟到的 agent_settled 同理(新 turn 空转立即「成功」)。所有 hook 的事件
27
+ * 落地前必须过 {@link ConnectionBasedRunner.isCurrentTurn} 守卫。
28
+ */
29
+ export interface TurnAnchor<TClient> {
30
+ readonly client: TClient;
31
+ readonly generation: number;
32
+ }
21
33
  export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent> implements AgentRunner {
22
34
  readonly kind: AgentKind;
23
35
  readonly sessionReader: AgentSessionReader;
@@ -115,7 +127,7 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
115
127
  /**
116
128
  * Mark this runner as evicted from the bridge's runner cache while a run may
117
129
  * still be holding/creating connections on it (queue-advance race during
118
- * interruptCurrentRun's await window, 2026-10-11). The active run's finally
130
+ * interruptCurrentRun's await window). The active run's finally
119
131
  * disposes connections eagerly instead of arming the idle TTL timer —
120
132
  * otherwise the agent server process lingers for up to 30 minutes holding
121
133
  * its session write lock (codex: "thread already has an active writer").
@@ -145,6 +157,25 @@ export declare abstract class ConnectionBasedRunner<TClient, TEvent = AgentEvent
145
157
  * server request 一律只记 debug,不得进入共享 notificationQueue。
146
158
  */
147
159
  protected isCurrentTurn(generation: number, client: TClient | null): boolean;
160
+ /**
161
+ * Acquire the per-turn connection and capture the callback anchor
162
+ * ({@link TurnAnchor}): acquire → activity ping → currentClient → anchor.
163
+ * Shared preamble of every setupTurn / runCompact; the acquired anchor is
164
+ * what the turn's hooks (and prompt-settle callbacks) validate against.
165
+ *
166
+ * `acquire` is a closure so subclasses keep their own acquire signatures
167
+ * (pi passes the spawn-bound `{ sessionId }`).
168
+ */
169
+ protected acquireForTurn(cwd: string, acquire: () => Promise<TClient>): Promise<TurnAnchor<TClient>>;
170
+ /** Per-cwd activity ping on the subclass connection manager (mirror of notifyIdle). */
171
+ protected abstract notifyConnectionActivity(cwd: string): void;
172
+ /**
173
+ * Wrap a per-turn callback with the isCurrentTurn guard: events from a
174
+ * finished turn are dropped with a debug log and never reach the turn
175
+ * dispatcher. 6 处守卫原有的日志粒度不一(ACP setupTurn 记 debug,其余静默),
176
+ * 统一为最详细版本:dropped 一律 debug,便于事后追溯。
177
+ */
178
+ protected forCurrentTurn<A extends unknown[]>(anchor: TurnAnchor<TClient>, label: string, handler: (...args: A) => void): (...args: A) => void;
148
179
  protected failTurn(message: string): void;
149
180
  protected resetRunState(): void;
150
181
  /** Release the connection + detach stale client hooks after a lost connection. */