lark-remote 0.1.6 → 0.1.8

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 (216) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.en.md +9 -1
  3. package/README.md +14 -2
  4. package/dist/bridge/approval-coordinator.d.ts +71 -34
  5. package/dist/bridge/approval-coordinator.js +225 -41
  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 +97 -13
  9. package/dist/bridge/index.js +227 -87
  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 +294 -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 +7 -16
  20. package/dist/card/card-shared.js +7 -30
  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.d.ts +7 -0
  24. package/dist/card/run-renderer.js +116 -238
  25. package/dist/card/run-state.d.ts +13 -0
  26. package/dist/card/run-state.js +25 -16
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/index.d.ts +112 -24
  40. package/dist/config/index.js +139 -37
  41. package/dist/connector/index.d.ts +69 -2
  42. package/dist/connector/index.js +107 -1
  43. package/dist/index.js +150 -118
  44. package/dist/instance-lock.js +4 -12
  45. package/dist/order/alias-resolve.d.ts +13 -0
  46. package/dist/order/alias-resolve.js +22 -0
  47. package/dist/order/alias-store.d.ts +38 -0
  48. package/dist/order/alias-store.js +106 -0
  49. package/dist/persistence/atomic-write.d.ts +8 -2
  50. package/dist/persistence/atomic-write.js +23 -7
  51. package/dist/restart.js +2 -1
  52. package/dist/router/card-action-payload.d.ts +30 -0
  53. package/dist/router/card-action-payload.js +39 -0
  54. package/dist/router/card-helpers.d.ts +63 -0
  55. package/dist/router/card-helpers.js +146 -0
  56. package/dist/router/config/claude.js +30 -1
  57. package/dist/router/config/codex.js +29 -50
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +5 -4
  61. package/dist/router/config/dsh.d.ts +51 -0
  62. package/dist/router/config/dsh.js +179 -0
  63. package/dist/router/config/index.d.ts +12 -0
  64. package/dist/router/config/index.js +28 -1
  65. package/dist/router/config/kimi.d.ts +4 -1
  66. package/dist/router/config/kimi.js +27 -0
  67. package/dist/router/config/opencode.js +18 -5
  68. package/dist/router/config/pi.js +4 -5
  69. package/dist/router/config/types.d.ts +17 -2
  70. package/dist/router/index.d.ts +76 -22
  71. package/dist/router/index.js +479 -548
  72. package/dist/router/utils.d.ts +2 -0
  73. package/dist/router/utils.js +16 -2
  74. package/dist/runner/agent-choices-common.d.ts +16 -0
  75. package/dist/runner/agent-choices-common.js +34 -0
  76. package/dist/runner/claude/runner.d.ts +47 -10
  77. package/dist/runner/claude/runner.js +117 -46
  78. package/dist/runner/claude/session.d.ts +166 -0
  79. package/dist/runner/claude/session.js +768 -0
  80. package/dist/runner/codex/app-server/protocol-types.d.ts +66 -39
  81. package/dist/runner/codex/app-server/protocol-types.js +2 -13
  82. package/dist/runner/codex/app-server/runner.d.ts +52 -59
  83. package/dist/runner/codex/app-server/runner.js +216 -338
  84. package/dist/runner/codex/app-server/translator.d.ts +8 -21
  85. package/dist/runner/codex/app-server/translator.js +45 -18
  86. package/dist/runner/codex/index.d.ts +0 -2
  87. package/dist/runner/codex/index.js +0 -2
  88. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  89. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  90. package/dist/runner/common/acp/protocol-types.d.ts +271 -0
  91. package/dist/runner/common/acp/protocol-types.js +77 -0
  92. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  93. package/dist/runner/common/connection-based-runner.js +261 -0
  94. package/dist/runner/{codex/app-server → common/jsonrpc}/client.d.ts +14 -7
  95. package/dist/runner/{codex/app-server → common/jsonrpc}/client.js +27 -18
  96. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  97. package/dist/runner/{codex/app-server → common/jsonrpc}/connection-manager.js +63 -28
  98. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.d.ts +5 -4
  99. package/dist/runner/{codex/app-server → common/jsonrpc}/transport.js +18 -19
  100. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  101. package/dist/runner/common/jsonrpc/types.js +9 -0
  102. package/dist/runner/common/process-stopper.d.ts +2 -1
  103. package/dist/runner/common/process-stopper.js +1 -0
  104. package/dist/runner/common/runner-utils.d.ts +6 -13
  105. package/dist/runner/common/runner-utils.js +6 -47
  106. package/dist/runner/common/spawning-runner.d.ts +24 -70
  107. package/dist/runner/common/spawning-runner.js +98 -152
  108. package/dist/runner/dsh/client.d.ts +95 -0
  109. package/dist/runner/dsh/client.js +418 -0
  110. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  111. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  112. package/dist/runner/dsh/index.d.ts +3 -0
  113. package/dist/runner/dsh/index.js +4 -0
  114. package/dist/runner/dsh/runner.d.ts +57 -0
  115. package/dist/runner/dsh/runner.js +191 -0
  116. package/dist/runner/dsh/translator.d.ts +28 -0
  117. package/dist/runner/dsh/translator.js +98 -0
  118. package/dist/runner/dsh/types.d.ts +155 -0
  119. package/dist/runner/dsh/types.js +14 -0
  120. package/dist/runner/index.d.ts +5 -4
  121. package/dist/runner/index.js +5 -4
  122. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  123. package/dist/runner/kimi/acp/runner.js +516 -0
  124. package/dist/runner/kimi/acp/translator.d.ts +138 -0
  125. package/dist/runner/kimi/acp/translator.js +441 -0
  126. package/dist/runner/kimi/index.d.ts +1 -1
  127. package/dist/runner/kimi/index.js +1 -1
  128. package/dist/runner/opencode/acp/index.d.ts +1 -0
  129. package/dist/runner/opencode/acp/index.js +2 -0
  130. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  131. package/dist/runner/opencode/acp/runner.js +429 -0
  132. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  133. package/dist/runner/opencode/acp/translator.js +345 -0
  134. package/dist/runner/opencode/index.d.ts +1 -1
  135. package/dist/runner/opencode/index.js +1 -1
  136. package/dist/runner/pi/index.d.ts +1 -1
  137. package/dist/runner/pi/index.js +1 -1
  138. package/dist/runner/pi/rpc/client.d.ts +52 -0
  139. package/dist/runner/pi/rpc/client.js +114 -0
  140. package/dist/runner/pi/rpc/index.d.ts +4 -0
  141. package/dist/runner/pi/rpc/index.js +4 -0
  142. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  143. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  144. package/dist/runner/pi/rpc/runner.d.ts +74 -0
  145. package/dist/runner/pi/rpc/runner.js +190 -0
  146. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  147. package/dist/runner/pi/rpc/translator.js +258 -0
  148. package/dist/runner/probe.js +6 -1
  149. package/dist/runner/question-common.d.ts +26 -0
  150. package/dist/runner/question-common.js +42 -0
  151. package/dist/runner/registry.d.ts +7 -8
  152. package/dist/runner/registry.js +3 -4
  153. package/dist/runner/resolve-agent-choices.js +18 -59
  154. package/dist/runner/sync-agent-choices.js +16 -67
  155. package/dist/runner/types.d.ts +97 -14
  156. package/dist/session/claude/session-index.js +2 -3
  157. package/dist/session/claude/session-reader.js +3 -6
  158. package/dist/session/claude/sessions.js +43 -28
  159. package/dist/session/codex/rollout-reader.d.ts +2 -2
  160. package/dist/session/codex/rollout-reader.js +14 -17
  161. package/dist/session/codex/sessions.d.ts +1 -1
  162. package/dist/session/codex/sessions.js +2 -2
  163. package/dist/session/common/content-blocks.d.ts +7 -0
  164. package/dist/session/common/content-blocks.js +13 -9
  165. package/dist/session/common/jsonl.d.ts +8 -25
  166. package/dist/session/common/jsonl.js +126 -261
  167. package/dist/session/common/pagination.d.ts +28 -0
  168. package/dist/session/common/pagination.js +33 -0
  169. package/dist/session/dsh/index.d.ts +1 -0
  170. package/dist/session/dsh/index.js +2 -0
  171. package/dist/session/dsh/sessions.d.ts +39 -0
  172. package/dist/session/dsh/sessions.js +237 -0
  173. package/dist/session/index.d.ts +1 -0
  174. package/dist/session/index.js +1 -0
  175. package/dist/session/kimi/sessions.d.ts +34 -1
  176. package/dist/session/kimi/sessions.js +208 -98
  177. package/dist/session/opencode/sessions.d.ts +17 -3
  178. package/dist/session/opencode/sessions.js +29 -29
  179. package/dist/session/pi/sessions.d.ts +5 -1
  180. package/dist/session/pi/sessions.js +12 -16
  181. package/dist/session/session-store.d.ts +11 -1
  182. package/dist/session/session-store.js +75 -178
  183. package/dist/update/index.d.ts +2 -2
  184. package/dist/update/index.js +2 -2
  185. package/dist/update/install.d.ts +20 -2
  186. package/dist/update/install.js +51 -7
  187. package/dist/workspace/index.d.ts +4 -0
  188. package/dist/workspace/index.js +8 -1
  189. package/package.json +2 -1
  190. package/dist/runner/codex/app-server/connection-manager.d.ts +0 -56
  191. package/dist/runner/codex/argv.d.ts +0 -33
  192. package/dist/runner/codex/argv.js +0 -40
  193. package/dist/runner/codex/factory.d.ts +0 -29
  194. package/dist/runner/codex/factory.js +0 -39
  195. package/dist/runner/codex/jsonl.d.ts +0 -37
  196. package/dist/runner/codex/jsonl.js +0 -221
  197. package/dist/runner/codex/runner.d.ts +0 -66
  198. package/dist/runner/codex/runner.js +0 -129
  199. package/dist/runner/common/exec-translator.d.ts +0 -73
  200. package/dist/runner/common/exec-translator.js +0 -105
  201. package/dist/runner/kimi/jsonl.d.ts +0 -28
  202. package/dist/runner/kimi/jsonl.js +0 -143
  203. package/dist/runner/kimi/runner.d.ts +0 -49
  204. package/dist/runner/kimi/runner.js +0 -105
  205. package/dist/runner/opencode/argv.d.ts +0 -32
  206. package/dist/runner/opencode/argv.js +0 -35
  207. package/dist/runner/opencode/jsonl.d.ts +0 -68
  208. package/dist/runner/opencode/jsonl.js +0 -295
  209. package/dist/runner/opencode/runner.d.ts +0 -61
  210. package/dist/runner/opencode/runner.js +0 -93
  211. package/dist/runner/pi/jsonl.d.ts +0 -128
  212. package/dist/runner/pi/jsonl.js +0 -249
  213. package/dist/runner/pi/runner.d.ts +0 -36
  214. package/dist/runner/pi/runner.js +0 -76
  215. package/dist/session/common/catch-up.d.ts +0 -12
  216. 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,14 +90,15 @@ 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();
86
97
  /** Approval coordinators keyed by runId. */
87
98
  approvalCoordinators = new Map();
88
99
  /**
89
- * 最近一次已完成的 codex app-server run(按 cwd)。run 结束后 activeRuns
90
- * 已清空,Compact 需要它来校验 runId 并提供 sessionId。
100
+ * 最近一次已完成的、runner 有 runCompact 的 run(按 cwd)。run 结束后
101
+ * activeRuns 已清空,Compact 需要它来校验 runId 并提供 sessionId。
91
102
  */
92
103
  lastCompactableCodexRun = new Map();
93
104
  /**
@@ -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();
@@ -648,6 +753,9 @@ export class Bridge {
648
753
  showToolResult: this.config.output.showToolResult,
649
754
  // Run card header shows the current agent name (e.g. "Claude · 思考中")
650
755
  agentKind,
756
+ // Compact 按钮能力门控与 resume 卡 / auto-resume 卡一致(hasRunCompact
757
+ // 鸭子探测):claude(stream-json,无 turn_started)也能拿到按钮。
758
+ compactSupported: this.hasRunCompact(cwd, agentKind),
651
759
  },
652
760
  });
653
761
  const activeRun = {
@@ -668,9 +776,8 @@ export class Bridge {
668
776
  * Handles idle watchdog, event processing, session sync, and state transitions.
669
777
  */
670
778
  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)。
779
+ // Runner 声明的 usage 权威来源(review P3-7):codex app-server = 'live'
780
+ // (turn/started 的 tokenUsage.last 是本 turn 增量)。其余 runner 未声明。
674
781
  const usageAuthority = runner.getUsageAuthority?.();
675
782
  // P2-1: idle watchdog 用单 interval + lastEventTs 截止时间判定,而非每事件
676
783
  // clearTimeout + setTimeout 重建 timer。高频事件流下 timer 重建次数与事件数
@@ -732,6 +839,15 @@ export class Bridge {
732
839
  // "15min 无事件即 stop" 的语义不变(§9.12)。周期取 idleTimeoutMs/2,使停滞
733
840
  // 后最多在 1.5× idleTimeoutMs 内触发(半个窗口延迟,watchdog 语义可接受)。
734
841
  const fireIdleTimeout = () => {
842
+ // 权限等待期间暂停空闲看门狗:claude/codex 等待人工决策时无 stdout
843
+ // 事件,「等审批」不是「挂死」。审批由 ApprovalCoordinator 的超时
844
+ // (默认 5 分钟 < 看门狗窗口)自愈:cancel 送达 + 中断 turn,不会无限
845
+ // 挂起。
846
+ const pendingApproval = this.approvalCoordinators.get(runId)?.pendingCount() ?? 0;
847
+ if (pendingApproval > 0) {
848
+ lastEventTs = Date.now();
849
+ return;
850
+ }
735
851
  if (idleInterval)
736
852
  clearInterval(idleInterval);
737
853
  idleInterval = null;
@@ -861,8 +977,8 @@ export class Bridge {
861
977
  // app-server 模式 setup 失败路径(thread/start 成功后 turn/start 失败)
862
978
  // 不会有 turn_started 事件,store 不会写回新线程 id;runner 已在 result
863
979
  // 里兜底上报(review P3-10)。此处仅当 runner 声明 live usage(app-server)
864
- // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。exec 模式与其
865
- // 余 agent 不在此列(它们各自有 system.init / turn_started 写回)。
980
+ // 且会话指针未动时写回,避免下条消息再开一个孤儿线程。其余 agent 不在
981
+ // 此列(它们各自有 system.init / turn_started 写回)。
866
982
  if (usageAuthority === 'live' &&
867
983
  event.session_id &&
868
984
  this.sessionStore.getSessionEpoch(ctx.userId, agentKind) === sessionEpochAtStart) {
@@ -918,11 +1034,11 @@ export class Bridge {
918
1034
  let coordinator = this.approvalCoordinators.get(runId);
919
1035
  if (!coordinator) {
920
1036
  coordinator = new ApprovalCoordinator({
921
- runId,
922
- userId: ctx.userId,
923
- chatId: ctx.chatId,
924
- workspace: cwd,
925
- approvalTimeoutMs: APPROVAL_TIMEOUT_MS,
1037
+ // claude 审批超时走 config(claude.approvalTimeoutMs,默认 5 分钟,
1038
+ // 对齐 codex 红线勿改短);codex 维持模块常量。
1039
+ approvalTimeoutMs: agentKind === 'claude'
1040
+ ? (this.config.claude?.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS)
1041
+ : APPROVAL_TIMEOUT_MS,
926
1042
  responder: async (requestId, response) => {
927
1043
  const r = runner;
928
1044
  if (typeof r.respondApproval === 'function') {
@@ -982,17 +1098,13 @@ export class Bridge {
982
1098
  finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
983
1099
  finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
984
1100
  finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
985
- // 统一 flow 字段 scope:live 优先(非 codex),jsonl 兜底;codex 例外。
1101
+ // 统一 flow 字段 scope:live 优先,jsonl 兜底。
986
1102
  // 当 live result 事件带了 input/output(即 liveInputTokens/liveOutputTokens
987
1103
  // 非 undefined),所有 flow 字段统一用 live scope(本 run),不用 jsonl
988
1104
  // 的 session 累计值覆盖,避免 cache%/Total 分子分母不同源。
989
1105
  // 无 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
1106
  // contextLength/compactCount 保持 jsonl 优先(它们是水位/历史计数)。
995
- const codexJsonlFirst = usageAuthority === 'jsonl' || (usageAuthority === undefined && agentKind === 'codex');
1107
+ const codexJsonlFirst = usageAuthority === 'jsonl';
996
1108
  if (!codexJsonlFirst && liveInputTokens !== undefined && liveOutputTokens !== undefined) {
997
1109
  // 非 codex 且 live 有 input/output → 所有 flow 字段用 live scope
998
1110
  finalInputTokens = liveInputTokens;
@@ -1002,7 +1114,10 @@ export class Bridge {
1002
1114
  // 由 formatUsageStats 的 max/兜底公式处理。
1003
1115
  }
1004
1116
  else {
1005
- // codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段
1117
+ // codex(jsonl 优先)或无 live usage → jsonl 兜底所有 flow 字段。
1118
+ // jsonl 的非累计字段已是末轮(本 run)scope(claude reader 修复后对齐
1119
+ // codex `last_token_usage` 语义,见 scalarScan),不再是 session 累计;
1120
+ // session 累计只经 cumulative* 字段透传,卡片"本 run ≤ 累计"不变量成立。
1006
1121
  finalInputTokens = finalUsage?.inputTokens ?? liveInputTokens;
1007
1122
  finalOutputTokens = finalUsage?.outputTokens ?? liveOutputTokens;
1008
1123
  finalCacheReadTokens = finalUsage?.cacheReadTokens ?? finalCacheReadTokens;
@@ -1193,15 +1308,19 @@ export class Bridge {
1193
1308
  // Compact 卡片需要 runId + sessionId;record 供终态卡上的 Compact 按钮
1194
1309
  // 校验与取参(activeRuns 已清空,无法再查到)。异常退出(error/
1195
1310
  // interrupted/idle_timeout)同样记录——上下文往往更大,压缩后再续。
1311
+ // §6.2-3:写入条件从「codex 终态」放宽为「runner 有 runCompact 的终态」
1312
+ // (codex app-server + kimi acp 均满足)。
1196
1313
  const runTerminal = cardSession.currentState.terminal;
1197
- if (activeRun.agentKind === 'codex' &&
1314
+ const runnerHasCompact = 'runCompact' in activeRun.runner &&
1315
+ typeof activeRun.runner.runCompact === 'function';
1316
+ if (runnerHasCompact &&
1198
1317
  activeRun.runner.lifetime === 'workspace' &&
1199
1318
  (runTerminal === 'done' ||
1200
1319
  runTerminal === 'error' ||
1201
1320
  runTerminal === 'interrupted' ||
1202
1321
  runTerminal === 'idle_timeout')) {
1203
1322
  const compactSessionId = cardSession.currentState.sessionId ??
1204
- this.sessionStore.getSessionId(ctx.userId, 'codex');
1323
+ this.sessionStore.getSessionId(ctx.userId, activeRun.agentKind);
1205
1324
  if (compactSessionId) {
1206
1325
  this.lastCompactableCodexRun.set(cwd, {
1207
1326
  runId: activeRun.runId,
@@ -1270,42 +1389,19 @@ export class Bridge {
1270
1389
  maxEvents: COMPLETION_NOTIFICATION_MAX_EVENTS,
1271
1390
  });
1272
1391
  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
- ],
1392
+ const card = buildSessionHistoryCard({
1393
+ sessionId,
1394
+ cwd,
1395
+ events,
1396
+ usage,
1397
+ }, {
1398
+ agentKind,
1399
+ headerText: `📂 \`${cwd}\`\n会话: **${sessionId}**`,
1400
+ title: '✅ 会话已完成',
1401
+ usageResult: 'completed',
1402
+ actions: [resumeUseButton(sessionId, agentKind)],
1403
+ headerTemplate: 'green',
1296
1404
  });
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
1405
  // P1-21: send through sendResult so enforceCardBudget guards the card
1310
1406
  // (previously sent via connector directly, bypassing the budget — long
1311
1407
  // sessions produced >28KB cards that Feishu silently rejected).
@@ -1325,22 +1421,31 @@ export class Bridge {
1325
1421
  // Approval action handlers
1326
1422
  // =============================================================================
1327
1423
  /**
1328
- * Handle an approval response from a card button click.
1329
- * Routes to the ApprovalCoordinator for the given run.
1424
+ * Resolve the ApprovalCoordinator for a run, or throw the standard
1425
+ * "已结束" error. Shared preamble of all approval card-action handlers.
1330
1426
  */
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);
1427
+ withCoordinator(runId, fn) {
1428
+ const coordinator = this.approvalCoordinators.get(runId);
1335
1429
  if (!coordinator) {
1336
1430
  // P2-7: run 已结束(coordinator 已释放)时点击审批按钮必须给用户明确反馈,
1337
1431
  // 不能静默。router 会把该异常转成 error toast。
1338
- log.warn(`[bridge] no approval coordinator for runId=${opts.runId.slice(0, 8)}`);
1432
+ getLogger().warn(`[bridge] no approval coordinator for runId=${runId.slice(0, 8)}`);
1339
1433
  throw new Error('任务已结束,审批无法响应');
1340
1434
  }
1341
- // Build ApprovalAction from the decision string
1342
- const action = decisionToApprovalAction(opts.decision);
1343
- await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1435
+ return fn(coordinator);
1436
+ }
1437
+ /**
1438
+ * Handle an approval response from a card button click.
1439
+ * Routes to the ApprovalCoordinator for the given run.
1440
+ */
1441
+ async handleApprovalRespond(opts) {
1442
+ const log = getLogger();
1443
+ log.info(`[bridge] handleApprovalRespond runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} decision=${opts.decision}`);
1444
+ return this.withCoordinator(opts.runId, async (coordinator) => {
1445
+ // Build ApprovalAction from the decision string
1446
+ const action = decisionToApprovalAction(opts.decision);
1447
+ await coordinator.submit(action, { requestId: opts.requestId, nonce: opts.nonce });
1448
+ });
1344
1449
  }
1345
1450
  /**
1346
1451
  * Handle a permission toggle from a card button click.
@@ -1349,22 +1454,57 @@ export class Bridge {
1349
1454
  async handleApprovalToggle(opts) {
1350
1455
  const log = getLogger();
1351
1456
  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 });
1457
+ return this.withCoordinator(opts.runId, async (coordinator) => {
1458
+ // selected 由卡片根据当前渲染状态传回(取反),不再硬编码只授不撤。
1459
+ const toggleAction = {
1460
+ permId: opts.permId,
1461
+ selected: opts.selected ?? true,
1462
+ };
1463
+ await coordinator.togglePerm(toggleAction, { requestId: opts.requestId });
1464
+ });
1465
+ }
1466
+ /**
1467
+ * Claude AskUserQuestion 选项点击(单选即时提交 / 多选切换勾选)。
1468
+ * 路由到 ApprovalCoordinator.toggleAnswer。
1469
+ */
1470
+ async handleApprovalAnswer(opts) {
1471
+ const log = getLogger();
1472
+ log.info(`[bridge] handleApprovalAnswer runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1473
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.toggleAnswer({ questionIndex: opts.questionIndex, option: opts.option }, { requestId: opts.requestId, nonce: opts.nonce }));
1474
+ }
1475
+ /**
1476
+ * Claude AskUserQuestion 多选问题的「提交答案」按钮。
1477
+ * 路由到 ApprovalCoordinator.submitAnswers。
1478
+ */
1479
+ async handleApprovalAnswerSubmit(opts) {
1480
+ const log = getLogger();
1481
+ log.info(`[bridge] handleApprovalAnswerSubmit runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1482
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.submitAnswers({ questionIndex: opts.questionIndex }, { requestId: opts.requestId, nonce: opts.nonce }));
1483
+ }
1484
+ /**
1485
+ * Claude AskUserQuestion 自定义答案(Other,review P3-4):自由文本直接
1486
+ * 作为该单选问题的答案提交。路由到 ApprovalCoordinator.answerCustom。
1487
+ */
1488
+ async handleApprovalAnswerCustom(opts) {
1489
+ const log = getLogger();
1490
+ log.info(`[bridge] handleApprovalAnswerCustom runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1491
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerCustom({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1492
+ }
1493
+ /**
1494
+ * Codex AskUserQuestion 补充说明(user_note,2026-08-18 live 验证模型理解
1495
+ * "user_note: <text>" 条目):记录到 coordinator,随答案一起提交。
1496
+ * 路由到 ApprovalCoordinator.answerNote。
1497
+ */
1498
+ async handleApprovalAnswerNote(opts) {
1499
+ const log = getLogger();
1500
+ log.info(`[bridge] handleApprovalAnswerNote runId=${opts.runId.slice(0, 8)}... requestId=${opts.requestId} question=${opts.questionIndex}`);
1501
+ return this.withCoordinator(opts.runId, (coordinator) => coordinator.answerNote({ questionIndex: opts.questionIndex, text: opts.text }, { requestId: opts.requestId, nonce: opts.nonce }));
1363
1502
  }
1364
1503
  /**
1365
- * Handle codex.compact card action — trigger a Codex compaction request.
1366
- * Validates the run exists and is in a terminal state, then calls
1367
- * the app-server runner's runCompact().
1504
+ * Handle codex.compact card action — trigger a compaction request for any
1505
+ * runCompact-capable runner(codex/kimi/opencode/pi/claude,按
1506
+ * lastCompactableCodexRun 记录的 agentKind 路由)。Validates the run exists
1507
+ * and is in a terminal state, then calls the runner's runCompact().
1368
1508
  */
1369
1509
  async handleCodexCompact(value, ctx) {
1370
1510
  const log = getLogger();
@@ -1406,7 +1546,7 @@ export class Bridge {
1406
1546
  });
1407
1547
  }
1408
1548
  /**
1409
- * Stream a Codex compaction to a card: start a RunCardSession with
1549
+ * Stream a compaction to a card: start a RunCardSession with
1410
1550
  * operationKind='compaction' (no recursive Compact button), consume
1411
1551
  * runner.runCompact() events, read authoritative jsonl usage, and finish
1412
1552
  * the card. Shared by handleCodexCompact (run card button) and
@@ -1460,7 +1600,7 @@ export class Bridge {
1460
1600
  // (实测差 ~9ms),compactCount 未读到则短重试,仍失败优雅降级(无统计)。
1461
1601
  let finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1462
1602
  for (let attempt = 0; !finalUsage?.compactCount && attempt < 2; attempt++) {
1463
- await new Promise((r) => setTimeout(r, 150));
1603
+ await sleep(150);
1464
1604
  finalUsage = this.resolveFinalUsage(sessionId, cwd, agentKind);
1465
1605
  }
1466
1606
  await cardSession.finish(sawResult ? 'done' : 'error', {
@@ -1536,7 +1676,7 @@ export class Bridge {
1536
1676
  await this.sendResult({ text: `⚠️ 未找到 session ${sessionId}(当前目录: ${cwd})` }, ctx);
1537
1677
  return;
1538
1678
  }
1539
- // Check that the runner has runCompact (codex app-server only).
1679
+ // Check that the runner has runCompact(codex/kimi/opencode/pi/claude 鸭子探测)。
1540
1680
  const runner = this.getRunner(cwd, agentKind);
1541
1681
  if (!('runCompact' in runner) ||
1542
1682
  typeof runner.runCompact !== 'function') {
@@ -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();