lark-remote 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +57 -34
  5. package/dist/bridge/approval-coordinator.js +191 -40
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +78 -7
  9. package/dist/bridge/index.js +202 -79
  10. package/dist/bridge/queue-manager.d.ts +2 -1
  11. package/dist/bridge/queue-manager.js +1 -0
  12. package/dist/card/approval-render.js +152 -36
  13. package/dist/card/bash-card-session.d.ts +2 -24
  14. package/dist/card/bash-card-session.js +6 -73
  15. package/dist/card/bash-renderer.js +2 -3
  16. package/dist/card/card-budget.js +29 -75
  17. package/dist/card/card-session.d.ts +48 -0
  18. package/dist/card/card-session.js +86 -0
  19. package/dist/card/card-shared.d.ts +1 -12
  20. package/dist/card/card-shared.js +1 -26
  21. package/dist/card/run-card-session.d.ts +2 -55
  22. package/dist/card/run-card-session.js +6 -104
  23. package/dist/card/run-renderer.js +99 -232
  24. package/dist/card/run-state.d.ts +13 -0
  25. package/dist/card/run-state.js +25 -16
  26. package/dist/card/text-truncate.d.ts +13 -12
  27. package/dist/card/text-truncate.js +13 -63
  28. package/dist/common/fs.d.ts +8 -0
  29. package/dist/common/fs.js +17 -0
  30. package/dist/common/guards.d.ts +0 -20
  31. package/dist/common/guards.js +0 -43
  32. package/dist/common/sleep.d.ts +2 -0
  33. package/dist/common/sleep.js +5 -0
  34. package/dist/common/truncate.d.ts +39 -0
  35. package/dist/common/truncate.js +92 -0
  36. package/dist/config/codex-config.d.ts +1 -1
  37. package/dist/config/codex-config.js +1 -1
  38. package/dist/config/index.d.ts +88 -23
  39. package/dist/config/index.js +105 -34
  40. package/dist/connector/index.d.ts +69 -2
  41. package/dist/connector/index.js +107 -1
  42. package/dist/index.js +123 -105
  43. package/dist/instance-lock.js +4 -12
  44. package/dist/order/alias-resolve.d.ts +13 -0
  45. package/dist/order/alias-resolve.js +22 -0
  46. package/dist/order/alias-store.d.ts +38 -0
  47. package/dist/order/alias-store.js +106 -0
  48. package/dist/persistence/atomic-write.d.ts +8 -2
  49. package/dist/persistence/atomic-write.js +23 -7
  50. package/dist/restart.js +2 -1
  51. package/dist/router/card-helpers.d.ts +63 -0
  52. package/dist/router/card-helpers.js +146 -0
  53. package/dist/router/config/claude.js +25 -1
  54. package/dist/router/config/codex.js +29 -50
  55. package/dist/router/config/common/model-patch.d.ts +22 -0
  56. package/dist/router/config/common/model-patch.js +26 -0
  57. package/dist/router/config/common/render.js +5 -4
  58. package/dist/router/config/index.d.ts +12 -0
  59. package/dist/router/config/index.js +25 -1
  60. package/dist/router/config/kimi.d.ts +4 -1
  61. package/dist/router/config/kimi.js +27 -0
  62. package/dist/router/config/opencode.js +18 -5
  63. package/dist/router/config/pi.js +4 -5
  64. package/dist/router/config/types.d.ts +11 -2
  65. package/dist/router/index.d.ts +56 -21
  66. package/dist/router/index.js +373 -548
  67. package/dist/router/utils.d.ts +2 -0
  68. package/dist/router/utils.js +4 -0
  69. package/dist/runner/agent-choices-common.d.ts +16 -0
  70. package/dist/runner/agent-choices-common.js +31 -0
  71. package/dist/runner/claude/runner.d.ts +47 -10
  72. package/dist/runner/claude/runner.js +117 -46
  73. package/dist/runner/claude/session.d.ts +166 -0
  74. package/dist/runner/claude/session.js +749 -0
  75. package/dist/runner/codex/app-server/protocol-types.d.ts +31 -39
  76. package/dist/runner/codex/app-server/protocol-types.js +1 -12
  77. package/dist/runner/codex/app-server/runner.d.ts +42 -59
  78. package/dist/runner/codex/app-server/runner.js +160 -337
  79. package/dist/runner/codex/app-server/translator.d.ts +1 -21
  80. package/dist/runner/codex/app-server/translator.js +0 -18
  81. package/dist/runner/codex/index.d.ts +0 -2
  82. package/dist/runner/codex/index.js +0 -2
  83. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  84. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  85. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  86. package/dist/runner/common/acp/protocol-types.js +75 -0
  87. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  88. package/dist/runner/common/connection-based-runner.js +261 -0
  89. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  90. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  91. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  92. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  93. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  95. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  96. package/dist/runner/common/jsonrpc/types.js +9 -0
  97. package/dist/runner/common/process-stopper.d.ts +2 -1
  98. package/dist/runner/common/process-stopper.js +1 -0
  99. package/dist/runner/common/runner-utils.d.ts +6 -13
  100. package/dist/runner/common/runner-utils.js +6 -47
  101. package/dist/runner/common/spawning-runner.d.ts +24 -70
  102. package/dist/runner/common/spawning-runner.js +98 -152
  103. package/dist/runner/index.d.ts +4 -4
  104. package/dist/runner/index.js +4 -4
  105. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  106. package/dist/runner/kimi/acp/runner.js +448 -0
  107. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  108. package/dist/runner/kimi/acp/translator.js +336 -0
  109. package/dist/runner/kimi/index.d.ts +1 -1
  110. package/dist/runner/kimi/index.js +1 -1
  111. package/dist/runner/opencode/acp/index.d.ts +1 -0
  112. package/dist/runner/opencode/acp/index.js +2 -0
  113. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  114. package/dist/runner/opencode/acp/runner.js +429 -0
  115. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  116. package/dist/runner/opencode/acp/translator.js +345 -0
  117. package/dist/runner/opencode/index.d.ts +1 -1
  118. package/dist/runner/opencode/index.js +1 -1
  119. package/dist/runner/pi/index.d.ts +1 -1
  120. package/dist/runner/pi/index.js +1 -1
  121. package/dist/runner/pi/rpc/client.d.ts +52 -0
  122. package/dist/runner/pi/rpc/client.js +114 -0
  123. package/dist/runner/pi/rpc/index.d.ts +4 -0
  124. package/dist/runner/pi/rpc/index.js +4 -0
  125. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  126. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  127. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  128. package/dist/runner/pi/rpc/runner.js +199 -0
  129. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  130. package/dist/runner/pi/rpc/translator.js +258 -0
  131. package/dist/runner/registry.d.ts +7 -8
  132. package/dist/runner/registry.js +3 -4
  133. package/dist/runner/resolve-agent-choices.js +18 -59
  134. package/dist/runner/sync-agent-choices.js +16 -67
  135. package/dist/runner/types.d.ts +43 -13
  136. package/dist/session/claude/session-index.js +2 -3
  137. package/dist/session/claude/session-reader.js +3 -6
  138. package/dist/session/claude/sessions.js +6 -13
  139. package/dist/session/codex/rollout-reader.d.ts +2 -2
  140. package/dist/session/codex/rollout-reader.js +14 -17
  141. package/dist/session/codex/sessions.d.ts +1 -1
  142. package/dist/session/codex/sessions.js +2 -2
  143. package/dist/session/common/content-blocks.d.ts +7 -0
  144. package/dist/session/common/content-blocks.js +13 -9
  145. package/dist/session/common/jsonl.d.ts +8 -25
  146. package/dist/session/common/jsonl.js +126 -261
  147. package/dist/session/common/pagination.d.ts +28 -0
  148. package/dist/session/common/pagination.js +33 -0
  149. package/dist/session/kimi/sessions.d.ts +34 -1
  150. package/dist/session/kimi/sessions.js +208 -98
  151. package/dist/session/opencode/sessions.d.ts +17 -3
  152. package/dist/session/opencode/sessions.js +29 -29
  153. package/dist/session/pi/sessions.d.ts +5 -1
  154. package/dist/session/pi/sessions.js +12 -16
  155. package/dist/session/session-store.d.ts +11 -1
  156. package/dist/session/session-store.js +75 -178
  157. package/dist/update/index.d.ts +2 -2
  158. package/dist/update/index.js +2 -2
  159. package/dist/update/install.d.ts +20 -2
  160. package/dist/update/install.js +51 -7
  161. package/package.json +1 -1
  162. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  163. package/dist/runner/codex/argv.d.ts +0 -33
  164. package/dist/runner/codex/argv.js +0 -40
  165. package/dist/runner/codex/factory.d.ts +0 -29
  166. package/dist/runner/codex/factory.js +0 -39
  167. package/dist/runner/codex/jsonl.d.ts +0 -37
  168. package/dist/runner/codex/jsonl.js +0 -221
  169. package/dist/runner/codex/runner.d.ts +0 -66
  170. package/dist/runner/codex/runner.js +0 -129
  171. package/dist/runner/common/exec-translator.d.ts +0 -73
  172. package/dist/runner/common/exec-translator.js +0 -105
  173. package/dist/runner/kimi/jsonl.d.ts +0 -28
  174. package/dist/runner/kimi/jsonl.js +0 -143
  175. package/dist/runner/kimi/runner.d.ts +0 -49
  176. package/dist/runner/kimi/runner.js +0 -105
  177. package/dist/runner/opencode/argv.d.ts +0 -32
  178. package/dist/runner/opencode/argv.js +0 -35
  179. package/dist/runner/opencode/jsonl.d.ts +0 -68
  180. package/dist/runner/opencode/jsonl.js +0 -295
  181. package/dist/runner/opencode/runner.d.ts +0 -61
  182. package/dist/runner/opencode/runner.js +0 -93
  183. package/dist/runner/pi/jsonl.d.ts +0 -128
  184. package/dist/runner/pi/jsonl.js +0 -249
  185. package/dist/runner/pi/runner.d.ts +0 -36
  186. package/dist/runner/pi/runner.js +0 -76
  187. package/dist/session/common/catch-up.d.ts +0 -12
  188. package/dist/session/common/catch-up.js +0 -31
@@ -1,18 +1,18 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { sleep } from '../common/sleep.js';
2
3
  import { getLogger } from '../logger/index.js';
3
4
  import { RunCardSession } from '../card/run-card-session.js';
4
5
  import { renderRunCard } from '../card/run-renderer.js';
5
6
  import { renderBashCard } from '../card/bash-renderer.js';
6
7
  import { BashCardSession, capBashOutput } from '../card/bash-card-session.js';
7
- import { markdownDiv } from '../card/collapsible.js';
8
- import { sessionEventPanel } from '../router/card-helpers.js';
9
- import { formatUsageStats } from '../router/index.js';
8
+ import { buildSessionHistoryCard } from '../router/card-helpers.js';
10
9
  import { agentDisplayName, resumeUseButton } from '../card/card-shared.js';
11
10
  import { enforceCardBudget } from '../card/card-budget.js';
12
11
  import { normalizeResultUsage } from '../runner/common/usage.js';
13
12
  import { BashProcessRunner } from '../runner/index.js';
14
13
  import { QueueManager, } from './queue-manager.js';
15
14
  import { ApprovalCoordinator, decisionToApprovalAction } from './approval-coordinator.js';
15
+ import { InboundMediaHandler } from './inbound-media.js';
16
16
  /**
17
17
  * Max events to read for the completion notification card (mirror router's
18
18
  * AUTO_RESUME_MAX_EVENTS = 5). Without a cap, readSessionContent returns every
@@ -22,6 +22,12 @@ import { ApprovalCoordinator, decisionToApprovalAction } from './approval-coordi
22
22
  const COMPLETION_NOTIFICATION_MAX_EVENTS = 5;
23
23
  /** Approval request timeout (ms) — pending approval expires after this. */
24
24
  const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
25
+ /**
26
+ * runner 缓存的工作区数量上限(review P3-3):超过后创建新工作区槽位时
27
+ * 顺带回收「非 active 且进程已停」的死缓存,防止随访问过的工作区数量
28
+ * 无界增长。活进程/在途 run 一律保留(workspace-lifetime 长驻连接不能误杀)。
29
+ */
30
+ const MAX_CACHED_RUNNER_WORKSPACES = 10;
25
31
  /**
26
32
  * Max idle time (no stdout events) before a claude run is considered hung
27
33
  * (§9.12). When this fires the bridge calls `runner.stop()` to unblock the
@@ -61,7 +67,9 @@ function terminalReactionEmoji(terminal) {
61
67
  * Runner/BridgeChannel seams.
62
68
  */
63
69
  export class Bridge {
70
+ /** 飞书连接通道(public:集成测试的 seam,替代 as unknown as 访问)。 */
64
71
  connector;
72
+ /** 会话存储(public:测试辅助读取/写入 cwd 等,替代私有访问)。 */
65
73
  sessionStore;
66
74
  config;
67
75
  workspaceStore;
@@ -70,6 +78,8 @@ export class Bridge {
70
78
  agentRegistry;
71
79
  /** Session reader registry for usage and completion cards. */
72
80
  sessionReaderRegistry;
81
+ /** 入站媒体(图片/文件)落盘 + 合批提示。 */
82
+ mediaHandler;
73
83
  /**
74
84
  * Per-workspace runner instances, keyed by (cwd, agentKind).
75
85
  * Fix 4 (2026-07-18): Each agentKind has its own cache slot, so switching
@@ -80,6 +90,7 @@ export class Bridge {
80
90
  */
81
91
  runners = new Map();
82
92
  /** Queue manager for per-workspace serial processing queue. */
93
+ /** 普通消息队列管理器(public:测试注入 queue-card 状态用)。 */
83
94
  queueManager;
84
95
  /** Active runs per workspace (cwd). Multiple workspaces can run in parallel. */
85
96
  activeRuns = new Map();
@@ -106,6 +117,11 @@ export class Bridge {
106
117
  this.agentRegistry = deps.agentRegistry;
107
118
  this.sessionReaderRegistry = deps.sessionReaderRegistry;
108
119
  this.queueManager = new QueueManager((workspace) => this.activeRuns.has(workspace), (chatId, card, opts) => this.connector.sendWithRetry(chatId, { card }, { replyTo: opts?.replyTo }), (messageId, card) => this.connector.updateCard(messageId, card));
120
+ this.mediaHandler = new InboundMediaHandler({
121
+ resolveCwd: (userId) => this.resolveCwd(userId),
122
+ getConfig: () => this.config,
123
+ send: async (ctx, text) => this.sendResult({ text }, ctx),
124
+ });
109
125
  }
110
126
  /**
111
127
  * Get or create a runner for the given workspace, keyed by current defaultAgent.
@@ -120,6 +136,20 @@ export class Bridge {
120
136
  if (workspaceMap?.has(kind)) {
121
137
  return workspaceMap.get(kind);
122
138
  }
139
+ // review P3-3:缓存超过上限且是新工作区时才回收死槽位——刚创建未 run
140
+ // 的 runner 同样是 isRunning=false,小缓存下立即回收会破坏「创建即注册」
141
+ // 的既有语义(exit-dispatcher 计数回归)。
142
+ if (this.runners.size >= MAX_CACHED_RUNNER_WORKSPACES && !this.runners.has(workspace)) {
143
+ for (const [cwd, slotMap] of [...this.runners.entries()]) {
144
+ if (this.activeRuns.has(cwd))
145
+ continue;
146
+ for (const [slotKind, r] of [...slotMap.entries()]) {
147
+ if (!r.isRunning) {
148
+ this.evictRunnerSlot(cwd, slotKind);
149
+ }
150
+ }
151
+ }
152
+ }
123
153
  // Create new runner for this (workspace, kind) slot
124
154
  const runner = this.agentRegistry.get(kind, workspace);
125
155
  // Call lifecycle methods for agent runners created via factory
@@ -147,10 +177,69 @@ export class Bridge {
147
177
  container.current = config;
148
178
  }
149
179
  }
180
+ /**
181
+ * Push updated approval-mode settings to active workspace-lifetime runners.
182
+ * `clearRunners()` deliberately skips workspaces with active runs; this
183
+ * gives those runners a chance to apply the new settings immediately via
184
+ * the unified duck method `updateApprovalMode` (codex → thread/settings/
185
+ * update, kimi → session/set_mode, opencode → session/set_mode) instead of
186
+ * keeping a stale snapshot until the runner is evicted/recreated.
187
+ */
188
+ syncActiveApprovalModes() {
189
+ for (const [cwd, active] of this.activeRuns) {
190
+ const runner = active.runner;
191
+ if (typeof runner.updateApprovalMode !== 'function')
192
+ continue;
193
+ const settings = this.approvalModeSettingsFor(active.agentKind);
194
+ if (!settings)
195
+ continue;
196
+ void runner.updateApprovalMode(settings).catch((err) => {
197
+ getLogger().warn(`[bridge] sync approval mode failed cwd=${cwd}: ${err.message}`);
198
+ });
199
+ }
200
+ }
201
+ /** Per-agent approval-mode settings snapshot from the current config. */
202
+ approvalModeSettingsFor(kind) {
203
+ const agents = this.config.agents;
204
+ switch (kind) {
205
+ case 'codex': {
206
+ const codexConfig = agents?.codex;
207
+ if (!codexConfig)
208
+ return undefined;
209
+ return {
210
+ approvalPolicy: codexConfig.approvalPolicy,
211
+ sandbox: codexConfig.sandbox,
212
+ };
213
+ }
214
+ case 'kimi': {
215
+ const kimiConfig = agents?.kimi;
216
+ if (!kimiConfig)
217
+ return undefined;
218
+ return { permissionMode: kimiConfig.permissionMode };
219
+ }
220
+ case 'opencode': {
221
+ const opencodeConfig = agents?.opencode;
222
+ if (!opencodeConfig)
223
+ return undefined;
224
+ return { mode: opencodeConfig.mode };
225
+ }
226
+ default:
227
+ return undefined;
228
+ }
229
+ }
150
230
  /** Get the current runner for a workspace. Used by router for agent-specific commands (P3.2). */
151
231
  getCurrentRunner(workspace) {
152
232
  return this.getRunner(workspace);
153
233
  }
234
+ /**
235
+ * Whether the runner for the given agent kind has runCompact capability.
236
+ * Used by router for compact button gating (design doc §6.2-2: duck-typing
237
+ * instead of hardcoding agentKind === 'codex').
238
+ */
239
+ hasRunCompact(workspace, kind = this.config.defaultAgent) {
240
+ const runner = this.getRunner(workspace, kind);
241
+ return ('runCompact' in runner && typeof runner.runCompact === 'function');
242
+ }
154
243
  /** 入队时刻快照:当前 defaultAgent + 该 agent 的 sessionId(无 session 则 undefined)。
155
244
  * 唯一捕获点:排队消息在 T0 把 agent+session 钉进 AgentBinding,随任务闭包带到 T1
156
245
  * 执行时刻,避免 /new、/config 在排队期间改写 live 状态导致语义漂移(方案 D4)。 */
@@ -216,6 +305,7 @@ export class Bridge {
216
305
  * Extract agent-specific run options from config.
217
306
  * Returns model/effort/etc. based on current defaultAgent.
218
307
  */
308
+ /** 解析某 agent 的 run 选项(forwardToClaude 用;测试直接调用的 seam)。 */
219
309
  getAgentRunOptions(agent = this.config.defaultAgent) {
220
310
  const agents = this.config.agents;
221
311
  const result = {};
@@ -421,7 +511,10 @@ export class Bridge {
421
511
  // its slot interrupted, hiding it from queue cards (A22).
422
512
  const stoppedSlot = this.queueManager.getExecutingSlot(cwd);
423
513
  await Promise.allSettled([
424
- active.session.finish('interrupted'),
514
+ // 用户主动中断(/stop、stop 卡按钮、queue.immediate)显式打标
515
+ // interruptedReason,与审批超时自动取消(approval_timeout)、审批
516
+ // 卡取消(approval_cancelled)在终态数据上可区分(方案 A)。
517
+ active.session.finish('interrupted', { interruptedReason: 'user_stop' }),
425
518
  runner.stop({ immediate: true }),
426
519
  ]);
427
520
  // The chain may have advanced while the stop was in flight: a new run
@@ -554,6 +647,18 @@ export class Bridge {
554
647
  await this.sendResult({ text: `发送文件失败: ${err.message}` }, ctx);
555
648
  }
556
649
  }
650
+ /** 入站媒体落盘入口(connector 下载完成后调用)。 */
651
+ async onInboundMedia(payload) {
652
+ await this.mediaHandler.handle(payload);
653
+ }
654
+ /** 文本消息到达时立即冲刷待合批的媒体保存提示。 */
655
+ flushMediaNotifications(userId, chatId) {
656
+ this.mediaHandler.flushAll(userId, chatId);
657
+ }
658
+ /** 冲刷全部待合批提示(/exit、/restart 干净退出前调用)。 */
659
+ async flushAllMediaNotifications() {
660
+ await this.mediaHandler.flushAllPending();
661
+ }
557
662
  /** Reconnect the underlying Feishu channel. */
558
663
  async reconnect() {
559
664
  await this.connector.reconnect();
@@ -668,9 +773,8 @@ export class Bridge {
668
773
  * Handles idle watchdog, event processing, session sync, and state transitions.
669
774
  */
670
775
  async runAgentStreamToEnd(runner, cardSession, sessionId, sessionEpochAtStart, ctx, cwd, runId, message, agentKind = this.config.defaultAgent) {
671
- // Runner 声明的 usage 权威来源(review P3-7):codex exec = 'jsonl',
672
- // codex app-server = 'live'(turn/started 的 tokenUsage.last 是本 turn 增量)。
673
- // 其余 runner 未声明,回退到 agentKind 推断(仅 codex 视为 jsonl-first)。
776
+ // Runner 声明的 usage 权威来源(review P3-7):codex app-server = 'live'
777
+ // (turn/started 的 tokenUsage.last 是本 turn 增量)。其余 runner 未声明。
674
778
  const usageAuthority = runner.getUsageAuthority?.();
675
779
  // P2-1: idle watchdog 用单 interval + lastEventTs 截止时间判定,而非每事件
676
780
  // clearTimeout + setTimeout 重建 timer。高频事件流下 timer 重建次数与事件数
@@ -732,6 +836,15 @@ export class Bridge {
732
836
  // "15min 无事件即 stop" 的语义不变(§9.12)。周期取 idleTimeoutMs/2,使停滞
733
837
  // 后最多在 1.5× idleTimeoutMs 内触发(半个窗口延迟,watchdog 语义可接受)。
734
838
  const fireIdleTimeout = () => {
839
+ // 权限等待期间暂停空闲看门狗:claude/codex 等待人工决策时无 stdout
840
+ // 事件,「等审批」不是「挂死」。审批由 ApprovalCoordinator 的超时
841
+ // (默认 5 分钟 < 看门狗窗口)自愈:cancel 送达 + 中断 turn,不会无限
842
+ // 挂起。
843
+ const pendingApproval = this.approvalCoordinators.get(runId)?.pendingCount() ?? 0;
844
+ if (pendingApproval > 0) {
845
+ lastEventTs = Date.now();
846
+ return;
847
+ }
735
848
  if (idleInterval)
736
849
  clearInterval(idleInterval);
737
850
  idleInterval = null;
@@ -861,8 +974,8 @@ export class Bridge {
861
974
  // app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
862
975
  // 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
863
976
  // 里兜底上报(review P3-10)。此处仅当 runner 声明 live usage(app-server)
864
- // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。exec 模式与其
865
- // 余 agent 不在此列(它们各自有 system.init / turn_started 写回)。
977
+ // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
978
+ // 此列(它们各自有 system.init / turn_started 写回)。
866
979
  if (usageAuthority === 'live' &&
867
980
  event.session_id &&
868
981
  this.sessionStore.getSessionEpoch(ctx.userId, agentKind) === sessionEpochAtStart) {
@@ -918,11 +1031,11 @@ export class Bridge {
918
1031
  let coordinator = this.approvalCoordinators.get(runId);
919
1032
  if (!coordinator) {
920
1033
  coordinator = new ApprovalCoordinator({
921
- runId,
922
- userId: ctx.userId,
923
- chatId: ctx.chatId,
924
- workspace: cwd,
925
- approvalTimeoutMs: APPROVAL_TIMEOUT_MS,
1034
+ // claude 审批超时走 config(claude.approvalTimeoutMs,默认 5 分钟,
1035
+ // 对齐 codex 红线勿改短);codex 维持模块常量。
1036
+ approvalTimeoutMs: agentKind === 'claude'
1037
+ ? (this.config.claude?.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS)
1038
+ : APPROVAL_TIMEOUT_MS,
926
1039
  responder: async (requestId, response) => {
927
1040
  const r = runner;
928
1041
  if (typeof r.respondApproval === 'function') {
@@ -982,17 +1095,13 @@ export class Bridge {
982
1095
  finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
983
1096
  finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
984
1097
  finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
985
- // 统一 flow 字段 scope:live 优先(非 codex),jsonl 兜底;codex 例外。
1098
+ // 统一 flow 字段 scope:live 优先,jsonl 兜底。
986
1099
  // 当 live result 事件带了 input/output(即 liveInputTokens/liveOutputTokens
987
1100
  // 非 undefined),所有 flow 字段统一用 live scope(本 run),不用 jsonl
988
1101
  // 的 session 累计值覆盖,避免 cache%/Total 分子分母不同源。
989
1102
  // 无 live usage 时(如 kimi),jsonl 值兜底。
990
- // codex 例外(2026-08-01):codex exec 的 turn.completed.usage 是会话累计
991
- // total_token_usage(resume 长会话下虚高数百倍),不是本 run 增量——live
992
- // 值必须被忽略,flow 字段一律取 jsonl 主线程文件的 per-turn 值;
993
- // jsonl 缺失(老版本无 token_count)时才回退 live。
994
1103
  // contextLength/compactCount 保持 jsonl 优先(它们是水位/历史计数)。
995
- const codexJsonlFirst = usageAuthority === 'jsonl' || (usageAuthority === undefined && agentKind === 'codex');
1104
+ const codexJsonlFirst = usageAuthority === 'jsonl';
996
1105
  if (!codexJsonlFirst && liveInputTokens !== undefined && liveOutputTokens !== undefined) {
997
1106
  // 非 codex 且 live 有 input/output → 所有 flow 字段用 live scope
998
1107
  finalInputTokens = liveInputTokens;
@@ -1193,15 +1302,19 @@ export class Bridge {
1193
1302
  // Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
1194
1303
  // 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
1195
1304
  // interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
1305
+ // §6.2-3:写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
1306
+ // (codex app-server + kimi acp 均满足)。
1196
1307
  const runTerminal = cardSession.currentState.terminal;
1197
- if (activeRun.agentKind === 'codex' &&
1308
+ const runnerHasCompact = 'runCompact' in activeRun.runner &&
1309
+ typeof activeRun.runner.runCompact === 'function';
1310
+ if (runnerHasCompact &&
1198
1311
  activeRun.runner.lifetime === 'workspace' &&
1199
1312
  (runTerminal === 'done' ||
1200
1313
  runTerminal === 'error' ||
1201
1314
  runTerminal === 'interrupted' ||
1202
1315
  runTerminal === 'idle_timeout')) {
1203
1316
  const compactSessionId = cardSession.currentState.sessionId ??
1204
- this.sessionStore.getSessionId(ctx.userId, 'codex');
1317
+ this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
1205
1318
  if (compactSessionId) {
1206
1319
  this.lastCompactableCodexRun.set(cwd, {
1207
1320
  runId: activeRun.runId,
@@ -1270,42 +1383,19 @@ export class Bridge {
1270
1383
  maxEvents: COMPLETION_NOTIFICATION_MAX_EVENTS,
1271
1384
  });
1272
1385
  const { events, usage } = content;
1273
- const elements = [
1274
- markdownDiv(`📂 \`${cwd}\`\n会话: **${sessionId}**`),
1275
- { tag: 'hr' },
1276
- ];
1277
- // Add session content in collapsible panels (last 2 expanded)
1278
- events.forEach((ev, i) => {
1279
- elements.push(sessionEventPanel(ev, i, events.length, 2, agentKind));
1280
- });
1281
- // Add usage stats at the end
1282
- const usageStatsStr = formatUsageStats(usage, { showResult: true, result: 'completed' });
1283
- if (usage) {
1284
- elements.push(markdownDiv(usageStatsStr));
1285
- }
1286
- // Add action button to resume this session - use column_set+column for 2.0
1287
- elements.push({
1288
- tag: 'column_set',
1289
- columns: [
1290
- {
1291
- tag: 'column',
1292
- width: 'auto',
1293
- elements: [resumeUseButton(sessionId, agentKind)],
1294
- },
1295
- ],
1386
+ const card = buildSessionHistoryCard({
1387
+ sessionId,
1388
+ cwd,
1389
+ events,
1390
+ usage,
1391
+ }, {
1392
+ agentKind,
1393
+ headerText: `📂 \`${cwd}\`\n会话: **${sessionId}**`,
1394
+ title: '✅ 会话已完成',
1395
+ usageResult: 'completed',
1396
+ actions: [resumeUseButton(sessionId, agentKind)],
1397
+ headerTemplate: 'green',
1296
1398
  });
1297
- const card = {
1298
- schema: '2.0',
1299
- config: { wide_screen_mode: true },
1300
- header: {
1301
- template: 'green',
1302
- title: {
1303
- tag: 'plain_text',
1304
- content: '✅ 会话已完成',
1305
- },
1306
- },
1307
- body: { elements },
1308
- };
1309
1399
  // P1-21: send through sendResult so enforceCardBudget guards the card
1310
1400
  // (previously sent via connector directly, bypassing the budget — long
1311
1401
  // sessions produced >28KB cards that Feishu silently rejected).
@@ -1325,22 +1415,31 @@ export class Bridge {
1325
1415
  // Approval action handlers
1326
1416
  // =============================================================================
1327
1417
  /**
1328
- * Handle an approval response from a card button click.
1329
- * Routes to the ApprovalCoordinator for the given run.
1418
+ * Resolve the ApprovalCoordinator for a run, or throw the standard
1419
+ * "已结束" error. Shared preamble of all approval card-action handlers.
1330
1420
  */
1331
- async handleApprovalRespond(opts) {
1332
- const log = getLogger();
1333
- log.info(`[bridge] handleApprovalRespond runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} decision=${opts.decision}`);
1334
- const coordinator = this.approvalCoordinators.get(opts.runId);
1421
+ withCoordinator(runId, fn) {
1422
+ const coordinator = this.approvalCoordinators.get(runId);
1335
1423
  if (!coordinator) {
1336
1424
  // P2-7: run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
1337
1425
  // 不能静默。router 会把该异常转成 error toast。
1338
- log.warn(`[bridge] no approval coordinator for runId=${opts.runId.slice(0, 8)}`);
1426
+ getLogger().warn(`[bridge] no approval coordinator for runId=${runId.slice(0, 8)}`);
1339
1427
  throw new Error('任务已结束,审批无法响应');
1340
1428
  }
1341
- // Build ApprovalAction from the decision string
1342
- const action = decisionToApprovalAction(opts.decision);
1343
- await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1429
+ return fn(coordinator);
1430
+ }
1431
+ /**
1432
+ * Handle an approval response from a card button click.
1433
+ * Routes to the ApprovalCoordinator for the given run.
1434
+ */
1435
+ async handleApprovalRespond(opts) {
1436
+ const log = getLogger();
1437
+ log.info(`[bridge] handleApprovalRespond runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} decision=${opts.decision}`);
1438
+ return this.withCoordinator(opts.runId, async (coordinator) => {
1439
+ // Build ApprovalAction from the decision string
1440
+ const action = decisionToApprovalAction(opts.decision);
1441
+ await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1442
+ });
1344
1443
  }
1345
1444
  /**
1346
1445
  * Handle a permission toggle from a card button click.
@@ -1349,17 +1448,41 @@ export class Bridge {
1349
1448
  async handleApprovalToggle(opts) {
1350
1449
  const log = getLogger();
1351
1450
  log.info(`[bridge] handleApprovalToggle runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} permId=${opts.permId}`);
1352
- const coordinator = this.approvalCoordinators.get(opts.runId);
1353
- if (!coordinator) {
1354
- log.warn(`[bridge] no approval coordinator for runId=${opts.runId.slice(0, 8)}`);
1355
- throw new Error('任务已结束,审批无法响应');
1356
- }
1357
- // selected 由卡片根据当前渲染状态传回(取反),不再硬编码只授不撤。
1358
- const toggleAction = {
1359
- permId: opts.permId,
1360
- selected: opts.selected ?? true,
1361
- };
1362
- await coordinator.togglePerm(toggleAction, { requestId: opts.requestId });
1451
+ return this.withCoordinator(opts.runId, async (coordinator) => {
1452
+ // selected 由卡片根据当前渲染状态传回(取反),不再硬编码只授不撤。
1453
+ const toggleAction = {
1454
+ permId: opts.permId,
1455
+ selected: opts.selected ?? true,
1456
+ };
1457
+ await coordinator.togglePerm(toggleAction, { requestId: opts.requestId });
1458
+ });
1459
+ }
1460
+ /**
1461
+ * Claude AskUserQuestion 选项点击(单选即时提交 / 多选切换勾选)。
1462
+ * 路由到 ApprovalCoordinator.toggleAnswer。
1463
+ */
1464
+ async handleApprovalAnswer(opts) {
1465
+ const log = getLogger();
1466
+ log.info(`[bridge] handleApprovalAnswer runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1467
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.toggleAnswer({ questionIndex: opts.questionIndex, option: opts.option }, { requestId: opts.requestId, nonce: opts.nonce }));
1468
+ }
1469
+ /**
1470
+ * Claude AskUserQuestion 多选问题的「提交答案」按钮。
1471
+ * 路由到 ApprovalCoordinator.submitAnswers。
1472
+ */
1473
+ async handleApprovalAnswerSubmit(opts) {
1474
+ const log = getLogger();
1475
+ log.info(`[bridge] handleApprovalAnswerSubmit runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1476
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
1477
+ }
1478
+ /**
1479
+ * Claude AskUserQuestion 自定义答案(Other,review P3-4):自由文本直接
1480
+ * 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
1481
+ */
1482
+ async handleApprovalAnswerCustom(opts) {
1483
+ const log = getLogger();
1484
+ log.info(`[bridge] handleApprovalAnswerCustom runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1485
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1363
1486
  }
1364
1487
  /**
1365
1488
  * Handle codex.compact card action — trigger a Codex compaction request.
@@ -1460,7 +1583,7 @@ export class Bridge {
1460
1583
  // (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
1461
1584
  let finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1462
1585
  for (let attempt = 0; !finalUsage?.compactCount && attempt < 2; attempt++) {
1463
- await new Promise((r) => setTimeout(r, 150));
1586
+ await sleep(150);
1464
1587
  finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1465
1588
  }
1466
1589
  await cardSession.finish(sawResult ? 'done' : 'error', {
@@ -101,7 +101,8 @@ export declare class QueueManager {
101
101
  * executing/cancelled state. The promise never rejects (send failures
102
102
  * resolve undefined), so awaiting it is always safe.
103
103
  */
104
- private queueCardMessages;
104
+ /** queue card 的 send promise 表(public:bridge 集成测试注入/断言用)。 */
105
+ queueCardMessages: Map<string, Promise<string | undefined>>;
105
106
  /** Track number of executing tasks per workspace (for queue card display). */
106
107
  private pendingOrExecutingCount;
107
108
  /**
@@ -38,6 +38,7 @@ export class QueueManager {
38
38
  * executing/cancelled state. The promise never rejects (send failures
39
39
  * resolve undefined), so awaiting it is always safe.
40
40
  */
41
+ /** queue card 的 send promise 表(public:bridge 集成测试注入/断言用)。 */
41
42
  queueCardMessages = new Map();
42
43
  /** Track number of executing tasks per workspace (for queue card display). */
43
44
  pendingOrExecutingCount = new Map();