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
@@ -117,67 +117,189 @@ function readCodexRolloutSummary(filePath) {
117
117
  return null;
118
118
  }
119
119
  }
120
+ function newScanState() {
121
+ return {
122
+ sessionMeta: null,
123
+ events: [],
124
+ realUserMessages: [],
125
+ lastRawUsage: undefined,
126
+ lastContextLimit: undefined,
127
+ lastTotalUsage: undefined,
128
+ previousTotals: undefined,
129
+ turnUsage: null,
130
+ compactCount: 0,
131
+ compactPreContextLength: undefined,
132
+ compactPostContextLength: undefined,
133
+ lastUserMessageSource: null,
134
+ userResponseItemCount: 0,
135
+ previousRawUsage: undefined,
136
+ };
137
+ }
138
+ function scanResult(state) {
139
+ return {
140
+ sessionMeta: state.sessionMeta,
141
+ events: state.events,
142
+ realUserMessages: state.realUserMessages,
143
+ lastRawUsage: state.lastRawUsage,
144
+ lastContextLimit: state.lastContextLimit,
145
+ lastTotalUsage: state.lastTotalUsage,
146
+ previousTotals: state.previousTotals,
147
+ turnUsage: state.turnUsage,
148
+ compactCount: state.compactCount,
149
+ compactPreContextLength: state.compactPreContextLength,
150
+ compactPostContextLength: state.compactPostContextLength,
151
+ };
152
+ }
153
+ function warnIfVocabularyDrift(state, filePath) {
154
+ // 兼容性漂移探针:文件里有 `role:"user"` 注入,却识别不出任何真人输入 ——
155
+ // 说明 codex 又换了 event_msg 词表,displayTitle 会静默变空(0.153 事故形态)。
156
+ // 只告警不兜底:误用注入文本当标题是已修掉的缺陷。
157
+ if (state.realUserMessages.length === 0 && state.userResponseItemCount > 0) {
158
+ getLogger().warn(`[codex-rollout-reader] no human user message recognised in ${filePath} ` +
159
+ `(${state.userResponseItemCount} role:"user" response_items seen) — codex rollout ` +
160
+ `event vocabulary may have changed; session title will be empty`);
161
+ }
162
+ }
163
+ function recordHumanMessage(state, human) {
164
+ const text = human.text.slice(0, 200);
165
+ const prev = state.realUserMessages[state.realUserMessages.length - 1];
166
+ // 过渡期防御:同一 turn 若被旧版 user_message 与新版 item_completed
167
+ // 各写一次,会得到相邻重复。仅在「通道切换且文本与上一条完全相同」
168
+ // 时跳过——同通道连续相同输入(真人连发两条一样的话)不受影响。
169
+ const duplicateAcrossChannels = state.lastUserMessageSource !== null &&
170
+ state.lastUserMessageSource !== human.source &&
171
+ prev === text;
172
+ if (!duplicateAcrossChannels) {
173
+ state.realUserMessages.push(text);
174
+ }
175
+ state.lastUserMessageSource = human.source;
176
+ }
177
+ function resolveRawUsage(info, previousTotals) {
178
+ const last = info?.last_token_usage;
179
+ const total = info?.total_token_usage;
180
+ let raw;
181
+ if (last) {
182
+ raw = last;
183
+ }
184
+ else if (total && previousTotals) {
185
+ raw = subtractRawUsage(total, previousTotals);
186
+ }
187
+ else {
188
+ raw = total;
189
+ }
190
+ return { raw, total };
191
+ }
192
+ function accumulateTurnUsage(state, raw) {
193
+ if (!state.turnUsage)
194
+ return;
195
+ // token_count 每次采样一条,末 turn 的所有采样求和(见 buildUsageFromScan)。
196
+ const cached = Math.min(raw.cached_input_tokens, raw.input_tokens);
197
+ state.turnUsage.input += raw.input_tokens - cached;
198
+ state.turnUsage.cached += cached;
199
+ state.turnUsage.output += raw.output_tokens;
200
+ state.turnUsage.total += raw.total_tokens;
201
+ }
202
+ function updateCompactPostContext(state, raw) {
203
+ // 压缩后的水位:压缩收尾 token_count 全 0(不满足 zero-filter),
204
+ // 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
205
+ // 水位回归该 turn,post-compact 值失效。
206
+ if (state.compactCount <= 0 || !raw)
207
+ return;
208
+ if (raw.total_tokens && !raw.input_tokens && !raw.cached_input_tokens && !raw.output_tokens) {
209
+ state.compactPostContextLength = raw.total_tokens;
210
+ }
211
+ else if (raw.input_tokens > 0) {
212
+ state.compactPostContextLength = undefined;
213
+ }
214
+ }
215
+ function handleTokenCount(state, payload) {
216
+ const info = payload.info;
217
+ const { raw, total } = resolveRawUsage(info, state.previousTotals);
218
+ if (total) {
219
+ state.previousTotals = total;
220
+ state.lastTotalUsage = total;
221
+ }
222
+ if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
223
+ state.previousRawUsage = state.lastRawUsage;
224
+ state.lastRawUsage = raw;
225
+ const ctxWindow = info?.model_context_window;
226
+ state.lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
227
+ accumulateTurnUsage(state, raw);
228
+ }
229
+ updateCompactPostContext(state, raw);
230
+ }
231
+ function handleEventMsg(state, payload) {
232
+ const human = extractRealUserText(payload);
233
+ if (human) {
234
+ recordHumanMessage(state, human);
235
+ return;
236
+ }
237
+ if (payload.type === 'task_started') {
238
+ // turn 边界:一个 turn 一条 task_started,从这里开始重新累计本 turn 的用量。
239
+ state.turnUsage = emptyTurnUsage();
240
+ return;
241
+ }
242
+ if (payload.type === 'token_count')
243
+ handleTokenCount(state, payload);
244
+ }
245
+ function handleCompacted(state, payload) {
246
+ // 顶层 compacted 事件:压缩次数 +1,压缩前水位取「压缩调用之前」那条非零
247
+ // token_count 的 total_tokens。真实 rollout 落盘顺序是
248
+ // 末轮 → 摘要调用 → compacted → 压缩后水位:摘要调用自身的 token_count 紧贴
249
+ // 在 compacted 之前,直接取 lastRawUsage 会把摘要调用的消耗当成压缩前窗口。
250
+ // compacted 自带的摘要调用 usage 用于判定并回退一条;旧数据无该字段时保持原语义。
251
+ state.compactCount++;
252
+ const compactCallTotal = readCompactedCallTotal(payload);
253
+ const preCompactUsage = compactCallTotal !== undefined && state.lastRawUsage?.total_tokens === compactCallTotal
254
+ ? state.previousRawUsage
255
+ : state.lastRawUsage;
256
+ if (preCompactUsage?.total_tokens !== undefined) {
257
+ state.compactPreContextLength = preCompactUsage.total_tokens;
258
+ }
259
+ }
260
+ function handleResponseItem(state, payload, timestamp) {
261
+ if (payload.type !== 'message' || !Array.isArray(payload.content))
262
+ return;
263
+ if (payload.role === 'user')
264
+ state.userResponseItemCount++;
265
+ const messages = extractMessageContent(payload);
266
+ for (const msg of messages) {
267
+ state.events.push({
268
+ type: msg.role === 'user' ? 'user' : 'assistant',
269
+ content: msg.text,
270
+ timestamp,
271
+ });
272
+ }
273
+ }
274
+ function dispatchRolloutLine(state, parsed, includeEvents) {
275
+ if (parsed.type === 'session_meta' && parsed.payload) {
276
+ state.sessionMeta = parsed.payload;
277
+ return;
278
+ }
279
+ if (parsed.type === 'event_msg' && parsed.payload) {
280
+ handleEventMsg(state, parsed.payload);
281
+ return;
282
+ }
283
+ if (parsed.type === 'compacted') {
284
+ handleCompacted(state, parsed.payload);
285
+ return;
286
+ }
287
+ if (includeEvents && parsed.type === 'response_item' && parsed.payload) {
288
+ handleResponseItem(state, parsed.payload, parsed.timestamp);
289
+ }
290
+ }
120
291
  /**
121
292
  * Scan a rollout file once, tracking user messages, usage and compaction state.
122
293
  * When `includeEvents` is true, response_item content is also extracted into
123
294
  * `events`; when false, response_item lines are skipped entirely (the summary
124
295
  * fast path).
296
+ *
297
+ * 真人输入只认 `user_message` / `item_completed.UserMessage`;`role:"user"`
298
+ * response_item 是注入 scaffolding,只用于漂移探针,不进 displayTitle。
125
299
  */
126
300
  function scanCodexRollout(filePath, includeEvents) {
127
- const lines = readJsonlLines(filePath);
128
- const events = [];
129
- let sessionMeta = null;
130
- // Real user input comes from the authoritative "human typed this" event —
131
- // `user_message` on old codex, `item_completed`/UserMessage on new codex
132
- // (see `extractRealUserText`). Injected scaffolding (project rules,
133
- // <environment_context>, permissions) is written as `role:"user"`
134
- // response_items but gets NEITHER event, so it stays excluded from
135
- // displayTitle/recap/summary. Regression 2026-07-13: the first `role:user`
136
- // message is the injected project rules, mistakenly shown as "最近输入".
137
- const realUserMessages = [];
138
- // Which channel produced the last pushed message — used only to collapse a
139
- // hypothetical double-write (same turn logged by both the legacy and the
140
- // item channel), never to drop real consecutive input.
141
- let lastUserMessageSource = null;
142
- // Drift probe: count of `role:"user"` response_items (injected scaffolding
143
- // included). If a file has those but yields zero real user messages, the
144
- // event vocabulary changed again and displayTitle silently went empty —
145
- // exactly the 0.153 compatibility incident shape.
146
- let userResponseItemCount = 0;
147
- // token_count tracking: codex emits cumulative `total_token_usage` and an
148
- // incremental `last_token_usage` **per sampling call** — one turn with tool
149
- // round-trips logs several `token_count` lines. We want the LAST turn's usage
150
- // = the sum of that turn's samplings (matches pi/opencode /resume "last turn"
151
- // display semantics). Turn boundaries come from `task_started`. When
152
- // last_token_usage is absent we derive it as total - previous_total.
153
- let lastRawUsage;
154
- /** 上一条被接受的 raw usage;compacted 分支回退一条时用(见下方注释)。 */
155
- let previousRawUsage;
156
- /**
157
- * 末个 turn 的累计用量;`null` = 文件里没有 `task_started`(旧数据),
158
- * 此时 flow 字段退回「末条 token_count」语义(见 buildUsageFromScan)。
159
- */
160
- let turnUsage = null;
161
- // Context window limit from the same token_count event as lastRawUsage
162
- // (codex reports info.model_context_window per turn; absent on old data).
163
- let lastContextLimit;
164
- // Final total_token_usage (cumulative across the whole session) for the
165
- // Run card's "累计" display. Codex emits this alongside last_token_usage.
166
- let lastTotalUsage;
167
- let previousTotals;
168
- // Compaction 统计:codex 的 compact turn 在会话文件里写顶层 `compacted`
169
- // 事件(含摘要),压缩前后的上下文水位由相邻 token_count 表达。压缩收尾的
170
- // token_count 增量是 input/cached/output 全 0、只有 total_tokens 有值
171
- // (窗口被摘要+replacement history 占据),现有 zero-filter 会跳过它,
172
- // 因此这里必须单独捕获,不能复用 lastRawUsage。
173
- // 单位口径:前后两侧统一用 total_tokens —— 压缩前取最近一次
174
- // 非零 token_count 的 total_tokens(input+output,即该 turn 在窗口内的全部
175
- // token),压缩后取收尾事件的 total_tokens(压缩后窗口)。不得一边 input
176
- // 一边 total,否则「压缩前 X → 压缩后 Y」两边单位不一致。
177
- let compactCount = 0;
178
- let compactPreContextLength;
179
- let compactPostContextLength;
180
- for (const line of lines) {
301
+ const state = newScanState();
302
+ for (const line of readJsonlLines(filePath)) {
181
303
  const trimmed = line.trim();
182
304
  if (!trimmed)
183
305
  continue;
@@ -185,138 +307,15 @@ function scanCodexRollout(filePath, includeEvents) {
185
307
  const parsed = JSON.parse(trimmed);
186
308
  if (!parsed || typeof parsed.type !== 'string')
187
309
  continue;
188
- if (parsed.type === 'session_meta' && parsed.payload) {
189
- sessionMeta = parsed.payload;
190
- }
191
- else if (parsed.type === 'event_msg' && parsed.payload) {
192
- const payload = parsed.payload;
193
- const human = extractRealUserText(payload);
194
- if (human) {
195
- const text = human.text.slice(0, 200);
196
- const prev = realUserMessages[realUserMessages.length - 1];
197
- // 过渡期防御:同一 turn 若被旧版 user_message 与新版 item_completed
198
- // 各写一次,会得到相邻重复。仅在「通道切换且文本与上一条完全相同」
199
- // 时跳过——同通道连续相同输入(真人连发两条一样的话)不受影响。
200
- const duplicateAcrossChannels = lastUserMessageSource !== null &&
201
- lastUserMessageSource !== human.source &&
202
- prev === text;
203
- if (!duplicateAcrossChannels) {
204
- realUserMessages.push(text);
205
- }
206
- lastUserMessageSource = human.source;
207
- }
208
- else if (payload.type === 'task_started') {
209
- // turn 边界:一个 turn 一条 task_started(与 task_complete 成对,
210
- // turn_id 见 session_meta 之后的 `turn_context`)。从这里开始重新
211
- // 累计本 turn 的用量。
212
- turnUsage = emptyTurnUsage();
213
- }
214
- else if (payload.type === 'token_count') {
215
- const info = payload.info;
216
- const last = info?.last_token_usage;
217
- const total = info?.total_token_usage;
218
- let raw;
219
- if (last) {
220
- raw = last;
221
- }
222
- else if (total && previousTotals) {
223
- raw = subtractRawUsage(total, previousTotals);
224
- }
225
- else {
226
- raw = total;
227
- }
228
- if (total) {
229
- previousTotals = total;
230
- lastTotalUsage = total;
231
- }
232
- if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
233
- previousRawUsage = lastRawUsage;
234
- lastRawUsage = raw;
235
- const ctxWindow = info?.model_context_window;
236
- lastContextLimit = typeof ctxWindow === 'number' ? ctxWindow : undefined;
237
- if (turnUsage) {
238
- const cached = Math.min(raw.cached_input_tokens, raw.input_tokens);
239
- turnUsage.input += raw.input_tokens - cached;
240
- turnUsage.cached += cached;
241
- turnUsage.output += raw.output_tokens;
242
- turnUsage.total += raw.total_tokens;
243
- }
244
- }
245
- // 压缩后的水位:压缩收尾 token_count 全 0(raw 不满足上面 zero-filter),
246
- // 但 total_tokens 表达真实窗口;之后若出现普通 turn(input>0),
247
- // 水位回归该 turn,post-compact 值失效。
248
- if (compactCount > 0 && raw) {
249
- if (raw.total_tokens &&
250
- !raw.input_tokens &&
251
- !raw.cached_input_tokens &&
252
- !raw.output_tokens) {
253
- compactPostContextLength = raw.total_tokens;
254
- }
255
- else if (raw.input_tokens > 0) {
256
- compactPostContextLength = undefined;
257
- }
258
- }
259
- }
260
- }
261
- else if (parsed.type === 'compacted') {
262
- // 顶层 compacted 事件:压缩次数 +1,压缩前水位取「压缩调用之前」那条非零
263
- // token_count 的 total_tokens。真实 rollout 落盘顺序是
264
- // 末轮 → 摘要调用 → compacted → 压缩后水位:摘要调用自身的 token_count 紧贴
265
- // 在 compacted 之前,直接取 lastRawUsage 会把摘要调用的消耗当成压缩前窗口
266
- // (实测把 363K 的窗口显示成 207K)。compacted 自带的摘要调用 usage 用于判定
267
- // 并回退一条;旧数据没有 latest_token_usage_record 时保持原语义。
268
- compactCount++;
269
- const compactCallTotal = readCompactedCallTotal(parsed.payload);
270
- const preCompactUsage = compactCallTotal !== undefined && lastRawUsage?.total_tokens === compactCallTotal
271
- ? previousRawUsage
272
- : lastRawUsage;
273
- if (preCompactUsage?.total_tokens !== undefined) {
274
- compactPreContextLength = preCompactUsage.total_tokens;
275
- }
276
- }
277
- else if (includeEvents && parsed.type === 'response_item' && parsed.payload) {
278
- const payload = parsed.payload;
279
- if (payload.type === 'message' && Array.isArray(payload.content)) {
280
- if (payload.role === 'user')
281
- userResponseItemCount++;
282
- // Extract text content for display
283
- const messages = extractMessageContent(payload);
284
- for (const msg of messages) {
285
- events.push({
286
- type: msg.role === 'user' ? 'user' : 'assistant',
287
- content: msg.text,
288
- timestamp: parsed.timestamp,
289
- });
290
- }
291
- }
292
- }
310
+ dispatchRolloutLine(state, parsed, includeEvents);
293
311
  }
294
312
  catch {
295
313
  // Skip malformed lines
296
314
  continue;
297
315
  }
298
316
  }
299
- // 兼容性漂移探针:文件里有 `role:"user"` 注入,却识别不出任何真人输入 ——
300
- // 说明 codex 又换了 event_msg 词表,displayTitle 会静默变空(0.153 事故形态)。
301
- // 只告警不兜底:误用注入文本当标题正是 2026-07-13 修掉的缺陷。
302
- if (realUserMessages.length === 0 && userResponseItemCount > 0) {
303
- getLogger().warn(`[codex-rollout-reader] no human user message recognised in ${filePath} ` +
304
- `(${userResponseItemCount} role:"user" response_items seen) — codex rollout ` +
305
- `event vocabulary may have changed; session title will be empty`);
306
- }
307
- return {
308
- sessionMeta,
309
- events,
310
- realUserMessages,
311
- lastRawUsage,
312
- lastContextLimit,
313
- lastTotalUsage,
314
- previousTotals,
315
- turnUsage,
316
- compactCount,
317
- compactPreContextLength,
318
- compactPostContextLength,
319
- };
317
+ warnIfVocabularyDrift(state, filePath);
318
+ return scanResult(state);
320
319
  }
321
320
  /**
322
321
  * Build ccusage-aligned usage from a scan's last-turn raw tokens.
@@ -720,7 +719,7 @@ export function clearSessionIndexCache() {
720
719
  * event. Codex emitted it ONLY for text the human actually typed; injected
721
720
  * scaffolding (project rules, `<environment_context>`, permissions) is
722
721
  * written as `role:"user"` response_items but gets NO such event, so it stays
723
- * out of displayTitle/recap/summary. (Regression 2026-07-13: the first
722
+ * out of displayTitle/recap/summary. (Without this, the first
724
723
  * `role:user` message is the injected project rules, once shown as 最近输入.)
725
724
  * - Current (CLI ≥0.153.4, transitional from 0.147.0): the `user_message` /
726
725
  * `agent_message` events were removed; the same authoritative text now rides
@@ -729,7 +728,7 @@ export function clearSessionIndexCache() {
729
728
  * input, `skill` parts are injected skill files (e.g. a `$skill-name`
730
729
  * invocation) that must not leak into the title.
731
730
  *
732
- * Real-data note (2026-09): no rollout file observed carries BOTH spellings;
731
+ * Real-data note: no rollout file observed carries BOTH spellings;
733
732
  * 0.147–0.149 are transitional (either spelling per file), 0.153.4+/0.154.0 are
734
733
  * item_completed-only. `source` reports which spelling matched so the scanner
735
734
  * can collapse a hypothetical double-write of the same turn.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * session list 故障路径的统一出口:error 日志 + 抛契约文案 + cause 保留。
3
+ *
4
+ * 契约红线:错误文案固定为 `<agent> session list 读取失败: <msg>`(逐字节),
5
+ * router 靠它展示「列表失败」——静默 [] 会让用户误以为没有 session,
6
+ * 与真空不可区分。
7
+ *
8
+ * 各家 ENOENT → 优雅空态的分支是正常路径,留在调用点,不经过这里。
9
+ */
10
+ export declare function throwSessionListError(agent: string, err: unknown): never;
@@ -0,0 +1,16 @@
1
+ import { getLogger } from '../../logger/index.js';
2
+ /**
3
+ * session list 故障路径的统一出口:error 日志 + 抛契约文案 + cause 保留。
4
+ *
5
+ * 契约红线:错误文案固定为 `<agent> session list 读取失败: <msg>`(逐字节),
6
+ * router 靠它展示「列表失败」——静默 [] 会让用户误以为没有 session,
7
+ * 与真空不可区分。
8
+ *
9
+ * 各家 ENOENT → 优雅空态的分支是正常路径,留在调用点,不经过这里。
10
+ */
11
+ export function throwSessionListError(agent, err) {
12
+ getLogger().error(`[${agent}-session-reader] session list failed: ${err}`);
13
+ const msg = err instanceof Error ? err.message : String(err);
14
+ throw new Error(`${agent} session list 读取失败: ${msg}`, { cause: err });
15
+ }
16
+ //# sourceMappingURL=throw-session-list-error.js.map