lark-remote 0.5.19 → 0.5.21

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 (136) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bridge/approval-actions.d.ts +1 -1
  3. package/dist/bridge/approval-actions.js +1 -1
  4. package/dist/bridge/bash-run.d.ts +71 -0
  5. package/dist/bridge/bash-run.js +217 -0
  6. package/dist/bridge/compact.d.ts +155 -0
  7. package/dist/bridge/compact.js +450 -0
  8. package/dist/bridge/final-usage.js +4 -4
  9. package/dist/bridge/inbound-media.js +72 -46
  10. package/dist/bridge/index.d.ts +106 -153
  11. package/dist/bridge/index.js +400 -1000
  12. package/dist/bridge/job-watch.d.ts +68 -0
  13. package/dist/bridge/job-watch.js +146 -0
  14. package/dist/bridge/queue-manager.d.ts +3 -3
  15. package/dist/bridge/queue-manager.js +10 -29
  16. package/dist/card/approval-render.d.ts +2 -2
  17. package/dist/card/approval-render.js +141 -130
  18. package/dist/card/card-budget.js +123 -98
  19. package/dist/card/card-helpers.d.ts +3 -3
  20. package/dist/card/card-helpers.js +6 -6
  21. package/dist/card/final-answer-file.js +1 -1
  22. package/dist/card/run-renderer.d.ts +3 -3
  23. package/dist/card/run-renderer.js +114 -159
  24. package/dist/card/run-state.d.ts +1 -15
  25. package/dist/card/run-state.js +358 -292
  26. package/dist/card/tool-render.d.ts +1 -1
  27. package/dist/card/tool-render.js +21 -17
  28. package/dist/card/usage-format.d.ts +22 -7
  29. package/dist/card/usage-format.js +119 -100
  30. package/dist/cli.js +15 -3
  31. package/dist/clone.js +2 -2
  32. package/dist/common/text-width.js +36 -24
  33. package/dist/common/tool-summary.js +132 -90
  34. package/dist/config/codex-config.js +130 -106
  35. package/dist/config/index.d.ts +16 -2
  36. package/dist/config/index.js +23 -8
  37. package/dist/connector/index.js +6 -6
  38. package/dist/error-classification.js +3 -2
  39. package/dist/inbound/placeholder.d.ts +1 -1
  40. package/dist/inbound/placeholder.js +1 -1
  41. package/dist/index.js +387 -281
  42. package/dist/logger/index.js +6 -6
  43. package/dist/message-handlers.d.ts +0 -5
  44. package/dist/message-handlers.js +254 -237
  45. package/dist/model-catalog.d.ts +2 -2
  46. package/dist/model-catalog.js +3 -8
  47. package/dist/platform/identity.js +1 -1
  48. package/dist/platform/path.d.ts +1 -1
  49. package/dist/platform/path.js +1 -1
  50. package/dist/platform/sleep-blocker.js +1 -1
  51. package/dist/platform/terminator-win32.js +2 -1
  52. package/dist/router/commands/ls.d.ts +3 -0
  53. package/dist/router/commands/ls.js +256 -175
  54. package/dist/router/commands/order.js +184 -159
  55. package/dist/router/commands/resume.js +190 -152
  56. package/dist/router/commands/ws.js +219 -174
  57. package/dist/router/config/dsh.d.ts +4 -8
  58. package/dist/router/config/dsh.js +3 -10
  59. package/dist/router/config/kimi.js +1 -1
  60. package/dist/router/config/types.d.ts +1 -1
  61. package/dist/router/config-diff.js +1 -1
  62. package/dist/router/index.d.ts +56 -11
  63. package/dist/router/index.js +401 -391
  64. package/dist/runner/bash/runner.js +2 -4
  65. package/dist/runner/claude/session.d.ts +1 -1
  66. package/dist/runner/claude/session.js +6 -4
  67. package/dist/runner/codex/app-server/runner.d.ts +9 -2
  68. package/dist/runner/codex/app-server/runner.js +30 -25
  69. package/dist/runner/codex/app-server/translator.d.ts +2 -5
  70. package/dist/runner/codex/app-server/translator.js +291 -238
  71. package/dist/runner/common/acp/acp-approval.d.ts +1 -1
  72. package/dist/runner/common/acp/acp-approval.js +1 -1
  73. package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
  74. package/dist/runner/common/acp/base-acp-runner.js +26 -65
  75. package/dist/runner/common/acp/base-translator.d.ts +12 -2
  76. package/dist/runner/common/acp/base-translator.js +50 -26
  77. package/dist/runner/common/acp/protocol-types.d.ts +1 -1
  78. package/dist/runner/common/acp/protocol-types.js +2 -2
  79. package/dist/runner/common/connection-based-runner.d.ts +32 -1
  80. package/dist/runner/common/connection-based-runner.js +32 -2
  81. package/dist/runner/common/jsonl-stream.js +154 -187
  82. package/dist/runner/common/jsonrpc/client.d.ts +29 -0
  83. package/dist/runner/common/jsonrpc/client.js +43 -12
  84. package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
  85. package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
  86. package/dist/runner/common/spawning-runner.d.ts +6 -1
  87. package/dist/runner/common/spawning-runner.js +9 -4
  88. package/dist/runner/common/turn-started.d.ts +8 -0
  89. package/dist/runner/common/turn-started.js +11 -0
  90. package/dist/runner/dsh/approval.js +84 -51
  91. package/dist/runner/dsh/catalog-probe.d.ts +17 -0
  92. package/dist/runner/dsh/catalog-probe.js +10 -0
  93. package/dist/runner/dsh/client.d.ts +7 -11
  94. package/dist/runner/dsh/client.js +356 -289
  95. package/dist/runner/dsh/exit-cancel.d.ts +2 -2
  96. package/dist/runner/dsh/exit-cancel.js +2 -2
  97. package/dist/runner/dsh/inflight.d.ts +1 -1
  98. package/dist/runner/dsh/inflight.js +1 -1
  99. package/dist/runner/dsh/mux-stream.js +28 -8
  100. package/dist/runner/dsh/runner.d.ts +44 -4
  101. package/dist/runner/dsh/runner.js +296 -199
  102. package/dist/runner/dsh/translator.d.ts +1 -31
  103. package/dist/runner/dsh/translator.js +180 -178
  104. package/dist/runner/index.d.ts +1 -1
  105. package/dist/runner/kimi/acp/runner.d.ts +1 -2
  106. package/dist/runner/kimi/acp/runner.js +82 -46
  107. package/dist/runner/kimi/acp/translator.d.ts +1 -1
  108. package/dist/runner/kimi/acp/translator.js +3 -4
  109. package/dist/runner/opencode/acp/error-monitor.js +5 -5
  110. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  111. package/dist/runner/opencode/acp/runner.js +4 -4
  112. package/dist/runner/pi/rpc/client.js +4 -13
  113. package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
  114. package/dist/runner/pi/rpc/protocol-types.js +1 -1
  115. package/dist/runner/pi/rpc/runner.d.ts +3 -0
  116. package/dist/runner/pi/rpc/runner.js +27 -42
  117. package/dist/runner/pi/rpc/translator.js +61 -60
  118. package/dist/runner/types.d.ts +2 -2
  119. package/dist/runner/types.js +1 -1
  120. package/dist/session/claude/sessions.d.ts +45 -0
  121. package/dist/session/claude/sessions.js +160 -122
  122. package/dist/session/codex/rollout-reader.js +181 -182
  123. package/dist/session/common/throw-session-list-error.d.ts +10 -0
  124. package/dist/session/common/throw-session-list-error.js +16 -0
  125. package/dist/session/dsh/sessions.js +291 -235
  126. package/dist/session/kimi/sessions.d.ts +40 -8
  127. package/dist/session/kimi/sessions.js +373 -301
  128. package/dist/session/opencode/sessions.d.ts +142 -1
  129. package/dist/session/opencode/sessions.js +378 -321
  130. package/dist/session/pi/sessions.js +4 -5
  131. package/dist/session/session-store.d.ts +9 -1
  132. package/dist/session/session-store.js +99 -99
  133. package/dist/update/version-check.js +2 -1
  134. package/dist/workspace/index.d.ts +1 -1
  135. package/dist/workspace/index.js +1 -1
  136. package/package.json +1 -1
@@ -51,239 +51,312 @@ export function reduceRunState(state, event) {
51
51
  const blocks = capStaleTextBlocks(next.blocks);
52
52
  return blocks === next.blocks ? next : { ...next, blocks };
53
53
  }
54
- /** 单事件 → 单次状态迁移。窗口后置裁剪统一由 `reduceRunState` 做,这里不管。 */
55
- function reduceRunStateInner(state, event) {
56
- // 只有终态才阻止事件处理。finalizing 是非终态,
57
- // 仍需累积 assistant 内容(thinking、text、tool_use、后台任务输出)。
58
- const terminal = state.terminal;
59
- if (isRunTerminal(terminal)) {
60
- return state;
54
+ // ── 内联分支归约器(模块顶层命名函数)──────────────────────────────────────
55
+ // 原来 goal_state / approval_* 五个分支直接写在 reduceRunStateInner 里,连同
56
+ // 内部 .map/.filter 箭头闭包一起把 CC 顶到 29。抽成命名函数后外层只留查表。
57
+ function reduceGoalStateEvent(state, event) {
58
+ const e = event;
59
+ // 覆盖写:每次观测都是宿主的整份快照(phase/轮次/目标全文),不做增量。
60
+ return { ...state, goal: e.goal ?? undefined, goalKnown: e.known };
61
+ }
62
+ function reduceApprovalRequestedEvent(state, event) {
63
+ const e = event;
64
+ const approvals = state.approvals ?? [];
65
+ const next = { view: e.view, expired: false };
66
+ let idx = -1;
67
+ for (let i = 0; i < approvals.length; i++) {
68
+ if (approvals[i].view.requestId === e.requestId) {
69
+ idx = i;
70
+ break;
71
+ }
61
72
  }
62
- if (event.type === 'system')
63
- return reduceSystemEvent(state, event);
64
- if (event.type === 'run_status')
65
- return reduceRunStatusEvent(state, event);
66
- if (event.type === 'goal_state') {
67
- // 覆盖写:每次观测都是宿主的整份快照(phase/轮次/目标全文),不做增量。
68
- return { ...state, goal: event.goal ?? undefined, goalKnown: event.known };
73
+ if (idx >= 0) {
74
+ // 同一 requestId 重复到达:原地替换,保持槽位顺序。
75
+ const replaced = approvals.slice();
76
+ replaced[idx] = next;
77
+ return { ...state, approvals: replaced };
69
78
  }
70
- if (event.type === 'background_jobs')
71
- return reduceBackgroundJobsEvent(state, event);
72
- if (event.type === 'result')
73
- return reduceResultEvent(state, event);
74
- if (event.type === 'final_answer')
75
- return reduceFinalAnswerEvent(state, event);
76
- if (event.type === 'assistant')
77
- return reduceAssistantEvent(state, event);
78
- if (event.type === 'user')
79
- return reduceToolResultEvent(state, event);
80
- if (event.type === 'plan')
81
- return reducePlanEvent(state, event);
82
- if (event.type === 'file_change')
83
- return reduceFileChangeEvent(state, event);
84
- if (event.type === 'notice')
85
- return reduceNoticeEvent(state, event);
86
- if (event.type === 'session_info')
87
- return reduceSessionInfoEvent(state, event);
88
- if (event.type === 'turn_started')
89
- return reduceTurnStartedEvent(state, event);
90
- if (event.type === 'turn_diff')
91
- return reduceTurnDiffEvent(state, event);
92
- if (event.type === 'approval_requested') {
93
- const approvals = state.approvals ?? [];
94
- const next = { view: event.view, expired: false };
95
- const idx = approvals.findIndex((a) => a.view.requestId === event.requestId);
96
- if (idx >= 0) {
97
- // 同一 requestId 重复到达:原地替换,保持槽位顺序。
98
- return {
99
- ...state,
100
- approvals: approvals.map((a, i) => (i === idx ? next : a)),
101
- };
102
- }
103
- return { ...state, approvals: [...approvals, next] };
79
+ return { ...state, approvals: [...approvals, next] };
80
+ }
81
+ function reduceApprovalResolvedEvent(state, event) {
82
+ const e = event;
83
+ const current = state.approvals ?? [];
84
+ const approvals = [];
85
+ for (const a of current) {
86
+ if (a.view.requestId !== e.requestId)
87
+ approvals.push(a);
104
88
  }
105
- if (event.type === 'approval_resolved') {
106
- return {
107
- ...state,
108
- approvals: (state.approvals ?? []).filter((a) => a.view.requestId !== event.requestId),
109
- };
89
+ return { ...state, approvals };
90
+ }
91
+ function reduceApprovalViewUpdatedEvent(state, event) {
92
+ const e = event;
93
+ const current = state.approvals ?? [];
94
+ const approvals = [];
95
+ for (const a of current) {
96
+ approvals.push(a.view.requestId === e.requestId ? { ...a, view: e.view } : a);
110
97
  }
111
- if (event.type === 'approval_view_updated') {
112
- return {
113
- ...state,
114
- approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, view: event.view } : a),
115
- };
98
+ return { ...state, approvals };
99
+ }
100
+ function reduceApprovalExpiredEvent(state, event) {
101
+ const e = event;
102
+ const current = state.approvals ?? [];
103
+ const approvals = [];
104
+ for (const a of current) {
105
+ approvals.push(a.view.requestId === e.requestId ? { ...a, expired: true } : a);
116
106
  }
117
- if (event.type === 'approval_expired') {
118
- return {
119
- ...state,
120
- approvalExpired: true,
121
- approvals: (state.approvals ?? []).map((a) => a.view.requestId === event.requestId ? { ...a, expired: true } : a),
122
- };
107
+ return { ...state, approvalExpired: true, approvals };
108
+ }
109
+ /**
110
+ * event.type → 归约器。用 Map 而非对象字面量:`constructor`/`toString` 这类
111
+ * 事件名会命中 Object.prototype。
112
+ *
113
+ * 表里的归约器各自持有窄事件参数(如 `ResultEvent`),而查表调用处按
114
+ * `event.type` 精确路由后传入的运行时值一定是该窄类型;这里用一次整体类型
115
+ * 断言把「窄参数函数」装进统一签名的表,运行时零影响(不改变任何行为)。
116
+ */
117
+ const EVENT_REDUCERS = new Map([
118
+ ['system', reduceSystemEvent],
119
+ ['run_status', reduceRunStatusEvent],
120
+ ['goal_state', reduceGoalStateEvent],
121
+ ['background_jobs', reduceBackgroundJobsEvent],
122
+ ['result', reduceResultEvent],
123
+ ['final_answer', reduceFinalAnswerEvent],
124
+ ['assistant', reduceAssistantEvent],
125
+ ['user', reduceToolResultEvent],
126
+ ['plan', reducePlanEvent],
127
+ ['file_change', reduceFileChangeEvent],
128
+ ['notice', reduceNoticeEvent],
129
+ ['session_info', reduceSessionInfoEvent],
130
+ ['turn_started', reduceTurnStartedEvent],
131
+ ['turn_diff', reduceTurnDiffEvent],
132
+ ['approval_requested', reduceApprovalRequestedEvent],
133
+ ['approval_resolved', reduceApprovalResolvedEvent],
134
+ ['approval_view_updated', reduceApprovalViewUpdatedEvent],
135
+ ['approval_expired', reduceApprovalExpiredEvent],
136
+ ]);
137
+ /** 单事件 → 单次状态迁移。窗口后置裁剪统一由 `reduceRunState` 做,这里不管。 */
138
+ function reduceRunStateInner(state, event) {
139
+ // 只有终态才阻止事件处理。finalizing 是非终态,
140
+ // 仍需累积 assistant 内容(thinking、text、tool_use、后台任务输出)。
141
+ if (isRunTerminal(state.terminal)) {
142
+ return state;
123
143
  }
124
- return state;
144
+ const reduce = EVENT_REDUCERS.get(event.type);
145
+ return reduce ? reduce(state, event) : state;
125
146
  }
126
147
  /** Handle app-server turn_started — records the operation kind (turn vs compact). */
127
148
  function reduceTurnStartedEvent(state, event) {
128
- if (event.type !== 'turn_started')
129
- return state;
130
149
  return {
131
150
  ...state,
132
151
  operationKind: event.operationKind,
133
152
  footer: state.footer ?? 'thinking',
134
153
  };
135
154
  }
155
+ /** 恒等返回:file_change 的 fallback 与 update 都返回同一个已构造块。 */
156
+ function constantBlock(block) {
157
+ return block;
158
+ }
159
+ // -- text --
160
+ function textDiffMatches(event, b) {
161
+ return b.kind === 'text' && b.itemId === event.itemId;
162
+ }
163
+ function makeTextDiffBlock(event, ts, complete) {
164
+ return {
165
+ kind: 'text',
166
+ itemId: event.itemId,
167
+ // 最新那条正文不设窗口(它是「输出」),capStaleTextBlocks 负责在它被
168
+ // 更新的正文顶替后补上进度窗口。
169
+ content: event.text,
170
+ timestamp: ts,
171
+ ...(complete ? { completedAt: ts } : {}),
172
+ };
173
+ }
174
+ function updateTextDiffBlock(event, ts, complete, existing) {
175
+ const textBlock = existing;
176
+ return {
177
+ ...textBlock,
178
+ content: event.text,
179
+ // kimi:块锚点时间改为本次 diff 时间(整轮累积流不断续写,
180
+ // 标题应反映“最后写入时刻”而非首见时刻;move-to-end 后位置
181
+ // 与标题才一致)。codex app-server 不置 refreshTimestamp,
182
+ // 保持“首见时间戳不刷新”的既有契约。
183
+ ...(event.refreshTimestamp ? { timestamp: ts } : {}),
184
+ ...(complete ? { completedAt: ts } : {}),
185
+ };
186
+ }
187
+ function applyTextDiff(next, event) {
188
+ if (event.text === undefined)
189
+ return next;
190
+ const ts = event.timestamp;
191
+ const complete = event.complete === true;
192
+ const blocks = markThinkingInactive(next.blocks);
193
+ const capped = upsertSnapshotBlock(blocks, textDiffMatches.bind(null, event), makeTextDiffBlock.bind(null, event, ts, complete), updateTextDiffBlock.bind(null, event, ts, complete), !complete);
194
+ return withCappedBlocks(next, capped, 'streaming');
195
+ }
196
+ // -- reasoning --
197
+ function reasoningDiffMatches(event, b) {
198
+ return b.kind === 'thinking' && b.itemId === event.itemId;
199
+ }
200
+ function makeReasoningDiffBlock(event, ts, complete) {
201
+ return {
202
+ kind: 'thinking',
203
+ itemId: event.itemId,
204
+ content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
205
+ active: !complete,
206
+ timestamp: ts,
207
+ ...(complete ? { completedAt: ts } : {}),
208
+ };
209
+ }
210
+ function updateReasoningDiffBlock(event, ts, complete, existing) {
211
+ const thinkingBlock = existing;
212
+ return {
213
+ ...thinkingBlock,
214
+ content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
215
+ active: !complete,
216
+ ...(event.refreshTimestamp ? { timestamp: ts } : {}),
217
+ ...(complete ? { completedAt: ts } : {}),
218
+ };
219
+ }
220
+ // reasoning 的 applier 由下方 applySnapshotDiff + REASONING_DIFF_SPEC 提供:
221
+ // 原本与 applyToolOutputDiff 逐字重复(仅 guard 字段 / 三个 handler / footer
222
+ // 不同),现合并为单一实现,差异提到数据表。字段顺序、事件顺序逐项保持。
223
+ // -- toolOutput --
224
+ function toolDiffMatches(event, b) {
225
+ return b.kind === 'tool' && b.tool.id === event.itemId;
226
+ }
227
+ function makeToolDiffBlock(event, ts, complete) {
228
+ // 身份只在首见时写入(existing 分支不动):toolName/toolInput/toolHint。
229
+ const inputStr = truncateDetail(stringifyUnknown(event.toolInput ?? {}));
230
+ return {
231
+ kind: 'tool',
232
+ tool: {
233
+ id: event.itemId,
234
+ name: event.toolName ?? 'command',
235
+ input: inputStr,
236
+ parsedInput: asRecord(inputStr),
237
+ ...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
238
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
239
+ status: complete ? (event.toolStatus ?? 'ok') : 'running',
240
+ startedAt: ts,
241
+ ...(complete ? { completedAt: ts } : {}),
242
+ },
243
+ };
244
+ }
245
+ function updateToolDiffBlock(event, ts, complete, existing) {
246
+ const tool = existing.tool;
247
+ return {
248
+ kind: 'tool',
249
+ tool: {
250
+ ...tool,
251
+ output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
252
+ status: complete ? (event.toolStatus ?? 'ok') : tool.status,
253
+ ...(complete ? { completedAt: ts } : {}),
254
+ },
255
+ };
256
+ }
257
+ // toolOutput 的 applier 同样由 applySnapshotDiff + TOOL_DIFF_SPEC 驱动(见下)。
258
+ // -- plan --
259
+ function planDiffMatches(event, b) {
260
+ return b.kind === 'plan' && b.itemId === event.itemId;
261
+ }
262
+ function makePlanDiffBlock(newPlan, event, ts, complete) {
263
+ return {
264
+ kind: 'plan',
265
+ itemId: event.itemId,
266
+ content: newPlan,
267
+ active: !complete,
268
+ timestamp: ts,
269
+ ...(complete ? { completedAt: ts } : {}),
270
+ };
271
+ }
272
+ function updatePlanDiffBlock(newPlan, complete, ts, existing) {
273
+ const planBlock = existing;
274
+ return {
275
+ ...planBlock,
276
+ content: newPlan,
277
+ active: !complete,
278
+ ...(complete ? { completedAt: ts } : {}),
279
+ };
280
+ }
281
+ function applyPlanDiff(next, event) {
282
+ if (event.plan === undefined)
283
+ return next;
284
+ const ts = event.timestamp;
285
+ const complete = event.complete === true;
286
+ const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
287
+ const capped = upsertSnapshotBlock(next.blocks, planDiffMatches.bind(null, event), makePlanDiffBlock.bind(null, newPlan, event, ts, complete), updatePlanDiffBlock.bind(null, newPlan, complete, ts), !complete);
288
+ return withCappedBlocks({ ...next, plan: newPlan }, capped);
289
+ }
290
+ // -- fileChanges --
291
+ function makeFileChangeBlock(fc, event, ts, complete) {
292
+ return {
293
+ kind: 'file_change',
294
+ itemId: event.itemId,
295
+ path: fc.path,
296
+ operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
297
+ diff: fc.diff,
298
+ timestamp: ts,
299
+ ...(complete ? { completedAt: ts } : {}),
300
+ };
301
+ }
302
+ function fileChangeMatches(fc, event, b) {
303
+ return b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path;
304
+ }
305
+ function applyFileChangesDiff(next, event) {
306
+ if (event.fileChanges === undefined)
307
+ return next;
308
+ const ts = event.timestamp;
309
+ const complete = event.complete === true;
310
+ let blocks = next.blocks;
311
+ for (const fc of event.fileChanges) {
312
+ const fileBlock = makeFileChangeBlock(fc, event, ts, complete);
313
+ // 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
314
+ blocks = upsertSnapshotBlock(blocks, fileChangeMatches.bind(null, fc, event), constantBlock.bind(null, fileBlock), constantBlock.bind(null, fileBlock), !complete);
315
+ }
316
+ return withCappedBlocks(next, blocks);
317
+ }
318
+ const REASONING_DIFF_SPEC = {
319
+ hasPayload: (e) => e.reasoning !== undefined,
320
+ matches: reasoningDiffMatches,
321
+ make: makeReasoningDiffBlock,
322
+ update: updateReasoningDiffBlock,
323
+ footer: 'thinking',
324
+ };
325
+ const TOOL_DIFF_SPEC = {
326
+ hasPayload: (e) => e.toolOutput !== undefined,
327
+ matches: toolDiffMatches,
328
+ make: makeToolDiffBlock,
329
+ update: updateToolDiffBlock,
330
+ footer: 'tool_running',
331
+ };
332
+ function applySnapshotDiff(spec, next, event) {
333
+ if (!spec.hasPayload(event))
334
+ return next;
335
+ const ts = event.timestamp;
336
+ const complete = event.complete === true;
337
+ const capped = upsertSnapshotBlock(next.blocks, spec.matches.bind(null, event), spec.make.bind(null, event, ts, complete), spec.update.bind(null, event, ts, complete), !complete);
338
+ return withCappedBlocks(next, capped, spec.footer);
339
+ }
340
+ /** 五个字段按固定顺序依次应用(与原实现的 5 个 if 分支顺序一致)。 */
341
+ const TURN_DIFF_APPLIERS = [
342
+ applyTextDiff,
343
+ applySnapshotDiff.bind(null, REASONING_DIFF_SPEC),
344
+ applySnapshotDiff.bind(null, TOOL_DIFF_SPEC),
345
+ applyPlanDiff,
346
+ applyFileChangesDiff,
347
+ ];
136
348
  /**
137
349
  * Handle app-server turn_diff events. The translator emits FULL per-item
138
350
  * snapshots (accumulated text/reasoning/tool output/plan, current file-change
139
351
  * set). Each snapshot is scoped to a thread item (event.itemId); the block is
140
352
  * replaced in place to avoid duplication, and a NEW block is appended on the
141
- * item's first sight — so interleaved items keep their real chronology and
142
- * the active streaming content sits at the bottom, like the exec path.
353
+ * item's first sight — so interleaved items keep their real chronology and the
354
+ * active streaming content sits at the bottom, like the exec path.
143
355
  */
144
356
  function reduceTurnDiffEvent(state, event) {
145
- if (event.type !== 'turn_diff')
146
- return state;
147
357
  let next = state;
148
- const ts = event.timestamp;
149
- const complete = event.complete === true;
150
- if (event.text !== undefined) {
151
- const blocks = markThinkingInactive(next.blocks);
152
- const capped = upsertSnapshotBlock(blocks, (b) => b.kind === 'text' && b.itemId === event.itemId, () => ({
153
- kind: 'text',
154
- itemId: event.itemId,
155
- // 最新那条正文不设窗口(它是「输出」),capStaleTextBlocks 负责在它被
156
- // 更新的正文顶替后补上进度窗口。
157
- content: event.text,
158
- timestamp: ts,
159
- ...(complete ? { completedAt: ts } : {}),
160
- }), (existing) => {
161
- const textBlock = existing;
162
- return {
163
- ...textBlock,
164
- content: event.text,
165
- // kimi:块锚点时间改为本次 diff 时间(整轮累积流不断续写,
166
- // 标题应反映“最后写入时刻”而非首见时刻;move-to-end 后位置
167
- // 与标题才一致)。codex app-server 不置 refreshTimestamp,
168
- // 保持“首见时间戳不刷新”的既有契约。
169
- ...(event.refreshTimestamp ? { timestamp: ts } : {}),
170
- ...(complete ? { completedAt: ts } : {}),
171
- };
172
- }, !complete);
173
- next = {
174
- ...next,
175
- blocks: keepLatestBlocks(capped),
176
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
177
- footer: 'streaming',
178
- };
179
- }
180
- if (event.reasoning !== undefined) {
181
- const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'thinking' && b.itemId === event.itemId, () => ({
182
- kind: 'thinking',
183
- itemId: event.itemId,
184
- content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
185
- active: !complete,
186
- timestamp: ts,
187
- ...(complete ? { completedAt: ts } : {}),
188
- }), (existing) => {
189
- const thinkingBlock = existing;
190
- return {
191
- ...thinkingBlock,
192
- content: keepTailMarked(event.reasoning, MAX_REASONING_CHARS),
193
- active: !complete,
194
- ...(event.refreshTimestamp ? { timestamp: ts } : {}),
195
- ...(complete ? { completedAt: ts } : {}),
196
- };
197
- }, !complete);
198
- next = {
199
- ...next,
200
- blocks: keepLatestBlocks(capped),
201
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
202
- footer: 'thinking',
203
- };
204
- }
205
- if (event.toolOutput !== undefined) {
206
- const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'tool' && b.tool.id === event.itemId, () => {
207
- // 身份只在首见时写入(existing 分支不动):toolName/toolInput/toolHint。
208
- const inputStr = truncateDetail(stringifyUnknown(event.toolInput ?? {}));
209
- return {
210
- kind: 'tool',
211
- tool: {
212
- id: event.itemId,
213
- name: event.toolName ?? 'command',
214
- input: inputStr,
215
- parsedInput: asRecord(inputStr),
216
- ...(event.toolHint !== undefined ? { toolHint: event.toolHint } : {}),
217
- output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
218
- status: complete ? (event.toolStatus ?? 'ok') : 'running',
219
- startedAt: ts,
220
- ...(complete ? { completedAt: ts } : {}),
221
- },
222
- };
223
- }, (existing) => ({
224
- kind: 'tool',
225
- tool: {
226
- ...existing.tool,
227
- output: keepTail(event.toolOutput, MAX_TOOL_DETAIL_CHARS),
228
- status: complete
229
- ? (event.toolStatus ?? 'ok')
230
- : existing.tool.status,
231
- ...(complete ? { completedAt: ts } : {}),
232
- },
233
- }), !complete);
234
- next = {
235
- ...next,
236
- blocks: keepLatestBlocks(capped),
237
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
238
- footer: 'tool_running',
239
- };
240
- }
241
- if (event.plan !== undefined) {
242
- const newPlan = keepTailMarked(event.plan, MAX_REASONING_CHARS * 2);
243
- const capped = upsertSnapshotBlock(next.blocks, (b) => b.kind === 'plan' && b.itemId === event.itemId, () => ({
244
- kind: 'plan',
245
- itemId: event.itemId,
246
- content: newPlan,
247
- active: !complete,
248
- timestamp: ts,
249
- ...(complete ? { completedAt: ts } : {}),
250
- }), (existing) => {
251
- const planBlock = existing;
252
- return {
253
- ...planBlock,
254
- content: newPlan,
255
- active: !complete,
256
- ...(complete ? { completedAt: ts } : {}),
257
- };
258
- }, !complete);
259
- next = {
260
- ...next,
261
- plan: newPlan,
262
- blocks: keepLatestBlocks(capped),
263
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
264
- };
265
- }
266
- if (event.fileChanges !== undefined) {
267
- let blocks = next.blocks;
268
- for (const fc of event.fileChanges) {
269
- const fileBlock = {
270
- kind: 'file_change',
271
- itemId: event.itemId,
272
- path: fc.path,
273
- operation: fc.kind === 'add' ? 'create' : fc.kind === 'delete' ? 'delete' : 'edit',
274
- diff: fc.diff,
275
- timestamp: ts,
276
- ...(complete ? { completedAt: ts } : {}),
277
- };
278
- // 同一 item 的同一路径原地替换(权威 diff 校正),新路径/新 item 追加。
279
- blocks = upsertSnapshotBlock(blocks, (b) => b.kind === 'file_change' && b.itemId === event.itemId && b.path === fc.path, () => fileBlock, () => fileBlock, !complete);
280
- }
281
- const capped = blocks;
282
- next = {
283
- ...next,
284
- blocks: keepLatestBlocks(capped),
285
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
286
- };
358
+ for (const applier of TURN_DIFF_APPLIERS) {
359
+ next = applier(next, event);
287
360
  }
288
361
  return next;
289
362
  }
@@ -313,8 +386,6 @@ function upsertSnapshotBlock(blocks, predicate, fallback, update, moveToEndWhenN
313
386
  }
314
387
  /** Handle system init and compact_boundary events. */
315
388
  function reduceSystemEvent(state, event) {
316
- if (event.type !== 'system')
317
- return state;
318
389
  if (event.subtype === 'init') {
319
390
  return { ...state, sessionId: event.session_id, model: event.model, footer: 'thinking' };
320
391
  }
@@ -336,8 +407,6 @@ function reduceSystemEvent(state, event) {
336
407
  * - errorMsg: 首个非 undefined 胜出(agent 的更具体)
337
408
  */
338
409
  function reduceResultEvent(state, event) {
339
- if (event.type !== 'result')
340
- return state;
341
410
  // Pre-init result guard: Claude CLI --resume emits a historical result (from
342
411
  // the previous turn) before sending system.init for the new run. Without this
343
412
  // guard, that stale result would prematurely transition to 'finalizing',
@@ -375,8 +444,6 @@ function reduceResultEvent(state, event) {
375
444
  }
376
445
  /** Record an authoritative protocol final answer and remove its progress copy. */
377
446
  function reduceFinalAnswerEvent(state, event) {
378
- if (event.type !== 'final_answer')
379
- return state;
380
447
  if (!event.text.trim() && !state.finalAnswer)
381
448
  return state;
382
449
  const blocks = event.itemId
@@ -443,8 +510,6 @@ function promoteFinalAnswer(state, source, authoritativeText) {
443
510
  * 不再走事件(那时已经没有 run 在消费流了)。
444
511
  */
445
512
  function reduceBackgroundJobsEvent(state, event) {
446
- if (event.type !== 'background_jobs')
447
- return state;
448
513
  return { ...state, backgroundJobs: event.jobs };
449
514
  }
450
515
  /**
@@ -489,43 +554,69 @@ function bridgeJobStatus(status) {
489
554
  *
490
555
  * 无变化时返回**同一个 state 引用**:bridge 靠它跳过无谓的卡片 patch。
491
556
  */
492
- export function syncBackgroundJobs(state, roster) {
493
- const tracked = state.backgroundJobs ?? [];
494
- const byId = new Map(roster.map((job) => [job.id, job]));
495
- const next = tracked.map((job) => {
496
- const row = byId.get(job.id);
497
- if (!row)
498
- return job.status === 'ended' ? job : { ...job, status: 'ended' };
499
- if (job.status === 'ended')
500
- return job;
501
- const status = row.status === undefined ? job.status : bridgeJobStatus(row.status);
502
- const label = row.label.trim() === '' ? job.label : row.label;
503
- const progress = row.progress ?? job.progress;
504
- if (status === job.status && label === job.label && progress === job.progress)
505
- return job;
506
- return {
507
- ...job,
508
- label,
509
- ...(status === undefined ? {} : { status }),
510
- ...(progress === undefined ? {} : { progress }),
511
- };
512
- });
513
- // 新出现的任务只收仍在跑的:与收尾采集同一口径(列出已结束的任务只会被
514
- // 读成"还在跑")。
515
- const seen = new Set(tracked.map((job) => job.id));
557
+ /** 单个已跟踪 job 与 roster 行的对账(原 tracked.map 回调)。 */
558
+ function syncTrackedJob(job, row) {
559
+ if (!row)
560
+ return job.status === 'ended' ? job : { ...job, status: 'ended' };
561
+ if (job.status === 'ended')
562
+ return job;
563
+ const status = row.status === undefined ? job.status : bridgeJobStatus(row.status);
564
+ const label = row.label.trim() === '' ? job.label : row.label;
565
+ const progress = row.progress ?? job.progress;
566
+ if (status === job.status && label === job.label && progress === job.progress)
567
+ return job;
568
+ return {
569
+ ...job,
570
+ label,
571
+ ...(status === undefined ? {} : { status }),
572
+ ...(progress === undefined ? {} : { progress }),
573
+ };
574
+ }
575
+ /** roster 里新出现且仍在跑的 job(原新增循环)。 */
576
+ function collectNewJobs(tracked, roster) {
577
+ const seen = new Set();
578
+ for (const job of tracked)
579
+ seen.add(job.id);
580
+ const added = [];
516
581
  for (const row of roster) {
517
582
  if (seen.has(row.id))
518
583
  continue;
519
584
  if (row.status !== undefined && !RUNNING_JOB_STATUSES.has(row.status))
520
585
  continue;
521
- next.push({
586
+ added.push({
522
587
  id: row.id,
523
588
  label: row.label,
524
589
  ...(row.status === undefined ? {} : { status: row.status }),
525
590
  ...(row.progress === undefined ? {} : { progress: row.progress }),
526
591
  });
527
592
  }
528
- if (next.length === tracked.length && next.every((job, index) => job === tracked[index])) {
593
+ return added;
594
+ }
595
+ /** next 与 tracked 是否逐项同一引用(原 length + every 判定)。 */
596
+ function jobsUnchanged(next, tracked) {
597
+ if (next.length !== tracked.length)
598
+ return false;
599
+ for (let i = 0; i < next.length; i++) {
600
+ if (next[i] !== tracked[i])
601
+ return false;
602
+ }
603
+ return true;
604
+ }
605
+ export function syncBackgroundJobs(state, roster) {
606
+ const tracked = state.backgroundJobs ?? [];
607
+ const byId = new Map();
608
+ for (const job of roster)
609
+ byId.set(job.id, job);
610
+ const next = [];
611
+ for (const job of tracked) {
612
+ next.push(syncTrackedJob(job, byId.get(job.id)));
613
+ }
614
+ // 新出现的任务只收仍在跑的:与收尾采集同一口径(列出已结束的任务只会被
615
+ // 读成"还在跑")。
616
+ for (const added of collectNewJobs(tracked, roster)) {
617
+ next.push(added);
618
+ }
619
+ if (jobsUnchanged(next, tracked)) {
529
620
  return state;
530
621
  }
531
622
  return { ...state, backgroundJobs: next };
@@ -556,8 +647,6 @@ export function markBackgroundJobsUnknown(state) {
556
647
  * 维度(上一步算出的上下文水位)擦掉。
557
648
  */
558
649
  function reduceRunStatusEvent(state, event) {
559
- if (event.type !== 'run_status')
560
- return state;
561
650
  const prev = state.liveStatus ?? {};
562
651
  const live = {
563
652
  ...prev,
@@ -575,8 +664,6 @@ function reduceRunStatusEvent(state, event) {
575
664
  }
576
665
  /** Handle assistant thinking/text/tool_use content blocks. */
577
666
  function reduceAssistantEvent(state, event) {
578
- if (event.type !== 'assistant')
579
- return state;
580
667
  let next = state;
581
668
  for (const content of event.message.content) {
582
669
  if (content.type === 'thinking') {
@@ -605,12 +692,7 @@ function reduceAssistantEvent(state, event) {
605
692
  timestamp: event.timestamp,
606
693
  },
607
694
  ];
608
- next = {
609
- ...next,
610
- blocks: keepLatestBlocks(newBlocks),
611
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
612
- footer: 'thinking',
613
- };
695
+ next = withCappedBlocks(next, newBlocks, 'thinking');
614
696
  }
615
697
  else if (content.type === 'text') {
616
698
  const blocks = markThinkingInactive(next.blocks);
@@ -634,12 +716,7 @@ function reduceAssistantEvent(state, event) {
634
716
  timestamp: event.timestamp,
635
717
  },
636
718
  ];
637
- next = {
638
- ...next,
639
- blocks: keepLatestBlocks(newBlocks),
640
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
641
- footer: 'streaming',
642
- };
719
+ next = withCappedBlocks(next, newBlocks, 'streaming');
643
720
  }
644
721
  else {
645
722
  // parse the truncated stored string ONCE here (not on every render)
@@ -674,12 +751,7 @@ function reduceAssistantEvent(state, event) {
674
751
  }
675
752
  else {
676
753
  const capped = [...blocks, { kind: 'tool', tool: incomingTool }];
677
- next = {
678
- ...next,
679
- blocks: keepLatestBlocks(capped),
680
- omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, capped.length - MAX_BLOCKS),
681
- footer: 'tool_running',
682
- };
754
+ next = withCappedBlocks(next, capped, 'tool_running');
683
755
  }
684
756
  }
685
757
  }
@@ -716,8 +788,6 @@ function mergeToolUse(existing, incoming) {
716
788
  * advances to 'streaming' on every content block (parity with old behavior);
717
789
  * only the blocks array is preserved by reference when nothing matches. */
718
790
  function reduceToolResultEvent(state, event) {
719
- if (event.type !== 'user')
720
- return state;
721
791
  let next = state;
722
792
  for (const content of event.message.content) {
723
793
  const targetId = content.tool_use_id;
@@ -745,8 +815,6 @@ function reduceToolResultEvent(state, event) {
745
815
  }
746
816
  /** Handle notice event — append a runtime notice (warning / reroute / compaction). */
747
817
  function reduceNoticeEvent(state, event) {
748
- if (event.type !== 'notice')
749
- return state;
750
818
  return {
751
819
  ...state,
752
820
  notices: [
@@ -762,8 +830,6 @@ function reduceNoticeEvent(state, event) {
762
830
  }
763
831
  /** Handle session_info event — update sessionTitle / mode only when present. */
764
832
  function reduceSessionInfoEvent(state, event) {
765
- if (event.type !== 'session_info')
766
- return state;
767
833
  return {
768
834
  ...state,
769
835
  ...(event.title !== undefined ? { sessionTitle: event.title } : {}),
@@ -772,8 +838,6 @@ function reduceSessionInfoEvent(state, event) {
772
838
  }
773
839
  /** Handle plan event — accumulate plan text for Codex-style agents. */
774
840
  function reducePlanEvent(state, event) {
775
- if (event.type !== 'plan')
776
- return state;
777
841
  const currentPlan = state.plan ?? '';
778
842
  const newPlan = keepTailMarked(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
779
843
  // Plan is a single evolving document — replace the existing plan block
@@ -783,17 +847,10 @@ function reducePlanEvent(state, event) {
783
847
  ...withoutOldPlan,
784
848
  { kind: 'plan', content: newPlan, active: true, timestamp: event.timestamp },
785
849
  ];
786
- return {
787
- ...state,
788
- plan: newPlan,
789
- blocks: keepLatestBlocks(newBlocks),
790
- omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
791
- };
850
+ return withCappedBlocks({ ...state, plan: newPlan }, newBlocks);
792
851
  }
793
852
  /** Handle file_change event — emit a file_change block for rendering (Codex-style agents). */
794
853
  function reduceFileChangeEvent(state, event) {
795
- if (event.type !== 'file_change')
796
- return state;
797
854
  // Add file_change block for rendering (collapsed by default)
798
855
  const newBlocks = [
799
856
  ...state.blocks,
@@ -805,11 +862,7 @@ function reduceFileChangeEvent(state, event) {
805
862
  timestamp: event.timestamp,
806
863
  },
807
864
  ];
808
- return {
809
- ...state,
810
- blocks: keepLatestBlocks(newBlocks),
811
- omittedBlocks: (state.omittedBlocks ?? 0) + Math.max(0, newBlocks.length - MAX_BLOCKS),
812
- };
865
+ return withCappedBlocks(state, newBlocks);
813
866
  }
814
867
  export function finishRun(state, terminal, meta = {}) {
815
868
  const withFinalAnswer = promoteFinalAnswer(state, 'inferred');
@@ -862,14 +915,27 @@ function stringifyUnknown(value) {
862
915
  try {
863
916
  // JSON.stringify(undefined) returns the value undefined, which would
864
917
  // violate this function's string contract and crash callers that take
865
- // .length on the result (2026-08-17 kimi ACP tool_call without rawInput
866
- // → truncateDetail TypeError). Normalize to ''.
918
+ // .length on the result. Normalize to ''.
867
919
  return JSON.stringify(value) ?? '';
868
920
  }
869
921
  catch {
870
922
  return String(value);
871
923
  }
872
924
  }
925
+ /**
926
+ * 尾块统一收口:keepLatestBlocks 截断 + omittedBlocks 计数 + footer 更新一步
927
+ * 完成。所有「追加/更新块后只保留最新 MAX_BLOCKS 块」的事件归约共用(此前
928
+ * 多处逐字重复);`blocks` 必须传截断前的数组,被丢出的最旧块才会计入
929
+ * omittedBlocks。`footer` 不传时保持原值(plan/file_change 归约不改 footer)。
930
+ */
931
+ function withCappedBlocks(next, blocks, footer) {
932
+ return {
933
+ ...next,
934
+ blocks: keepLatestBlocks(blocks),
935
+ omittedBlocks: (next.omittedBlocks ?? 0) + Math.max(0, blocks.length - MAX_BLOCKS),
936
+ ...(footer === undefined ? {} : { footer }),
937
+ };
938
+ }
873
939
  function keepLatestBlocks(blocks) {
874
940
  return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
875
941
  }
@@ -879,8 +945,8 @@ const TRUNCATION_MARKER_RE = /^…(前 (\d+) 字符已省略)\n/;
879
945
  * 给「已经不再是最新」的 text 块补上 `MAX_TEXT_CHARS` 存储窗口。
880
946
  *
881
947
  * 为什么最新一条不截:它就是卡片上的**输出**,终态还会被 `promoteFinalAnswer`
882
- * 提成最终回答。提前按窗口切掉头部,渲染层再怎么回答优先也补不回来(2026-11-01
883
- * 输出块被截断缺陷:16074 字符的回答只进到渲染层 12000 字符,且是从中间开始的)。
948
+ * 提成最终回答。提前按窗口切掉头部,渲染层再怎么回答优先也补不回来
949
+ * (16074 字符的回答只进到渲染层 12000 字符,且是从中间开始的)。
884
950
  *
885
951
  * 为什么其余 text 块仍然截:它们只是进度信息,卡片本来就只会展示最新几步;
886
952
  * 存储层必须保持有界(`MAX_BLOCKS` × 窗口),否则长 run 的内存随历史线性增长。
@@ -915,7 +981,7 @@ function capStaleTextBlocks(blocks) {
915
981
  * keepTail + 截断标记:被去头的存储内容以前缀如实标注(信息保真红线:禁止静默丢弃)。
916
982
  * 标记计入 maxChars 预算(存储总量仍 ≤ maxChars,保持既有长度不变量)。
917
983
  *
918
- * 计数口径(2026-10 修正):只统计**真实丢弃的正文**,不含标记自身开销。
984
+ * 计数口径:只统计**真实丢弃的正文**,不含标记自身开销。
919
985
  * 关键点是增量流下 `value` 往往已带上一次的标记(`marker + kept`);若直接
920
986
  * 用 `value.length - kept.length` 计费,会把上一帧的标记文本也算进「已省略」,
921
987
  * 造成每帧显示十几字符的虚高。这里先剥离旧标记,把 N 定义为**从该块起始累计