lark-remote 0.5.12 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/README.en.md +1 -14
  3. package/README.md +1 -14
  4. package/dist/agent-kinds.d.ts +27 -0
  5. package/dist/agent-kinds.js +38 -0
  6. package/dist/bridge/approval-actions.d.ts +95 -0
  7. package/dist/bridge/approval-actions.js +104 -0
  8. package/dist/bridge/approval-coordinator.d.ts +1 -0
  9. package/dist/bridge/approval-coordinator.js +12 -24
  10. package/dist/bridge/final-usage.d.ts +8 -17
  11. package/dist/bridge/final-usage.js +1 -18
  12. package/dist/bridge/index.d.ts +52 -38
  13. package/dist/bridge/index.js +255 -433
  14. package/dist/bridge/queue-manager.d.ts +25 -1
  15. package/dist/bridge/queue-manager.js +179 -160
  16. package/dist/bridge/run-approval.d.ts +16 -0
  17. package/dist/bridge/run-approval.js +41 -0
  18. package/dist/bridge/run-usage.d.ts +32 -0
  19. package/dist/bridge/run-usage.js +87 -0
  20. package/dist/bridge/session-writeback.d.ts +19 -0
  21. package/dist/bridge/session-writeback.js +36 -0
  22. package/dist/card/approval-render.d.ts +3 -2
  23. package/dist/card/approval-render.js +3 -2
  24. package/dist/card/bash-card-session.js +2 -5
  25. package/dist/{router → card}/card-helpers.d.ts +7 -7
  26. package/dist/{router → card}/card-helpers.js +8 -8
  27. package/dist/card/card-session.d.ts +6 -2
  28. package/dist/card/card-session.js +28 -28
  29. package/dist/card/card-shared.d.ts +4 -4
  30. package/dist/card/card-shared.js +4 -10
  31. package/dist/card/collapsible.js +3 -2
  32. package/dist/card/final-answer-overflow.d.ts +1 -0
  33. package/dist/card/final-answer-overflow.js +46 -0
  34. package/dist/card/run-card-session.d.ts +2 -2
  35. package/dist/card/run-card-session.js +4 -9
  36. package/dist/card/run-renderer.d.ts +14 -0
  37. package/dist/card/run-renderer.js +126 -23
  38. package/dist/card/run-state.d.ts +40 -51
  39. package/dist/card/run-state.js +96 -34
  40. package/dist/card/usage-format.d.ts +30 -0
  41. package/dist/{router → card}/usage-format.js +3 -10
  42. package/dist/clone.js +6 -1
  43. package/dist/config/codex-config.js +4 -4
  44. package/dist/config/index.d.ts +18 -20
  45. package/dist/config/index.js +6 -20
  46. package/dist/config/pi-config.js +6 -4
  47. package/dist/connector/index.d.ts +2 -4
  48. package/dist/connector/index.js +7 -11
  49. package/dist/{runner/dsh → dsh}/auth.js +3 -4
  50. package/dist/dsh/home.d.ts +2 -0
  51. package/dist/dsh/home.js +8 -0
  52. package/dist/dsh/protocol.d.ts +9 -0
  53. package/dist/dsh/protocol.js +61 -0
  54. package/dist/inbound/turn-assembler.js +6 -0
  55. package/dist/index.js +25 -414
  56. package/dist/message-handlers.d.ts +26 -0
  57. package/dist/message-handlers.js +275 -0
  58. package/dist/model-catalog.d.ts +14 -0
  59. package/dist/model-catalog.js +25 -0
  60. package/dist/platform/path.d.ts +9 -0
  61. package/dist/platform/path.js +18 -0
  62. package/dist/router/active-run-usage.d.ts +4 -0
  63. package/dist/router/active-run-usage.js +12 -0
  64. package/dist/router/card-action-payload.d.ts +1 -1
  65. package/dist/router/card-action-payload.js +1 -1
  66. package/dist/router/commands/ls.d.ts +9 -0
  67. package/dist/router/commands/ls.js +243 -0
  68. package/dist/router/commands/order.d.ts +12 -0
  69. package/dist/router/commands/order.js +178 -0
  70. package/dist/router/commands/resume.d.ts +48 -0
  71. package/dist/router/commands/resume.js +246 -0
  72. package/dist/router/commands/ws.d.ts +15 -0
  73. package/dist/router/commands/ws.js +189 -0
  74. package/dist/router/config/claude.js +5 -4
  75. package/dist/router/config/codex.js +2 -6
  76. package/dist/router/config/common/render.d.ts +5 -1
  77. package/dist/router/config/common/render.js +6 -13
  78. package/dist/router/config/common/turn-idle.d.ts +2 -0
  79. package/dist/router/config/common/turn-idle.js +9 -0
  80. package/dist/router/config/dsh.d.ts +1 -1
  81. package/dist/router/config/index.d.ts +1 -1
  82. package/dist/router/config/kimi.js +2 -6
  83. package/dist/router/config/opencode.js +2 -6
  84. package/dist/router/config/types.d.ts +11 -3
  85. package/dist/router/index.d.ts +55 -33
  86. package/dist/router/index.js +233 -1376
  87. package/dist/router/paging.d.ts +13 -0
  88. package/dist/router/paging.js +20 -0
  89. package/dist/router/path-validation.d.ts +27 -0
  90. package/dist/router/path-validation.js +45 -0
  91. package/dist/runner/agent-choices-common.d.ts +1 -1
  92. package/dist/runner/claude/runner.d.ts +23 -6
  93. package/dist/runner/claude/runner.js +6 -20
  94. package/dist/runner/claude/session.js +1 -1
  95. package/dist/runner/codex/app-server/runner.d.ts +3 -1
  96. package/dist/runner/codex/app-server/translator.d.ts +13 -0
  97. package/dist/runner/codex/app-server/translator.js +114 -151
  98. package/dist/runner/common/acp/base-acp-runner.d.ts +3 -1
  99. package/dist/runner/common/acp/base-translator.d.ts +3 -8
  100. package/dist/runner/common/acp/base-translator.js +16 -7
  101. package/dist/runner/common/connection-based-runner.d.ts +3 -1
  102. package/dist/runner/common/jsonrpc/transport.js +17 -7
  103. package/dist/runner/common/spawning-runner.js +10 -4
  104. package/dist/runner/common/turn-started.d.ts +2 -0
  105. package/dist/runner/common/turn-started.js +10 -0
  106. package/dist/runner/dsh/client.d.ts +2 -10
  107. package/dist/runner/dsh/client.js +13 -220
  108. package/dist/runner/dsh/daemon.d.ts +0 -2
  109. package/dist/runner/dsh/daemon.js +9 -7
  110. package/dist/runner/dsh/errors.d.ts +5 -0
  111. package/dist/runner/dsh/errors.js +11 -0
  112. package/dist/runner/dsh/exit-cancel.d.ts +0 -2
  113. package/dist/runner/dsh/exit-cancel.js +2 -2
  114. package/dist/runner/dsh/inflight.d.ts +0 -2
  115. package/dist/runner/dsh/inflight.js +1 -1
  116. package/dist/runner/dsh/mux-stream.d.ts +26 -0
  117. package/dist/runner/dsh/mux-stream.js +176 -0
  118. package/dist/runner/dsh/runner.d.ts +21 -1
  119. package/dist/runner/dsh/runner.js +76 -73
  120. package/dist/runner/dsh/translator.d.ts +1 -11
  121. package/dist/runner/dsh/translator.js +1 -1
  122. package/dist/runner/index.d.ts +1 -1
  123. package/dist/runner/kimi/acp/runner.d.ts +22 -1
  124. package/dist/runner/kimi/acp/runner.js +83 -60
  125. package/dist/runner/kimi/acp/translator.js +1 -12
  126. package/dist/runner/opencode/acp/error-monitor.d.ts +11 -0
  127. package/dist/runner/opencode/acp/error-monitor.js +31 -2
  128. package/dist/runner/opencode/acp/runner.d.ts +3 -1
  129. package/dist/runner/opencode/acp/translator.js +1 -12
  130. package/dist/runner/pi/rpc/runner.d.ts +2 -1
  131. package/dist/runner/pi/rpc/translator.d.ts +2 -8
  132. package/dist/runner/pi/rpc/translator.js +2 -7
  133. package/dist/runner/probe.d.ts +1 -1
  134. package/dist/runner/probe.js +10 -9
  135. package/dist/runner/registry.d.ts +2 -20
  136. package/dist/runner/registry.js +0 -32
  137. package/dist/runner/types.d.ts +37 -155
  138. package/dist/runner/types.js +4 -24
  139. package/dist/session/claude/session-index.d.ts +11 -4
  140. package/dist/session/claude/session-index.js +34 -10
  141. package/dist/session/claude/session-reader.d.ts +1 -1
  142. package/dist/session/claude/sessions.d.ts +2 -2
  143. package/dist/session/claude/sessions.js +41 -26
  144. package/dist/session/codex/rollout-reader.d.ts +1 -1
  145. package/dist/session/codex/rollout-reader.js +14 -13
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +4 -2
  148. package/dist/session/common/jsonl.js +7 -1
  149. package/dist/session/common/pagination.d.ts +2 -0
  150. package/dist/session/common/pagination.js +1 -1
  151. package/dist/session/common/recency.d.ts +6 -0
  152. package/dist/session/common/recency.js +7 -5
  153. package/dist/session/common/two-pass.d.ts +1 -1
  154. package/dist/session/common/usage-accumulator.d.ts +1 -1
  155. package/dist/session/dsh/sessions.d.ts +1 -1
  156. package/dist/session/dsh/sessions.js +37 -12
  157. package/dist/session/kimi/sessions.d.ts +1 -1
  158. package/dist/session/kimi/sessions.js +37 -10
  159. package/dist/session/opencode/sessions.d.ts +1 -1
  160. package/dist/session/opencode/sessions.js +53 -48
  161. package/dist/session/pi/sessions.d.ts +1 -1
  162. package/dist/session/pi/sessions.js +23 -9
  163. package/dist/session/registry.d.ts +2 -1
  164. package/dist/session/session-store.d.ts +1 -2
  165. package/dist/session/session-store.js +14 -7
  166. package/dist/session-contract.d.ts +178 -0
  167. package/dist/session-contract.js +9 -0
  168. package/package.json +1 -1
  169. package/dist/router/usage-format.d.ts +0 -60
  170. /package/dist/{runner/dsh → dsh}/auth.d.ts +0 -0
  171. /package/dist/{runner/dsh → dsh}/types.d.ts +0 -0
  172. /package/dist/{runner/dsh → dsh}/types.js +0 -0
@@ -5,6 +5,7 @@
5
5
  * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
6
  */
7
7
  import type { ApprovalView } from '../runner/types.js';
8
+ import { type RunTerminal } from './run-state.js';
8
9
  /**
9
10
  * Render the approval area for a run card.
10
11
  * Returns an array of CardKit 2.0 elements to append to the card body.
@@ -12,7 +13,7 @@ import type { ApprovalView } from '../runner/types.js';
12
13
  export declare function renderApprovalArea(approval: ApprovalView | undefined, opts?: {
13
14
  expired?: boolean;
14
15
  runId?: string;
15
- terminal?: string;
16
+ terminal?: RunTerminal;
16
17
  }): object[];
17
18
  /**
18
19
  * 紧凑审批区(run 卡 skeleton/truncatedSkeleton 兜底档专用):固定小体积的
@@ -26,5 +27,5 @@ export declare function renderApprovalArea(approval: ApprovalView | undefined, o
26
27
  export declare function renderApprovalAreaCompact(approval: ApprovalView | undefined, opts?: {
27
28
  expired?: boolean;
28
29
  runId?: string;
29
- terminal?: string;
30
+ terminal?: RunTerminal;
30
31
  }): object[];
@@ -9,6 +9,7 @@ import { truncateMarkdownTables } from './text-truncate.js';
9
9
  import { truncateUtf8 } from '../common/truncate.js';
10
10
  import { cardButton } from './card-shared.js';
11
11
  import { countCardElements } from './card-budget.js';
12
+ import { isRunActive } from './run-state.js';
12
13
  /** Unique nonce for card button callbacks (SDK dedup + router validation). */
13
14
  function mkNonce() {
14
15
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
@@ -156,9 +157,9 @@ export function renderApprovalAreaCompact(approval, opts) {
156
157
  }
157
158
  return elements;
158
159
  }
159
- /** 终态隐藏判定(renderApprovalArea 与 compact 共用)。 */
160
+ /** 终态隐藏判定(renderApprovalArea 与 compact 共用):仍在推进的 run 不隐藏按钮。 */
160
161
  function isTerminalHidden(opts) {
161
- return !!opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing';
162
+ return !!opts?.terminal && !isRunActive(opts.terminal);
162
163
  }
163
164
  /**
164
165
  * Render approval action buttons from the real protocol decision list.
@@ -76,11 +76,8 @@ export class BashCardSession extends CardSession {
76
76
  if (this.state.terminal !== 'running') {
77
77
  return;
78
78
  }
79
- this.cancelPendingFlush();
80
- // 若有 in-flight flush 正在 await updateCard,必须等它完成后再发终态 patch,
81
- // 保证顺序(pre-terminal 先落地 → terminal 后落地 → terminal 胜出)。
82
- if (this.flushP)
83
- await this.flushP;
79
+ // 终态 patch 必须排在已开始的 pre-terminal flush 之后。
80
+ await this.prepareTerminalUpdate();
84
81
  this.state = {
85
82
  ...this.state,
86
83
  terminal,
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Shared CardKit helpers for router cards (session history, dashboard,
3
- * ls, ws, etc.).
2
+ * 卡片构造的共享工具(会话历史卡、分页条、搜索条、ls/ws 面板等)。
4
3
  *
5
- * Re-exports `markdownDiv` from `card/collapsible.ts` and adds session-content
6
- * specific builders (e.g. `sessionEventPanel`) so `router/index.ts` can fold
7
- * long session histories into collapsible panels without duplicating logic.
4
+ * 归属 card/:这些是纯建卡函数,此前住在 router/ 导致 card→router 与
5
+ * bridge→router 两条反向依赖。`markdownDiv` 从 `card/collapsible.ts` 复用,
6
+ * 会话内容相关的 builder(如 `sessionEventPanel`)在这里单源,router 与
7
+ * bridge 都只调用不复制。
8
8
  */
9
- import { markdownDiv } from '../card/collapsible.js';
10
- import type { AgentSessionContentEvent } from '../runner/index.js';
9
+ import { markdownDiv } from './collapsible.js';
10
+ import type { AgentSessionContentEvent } from '../session-contract.js';
11
11
  import { formatUsageStats } from './usage-format.js';
12
12
  type SessionUsageLike = Parameters<typeof formatUsageStats>[0];
13
13
  export declare function sessionEventPanel(ev: AgentSessionContentEvent, index: number, totalEvents: number, tailExpandedCount?: number, agentKind?: string): object;
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Shared CardKit helpers for router cards (session history, dashboard,
3
- * ls, ws, etc.).
2
+ * 卡片构造的共享工具(会话历史卡、分页条、搜索条、ls/ws 面板等)。
4
3
  *
5
- * Re-exports `markdownDiv` from `card/collapsible.ts` and adds session-content
6
- * specific builders (e.g. `sessionEventPanel`) so `router/index.ts` can fold
7
- * long session histories into collapsible panels without duplicating logic.
4
+ * 归属 card/:这些是纯建卡函数,此前住在 router/ 导致 card→router 与
5
+ * bridge→router 两条反向依赖。`markdownDiv` 从 `card/collapsible.ts` 复用,
6
+ * 会话内容相关的 builder(如 `sessionEventPanel`)在这里单源,router 与
7
+ * bridge 都只调用不复制。
8
8
  */
9
- import { agentDisplayName, cardButton } from '../card/card-shared.js';
10
- import { collapsibleMarkdownPanel, markdownDiv } from '../card/collapsible.js';
11
- import { formatMonthDayClock } from '../card/time.js';
9
+ import { agentDisplayName, cardButton } from './card-shared.js';
10
+ import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
11
+ import { formatMonthDayClock } from './time.js';
12
12
  import { displayWidth, truncateMiddleToDisplayWidth } from '../common/text-width.js';
13
13
  import { formatUsageStats } from './usage-format.js';
14
14
  /**
@@ -107,15 +107,19 @@ export declare abstract class CardSession<S, RO = unknown> {
107
107
  start(): Promise<void>;
108
108
  settle(): Promise<StreamSettleResult>;
109
109
  /**
110
- * 渲染当前卡片。所有生产子类的 renderer 都自带 stringify 级预算保护
110
+ * 渲染当前状态对应的卡片(单一渲染入口)。
111
+ *
112
+ * 所有生产子类的 renderer 都自带 stringify 级预算保护
111
113
  * (run/bash 的 degraded/extreme 路径);发送前 Bridge 还会再走一次
112
114
  * enforceCardBudget,因此这里不再重复裁剪。
113
115
  */
114
- protected safeRenderCard(): object;
116
+ protected renderCurrentCard(): object;
115
117
  /**
116
118
  * Push an update to the card. Tries controller.update first, falls back to
117
119
  * connector.updateCard if the controller is unavailable or fails.
118
120
  */
119
121
  protected updateCard(): Promise<void>;
122
+ private waitForControllerReady;
123
+ protected prepareTerminalUpdate(): Promise<void>;
120
124
  protected release(): void;
121
125
  }
@@ -138,17 +138,7 @@ export class CardSession {
138
138
  // 二次调用也套 timeout 保护:若 controllerReady 永不 settle(首次超时失败),
139
139
  // 不能永久挂起。复用同样的 startTimeoutMs 超时。streamOutcome 已存在,
140
140
  // 不重复创建 stream。
141
- let timer;
142
- const timeout = new Promise((_, reject) => {
143
- timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
144
- });
145
- try {
146
- await Promise.race([this.controllerReady, timeout]);
147
- }
148
- finally {
149
- if (timer)
150
- clearTimeout(timer);
151
- }
141
+ await this.waitForControllerReady();
152
142
  return;
153
143
  }
154
144
  // NOTE: the two renderCard calls below are NOT redundant. The streamCard
@@ -160,11 +150,11 @@ export class CardSession {
160
150
  // those early events. The"reuse first render" optimization was
161
151
  // rejected for this reason (premise "two renders with identical input"
162
152
  // does not hold).
163
- const stream = this.connector.streamCard(this.chatId, this.safeRenderCard(), async (controller) => {
153
+ const stream = this.connector.streamCard(this.chatId, this.renderCurrentCard(), async (controller) => {
164
154
  this.controller = controller;
165
155
  this.messageId = controller.messageId;
166
156
  try {
167
- await controller.update(this.safeRenderCard());
157
+ await controller.update(this.renderCurrentCard());
168
158
  }
169
159
  catch (error) {
170
160
  getLogger().warn(`${this.logPrefix} initial controller update failed:`, error);
@@ -177,17 +167,7 @@ export class CardSession {
177
167
  this.rejectController(error);
178
168
  return { ok: false, error };
179
169
  });
180
- let timer;
181
- const timeout = new Promise((_, reject) => {
182
- timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
183
- });
184
- try {
185
- await Promise.race([this.controllerReady, timeout]);
186
- }
187
- finally {
188
- if (timer)
189
- clearTimeout(timer);
190
- }
170
+ await this.waitForControllerReady();
191
171
  }
192
172
  async settle() {
193
173
  this.release();
@@ -206,7 +186,7 @@ export class CardSession {
206
186
  if (!this.messageId)
207
187
  return 'unsent';
208
188
  try {
209
- await this.connector.updateCard(this.messageId, this.safeRenderCard());
189
+ await this.connector.updateCard(this.messageId, this.renderCurrentCard());
210
190
  return 'updated';
211
191
  }
212
192
  catch (error) {
@@ -215,11 +195,13 @@ export class CardSession {
215
195
  }
216
196
  }
217
197
  /**
218
- * 渲染当前卡片。所有生产子类的 renderer 都自带 stringify 级预算保护
198
+ * 渲染当前状态对应的卡片(单一渲染入口)。
199
+ *
200
+ * 所有生产子类的 renderer 都自带 stringify 级预算保护
219
201
  * (run/bash 的 degraded/extreme 路径);发送前 Bridge 还会再走一次
220
202
  * enforceCardBudget,因此这里不再重复裁剪。
221
203
  */
222
- safeRenderCard() {
204
+ renderCurrentCard() {
223
205
  return this.renderCard(this.state, this.renderOptions);
224
206
  }
225
207
  /**
@@ -227,7 +209,7 @@ export class CardSession {
227
209
  * connector.updateCard if the controller is unavailable or fails.
228
210
  */
229
211
  async updateCard() {
230
- const safeCard = this.safeRenderCard();
212
+ const safeCard = this.renderCurrentCard();
231
213
  if (this.controller) {
232
214
  try {
233
215
  await this.controller.update(safeCard);
@@ -247,6 +229,24 @@ export class CardSession {
247
229
  }
248
230
  }
249
231
  }
232
+ async waitForControllerReady() {
233
+ let timer;
234
+ const timeout = new Promise((_, reject) => {
235
+ timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
236
+ });
237
+ try {
238
+ await Promise.race([this.controllerReady, timeout]);
239
+ }
240
+ finally {
241
+ if (timer)
242
+ clearTimeout(timer);
243
+ }
244
+ }
245
+ async prepareTerminalUpdate() {
246
+ this.cancelPendingFlush();
247
+ if (this.flushP)
248
+ await this.flushP;
249
+ }
250
250
  release() {
251
251
  if (this.released)
252
252
  return;
@@ -1,10 +1,10 @@
1
- import { type AgentKind } from '../runner/index.js';
1
+ import type { AgentKind } from '../agent-kinds.js';
2
2
  /**
3
3
  * Display name for an agent kind in the run card header.
4
4
  *
5
- * Reads from the global `AgentRegistry` (set up in `index.ts` at startup)
6
- * when available. Falls back to a hardcoded map for callers that don't
7
- * wire up the registry (notably unit tests of pure render helpers).
5
+ * Display names are static UI labels, so the constant is the single source.
6
+ * Keeping a second mutable registry here made render helpers depend on startup
7
+ * order and forced tests to reset global state.
8
8
  */
9
9
  export declare function agentDisplayName(kind: string): string;
10
10
  export declare function terminalToLabel(terminal: string): string;
@@ -1,18 +1,12 @@
1
- import { AgentRegistry } from '../runner/index.js';
2
- import { AGENT_DISPLAY_NAMES } from '../runner/types.js';
1
+ import { AGENT_DISPLAY_NAMES } from '../agent-kinds.js';
3
2
  /**
4
3
  * Display name for an agent kind in the run card header.
5
4
  *
6
- * Reads from the global `AgentRegistry` (set up in `index.ts` at startup)
7
- * when available. Falls back to a hardcoded map for callers that don't
8
- * wire up the registry (notably unit tests of pure render helpers).
5
+ * Display names are static UI labels, so the constant is the single source.
6
+ * Keeping a second mutable registry here made render helpers depend on startup
7
+ * order and forced tests to reset global state.
9
8
  */
10
9
  export function agentDisplayName(kind) {
11
- const globalReg = AgentRegistry.getGlobalInstance();
12
- if (globalReg) {
13
- return globalReg.getDisplayName(kind);
14
- }
15
- // Fallback for render helpers that run without the global registry.
16
10
  return AGENT_DISPLAY_NAMES[kind] ?? kind;
17
11
  }
18
12
  const TERMINAL_LABELS = {
@@ -76,8 +76,9 @@ export function markdownDiv(content, textSize) {
76
76
  * to be interpreted as a literal. Escape it to preserve the raw backslash.
77
77
  *
78
78
  * Note: Square brackets [] and pipe | are NOT escaped -- they are valid markdown
79
- * (links, tables) and escaping them breaks link rendering. Verified safe via
80
- * tests/escape-bracket-experiment.test.ts: unescaped []| does not trigger 11311.
79
+ * (links, tables) and escaping them breaks link rendering. Verified by the live
80
+ * regression test tests/feishu-live/escape-bracket-regression.test.ts: unescaped
81
+ * []| does not trigger 11311.
81
82
  */
82
83
  function escapeMarkdown(content) {
83
84
  if (!content)
@@ -0,0 +1 @@
1
+ export declare function splitFinalAnswer(text: string, maxBytes?: number): string[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Split an authoritative final answer into plain-text messages without losing
3
+ * any content. Feishu cards have a hard size budget; a long answer must spill
4
+ * rather than being silently truncated inside the progress card.
5
+ */
6
+ const FINAL_ANSWER_CHUNK_BYTES = 20_000;
7
+ export function splitFinalAnswer(text, maxBytes = FINAL_ANSWER_CHUNK_BYTES) {
8
+ if (!text || maxBytes <= 0)
9
+ return text ? [text] : [];
10
+ const chunks = [];
11
+ let rest = text;
12
+ let remainingBytes = Buffer.byteLength(rest, 'utf8');
13
+ while (remainingBytes > maxBytes) {
14
+ let bytes = 0;
15
+ let end = 0;
16
+ let lastNewline = 0;
17
+ for (const char of rest) {
18
+ const charBytes = Buffer.byteLength(char, 'utf8');
19
+ if (bytes + charBytes > maxBytes)
20
+ break;
21
+ bytes += charBytes;
22
+ end += char.length;
23
+ if (char === '\n')
24
+ lastNewline = end;
25
+ }
26
+ if (end === 0) {
27
+ // A caller can choose a byte limit smaller than one codepoint. Keep one
28
+ // codepoint per chunk rather than looping forever with a zero-length cut.
29
+ const first = Array.from(rest)[0];
30
+ chunks.push(first);
31
+ rest = rest.slice(first.length);
32
+ remainingBytes -= Buffer.byteLength(first, 'utf8');
33
+ continue;
34
+ }
35
+ // Prefer a paragraph/line boundary when it does not waste most of a chunk.
36
+ const cut = lastNewline > 0 && lastNewline >= Math.floor(end * 0.6) ? lastNewline : end;
37
+ const chunk = rest.slice(0, cut);
38
+ chunks.push(chunk);
39
+ rest = rest.slice(cut);
40
+ remainingBytes -= Buffer.byteLength(chunk, 'utf8');
41
+ }
42
+ if (rest)
43
+ chunks.push(rest);
44
+ return chunks;
45
+ }
46
+ //# sourceMappingURL=final-answer-overflow.js.map
@@ -1,6 +1,6 @@
1
1
  import { CardSession, type CardChannel } from './card-session.js';
2
2
  import type { AgentEvent } from '../runner/index.js';
3
- import { type FinishMeta, type RunState, type RunTerminal } from './run-state.js';
3
+ import { type FinishMeta, type RunFinalTerminal, type RunState } from './run-state.js';
4
4
  import { type RunCardRenderOptions } from './run-renderer.js';
5
5
  export type { CardChannel as RunCardChannel };
6
6
  export declare class RunCardSession extends CardSession<RunState, RunCardRenderOptions> {
@@ -20,5 +20,5 @@ export declare class RunCardSession extends CardSession<RunState, RunCardRenderO
20
20
  protected renderCard(state: RunState, options: RunCardRenderOptions): object;
21
21
  protected isFlushableState(): boolean;
22
22
  push(event: AgentEvent): Promise<void>;
23
- finish(terminal: Exclude<RunTerminal, 'running' | 'finalizing'>, meta?: FinishMeta): Promise<void>;
23
+ finish(terminal: RunFinalTerminal, meta?: FinishMeta): Promise<void>;
24
24
  }
@@ -1,5 +1,5 @@
1
1
  import { CardSession } from './card-session.js';
2
- import { createInitialRunState, finishRun, reduceRunState, } from './run-state.js';
2
+ import { createInitialRunState, finishRun, isRunActive, reduceRunState, } from './run-state.js';
3
3
  import { renderRunCard } from './run-renderer.js';
4
4
  export class RunCardSession extends CardSession {
5
5
  constructor(opts) {
@@ -25,7 +25,7 @@ export class RunCardSession extends CardSession {
25
25
  isFlushableState() {
26
26
  // finish() 已转终态(done/error/interrupted/idle_timeout)时不再 patch;
27
27
  // 'finalizing' 是非终态,in-flight/延迟 flush 仍可渲染累积事件。
28
- return this.state.terminal === 'running' || this.state.terminal === 'finalizing';
28
+ return isRunActive(this.state.terminal);
29
29
  }
30
30
  async push(event) {
31
31
  // state 每事件立即 reduce(始终最新),但 updateCard 延迟到合批窗口末尾。
@@ -46,13 +46,8 @@ export class RunCardSession extends CardSession {
46
46
  }
47
47
  async finish(terminal, meta = {}) {
48
48
  // 终态必须即时显示(spec):取消合批窗口,不等延迟 flush 调度。
49
- this.cancelPendingFlush();
50
- // 若有 in-flight flush 正在 await updateCard,必须等它完成后再发终态 patch。
51
- // 否则两条并发 controller.update 无 FIFO 保证,in-flight 的 pre-terminal
52
- // patch 可能在终态 patch 之后 resolve → 用户看到"思考中"终帧。
53
- // await 保证顺序:pre-terminal 先落地 → terminal 后落地 → terminal 胜出。
54
- if (this.flushP)
55
- await this.flushP;
49
+ // 终态 patch 必须排在已开始的 pre-terminal flush 之后。
50
+ await this.prepareTerminalUpdate();
56
51
  this.state = finishRun(this.state, terminal, meta);
57
52
  await this.updateCard();
58
53
  this.release();
@@ -20,6 +20,12 @@ export interface RunCardRenderOptions {
20
20
  */
21
21
  sessionFallbackAvailable?: boolean;
22
22
  }
23
+ export interface RunCardRenderOutcome {
24
+ card: object;
25
+ finalAnswerStatus: 'none' | 'full' | 'overflow';
26
+ /** Full text to deliver outside the card when finalAnswerStatus is overflow. */
27
+ finalAnswerText?: string;
28
+ }
23
29
  type BlockGroup = {
24
30
  kind: 'thinking';
25
31
  content: string;
@@ -73,4 +79,12 @@ export declare function estimateCardBytes(state: RunState, options?: RunCardRend
73
79
  * CardKit 2.0 renderer - simplified for streaming (no tabs)
74
80
  */
75
81
  export declare function renderRunCard(state: RunState, options?: RunCardRenderOptions): object;
82
+ /**
83
+ * Render a run card with metadata the bridge needs to deliver an answer that
84
+ * cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
85
+ * by streaming sessions.
86
+ */
87
+ export declare function renderRunCardDetailed(state: RunState, options?: RunCardRenderOptions): RunCardRenderOutcome;
88
+ /** Exact-match marker used in the card before the answer is sent separately. */
89
+ export declare const FINAL_ANSWER_OVERFLOW_MARKER = "_\u6700\u7EC8\u56DE\u7B54\u8F83\u957F\uFF0C\u5B8C\u6574\u5185\u5BB9\u5DF2\u968F\u540E\u53D1\u9001_";
76
90
  export {};
@@ -1,4 +1,4 @@
1
- import { isRunTerminal, } from './run-state.js';
1
+ import { isRunActive, isRunTerminal, } from './run-state.js';
2
2
  import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, cardButton, agentDisplayName, } from './card-shared.js';
3
3
  import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
4
4
  import { toolBodyMd, toolHeaderText } from './tool-render.js';
@@ -8,7 +8,7 @@ import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
8
8
  import { displayWidth, truncateToDisplayWidth } from '../common/text-width.js';
9
9
  import { isContentFilterFailure } from '../common/turn-failure.js';
10
10
  import { formatClock, formatTimestamp } from './time.js';
11
- import { formatUsageStats } from '../router/usage-format.js';
11
+ import { formatUsageStats } from './usage-format.js';
12
12
  import { renderApprovalArea, renderApprovalAreaCompact } from './approval-render.js';
13
13
  const REASONING_BYTES = 4_500;
14
14
  const TEXT_BYTES = 10_000;
@@ -406,7 +406,7 @@ const EXTREME_TIER = {
406
406
  function actionButtonList(state, options) {
407
407
  const actionButtons = [];
408
408
  // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
409
- const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
409
+ const showStop = isRunActive(state.terminal);
410
410
  if (showStop) {
411
411
  actionButtons.push(stopButton(state.runId));
412
412
  }
@@ -867,6 +867,20 @@ function measurePanelBytes(title, expanded, border, content) {
867
867
  * CardKit 2.0 renderer - simplified for streaming (no tabs)
868
868
  */
869
869
  export function renderRunCard(state, options = {}) {
870
+ return renderRunCardDetailed(state, options).card;
871
+ }
872
+ /**
873
+ * Render a run card with metadata the bridge needs to deliver an answer that
874
+ * cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
875
+ * by streaming sessions.
876
+ */
877
+ export function renderRunCardDetailed(state, options = {}) {
878
+ if (state.finalAnswer?.content.trim()) {
879
+ return buildFinalAnswerPriorityCard(state, options);
880
+ }
881
+ return { card: renderLegacyRunCard(state, options), finalAnswerStatus: 'none' };
882
+ }
883
+ function renderLegacyRunCard(state, options) {
870
884
  // 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
871
885
  // degraded。groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
872
886
  // 避免影子测量 + 正式渲染对每个超限块双倍截断。
@@ -927,6 +941,110 @@ export function renderRunCard(state, options = {}) {
927
941
  };
928
942
  return truncatedSkeleton;
929
943
  }
944
+ /** Exact-match marker used in the card before the answer is sent separately. */
945
+ export const FINAL_ANSWER_OVERFLOW_MARKER = '_最终回答较长,完整内容已随后发送_';
946
+ /**
947
+ * Build a card around the authoritative final answer.
948
+ *
949
+ * The answer is measured first with the fixed controls. Progress groups are
950
+ * added newest-first only while the whole card still fits. If the answer
951
+ * cannot fit even without progress/summary, the card carries an explicit
952
+ * marker and the bridge sends the complete answer separately.
953
+ */
954
+ function buildFinalAnswerPriorityCard(state, options) {
955
+ const finalAnswer = state.finalAnswer;
956
+ const groups = groupBlocks(state.blocks);
957
+ const prepared = prepareGroupContent(groups);
958
+ const safeAnswer = truncateMarkdownTables(finalAnswer.content);
959
+ const requiresOverflow = safeAnswer !== finalAnswer.content;
960
+ const ts = formatTimestamp(finalAnswer.timestamp);
961
+ const answerElements = renderTextBlock(safeAnswer, ts, state.terminal !== 'running');
962
+ const fixedStart = [statusRow(state), ...omittedBlocksHint(state)];
963
+ const summary = buildSummaryContent(state, { includeNotices: false });
964
+ const controls = [...actionRow(state, options), ...approvalArea(state)];
965
+ const buildWithProgress = (start, includeSummary) => {
966
+ const renderedProgress = buildAnswerProgressContent(state, prepared, groups.slice(start));
967
+ const progress = start < groups.length
968
+ ? [markdownDiv(`_💡 已省略 ${start} 个早期步骤_`), ...renderedProgress]
969
+ : renderedProgress;
970
+ return assembleRunCard(state, options, [
971
+ ...fixedStart,
972
+ ...progress,
973
+ ...(requiresOverflow ? [markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER)] : []),
974
+ ...answerElements,
975
+ ...(includeSummary ? summary : []),
976
+ ...controls,
977
+ ]);
978
+ };
979
+ let best = buildWithProgress(groups.length, true);
980
+ if (withinRunCardBudget(best)) {
981
+ for (let start = groups.length - 1; start >= 0; start--) {
982
+ const candidate = buildWithProgress(start, true);
983
+ if (!withinRunCardBudget(candidate))
984
+ break;
985
+ best = candidate;
986
+ }
987
+ return {
988
+ card: best,
989
+ finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
990
+ ...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
991
+ };
992
+ }
993
+ // The answer outranks the usage summary. Keep the answer and controls first,
994
+ // then retry without the summary before declaring a physical overflow.
995
+ const withoutSummary = buildWithProgress(groups.length, false);
996
+ if (withinRunCardBudget(withoutSummary)) {
997
+ return {
998
+ card: withoutSummary,
999
+ finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
1000
+ ...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
1001
+ };
1002
+ }
1003
+ const overflowCard = assembleRunCard(state, options, [
1004
+ ...fixedStart,
1005
+ markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER),
1006
+ ...controls,
1007
+ ]);
1008
+ return {
1009
+ card: overflowCard,
1010
+ finalAnswerStatus: 'overflow',
1011
+ finalAnswerText: finalAnswer.content,
1012
+ };
1013
+ }
1014
+ /** Render a contiguous progress suffix for the answer-priority card. */
1015
+ function buildAnswerProgressContent(state, prepared, visible) {
1016
+ const elements = [];
1017
+ const outputMax = toolOutputBudget(state);
1018
+ const contentItems = mergeNoticesIntoContent(visible, state.notices);
1019
+ for (const item of contentItems) {
1020
+ if (item.kind === 'notice') {
1021
+ elements.push(renderNoticeRow(item.notice));
1022
+ continue;
1023
+ }
1024
+ const group = item;
1025
+ if (group.kind === 'tool') {
1026
+ elements.push(renderTool(group.tool, outputMax));
1027
+ continue;
1028
+ }
1029
+ if (group.kind === 'text') {
1030
+ const content = prepared.get(group) ?? '';
1031
+ const ts = formatTimestamp(group.timestamp);
1032
+ elements.push(...renderTextBlock(content, ts, state.terminal !== 'running'));
1033
+ continue;
1034
+ }
1035
+ const spec = describePanelGroup(group, prepared);
1036
+ if (!spec)
1037
+ continue;
1038
+ elements.push(collapsibleMarkdownPanel({
1039
+ title: spec.title,
1040
+ expanded: spec.expanded,
1041
+ border: spec.border,
1042
+ content: spec.content,
1043
+ textSize: 'notation',
1044
+ }));
1045
+ }
1046
+ return elements;
1047
+ }
930
1048
  /**
931
1049
  * Run 卡片的双预算检查:UTF-8 字节 ≤ 28KB **且** 元素数 ≤ `MAX_CARD_ELEMENTS`。
932
1050
  *
@@ -1037,7 +1155,7 @@ function buildSummaryContent(state, options = {}) {
1037
1155
  }
1038
1156
  else {
1039
1157
  const result = state.resultSubtype ?? 'success';
1040
- const hasContent = state.blocks.length > 0;
1158
+ const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
1041
1159
  // 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
1042
1160
  // 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号
1043
1161
  // (2026-09 用户反馈)。所有会压缩的 agent(codex/claude/kimi/opencode/pi)
@@ -1047,25 +1165,10 @@ function buildSummaryContent(state, options = {}) {
1047
1165
  : state.operationKind === 'compaction'
1048
1166
  ? '\n\n🗜 Compact 完成'
1049
1167
  : '\n\n(未返回内容)';
1050
- const usageStatsStr = formatUsageStats({
1051
- contextLength: state.contextLength,
1052
- contextLimit: state.contextLimit,
1053
- compactCount: state.compactCount,
1054
- compactPreContextLength: state.compactPreContextLength,
1055
- cacheReadTokens: state.cacheReadTokens,
1056
- cacheCreationTokens: state.cacheCreationTokens,
1057
- totalTokens: state.totalTokens,
1058
- inputTokens: state.inputTokens,
1059
- outputTokens: state.outputTokens,
1060
- cumulativeTotalTokens: state.cumulativeTotalTokens,
1061
- cumulativeInputTokens: state.cumulativeInputTokens,
1062
- cumulativeOutputTokens: state.cumulativeOutputTokens,
1063
- cumulativeCacheReadTokens: state.cumulativeCacheReadTokens,
1064
- cumulativeCacheCreationTokens: state.cumulativeCacheCreationTokens,
1065
- model: state.model,
1066
- costUsd: state.costUsd,
1067
- reasoningTokens: state.reasoningTokens,
1068
- }, {
1168
+ // RunState 的用量字段就是 formatUsageStats 的入参形态(同为
1169
+ // `UsageSnapshot`)——原先这里逐字段抄了 17 行,加一个字段要同时改
1170
+ // RunState、FinishMeta 和这张清单(§9.21)。
1171
+ const usageStatsStr = formatUsageStats(state, {
1069
1172
  showResult: true,
1070
1173
  result,
1071
1174
  // 本轮结束但目标继续:不能报"已完成",也不能说"出错"。