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,11 +1,10 @@
1
1
  import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, agentDisplayName, } from './card-shared.js';
2
2
  import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
3
3
  import { toolBodyMd, toolHeaderText } from './tool-render.js';
4
- import { truncateUtf8, truncateMarkdownTables } from './text-truncate.js';
4
+ import { truncateUtf8, truncateMarkdownTables, CARD_BUDGET_BYTES } from './text-truncate.js';
5
5
  import { formatTimestamp } from './time.js';
6
- import { formatUsageStats } from '../router/index.js';
6
+ import { formatUsageStats } from '../router/utils.js';
7
7
  import { renderApprovalArea } from './approval-render.js';
8
- const CARD_BUDGET_BYTES = 28_000;
9
8
  const REASONING_BYTES = 4_500;
10
9
  const TEXT_BYTES = 10_000;
11
10
  const DEGRADED_THINKING_KEEP = 2;
@@ -26,13 +25,21 @@ function measureJson(obj) {
26
25
  /** markdownDiv('', 'notation') 的序列化字节(空内容 div 的常量开销,估算时复用)。 */
27
26
  const EMPTY_MD_DIV_BYTES = measureJson(markdownDiv('', 'notation'));
28
27
  /**
29
- * Compact 按钮可见性:普通 run(operationKind='turn')到达任意终态即可压缩。
28
+ * Compact 按钮可见性:到达任意终态即可压缩。
30
29
  * 异常退出(error/interrupted/idle_timeout)时上下文往往更大,更需要压缩后
31
30
  * 再继续,因此不再要求 resultSubtype==='success'。compaction 卡
32
31
  * (operationKind='compaction')永不显示,防止递归 Compact。
32
+ *
33
+ * 能力门控(compactSupported !== false)与执行侧 `'runCompact' in runner`
34
+ * 鸭子探测对齐:claude 走 stream-json,不发射 turn_started,operationKind
35
+ * 恒 undefined——若以 operationKind==='turn' 作门控,claude 终态卡永远拿
36
+ * 不到按钮(2026-08-19 缺陷)。compaction 卡的「防递归」仍由 operationKind
37
+ * 保证,两者是不同质的判断。
33
38
  */
34
- function shouldShowCompactButton(state) {
35
- if (state.operationKind !== 'turn')
39
+ function shouldShowCompactButton(state, options) {
40
+ if (state.operationKind === 'compaction')
41
+ return false;
42
+ if (options.compactSupported === false)
36
43
  return false;
37
44
  return (state.terminal === 'done' ||
38
45
  state.terminal === 'error' ||
@@ -189,96 +196,35 @@ function toolTitle(tool) {
189
196
  const title = toolHeaderText(tool);
190
197
  return ts ? `${title} (${ts})` : title;
191
198
  }
192
- /**
193
- * Build degraded elements when full card exceeds 28KB budget.
194
- * Strategy: text fully preserved, thinking keeps last 2, tools keep last N (compressed).
195
- *
196
- * 性能优化:单遍遍历 state.blocks 按 kind 分桶(thinking/tool/other),替代原先
197
- * 多次 state.blocks.filter() 扫描。分桶时保持原相对顺序,后续用 groupBlocks
198
- * 渲染时序不变。
199
- */
200
- function buildDegradedElements(state, options) {
201
- const elements = [];
202
- elements.push(statusRow(state));
203
- // 单遍分桶:thinking / tool / other(保持原相对顺序)
204
- const { thinking: allThinkingBlocks, tool: allToolBlocks } = bucketThinkingAndTool(state.blocks);
205
- if (allThinkingBlocks.length > DEGRADED_THINKING_KEEP) {
206
- const omitted = allThinkingBlocks.length - DEGRADED_THINKING_KEEP;
207
- elements.push(markdownDiv(`_💡 ${omitted} 个早期思考已省略_`));
208
- }
209
- // Keep only the last N thinking blocks for display
210
- const thinkingBlocksToShow = allThinkingBlocks.slice(-DEGRADED_THINKING_KEEP);
211
- const toolsToShow = allToolBlocks.slice(-DEGRADED_TOOL_KEEP);
212
- const toolsOmitted = allToolBlocks.length - DEGRADED_TOOL_KEEP;
213
- // Add omit hint for tools if any were omitted
214
- if (toolsOmitted > 0) {
215
- elements.push(markdownDiv(`_💡 另外 ${toolsOmitted} 个工具调用已省略_`));
216
- }
217
- // Build a filtered blocks list that only includes the thinking blocks we want to show
218
- // and the tool blocks we want to show.
219
- // P2-6: use object references (like the extreme path) instead of timestamps.
220
- // RunBlock.timestamp has millisecond precision; ≥2 thinking blocks sharing a
221
- // timestamp collapse to one Set entry, so `has(block.timestamp)` lets ALL
222
- // same-timestamp blocks (including ones meant to be omitted) pass through —
223
- // inflating the degraded card past 28KB and contradicting the "N omitted"
224
- // hint. `thinkingBlocksToShow` is a slice of `allThinkingBlocks`, so the
225
- // blocks retain stable references and the Set membership check is exact.
226
- const thinkingSetToShow = new Set(thinkingBlocksToShow);
227
- const toolIdsToShow = new Set(toolsToShow.map((b) => b.tool.id));
228
- const filteredBlocks = state.blocks.filter((block) => {
229
- if (block.kind === 'thinking') {
230
- return thinkingSetToShow.has(block);
231
- }
232
- if (block.kind === 'tool') {
233
- return toolIdsToShow.has(block.tool.id);
234
- }
235
- return true; // keep all non-thinking, non-tool blocks
236
- });
237
- // Render thinking/text/tool in chronological order using groupBlocks
238
- for (const group of groupBlocks(filteredBlocks)) {
239
- if (group.kind === 'thinking') {
240
- if (options.showThinking === false)
241
- continue;
242
- const ts = formatTimestamp(group.timestamp);
243
- const title = '💭 **思考完成**';
244
- const content = truncateUtf8(group.content, REASONING_BYTES);
245
- const header = ts ? `${title} (${ts})` : title;
246
- elements.push(collapsibleMarkdownPanel({
247
- title: header,
248
- expanded: false,
249
- border: 'grey',
250
- content,
251
- textSize: 'notation',
252
- }));
253
- }
254
- else if (group.kind === 'text') {
255
- const content = truncateUtf8(group.content, TEXT_BYTES, true, '…(已截断)\n');
256
- const ts = formatTimestamp(group.timestamp);
257
- const els = renderTextBlock(content, ts, true);
258
- elements.push(...els);
259
- }
260
- else if (group.kind === 'tool') {
261
- // Degraded: show tool individually (each tool is independent now)
262
- if (options.showToolUse === false)
263
- continue;
264
- elements.push(renderTool(group.tool, true, options.showToolResult !== false)); // finalized=true
265
- }
266
- }
267
- elements.push(...buildSummaryContent(state));
268
- // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
199
+ const DEGRADED_TIER = {
200
+ thinkingKeep: DEGRADED_THINKING_KEEP,
201
+ thinkingBytes: REASONING_BYTES,
202
+ toolKeep: DEGRADED_TOOL_KEEP,
203
+ textBytes: TEXT_BYTES,
204
+ };
205
+ const EXTREME_TIER = {
206
+ thinkingKeep: 1,
207
+ thinkingBytes: DEGRADED_THINKING_BYTES,
208
+ toolKeep: EXTREME_TOOL_KEEP,
209
+ textBytes: DEGRADED_TEXT_BYTES,
210
+ };
211
+ /** Bottom action row: stop (if running) + compact (if applicable) + new session (always). */
212
+ function actionRow(state, options) {
269
213
  const actionButtons = [];
270
214
  // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
271
215
  const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
272
216
  if (showStop) {
273
217
  actionButtons.push(stopButton(state.runId));
274
218
  }
275
- // Compact 按钮:普通 turn 到达任意终态即可压缩(含异常退出),compaction 卡除外
276
- if (shouldShowCompactButton(state)) {
219
+ // Compact 按钮:到达任意终态即可压缩(含异常退出),compaction 卡除外
220
+ if (shouldShowCompactButton(state, options)) {
277
221
  actionButtons.push(compactButton(state.runId));
278
222
  }
279
223
  actionButtons.push(newSessionButton());
280
- if (actionButtons.length > 0) {
281
- elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
224
+ if (actionButtons.length === 0)
225
+ return [];
226
+ return [
227
+ { tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
282
228
  {
283
229
  tag: 'column_set',
284
230
  columns: actionButtons.map((btn) => ({
@@ -286,10 +232,12 @@ function buildDegradedElements(state, options) {
286
232
  width: 'auto',
287
233
  elements: [btn],
288
234
  })),
289
- });
290
- }
291
- // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
292
- // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
235
+ },
236
+ ];
237
+ }
238
+ /** 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。 */
239
+ function approvalArea(state) {
240
+ const elements = [];
293
241
  for (const slot of state.approvals ?? []) {
294
242
  elements.push(...renderApprovalArea(slot.view, {
295
243
  expired: slot.expired,
@@ -299,49 +247,65 @@ function buildDegradedElements(state, options) {
299
247
  }
300
248
  return elements;
301
249
  }
250
+ /** Assemble the card shell (schema/config/header) around body elements. */
251
+ function assembleRunCard(state, options, elements) {
252
+ return {
253
+ schema: '2.0',
254
+ config: { wide_screen_mode: true, update_multi: true },
255
+ header: {
256
+ template: headerTemplate2(state),
257
+ title: { content: headerTitle2(state, options), tag: 'plain_text' },
258
+ },
259
+ body: { elements },
260
+ };
261
+ }
302
262
  /**
303
- * Extreme fallback when even degraded card exceeds 28KB.
304
- * Strategy: text truncated to 5KB tail, thinking only last 1 truncated to 1KB,
305
- * tools keep last N (more aggressive than degraded).
306
- *
307
- * 性能优化:单遍分桶替代多次 state.blocks.filter()。
263
+ * Build a budget-fallback tier (degraded or extreme): keep the last N thinking
264
+ * blocks and last N tools, truncate text/thinking per tier, then render in
265
+ * chronological order. Shared by buildDegradedElements / buildExtremeFallback.
308
266
  */
309
- function buildExtremeFallbackElements(state, options) {
267
+ function buildFallbackElements(state, options, tier) {
310
268
  const elements = [];
311
269
  elements.push(statusRow(state));
312
- // 单遍分桶:thinking / tool
270
+ // 单遍分桶:thinking / tool(保持原相对顺序)
313
271
  const { thinking: allThinkingBlocks, tool: allToolBlocks } = bucketThinkingAndTool(state.blocks);
314
- // Thinking: only keep last 1 for extreme fallback
315
- if (allThinkingBlocks.length > 1) {
316
- const omitted = allThinkingBlocks.length - 1;
272
+ if (allThinkingBlocks.length > tier.thinkingKeep) {
273
+ const omitted = allThinkingBlocks.length - tier.thinkingKeep;
317
274
  elements.push(markdownDiv(`_💡 ${omitted} 个早期思考已省略_`));
318
275
  }
319
- const thinkingBlockToShow = allThinkingBlocks.length > 0 ? allThinkingBlocks[allThinkingBlocks.length - 1] : null;
320
- const toolsToShow = allToolBlocks.slice(-EXTREME_TOOL_KEEP);
321
- const toolsOmitted = allToolBlocks.length - EXTREME_TOOL_KEEP;
276
+ const thinkingBlocksToShow = allThinkingBlocks.slice(-tier.thinkingKeep);
277
+ const toolsToShow = allToolBlocks.slice(-tier.toolKeep);
278
+ const toolsOmitted = allToolBlocks.length - tier.toolKeep;
322
279
  // Add omit hint for tools if any were omitted
323
280
  if (toolsOmitted > 0) {
324
281
  elements.push(markdownDiv(`_💡 另外 ${toolsOmitted} 个工具调用已省略_`));
325
282
  }
326
- // Build filtered blocks: only last thinking, last N tools, all text
283
+ // P2-6: use object references (like the extreme path) instead of timestamps.
284
+ // RunBlock.timestamp has millisecond precision; ≥2 thinking blocks sharing a
285
+ // timestamp collapse to one Set entry, so `has(block.timestamp)` lets ALL
286
+ // same-timestamp blocks (including ones meant to be omitted) pass through —
287
+ // inflating the card past 28KB and contradicting the "N omitted" hint.
288
+ // `thinkingBlocksToShow` is a slice of `allThinkingBlocks`, so the blocks
289
+ // retain stable references and the Set membership check is exact.
290
+ const thinkingSetToShow = new Set(thinkingBlocksToShow);
327
291
  const toolIdsToShow = new Set(toolsToShow.map((b) => b.tool.id));
328
292
  const filteredBlocks = state.blocks.filter((block) => {
329
293
  if (block.kind === 'thinking') {
330
- return block === thinkingBlockToShow;
294
+ return thinkingSetToShow.has(block);
331
295
  }
332
296
  if (block.kind === 'tool') {
333
297
  return toolIdsToShow.has(block.tool.id);
334
298
  }
335
- return true; // keep all text and other blocks
299
+ return true; // keep all non-thinking, non-tool blocks
336
300
  });
337
- // Render in chronological order
301
+ // Render thinking/text/tool in chronological order using groupBlocks
338
302
  for (const group of groupBlocks(filteredBlocks)) {
339
303
  if (group.kind === 'thinking') {
340
304
  if (options.showThinking === false)
341
305
  continue;
342
306
  const ts = formatTimestamp(group.timestamp);
343
307
  const title = '💭 **思考完成**';
344
- const content = truncateUtf8(group.content, DEGRADED_THINKING_BYTES);
308
+ const content = truncateUtf8(group.content, tier.thinkingBytes);
345
309
  const header = ts ? `${title} (${ts})` : title;
346
310
  elements.push(collapsibleMarkdownPanel({
347
311
  title: header,
@@ -352,56 +316,40 @@ function buildExtremeFallbackElements(state, options) {
352
316
  }));
353
317
  }
354
318
  else if (group.kind === 'text') {
355
- const content = truncateUtf8(group.content, DEGRADED_TEXT_BYTES, true, '…(已截断)\n');
319
+ const content = truncateUtf8(group.content, tier.textBytes, true, '…(已截断)\n');
356
320
  const ts = formatTimestamp(group.timestamp);
357
- const els = renderTextBlock(content, ts, true);
358
- elements.push(...els);
321
+ elements.push(...renderTextBlock(content, ts, true));
359
322
  }
360
323
  else if (group.kind === 'tool') {
361
- // Extreme fallback: show tool individually
324
+ // Show tool individually (each tool is independent now)
362
325
  if (options.showToolUse === false)
363
326
  continue;
364
- elements.push(renderTool(group.tool, true, options.showToolResult !== false));
327
+ elements.push(renderTool(group.tool, true, options.showToolResult !== false)); // finalized=true
365
328
  }
366
329
  }
367
- if (elements.length === 0) {
330
+ if (elements.length === 0 && tier === EXTREME_TIER) {
368
331
  elements.push(markdownDiv('_暂无输出_'));
369
332
  }
370
333
  elements.push(...buildSummaryContent(state));
371
- // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
372
- const actionButtons = [];
373
- // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
374
- const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
375
- if (showStop) {
376
- actionButtons.push(stopButton(state.runId));
377
- }
378
- // Compact 按钮:普通 turn 到达任意终态即可压缩(含异常退出),compaction 卡除外
379
- if (shouldShowCompactButton(state)) {
380
- actionButtons.push(compactButton(state.runId));
381
- }
382
- actionButtons.push(newSessionButton());
383
- if (actionButtons.length > 0) {
384
- elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
385
- {
386
- tag: 'column_set',
387
- columns: actionButtons.map((btn) => ({
388
- tag: 'column',
389
- width: 'auto',
390
- elements: [btn],
391
- })),
392
- });
393
- }
394
- // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
395
- // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
396
- for (const slot of state.approvals ?? []) {
397
- elements.push(...renderApprovalArea(slot.view, {
398
- expired: slot.expired,
399
- runId: state.runId,
400
- terminal: state.terminal,
401
- }));
402
- }
334
+ elements.push(...actionRow(state, options));
335
+ elements.push(...approvalArea(state));
403
336
  return elements;
404
337
  }
338
+ /**
339
+ * Build degraded elements when full card exceeds 28KB budget.
340
+ * Strategy: text fully preserved, thinking keeps last 2, tools keep last N (compressed).
341
+ */
342
+ function buildDegradedElements(state, options) {
343
+ return buildFallbackElements(state, options, DEGRADED_TIER);
344
+ }
345
+ /**
346
+ * Extreme fallback when even degraded card exceeds 28KB.
347
+ * Strategy: text truncated to 5KB tail, thinking only last 1 truncated to 1KB,
348
+ * tools keep last N (more aggressive than degraded).
349
+ */
350
+ function buildExtremeFallbackElements(state, options) {
351
+ return buildFallbackElements(state, options, EXTREME_TIER);
352
+ }
405
353
  /** Build the status row element (shared across full/degraded/extreme). */
406
354
  function statusRow(state) {
407
355
  const statusLabel = statusTagLabel(state.terminal);
@@ -434,7 +382,7 @@ function statusRow(state) {
434
382
  * stop while running/finalizing). No block content is rendered, so the size is
435
383
  * bounded by static structure regardless of input — closing the safety-net gap.
436
384
  */
437
- function buildSkeletonElements(state) {
385
+ function buildSkeletonElements(state, options) {
438
386
  const elements = [];
439
387
  elements.push(statusRow(state));
440
388
  elements.push(markdownDiv('_⚠️ 输出过大且含大量转义字符,已省略全部内容以避免超限;完整内容请查日志_'));
@@ -442,24 +390,7 @@ function buildSkeletonElements(state) {
442
390
  elements.push(...buildSummaryContent(state));
443
391
  // Bottom action row: stop (if running/finalizing) + compact (if applicable) + new session (always).
444
392
  // Degraded paths must keep the action buttons reachable (design constraint).
445
- const actionButtons = [];
446
- const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
447
- if (showStop) {
448
- actionButtons.push(stopButton(state.runId));
449
- }
450
- if (shouldShowCompactButton(state)) {
451
- actionButtons.push(compactButton(state.runId));
452
- }
453
- actionButtons.push(newSessionButton());
454
- elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
455
- {
456
- tag: 'column_set',
457
- columns: actionButtons.map((btn) => ({
458
- tag: 'column',
459
- width: 'auto',
460
- elements: [btn],
461
- })),
462
- });
393
+ elements.push(...actionRow(state, options));
463
394
  return elements;
464
395
  }
465
396
  /**
@@ -527,6 +458,9 @@ export function estimateCardBytes(state, options = {}, shared) {
527
458
  if (state.terminal === 'running' || state.terminal === 'finalizing') {
528
459
  actionButtons.push(stopButton(state.runId));
529
460
  }
461
+ if (shouldShowCompactButton(state, options)) {
462
+ actionButtons.push(compactButton(state.runId));
463
+ }
530
464
  actionButtons.push(newSessionButton());
531
465
  total += measureJson([
532
466
  { tag: 'div', text: { content: '‎', tag: 'lark_md' } },
@@ -616,105 +550,41 @@ function measurePanelBytes(title, expanded, border, content) {
616
550
  * CardKit 2.0 renderer - simplified for streaming (no tabs)
617
551
  */
618
552
  export function renderRunCard(state, options = {}) {
619
- const { terminal, runId } = state;
620
553
  // 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
621
554
  // degraded。P1-2:groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
622
555
  // 避免影子测量 + 正式渲染对每个超限块双倍截断。
623
556
  const groups = groupBlocks(state.blocks);
624
557
  const prepared = prepareGroupContent(groups);
625
558
  const estimate = estimateCardBytes(state, options, { groups, prepared });
626
- // Stop button: show when running or finalizing (process not yet exited)
627
- const showStop = terminal === 'running' || terminal === 'finalizing';
628
559
  // 构建完整卡(仅在估算低于阈值时,避免大 state 浪费 render + stringify)
629
560
  if (estimate < DEGRADED_THRESHOLD) {
630
561
  const elements = [
631
562
  statusRow(state),
632
563
  ...buildChronologicalContent(state, options, prepared, groups),
633
564
  ...buildSummaryContent(state),
565
+ ...actionRow(state, options),
566
+ ...approvalArea(state),
634
567
  ];
635
- // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
636
- const actionButtons = [];
637
- if (showStop) {
638
- actionButtons.push(stopButton(runId));
639
- }
640
- if (shouldShowCompactButton(state)) {
641
- actionButtons.push(compactButton(runId));
642
- }
643
- actionButtons.push(newSessionButton());
644
- if (actionButtons.length > 0) {
645
- elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
646
- {
647
- tag: 'column_set',
648
- columns: actionButtons.map((btn) => ({
649
- tag: 'column',
650
- width: 'auto',
651
- elements: [btn],
652
- })),
653
- });
654
- }
655
- // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
656
- // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
657
- for (const slot of state.approvals ?? []) {
658
- elements.push(...renderApprovalArea(slot.view, {
659
- expired: slot.expired,
660
- runId: state.runId,
661
- terminal: state.terminal,
662
- }));
663
- }
664
- const card = {
665
- schema: '2.0',
666
- config: { wide_screen_mode: true, update_multi: true },
667
- header: {
668
- template: headerTemplate2(state),
669
- title: { content: headerTitle2(state, options), tag: 'plain_text' },
670
- },
671
- body: { elements },
672
- };
568
+ const card = assembleRunCard(state, options, elements);
673
569
  // Check budget — stringify 兜底(估算可能低估,仍保留安全网)
674
570
  if (Buffer.byteLength(JSON.stringify(card), 'utf8') <= CARD_BUDGET_BYTES)
675
571
  return card;
676
572
  // 超预算 → 继续走 degraded(不 return,fallthrough)
677
573
  }
678
574
  // 估算 ≥ 阈值 或 完整卡 stringify 超预算 — 走 degraded 渲染
679
- const degradedElements = buildDegradedElements(state, options);
680
- const degradedCard = {
681
- schema: '2.0',
682
- config: { wide_screen_mode: true, update_multi: true },
683
- header: {
684
- template: headerTemplate2(state),
685
- title: { content: headerTitle2(state, options), tag: 'plain_text' },
686
- },
687
- body: { elements: degradedElements },
688
- };
575
+ const degradedCard = assembleRunCard(state, options, buildDegradedElements(state, options));
689
576
  if (Buffer.byteLength(JSON.stringify(degradedCard), 'utf8') <= CARD_BUDGET_BYTES) {
690
577
  return degradedCard;
691
578
  }
692
579
  // Extreme fallback: even degraded exceeds budget
693
- const extremeElements = buildExtremeFallbackElements(state, options);
694
- const extremeCard = {
695
- schema: '2.0',
696
- config: { wide_screen_mode: true, update_multi: true },
697
- header: {
698
- template: headerTemplate2(state),
699
- title: { content: headerTitle2(state, options), tag: 'plain_text' },
700
- },
701
- body: { elements: extremeElements },
702
- };
580
+ const extremeCard = assembleRunCard(state, options, buildExtremeFallbackElements(state, options));
703
581
  // Final safety net: extreme fallback 是最后一层,但高转义内容(反斜杠 4× 膨胀)
704
582
  // 可能让 extreme 产物本身 >28KB。补上 stringify 兜底(与 normal/degraded 对称):
705
583
  // 超预算则降级到结构性保证 ≤28KB 的 skeleton 卡,避免飞书 11310 整卡不可用。
706
584
  if (Buffer.byteLength(JSON.stringify(extremeCard), 'utf8') <= CARD_BUDGET_BYTES) {
707
585
  return extremeCard;
708
586
  }
709
- const skeletonCard = {
710
- schema: '2.0',
711
- config: { wide_screen_mode: true, update_multi: true },
712
- header: {
713
- template: headerTemplate2(state),
714
- title: { content: headerTitle2(state, options), tag: 'plain_text' },
715
- },
716
- body: { elements: buildSkeletonElements(state) },
717
- };
587
+ const skeletonCard = assembleRunCard(state, options, buildSkeletonElements(state, options));
718
588
  // skeleton 卡是静态结构,理论上必 ≤28KB;保留兜底断言以防未来结构膨胀
719
589
  if (Buffer.byteLength(JSON.stringify(skeletonCard), 'utf8') <= CARD_BUDGET_BYTES) {
720
590
  return skeletonCard;
@@ -823,8 +693,16 @@ function buildSummaryContent(state) {
823
693
  else if (state.terminal === 'interrupted') {
824
694
  // 审批超时(approval_expired 已标记)是「无人响应被自动取消」,不是用户
825
695
  // 主动终止;如实展示原因,避免用户误判为 Agent 出错或自己操作过。
826
- const expiredApproval = (state.approvals ?? []).some((a) => a.expired);
827
- elements.push(markdownDiv(expiredApproval ? '⏰ **审批超时未响应,已自动取消**' : '⏹ **已被用户终止**'));
696
+ // 顶层 approvalExpired 优先:approval_resolved 会移除审批条目,但标记
697
+ // 必须活到终态渲染(2026-08-15 事故回归)。
698
+ const expiredApproval = state.interruptedReason === 'approval_timeout' ||
699
+ state.approvalExpired === true ||
700
+ (state.approvals ?? []).some((a) => a.expired);
701
+ elements.push(markdownDiv(expiredApproval
702
+ ? '⏰ **审批超时未响应,已自动取消**'
703
+ : state.interruptedReason === 'approval_cancelled'
704
+ ? '⏹ **已取消审批,任务终止**'
705
+ : '⏹ **已被用户终止**'));
828
706
  }
829
707
  else if (state.terminal === 'idle_timeout') {
830
708
  elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
@@ -1,6 +1,8 @@
1
1
  import type { AgentEvent, ApprovalView } from '../runner/index.js';
2
2
  export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout';
3
3
  export type RunFooter = 'thinking' | 'tool_running' | 'streaming' | null;
4
+ /** 中断来源(interrupted 终态的原因),供卡片如实归因。 */
5
+ export type InterruptedReason = 'approval_timeout' | 'user_stop' | 'approval_cancelled';
4
6
  type ToolStatus = 'running' | 'ok' | 'error';
5
7
  export interface ToolEntry {
6
8
  id: string;
@@ -61,6 +63,8 @@ export interface RunState {
61
63
  blocks: RunBlock[];
62
64
  sessionId?: string;
63
65
  resultSubtype?: 'success' | 'error' | 'interrupted';
66
+ /** interrupted 终态的中断来源;undefined = 未归类(渲染默认文案)。 */
67
+ interruptedReason?: InterruptedReason;
64
68
  contextLength?: number;
65
69
  /** 当前模型 context window 上限(codex jsonl 提供);卡片据此渲染百分比。 */
66
70
  contextLimit?: number;
@@ -98,9 +102,18 @@ export interface RunState {
98
102
  view: ApprovalView;
99
103
  expired?: boolean;
100
104
  }>;
105
+ /**
106
+ * 审批是否曾过期(桥侧 timer 触发,approval_expired 事件置位)。
107
+ * 独立于 approvals 数组持久保存:cancel 送达后 server 会回
108
+ * approval_resolved,把对应审批条目从数组移除,但过期原因必须活到
109
+ * 终态渲染(2026-08-15 事故:标记被消费后卡片误报「已被用户终止」)。
110
+ */
111
+ approvalExpired?: boolean;
101
112
  }
102
113
  export interface FinishMeta {
103
114
  resultSubtype?: 'success' | 'error' | 'interrupted';
115
+ /** 中断来源:由桥侧在 finish 时显式传入(/stop → user_stop、审批卡取消 → approval_cancelled)。 */
116
+ interruptedReason?: InterruptedReason;
104
117
  contextLength?: number;
105
118
  /** 当前模型 context window 上限;透传到 RunState 供 done 卡片显示百分比。 */
106
119
  contextLimit?: number;