lark-remote 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  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 +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  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/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  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 +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. 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;
@@ -32,8 +33,205 @@ export function reduceRunState(state, event) {
32
33
  return reducePlanEvent(state, event);
33
34
  if (event.type === 'file_change')
34
35
  return reduceFileChangeEvent(state, event);
36
+ if (event.type === 'turn_started')
37
+ return reduceTurnStartedEvent(state, event);
38
+ if (event.type === 'turn_diff')
39
+ return reduceTurnDiffEvent(state, event);
40
+ if (event.type === 'approval_requested') {
41
+ const approvals = state.approvals ?? [];
42
+ const next = { view: event.view, expired: false };
43
+ const idx = approvals.findIndex((a) => a.view.requestId === event.requestId);
44
+ if (idx >= 0) {
45
+ // 同一 requestId 重复到达:原地替换,保持槽位顺序。
46
+ return {
47
+ ...state,
48
+ approvals: approvals.map((a, i) => (i === idx ? next : a)),
49
+ };
50
+ }
51
+ return { ...state, approvals: [...approvals, next] };
52
+ }
53
+ if (event.type === 'approval_resolved') {
54
+ return {
55
+ ...state,
56
+ approvals: (state.approvals ?? []).filter((a) => a.view.requestId !== event.requestId),
57
+ };
58
+ }
59
+ if (event.type === 'approval_view_updated') {
60
+ return {
61
+ ...state,
62
+ approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, view: event.view } : a),
63
+ };
64
+ }
65
+ if (event.type === 'approval_expired') {
66
+ return {
67
+ ...state,
68
+ approvalExpired: true,
69
+ approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, expired: true } : a),
70
+ };
71
+ }
35
72
  return state;
36
73
  }
74
+ /** Handle app-server turn_started — records the operation kind (turn vs compact). */
75
+ function reduceTurnStartedEvent(state, event) {
76
+ if (event.type !== 'turn_started')
77
+ return state;
78
+ return {
79
+ ...state,
80
+ operationKind: event.operationKind,
81
+ footer: state.footer ?? 'thinking',
82
+ };
83
+ }
84
+ /**
85
+ * Handle app-server turn_diff events. The translator emits FULL per-item
86
+ * snapshots (accumulated text/reasoning/tool output/plan, current file-change
87
+ * set). Each snapshot is scoped to a thread item (event.itemId); the block is
88
+ * replaced in place to avoid duplication, and a NEW block is appended on the
89
+ * item's first sight — so interleaved items keep their real chronology and
90
+ * the active streaming content sits at the bottom, like the exec path.
91
+ */
92
+ function reduceTurnDiffEvent(state, event) {
93
+ if (event.type !== 'turn_diff')
94
+ return state;
95
+ let next = state;
96
+ const ts = event.timestamp;
97
+ const complete = event.complete === true;
98
+ if (event.text !== undefined) {
99
+ const blocks = markThinkingInactive(next.blocks);
100
+ next = {
101
+ ...next,
102
+ blocks: keepLatestBlocks(upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
103
+ kind: 'text',
104
+ itemId: event.itemId,
105
+ content: keepTail(event.text, MAX_TEXT_CHARS),
106
+ timestamp: ts,
107
+ ...(complete ? { completedAt: ts } : {}),
108
+ }), (existing) => {
109
+ const textBlock = existing;
110
+ return {
111
+ ...textBlock,
112
+ content: keepTail(event.text, MAX_TEXT_CHARS),
113
+ ...(complete ? { completedAt: ts } : {}),
114
+ };
115
+ }, !complete)),
116
+ footer: 'streaming',
117
+ };
118
+ }
119
+ if (event.reasoning !== undefined) {
120
+ next = {
121
+ ...next,
122
+ blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
123
+ kind: 'thinking',
124
+ itemId: event.itemId,
125
+ content: keepTail(event.reasoning, MAX_REASONING_CHARS),
126
+ active: !complete,
127
+ timestamp: ts,
128
+ ...(complete ? { completedAt: ts } : {}),
129
+ }), (existing) => {
130
+ const thinkingBlock = existing;
131
+ return {
132
+ ...thinkingBlock,
133
+ content: keepTail(event.reasoning, MAX_REASONING_CHARS),
134
+ active: !complete,
135
+ ...(complete ? { completedAt: ts } : {}),
136
+ };
137
+ }, !complete)),
138
+ footer: 'thinking',
139
+ };
140
+ }
141
+ if (event.toolOutput !== undefined) {
142
+ next = {
143
+ ...next,
144
+ blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => ({
145
+ kind: 'tool',
146
+ tool: {
147
+ id: event.itemId,
148
+ name: 'command',
149
+ input: '',
150
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
151
+ status: complete ? (event.toolStatus ?? 'ok') : 'running',
152
+ startedAt: ts,
153
+ ...(complete ? { completedAt: ts } : {}),
154
+ },
155
+ }), (existing) => ({
156
+ kind: 'tool',
157
+ tool: {
158
+ ...existing.tool,
159
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
160
+ status: complete
161
+ ? (event.toolStatus ?? 'ok')
162
+ : existing.tool.status,
163
+ ...(complete ? { completedAt: ts } : {}),
164
+ },
165
+ }), !complete)),
166
+ footer: 'tool_running',
167
+ };
168
+ }
169
+ if (event.plan !== undefined) {
170
+ const newPlan = keepTail(event.plan, MAX_REASONING_CHARS * 2);
171
+ next = {
172
+ ...next,
173
+ plan: newPlan,
174
+ blocks: keepLatestBlocks(upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
175
+ kind: 'plan',
176
+ itemId: event.itemId,
177
+ content: newPlan,
178
+ active: !complete,
179
+ timestamp: ts,
180
+ ...(complete ? { completedAt: ts } : {}),
181
+ }), (existing) => {
182
+ const planBlock = existing;
183
+ return {
184
+ ...planBlock,
185
+ content: newPlan,
186
+ active: !complete,
187
+ ...(complete ? { completedAt: ts } : {}),
188
+ };
189
+ }, !complete)),
190
+ };
191
+ }
192
+ if (event.fileChanges !== undefined) {
193
+ let blocks = next.blocks;
194
+ for (const fc of event.fileChanges) {
195
+ const fileBlock = {
196
+ kind: 'file_change',
197
+ itemId: event.itemId,
198
+ path: fc.path,
199
+ operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
200
+ diff: fc.diff,
201
+ timestamp: ts,
202
+ ...(complete ? { completedAt: ts } : {}),
203
+ };
204
+ // 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
205
+ blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
206
+ }
207
+ next = { ...next, blocks: keepLatestBlocks(blocks) };
208
+ }
209
+ return next;
210
+ }
211
+ /**
212
+ * Replace the last block matching `predicate` with `make`; if none matches,
213
+ * append a block built by `fallback`. Snapshot semantics for app-server
214
+ * turn_diff streams.
215
+ *
216
+ * `moveToEndWhenNotLast`: 当命中块不在数组末尾且为流式更新(complete=false)
217
+ * 时,把更新后的块移到末尾。正常协议下 item 串行、流式块总是最后一个,
218
+ * 此分支不触发;若真实 server 在同一个 item 上跨其他 item 续流 delta(违反
219
+ * item 生命周期),流式内容仍会出现在卡片底部,而不是钉在旧位置。
220
+ */
221
+ function upsertSnapshotBlock(blocks, predicate, fallback, update, moveToEndWhenNotLast = false) {
222
+ for (let i = blocks.length - 1; i >= 0; i--) {
223
+ if (predicate(blocks[i])) {
224
+ const copy = blocks.slice();
225
+ copy[i] = update ? update(blocks[i]) : fallback();
226
+ if (moveToEndWhenNotLast && i < copy.length - 1) {
227
+ const [moved] = copy.splice(i, 1);
228
+ copy.push(moved);
229
+ }
230
+ return copy;
231
+ }
232
+ }
233
+ return [...blocks, fallback()];
234
+ }
37
235
  /** Handle system init and compact_boundary events. */
38
236
  function reduceSystemEvent(state, event) {
39
237
  if (event.type !== 'system')
@@ -71,8 +269,13 @@ function reduceResultEvent(state, event) {
71
269
  const resultEvent = event;
72
270
  const incomingSubtype = event.subtype;
73
271
  const incomingErrorMsg = incomingSubtype === 'error' ? (resultEvent.errorMessage ?? 'Agent 返回错误结果') : undefined;
74
- // 合并 subtype:error 优先(任一为 error 即 error)
75
- const mergedSubtype = state.resultSubtype === 'error' || incomingSubtype === 'error' ? 'error' : 'success';
272
+ // 合并 subtype:error 优先,其次 interrupted(任一为 error 即 error;
273
+ // 否则任一为 interrupted 即 interrupted;interrupted 不得并入 success)。
274
+ const mergedSubtype = state.resultSubtype === 'error' || incomingSubtype === 'error'
275
+ ? 'error'
276
+ : state.resultSubtype === 'interrupted' || incomingSubtype === 'interrupted'
277
+ ? 'interrupted'
278
+ : 'success';
76
279
  // 合并 errorMsg:首个非 undefined 胜出(existing ?? incoming)
77
280
  const mergedErrorMsg = state.errorMsg ?? incomingErrorMsg;
78
281
  return {
@@ -97,7 +300,7 @@ function reduceAssistantEvent(state, event) {
97
300
  ...next.blocks.slice(0, -1),
98
301
  {
99
302
  kind: 'thinking',
100
- content: keepLatest(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
303
+ content: keepTail(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
101
304
  active: true,
102
305
  timestamp: last.timestamp ?? event.timestamp,
103
306
  },
@@ -106,7 +309,7 @@ function reduceAssistantEvent(state, event) {
106
309
  ...next.blocks,
107
310
  {
108
311
  kind: 'thinking',
109
- content: keepLatest(content.thinking, MAX_REASONING_CHARS),
312
+ content: keepTail(content.thinking, MAX_REASONING_CHARS),
110
313
  active: true,
111
314
  timestamp: event.timestamp,
112
315
  },
@@ -125,7 +328,7 @@ function reduceAssistantEvent(state, event) {
125
328
  ...blocks.slice(0, -1),
126
329
  {
127
330
  kind: 'text',
128
- content: keepLatest(last.content + content.text, MAX_TEXT_CHARS),
331
+ content: keepTail(last.content + content.text, MAX_TEXT_CHARS),
129
332
  timestamp: last.timestamp ?? event.timestamp,
130
333
  },
131
334
  ]
@@ -133,7 +336,7 @@ function reduceAssistantEvent(state, event) {
133
336
  ...blocks,
134
337
  {
135
338
  kind: 'text',
136
- content: keepLatest(content.text, MAX_TEXT_CHARS),
339
+ content: keepTail(content.text, MAX_TEXT_CHARS),
137
340
  timestamp: event.timestamp,
138
341
  },
139
342
  ];
@@ -211,7 +414,7 @@ function reducePlanEvent(state, event) {
211
414
  if (event.type !== 'plan')
212
415
  return state;
213
416
  const currentPlan = state.plan ?? '';
214
- const newPlan = keepLatest(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
417
+ const newPlan = keepTail(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
215
418
  // Plan is a single evolving document — replace the existing plan block
216
419
  // instead of appending a new one (avoids N overlapping blocks after N events).
217
420
  const withoutOldPlan = state.blocks.filter((b) => b.kind !== 'plan');
@@ -257,12 +460,18 @@ export function finishRun(state, terminal, meta = {}) {
257
460
  return state;
258
461
  return { ...state, ...sanitizeMeta(meta) };
259
462
  }
463
+ // interrupted 终态的中断来源:显式传入优先;否则从顶层持久标记推导
464
+ // (approval_expired 已置位但调用方未传 reason 的路径,如协调器过期自动
465
+ // cancel。原因必须在终态确定,不依赖调用方记得传参)。
466
+ const derivedReason = meta.interruptedReason ??
467
+ (terminal === 'interrupted' && state.approvalExpired === true ? 'approval_timeout' : undefined);
260
468
  return {
261
469
  ...state,
262
470
  terminal,
263
471
  footer: null,
264
472
  blocks: markThinkingInactive(state.blocks),
265
473
  ...sanitizeMeta(meta),
474
+ ...(derivedReason !== undefined ? { interruptedReason: derivedReason } : {}),
266
475
  };
267
476
  }
268
477
  /** Remove undefined/null meta fields to avoid overwriting existing state with nullish values. */
@@ -282,7 +491,11 @@ function stringifyUnknown(value) {
282
491
  if (typeof value === 'string')
283
492
  return value;
284
493
  try {
285
- 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) ?? '';
286
499
  }
287
500
  catch {
288
501
  return String(value);
@@ -306,9 +519,6 @@ function tryParseRecord(input) {
306
519
  return null;
307
520
  }
308
521
  }
309
- function keepLatest(value, maxChars) {
310
- return value.length <= maxChars ? value : value.slice(-maxChars);
311
- }
312
522
  function keepLatestBlocks(blocks) {
313
523
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
314
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 {};