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,3 +1,4 @@
1
+ import { keepTail } from '../common/truncate.js';
1
2
  const MAX_REASONING_CHARS = 4_000;
2
3
  const MAX_TEXT_CHARS = 12_000;
3
4
  const MAX_TOOL_DETAIL_CHARS = 3_000;
@@ -64,6 +65,7 @@ export function reduceRunState(state, event) {
64
65
  if (event.type === 'approval_expired') {
65
66
  return {
66
67
  ...state,
68
+ approvalExpired: true,
67
69
  approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, expired: true } : a),
68
70
  };
69
71
  }
@@ -100,14 +102,14 @@ function reduceTurnDiffEvent(state, event) {
100
102
  blocks: keepLatestBlocks(upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
101
103
  kind: 'text',
102
104
  itemId: event.itemId,
103
- content: keepLatest(event.text, MAX_TEXT_CHARS),
105
+ content: keepTail(event.text, MAX_TEXT_CHARS),
104
106
  timestamp: ts,
105
107
  ...(complete ? { completedAt: ts } : {}),
106
108
  }), (existing) => {
107
109
  const textBlock = existing;
108
110
  return {
109
111
  ...textBlock,
110
- content: keepLatest(event.text, MAX_TEXT_CHARS),
112
+ content: keepTail(event.text, MAX_TEXT_CHARS),
111
113
  ...(complete ? { completedAt: ts } : {}),
112
114
  };
113
115
  }, !complete)),
@@ -120,7 +122,7 @@ function reduceTurnDiffEvent(state, event) {
120
122
  blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
121
123
  kind: 'thinking',
122
124
  itemId: event.itemId,
123
- content: keepLatest(event.reasoning, MAX_REASONING_CHARS),
125
+ content: keepTail(event.reasoning, MAX_REASONING_CHARS),
124
126
  active: !complete,
125
127
  timestamp: ts,
126
128
  ...(complete ? { completedAt: ts } : {}),
@@ -128,7 +130,7 @@ function reduceTurnDiffEvent(state, event) {
128
130
  const thinkingBlock = existing;
129
131
  return {
130
132
  ...thinkingBlock,
131
- content: keepLatest(event.reasoning, MAX_REASONING_CHARS),
133
+ content: keepTail(event.reasoning, MAX_REASONING_CHARS),
132
134
  active: !complete,
133
135
  ...(complete ? { completedAt: ts } : {}),
134
136
  };
@@ -145,7 +147,7 @@ function reduceTurnDiffEvent(state, event) {
145
147
  id: event.itemId,
146
148
  name: 'command',
147
149
  input: '',
148
- output: keepLatest(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
150
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
149
151
  status: complete ? (event.toolStatus ?? 'ok') : 'running',
150
152
  startedAt: ts,
151
153
  ...(complete ? { completedAt: ts } : {}),
@@ -154,7 +156,7 @@ function reduceTurnDiffEvent(state, event) {
154
156
  kind: 'tool',
155
157
  tool: {
156
158
  ...existing.tool,
157
- output: keepLatest(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
159
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
158
160
  status: complete
159
161
  ? (event.toolStatus ?? 'ok')
160
162
  : existing.tool.status,
@@ -165,7 +167,7 @@ function reduceTurnDiffEvent(state, event) {
165
167
  };
166
168
  }
167
169
  if (event.plan !== undefined) {
168
- const newPlan = keepLatest(event.plan, MAX_REASONING_CHARS * 2);
170
+ const newPlan = keepTail(event.plan, MAX_REASONING_CHARS * 2);
169
171
  next = {
170
172
  ...next,
171
173
  plan: newPlan,
@@ -298,7 +300,7 @@ function reduceAssistantEvent(state, event) {
298
300
  ...next.blocks.slice(0, -1),
299
301
  {
300
302
  kind: 'thinking',
301
- content: keepLatest(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
303
+ content: keepTail(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
302
304
  active: true,
303
305
  timestamp: last.timestamp ?? event.timestamp,
304
306
  },
@@ -307,7 +309,7 @@ function reduceAssistantEvent(state, event) {
307
309
  ...next.blocks,
308
310
  {
309
311
  kind: 'thinking',
310
- content: keepLatest(content.thinking, MAX_REASONING_CHARS),
312
+ content: keepTail(content.thinking, MAX_REASONING_CHARS),
311
313
  active: true,
312
314
  timestamp: event.timestamp,
313
315
  },
@@ -326,7 +328,7 @@ function reduceAssistantEvent(state, event) {
326
328
  ...blocks.slice(0, -1),
327
329
  {
328
330
  kind: 'text',
329
- content: keepLatest(last.content + content.text, MAX_TEXT_CHARS),
331
+ content: keepTail(last.content + content.text, MAX_TEXT_CHARS),
330
332
  timestamp: last.timestamp ?? event.timestamp,
331
333
  },
332
334
  ]
@@ -334,7 +336,7 @@ function reduceAssistantEvent(state, event) {
334
336
  ...blocks,
335
337
  {
336
338
  kind: 'text',
337
- content: keepLatest(content.text, MAX_TEXT_CHARS),
339
+ content: keepTail(content.text, MAX_TEXT_CHARS),
338
340
  timestamp: event.timestamp,
339
341
  },
340
342
  ];
@@ -412,7 +414,7 @@ function reducePlanEvent(state, event) {
412
414
  if (event.type !== 'plan')
413
415
  return state;
414
416
  const currentPlan = state.plan ?? '';
415
- const newPlan = keepLatest(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
417
+ const newPlan = keepTail(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
416
418
  // Plan is a single evolving document — replace the existing plan block
417
419
  // instead of appending a new one (avoids N overlapping blocks after N events).
418
420
  const withoutOldPlan = state.blocks.filter((b) => b.kind !== 'plan');
@@ -458,12 +460,18 @@ export function finishRun(state, terminal, meta = {}) {
458
460
  return state;
459
461
  return { ...state, ...sanitizeMeta(meta) };
460
462
  }
463
+ // interrupted 终态的中断来源:显式传入优先;否则从顶层持久标记推导
464
+ // (approval_expired 已置位但调用方未传 reason 的路径,如协调器过期自动
465
+ // cancel。原因必须在终态确定,不依赖调用方记得传参)。
466
+ const derivedReason = meta.interruptedReason ??
467
+ (terminal === 'interrupted' && state.approvalExpired === true ? 'approval_timeout' : undefined);
461
468
  return {
462
469
  ...state,
463
470
  terminal,
464
471
  footer: null,
465
472
  blocks: markThinkingInactive(state.blocks),
466
473
  ...sanitizeMeta(meta),
474
+ ...(derivedReason !== undefined ? { interruptedReason: derivedReason } : {}),
467
475
  };
468
476
  }
469
477
  /** Remove undefined/null meta fields to avoid overwriting existing state with nullish values. */
@@ -483,7 +491,11 @@ function stringifyUnknown(value) {
483
491
  if (typeof value === 'string')
484
492
  return value;
485
493
  try {
486
- return JSON.stringify(value);
494
+ // JSON.stringify(undefined) returns the value undefined, which would
495
+ // violate this function's string contract and crash callers that take
496
+ // .length on the result (2026-08-17 kimi ACP tool_call without rawInput
497
+ // → truncateDetail TypeError). Normalize to ''.
498
+ return JSON.stringify(value) ?? '';
487
499
  }
488
500
  catch {
489
501
  return String(value);
@@ -507,9 +519,6 @@ function tryParseRecord(input) {
507
519
  return null;
508
520
  }
509
521
  }
510
- function keepLatest(value, maxChars) {
511
- return value.length <= maxChars ? value : value.slice(-maxChars);
512
- }
513
522
  function keepLatestBlocks(blocks) {
514
523
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
515
524
  }
@@ -1,20 +1,27 @@
1
1
  /**
2
2
  * UTF-8 byte-safe text truncation primitives.
3
3
  *
4
- * Shared by the live run renderer (`run-renderer.ts`), the static card budget
5
- * enforcer (`card-budget.ts`), and the opencode session reader
6
- * (`src/session/opencode/sessions.ts`) so that huge tool outputs are folded at a single
7
- * place instead of being reimplemented per consumer.
4
+ * The core `fitUtf8` / `truncateUtf8` / `DEFAULT_TRUNCATE_SUFFIX` primitives
5
+ * live in `src/common/truncate.ts` (shared with session readers to avoid a
6
+ * card→session layer inversion); this module re-exports them and adds the
7
+ * card-specific markdown-table helpers.
8
8
  *
9
9
  * Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
10
10
  * naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
11
11
  * invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
12
12
  * `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
13
13
  */
14
+ import { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 } from '../common/truncate.js';
15
+ export { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 };
14
16
  /** Feishu 11310 limit: max markdown tables per card. */
15
17
  export declare const FEISHU_MAX_TABLES = 5;
16
- /** Default truncation suffix, shared across all consumers for UX consistency. */
17
- export declare const DEFAULT_TRUNCATE_SUFFIX = "\u2026\uFF08\u5DF2\u622A\u65AD\uFF09";
18
+ /**
19
+ * Feishu 单卡字节上限(安全阈值)。
20
+ *
21
+ * 供 run-renderer(流式卡)、card-budget(静态卡)、bash-renderer(bash 卡)
22
+ * 共用,避免三处各自定义后数值漂移。
23
+ */
24
+ export declare const CARD_BUDGET_BYTES = 28000;
18
25
  /**
19
26
  * Max byte budget for a single session `tool_result` event's content when
20
27
  * replaying an opencode session (L2 pre-fold). The static card budget enforcer
@@ -24,12 +31,6 @@ export declare const DEFAULT_TRUNCATE_SUFFIX = "\u2026\uFF08\u5DF2\u622A\u65AD\u
24
31
  * 500KB file listing) never inflates the replay payload to megabytes.
25
32
  */
26
33
  export declare const TOOL_RESULT_MAX_BYTES = 4000;
27
- /**
28
- * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
29
- * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
30
- * the tail and prepends the suffix (useful for "show the most recent N bytes").
31
- */
32
- export declare function truncateUtf8(value: string, maxBytes: number, fromEnd?: boolean, suffix?: string): string;
33
34
  /**
34
35
  * Count the number of markdown tables in a string.
35
36
  *
@@ -1,20 +1,27 @@
1
1
  /**
2
2
  * UTF-8 byte-safe text truncation primitives.
3
3
  *
4
- * Shared by the live run renderer (`run-renderer.ts`), the static card budget
5
- * enforcer (`card-budget.ts`), and the opencode session reader
6
- * (`src/session/opencode/sessions.ts`) so that huge tool outputs are folded at a single
7
- * place instead of being reimplemented per consumer.
4
+ * The core `fitUtf8` / `truncateUtf8` / `DEFAULT_TRUNCATE_SUFFIX` primitives
5
+ * live in `src/common/truncate.ts` (shared with session readers to avoid a
6
+ * card→session layer inversion); this module re-exports them and adds the
7
+ * card-specific markdown-table helpers.
8
8
  *
9
9
  * Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
10
10
  * naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
11
11
  * invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
12
12
  * `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
13
13
  */
14
+ import { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 } from '../common/truncate.js';
15
+ export { DEFAULT_TRUNCATE_SUFFIX, truncateUtf8 };
14
16
  /** Feishu 11310 limit: max markdown tables per card. */
15
17
  export const FEISHU_MAX_TABLES = 5;
16
- /** Default truncation suffix, shared across all consumers for UX consistency. */
17
- export const DEFAULT_TRUNCATE_SUFFIX = '…(已截断)';
18
+ /**
19
+ * Feishu 单卡字节上限(安全阈值)。
20
+ *
21
+ * 供 run-renderer(流式卡)、card-budget(静态卡)、bash-renderer(bash 卡)
22
+ * 共用,避免三处各自定义后数值漂移。
23
+ */
24
+ export const CARD_BUDGET_BYTES = 28_000;
18
25
  /**
19
26
  * Max byte budget for a single session `tool_result` event's content when
20
27
  * replaying an opencode session (L2 pre-fold). The static card budget enforcer
@@ -24,63 +31,6 @@ export const DEFAULT_TRUNCATE_SUFFIX = '…(已截断)';
24
31
  * 500KB file listing) never inflates the replay payload to megabytes.
25
32
  */
26
33
  export const TOOL_RESULT_MAX_BYTES = 4000;
27
- /**
28
- * Fit a UTF-8 string into `maxBytes` by iterating Unicode codepoints, never
29
- * splitting a multi-byte sequence. When `fromEnd` is true, keeps the TAIL
30
- * (useful for logs/tails); otherwise keeps the HEAD.
31
- *
32
- * P1-2 (2026-08-02): 单遍累计单字符字节数,替代逐字符 `Buffer.byteLength(result +
33
- * char)` + `result += char`(后者每轮新建并扫描不断增长的字符串,O(budget²))。
34
- * 字节长度对拼接可加(byteLength(a+b) = byteLength(a) + byteLength(b)),故累计
35
- * 与旧实现的逐字符判断逐字节等价;最后一次 slice/join 物化结果。混合 CJK/emoji
36
- * 样本实测 ~150× 提速(33.7ms/次 → 0.2ms/次,见 tests/anchor/priority/p0-fit-utf8.test.ts)。
37
- */
38
- function fitUtf8(value, maxBytes, fromEnd = false) {
39
- if (fromEnd) {
40
- // tail 分支:需要从末尾找截断起点,Array.from 一次物化(旧实现同样 Array.from().reverse(),
41
- // 无内存回归);数组仅用于定位,之后 slice+join。
42
- const codepoints = Array.from(value);
43
- let bytes = 0;
44
- let start = codepoints.length;
45
- for (let i = codepoints.length - 1; i >= 0; i--) {
46
- const cb = Buffer.byteLength(codepoints[i], 'utf8');
47
- if (bytes + cb > maxBytes)
48
- break;
49
- bytes += cb;
50
- start = i;
51
- }
52
- return codepoints.slice(start).join('');
53
- }
54
- // head 分支:惰性 for-of 迭代 + 字符串索引累计(旧实现同 O(1) 内存)。
55
- // 不物化数组——opencode-sessions 会对 MB 级 tool_result 做 head 截断,
56
- // Array.from 全量分配会引入不必要的瞬态内存。
57
- let bytes = 0;
58
- let end = 0;
59
- for (const char of value) {
60
- const cb = Buffer.byteLength(char, 'utf8');
61
- if (bytes + cb > maxBytes)
62
- break;
63
- bytes += cb;
64
- end += char.length; // for-of 产出完整码点;char.length 为 1 或 2(代理对),
65
- // end 落在码点边界上,value.slice(0, end) 不会切分代理对
66
- }
67
- return value.slice(0, end);
68
- }
69
- /**
70
- * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
71
- * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
72
- * the tail and prepends the suffix (useful for "show the most recent N bytes").
73
- */
74
- export function truncateUtf8(value, maxBytes, fromEnd = false, suffix = DEFAULT_TRUNCATE_SUFFIX) {
75
- if (Buffer.byteLength(value, 'utf8') <= maxBytes)
76
- return value;
77
- const suffixBytes = Buffer.byteLength(suffix, 'utf8');
78
- const budget = maxBytes - suffixBytes;
79
- if (budget <= 0)
80
- return suffix;
81
- const truncated = fitUtf8(value, budget, fromEnd);
82
- return fromEnd ? `${suffix}\n${truncated}` : `${truncated}${suffix}`;
83
- }
84
34
  /**
85
35
  * Count the number of markdown tables in a string.
86
36
  *
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Unlink a file, silently ignoring ENOENT and other errors.
3
+ *
4
+ * Use for cleanup paths (pid files, temp files, stale locks) where the
5
+ * file may already be gone or was never created — a missing file is not
6
+ * an error worth propagating.
7
+ */
8
+ export declare function silentlyUnlink(path: string): void;
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';
2
+ /**
3
+ * Unlink a file, silently ignoring ENOENT and other errors.
4
+ *
5
+ * Use for cleanup paths (pid files, temp files, stale locks) where the
6
+ * file may already be gone or was never created — a missing file is not
7
+ * an error worth propagating.
8
+ */
9
+ export function silentlyUnlink(path) {
10
+ try {
11
+ fs.unlinkSync(path);
12
+ }
13
+ catch {
14
+ /* ignore — file may not exist */
15
+ }
16
+ }
17
+ //# sourceMappingURL=fs.js.map
@@ -11,27 +11,7 @@
11
11
  * Used as a prerequisite before accessing string-keyed properties.
12
12
  */
13
13
  export declare function isRecord(v: unknown): v is Record<string, unknown>;
14
- /**
15
- * Narrows `v` to `Record<string, unknown>` when it is a non-null object,
16
- * otherwise returns `undefined`.
17
- */
18
- export declare function recordValue(v: unknown): Record<string, unknown> | undefined;
19
14
  /**
20
15
  * Returns the string value when `v` is a `string`, otherwise `undefined`.
21
16
  */
22
17
  export declare function stringValue(v: unknown): string | undefined;
23
- /**
24
- * Returns the number value when `v` is a `number`, otherwise `undefined`.
25
- */
26
- export declare function numberValue(v: unknown): number | undefined;
27
- /**
28
- * Extracts a human-readable error message from an opaque error object.
29
- *
30
- * Priority:
31
- * 1. `raw.message` (string)
32
- * 2. `raw.error.message` (string, when `raw.error` is a record)
33
- * 3. `raw.error.data.message` (string, AI SDK wraps provider errors as {error:{data:{message}}})
34
- * 4. `raw.error` itself (when it is a string)
35
- * 5. `fallback`
36
- */
37
- export declare function extractErrorMessage(raw: Record<string, unknown>, fallback: string): string;
@@ -13,53 +13,10 @@
13
13
  export function isRecord(v) {
14
14
  return typeof v === 'object' && v !== null;
15
15
  }
16
- /**
17
- * Narrows `v` to `Record<string, unknown>` when it is a non-null object,
18
- * otherwise returns `undefined`.
19
- */
20
- export function recordValue(v) {
21
- return isRecord(v) ? v : undefined;
22
- }
23
16
  /**
24
17
  * Returns the string value when `v` is a `string`, otherwise `undefined`.
25
18
  */
26
19
  export function stringValue(v) {
27
20
  return typeof v === 'string' ? v : undefined;
28
21
  }
29
- /**
30
- * Returns the number value when `v` is a `number`, otherwise `undefined`.
31
- */
32
- export function numberValue(v) {
33
- return typeof v === 'number' ? v : undefined;
34
- }
35
- /**
36
- * Extracts a human-readable error message from an opaque error object.
37
- *
38
- * Priority:
39
- * 1. `raw.message` (string)
40
- * 2. `raw.error.message` (string, when `raw.error` is a record)
41
- * 3. `raw.error.data.message` (string, AI SDK wraps provider errors as {error:{data:{message}}})
42
- * 4. `raw.error` itself (when it is a string)
43
- * 5. `fallback`
44
- */
45
- export function extractErrorMessage(raw, fallback) {
46
- const msg = stringValue(raw.message);
47
- if (msg !== undefined)
48
- return msg;
49
- const err = raw.error;
50
- if (isRecord(err)) {
51
- const subMsg = stringValue(err.message);
52
- if (subMsg !== undefined)
53
- return subMsg;
54
- const data = recordValue(err.data);
55
- if (data) {
56
- const dataMsg = stringValue(data.message);
57
- if (dataMsg !== undefined)
58
- return dataMsg;
59
- }
60
- }
61
- if (typeof err === 'string')
62
- return err;
63
- return fallback;
64
- }
65
22
  //# sourceMappingURL=guards.js.map
@@ -0,0 +1,2 @@
1
+ /** Wait for `ms` milliseconds. */
2
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,5 @@
1
+ /** Wait for `ms` milliseconds. */
2
+ export function sleep(ms) {
3
+ return new Promise((resolve) => setTimeout(resolve, ms));
4
+ }
5
+ //# sourceMappingURL=sleep.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared text truncation primitives.
3
+ *
4
+ * Centralizes the two truncation utilities previously split across the card
5
+ * layer (`card-shared.ts` char-based `truncate` and `card/text-truncate.ts`
6
+ * byte-safe `truncateUtf8`). Session readers import from here instead of the
7
+ * card layer (fixing the layer inversion), and the card layer re-exports for
8
+ * backward compatibility.
9
+ */
10
+ /** Default truncation suffix, shared across all consumers for UX consistency. */
11
+ export declare const DEFAULT_TRUNCATE_SUFFIX = "\u2026\uFF08\u5DF2\u622A\u65AD\uFF09";
12
+ /**
13
+ * Max byte budget for a single session `tool_result` event's content when
14
+ * replaying an opencode session (L2 pre-fold). Pre-folding here bounds the
15
+ * in-memory `events[]` array and the intermediate card JSON so a single
16
+ * pathological tool_result never inflates the replay payload to megabytes.
17
+ */
18
+ export declare const TOOL_RESULT_MAX_BYTES = 4000;
19
+ /**
20
+ * Fit a UTF-8 string into `maxBytes` by iterating Unicode codepoints, never
21
+ * splitting a multi-byte sequence. When `fromEnd` is true, keeps the TAIL
22
+ * (useful for logs/tails); otherwise keeps the HEAD.
23
+ */
24
+ export declare function fitUtf8(value: string, maxBytes: number, fromEnd?: boolean): string;
25
+ /**
26
+ * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
27
+ * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
28
+ * the tail and prepends the suffix (useful for "show the most recent N bytes").
29
+ */
30
+ export declare function truncateUtf8(value: string, maxBytes: number, fromEnd?: boolean, suffix?: string): string;
31
+ interface TruncateOptions {
32
+ suffix?: string;
33
+ normalizeWhitespace?: boolean;
34
+ }
35
+ /** Char-based truncation with optional suffix / whitespace normalization. */
36
+ export declare function truncate(str: string, max: number, options?: TruncateOptions): string;
37
+ /** Char-level tail keep (used for store-time caps, e.g. bash output). */
38
+ export declare function keepTail(value: string, maxChars: number): string;
39
+ export {};
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared text truncation primitives.
3
+ *
4
+ * Centralizes the two truncation utilities previously split across the card
5
+ * layer (`card-shared.ts` char-based `truncate` and `card/text-truncate.ts`
6
+ * byte-safe `truncateUtf8`). Session readers import from here instead of the
7
+ * card layer (fixing the layer inversion), and the card layer re-exports for
8
+ * backward compatibility.
9
+ */
10
+ /** Default truncation suffix, shared across all consumers for UX consistency. */
11
+ export const DEFAULT_TRUNCATE_SUFFIX = '…(已截断)';
12
+ /**
13
+ * Max byte budget for a single session `tool_result` event's content when
14
+ * replaying an opencode session (L2 pre-fold). Pre-folding here bounds the
15
+ * in-memory `events[]` array and the intermediate card JSON so a single
16
+ * pathological tool_result never inflates the replay payload to megabytes.
17
+ */
18
+ export const TOOL_RESULT_MAX_BYTES = 4000;
19
+ /**
20
+ * Fit a UTF-8 string into `maxBytes` by iterating Unicode codepoints, never
21
+ * splitting a multi-byte sequence. When `fromEnd` is true, keeps the TAIL
22
+ * (useful for logs/tails); otherwise keeps the HEAD.
23
+ */
24
+ export function fitUtf8(value, maxBytes, fromEnd = false) {
25
+ if (fromEnd) {
26
+ const codepoints = Array.from(value);
27
+ let bytes = 0;
28
+ let start = codepoints.length;
29
+ for (let i = codepoints.length - 1; i >= 0; i--) {
30
+ const cb = Buffer.byteLength(codepoints[i], 'utf8');
31
+ if (bytes + cb > maxBytes)
32
+ break;
33
+ bytes += cb;
34
+ start = i;
35
+ }
36
+ return codepoints.slice(start).join('');
37
+ }
38
+ // head 分支:惰性 for-of 迭代 + 字符串索引累计(O(1) 内存)。
39
+ let bytes = 0;
40
+ let end = 0;
41
+ for (const char of value) {
42
+ const cb = Buffer.byteLength(char, 'utf8');
43
+ if (bytes + cb > maxBytes)
44
+ break;
45
+ bytes += cb;
46
+ end += char.length;
47
+ }
48
+ return value.slice(0, end);
49
+ }
50
+ /**
51
+ * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
52
+ * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
53
+ * the tail and prepends the suffix (useful for "show the most recent N bytes").
54
+ */
55
+ export function truncateUtf8(value, maxBytes, fromEnd = false, suffix = DEFAULT_TRUNCATE_SUFFIX) {
56
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
57
+ return value;
58
+ const suffixBytes = Buffer.byteLength(suffix, 'utf8');
59
+ const budget = maxBytes - suffixBytes;
60
+ if (budget <= 0)
61
+ return suffix;
62
+ const truncated = fitUtf8(value, budget, fromEnd);
63
+ return fromEnd ? `${suffix}\n${truncated}` : `${truncated}${suffix}`;
64
+ }
65
+ /** Char-based truncation with optional suffix / whitespace normalization. */
66
+ export function truncate(str, max, options) {
67
+ const suffix = options?.suffix ?? '…';
68
+ const normalize = options?.normalizeWhitespace ?? false;
69
+ let s = str;
70
+ if (normalize)
71
+ s = s.replace(/\s+/g, ' ').trim();
72
+ if (s.length <= max)
73
+ return s;
74
+ const suffixLen = suffix.length;
75
+ const budget = max - suffixLen;
76
+ if (budget <= 0)
77
+ return suffix;
78
+ // Iterate by Unicode codepoint (for...of) so the cut never lands inside a
79
+ // surrogate pair (P2-31 anchor: must not emit a lone surrogate).
80
+ let end = 0;
81
+ for (const ch of s) {
82
+ if (end + ch.length > budget)
83
+ break;
84
+ end += ch.length; // 1 (BMP) or 2 (surrogate pair) — slice lands on a boundary
85
+ }
86
+ return s.slice(0, end) + suffix;
87
+ }
88
+ /** Char-level tail keep (used for store-time caps, e.g. bash output). */
89
+ export function keepTail(value, maxChars) {
90
+ return value.length <= maxChars ? value : value.slice(-maxChars);
91
+ }
92
+ //# sourceMappingURL=truncate.js.map
@@ -80,7 +80,7 @@ export declare function getCodexCatalogModels(codexHome?: string): BundledModelI
80
80
  /**
81
81
  * Test utility: clear the catalog cache. Tests that exercise the bundled
82
82
  * command path (`--bundled`) directly use `invalidateCodexBundledTestCache`
83
- * from `codex-bundled-test-helpers.ts`.
83
+ * from `tests/lib/codex-bundled-test-helpers.ts`.
84
84
  */
85
85
  export declare function invalidateCodexBundledCache(): void;
86
86
  /**
@@ -208,7 +208,7 @@ export function getCodexCatalogModels(codexHome) {
208
208
  /**
209
209
  * Test utility: clear the catalog cache. Tests that exercise the bundled
210
210
  * command path (`--bundled`) directly use `invalidateCodexBundledTestCache`
211
- * from `codex-bundled-test-helpers.ts`.
211
+ * from `tests/lib/codex-bundled-test-helpers.ts`.
212
212
  */
213
213
  export function invalidateCodexBundledCache() {
214
214
  catalogCache = null;