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.
- package/CHANGELOG.md +29 -0
- package/dist/bridge/approval-actions.d.ts +1 -1
- package/dist/bridge/approval-actions.js +1 -1
- package/dist/bridge/bash-run.d.ts +71 -0
- package/dist/bridge/bash-run.js +217 -0
- package/dist/bridge/compact.d.ts +155 -0
- package/dist/bridge/compact.js +450 -0
- package/dist/bridge/final-usage.js +4 -4
- package/dist/bridge/inbound-media.js +72 -46
- package/dist/bridge/index.d.ts +106 -153
- package/dist/bridge/index.js +400 -1000
- package/dist/bridge/job-watch.d.ts +68 -0
- package/dist/bridge/job-watch.js +146 -0
- package/dist/bridge/queue-manager.d.ts +3 -3
- package/dist/bridge/queue-manager.js +10 -29
- package/dist/card/approval-render.d.ts +2 -2
- package/dist/card/approval-render.js +141 -130
- package/dist/card/card-budget.js +123 -98
- package/dist/card/card-helpers.d.ts +3 -3
- package/dist/card/card-helpers.js +6 -6
- package/dist/card/final-answer-file.js +1 -1
- package/dist/card/run-renderer.d.ts +3 -3
- package/dist/card/run-renderer.js +114 -159
- package/dist/card/run-state.d.ts +1 -15
- package/dist/card/run-state.js +358 -292
- package/dist/card/tool-render.d.ts +1 -1
- package/dist/card/tool-render.js +21 -17
- package/dist/card/usage-format.d.ts +22 -7
- package/dist/card/usage-format.js +119 -100
- package/dist/cli.js +15 -3
- package/dist/clone.js +2 -2
- package/dist/common/text-width.js +36 -24
- package/dist/common/tool-summary.js +132 -90
- package/dist/config/codex-config.js +130 -106
- package/dist/config/index.d.ts +16 -2
- package/dist/config/index.js +23 -8
- package/dist/connector/index.js +6 -6
- package/dist/error-classification.js +3 -2
- package/dist/inbound/placeholder.d.ts +1 -1
- package/dist/inbound/placeholder.js +1 -1
- package/dist/index.js +387 -281
- package/dist/logger/index.js +6 -6
- package/dist/message-handlers.d.ts +0 -5
- package/dist/message-handlers.js +254 -237
- package/dist/model-catalog.d.ts +2 -2
- package/dist/model-catalog.js +3 -8
- package/dist/platform/identity.js +1 -1
- package/dist/platform/path.d.ts +1 -1
- package/dist/platform/path.js +1 -1
- package/dist/platform/sleep-blocker.js +1 -1
- package/dist/platform/terminator-win32.js +2 -1
- package/dist/router/commands/ls.d.ts +3 -0
- package/dist/router/commands/ls.js +256 -175
- package/dist/router/commands/order.js +184 -159
- package/dist/router/commands/resume.js +190 -152
- package/dist/router/commands/ws.js +219 -174
- package/dist/router/config/dsh.d.ts +4 -8
- package/dist/router/config/dsh.js +3 -10
- package/dist/router/config/kimi.js +1 -1
- package/dist/router/config/types.d.ts +1 -1
- package/dist/router/config-diff.js +1 -1
- package/dist/router/index.d.ts +56 -11
- package/dist/router/index.js +401 -391
- package/dist/runner/bash/runner.js +2 -4
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +6 -4
- package/dist/runner/codex/app-server/runner.d.ts +9 -2
- package/dist/runner/codex/app-server/runner.js +30 -25
- package/dist/runner/codex/app-server/translator.d.ts +2 -5
- package/dist/runner/codex/app-server/translator.js +291 -238
- package/dist/runner/common/acp/acp-approval.d.ts +1 -1
- package/dist/runner/common/acp/acp-approval.js +1 -1
- package/dist/runner/common/acp/base-acp-runner.d.ts +5 -2
- package/dist/runner/common/acp/base-acp-runner.js +26 -65
- package/dist/runner/common/acp/base-translator.d.ts +12 -2
- package/dist/runner/common/acp/base-translator.js +50 -26
- package/dist/runner/common/acp/protocol-types.d.ts +1 -1
- package/dist/runner/common/acp/protocol-types.js +2 -2
- package/dist/runner/common/connection-based-runner.d.ts +32 -1
- package/dist/runner/common/connection-based-runner.js +32 -2
- package/dist/runner/common/jsonl-stream.js +154 -187
- package/dist/runner/common/jsonrpc/client.d.ts +29 -0
- package/dist/runner/common/jsonrpc/client.js +43 -12
- package/dist/runner/common/jsonrpc/connection-manager.d.ts +2 -2
- package/dist/runner/common/jsonrpc/connection-manager.js +4 -4
- package/dist/runner/common/spawning-runner.d.ts +6 -1
- package/dist/runner/common/spawning-runner.js +9 -4
- package/dist/runner/common/turn-started.d.ts +8 -0
- package/dist/runner/common/turn-started.js +11 -0
- package/dist/runner/dsh/approval.js +84 -51
- package/dist/runner/dsh/catalog-probe.d.ts +17 -0
- package/dist/runner/dsh/catalog-probe.js +10 -0
- package/dist/runner/dsh/client.d.ts +7 -11
- package/dist/runner/dsh/client.js +356 -289
- package/dist/runner/dsh/exit-cancel.d.ts +2 -2
- package/dist/runner/dsh/exit-cancel.js +2 -2
- package/dist/runner/dsh/inflight.d.ts +1 -1
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.js +28 -8
- package/dist/runner/dsh/runner.d.ts +44 -4
- package/dist/runner/dsh/runner.js +296 -199
- package/dist/runner/dsh/translator.d.ts +1 -31
- package/dist/runner/dsh/translator.js +180 -178
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +1 -2
- package/dist/runner/kimi/acp/runner.js +82 -46
- package/dist/runner/kimi/acp/translator.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +3 -4
- package/dist/runner/opencode/acp/error-monitor.js +5 -5
- package/dist/runner/opencode/acp/runner.d.ts +2 -2
- package/dist/runner/opencode/acp/runner.js +4 -4
- package/dist/runner/pi/rpc/client.js +4 -13
- package/dist/runner/pi/rpc/protocol-types.d.ts +1 -1
- package/dist/runner/pi/rpc/protocol-types.js +1 -1
- package/dist/runner/pi/rpc/runner.d.ts +3 -0
- package/dist/runner/pi/rpc/runner.js +27 -42
- package/dist/runner/pi/rpc/translator.js +61 -60
- package/dist/runner/types.d.ts +2 -2
- package/dist/runner/types.js +1 -1
- package/dist/session/claude/sessions.d.ts +45 -0
- package/dist/session/claude/sessions.js +160 -122
- package/dist/session/codex/rollout-reader.js +181 -182
- package/dist/session/common/throw-session-list-error.d.ts +10 -0
- package/dist/session/common/throw-session-list-error.js +16 -0
- package/dist/session/dsh/sessions.js +291 -235
- package/dist/session/kimi/sessions.d.ts +40 -8
- package/dist/session/kimi/sessions.js +373 -301
- package/dist/session/opencode/sessions.d.ts +142 -1
- package/dist/session/opencode/sessions.js +378 -321
- package/dist/session/pi/sessions.js +4 -5
- package/dist/session/session-store.d.ts +9 -1
- package/dist/session/session-store.js +99 -99
- package/dist/update/version-check.js +2 -1
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/index.js +1 -1
- package/package.json +1 -1
package/dist/card/run-state.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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 (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
149
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
883
|
-
*
|
|
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
|
-
*
|
|
984
|
+
* 计数口径:只统计**真实丢弃的正文**,不含标记自身开销。
|
|
919
985
|
* 关键点是增量流下 `value` 往往已带上一次的标记(`marker + kept`);若直接
|
|
920
986
|
* 用 `value.length - kept.length` 计费,会把上一帧的标记文本也算进「已省略」,
|
|
921
987
|
* 造成每帧显示十几字符的虚高。这里先剥离旧标记,把 N 定义为**从该块起始累计
|