lark-remote 0.1.12 → 0.2.0
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 +37 -0
- package/README.en.md +2 -6
- package/README.md +3 -7
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/inbound-media.d.ts +0 -3
- package/dist/bridge/inbound-media.js +0 -1
- package/dist/bridge/index.d.ts +7 -7
- package/dist/bridge/index.js +100 -133
- package/dist/bridge/queue-manager.js +4 -3
- package/dist/card/approval-render.js +39 -94
- package/dist/card/bash-renderer.js +12 -21
- package/dist/card/card-shared.d.ts +14 -5
- package/dist/card/card-shared.js +23 -34
- package/dist/card/run-renderer.d.ts +0 -22
- package/dist/card/run-renderer.js +206 -153
- package/dist/card/run-state.d.ts +29 -1
- package/dist/card/run-state.js +172 -84
- package/dist/card/text-truncate.d.ts +0 -9
- package/dist/card/text-truncate.js +0 -9
- package/dist/card/tool-render.d.ts +4 -1
- package/dist/card/tool-render.js +76 -32
- package/dist/cli.js +2 -2
- package/dist/common/fs.d.ts +14 -0
- package/dist/common/fs.js +57 -3
- package/dist/common/truncate.d.ts +0 -6
- package/dist/common/truncate.js +1 -1
- package/dist/common/ttl-cache.d.ts +24 -0
- package/dist/common/ttl-cache.js +53 -0
- package/dist/config/codex-config.js +14 -11
- package/dist/config/index.d.ts +0 -8
- package/dist/config/index.js +0 -16
- package/dist/config/kimi-config.d.ts +1 -1
- package/dist/config/kimi-config.js +2 -1
- package/dist/config/opencode-config.d.ts +1 -1
- package/dist/config/opencode-config.js +16 -10
- package/dist/config/qr.d.ts +36 -0
- package/dist/config/qr.js +124 -0
- package/dist/config/wizard.js +69 -27
- package/dist/connector/index.d.ts +3 -2
- package/dist/connector/index.js +5 -3
- package/dist/index.js +14 -41
- package/dist/instance-lock.js +7 -0
- package/dist/persistence/atomic-write.d.ts +2 -0
- package/dist/persistence/atomic-write.js +20 -5
- package/dist/persistence/load-json-file.d.ts +1 -2
- package/dist/persistence/load-json-file.js +2 -3
- package/dist/platform/agent-stopper.d.ts +22 -0
- package/dist/platform/agent-stopper.js +19 -0
- package/dist/platform/command.d.ts +11 -0
- package/dist/platform/command.js +107 -0
- package/dist/platform/identity.d.ts +33 -0
- package/dist/platform/identity.js +187 -0
- package/dist/platform/path.d.ts +24 -0
- package/dist/platform/path.js +110 -0
- package/dist/platform/select.d.ts +9 -0
- package/dist/platform/select.js +12 -0
- package/dist/platform/shell.d.ts +51 -0
- package/dist/platform/shell.js +75 -0
- package/dist/platform/spawn.d.ts +41 -0
- package/dist/platform/spawn.js +77 -0
- package/dist/platform/terminator-posix.d.ts +38 -0
- package/dist/platform/terminator-posix.js +103 -0
- package/dist/platform/terminator-win32.d.ts +21 -0
- package/dist/platform/terminator-win32.js +110 -0
- package/dist/platform/terminator.d.ts +44 -0
- package/dist/platform/terminator.js +24 -0
- package/dist/platform/types.d.ts +23 -0
- package/dist/platform/types.js +5 -0
- package/dist/restart.js +4 -2
- package/dist/router/card-helpers.d.ts +33 -6
- package/dist/router/card-helpers.js +67 -11
- package/dist/router/config/common/render.d.ts +1 -1
- package/dist/router/config/common/render.js +1 -1
- package/dist/router/config-diff.d.ts +4 -0
- package/dist/router/config-diff.js +70 -0
- package/dist/router/index.d.ts +73 -6
- package/dist/router/index.js +663 -590
- package/dist/router/utils.d.ts +6 -0
- package/dist/router/utils.js +12 -0
- package/dist/runner/bash/runner.d.ts +9 -2
- package/dist/runner/bash/runner.js +45 -20
- package/dist/runner/claude/session.d.ts +4 -4
- package/dist/runner/claude/session.js +18 -9
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- package/dist/runner/codex/app-server/runner.d.ts +1 -16
- package/dist/runner/codex/app-server/runner.js +2 -2
- package/dist/runner/codex/app-server/translator.d.ts +18 -0
- package/dist/runner/codex/app-server/translator.js +146 -0
- package/dist/runner/common/acp/base-acp-runner.d.ts +116 -0
- package/dist/runner/common/acp/base-acp-runner.js +248 -0
- package/dist/runner/common/acp/base-translator.d.ts +141 -0
- package/dist/runner/common/acp/base-translator.js +271 -0
- package/dist/runner/common/acp/protocol-helpers.d.ts +70 -0
- package/dist/runner/common/acp/protocol-helpers.js +82 -0
- package/dist/runner/common/connection-based-runner.d.ts +0 -2
- package/dist/runner/common/connection-based-runner.js +1 -1
- package/dist/runner/common/jsonrpc/transport.d.ts +2 -0
- package/dist/runner/common/jsonrpc/transport.js +25 -5
- package/dist/runner/common/process-stopper.d.ts +4 -26
- package/dist/runner/common/process-stopper.js +4 -76
- package/dist/runner/common/runner-utils.d.ts +0 -5
- package/dist/runner/common/runner-utils.js +0 -7
- package/dist/runner/common/spawning-runner.d.ts +42 -132
- package/dist/runner/common/spawning-runner.js +52 -271
- package/dist/runner/common/usage.d.ts +2 -0
- package/dist/runner/common/usage.js +2 -0
- package/dist/runner/dsh/index.d.ts +0 -2
- package/dist/runner/dsh/index.js +0 -2
- package/dist/runner/dsh/runner.d.ts +0 -2
- package/dist/runner/dsh/runner.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +63 -53
- package/dist/runner/kimi/acp/runner.js +189 -334
- package/dist/runner/kimi/acp/translator.d.ts +18 -101
- package/dist/runner/kimi/acp/translator.js +61 -255
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +74 -297
- package/dist/runner/opencode/acp/translator.d.ts +28 -104
- package/dist/runner/opencode/acp/translator.js +54 -249
- package/dist/runner/pi/rpc/index.d.ts +0 -2
- package/dist/runner/pi/rpc/index.js +0 -2
- package/dist/runner/pi/rpc/translator.d.ts +3 -0
- package/dist/runner/pi/rpc/translator.js +47 -9
- package/dist/runner/probe.d.ts +8 -11
- package/dist/runner/probe.js +17 -21
- package/dist/runner/registry.d.ts +4 -4
- package/dist/runner/registry.js +4 -4
- package/dist/runner/resolve-agent-choices.js +6 -2
- package/dist/runner/sync-agent-choices.js +1 -1
- package/dist/runner/types.d.ts +38 -1
- package/dist/session/claude/session-index.js +3 -1
- package/dist/session/claude/sessions.js +37 -64
- package/dist/session/codex/rollout-reader.d.ts +10 -14
- package/dist/session/codex/rollout-reader.js +126 -48
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/pagination.d.ts +0 -2
- package/dist/session/common/pagination.js +1 -1
- package/dist/session/common/recency.d.ts +7 -0
- package/dist/session/common/recency.js +10 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/common/usage-accumulator.d.ts +14 -0
- package/dist/session/common/usage-accumulator.js +15 -0
- package/dist/session/dsh/sessions.js +15 -11
- package/dist/session/kimi/sessions.d.ts +24 -4
- package/dist/session/kimi/sessions.js +99 -103
- package/dist/session/opencode/sessions.d.ts +2 -14
- package/dist/session/opencode/sessions.js +35 -15
- package/dist/session/pi/sessions.js +64 -95
- package/dist/update/index.d.ts +2 -3
- package/dist/update/index.js +1 -2
- package/dist/update/install.d.ts +2 -1
- package/dist/update/install.js +58 -9
- package/dist/update/version-check.d.ts +5 -0
- package/dist/update/version-check.js +9 -0
- package/package.json +9 -7
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
|
@@ -9,9 +9,13 @@ export function resolveAgentChoices(config) {
|
|
|
9
9
|
if (agent === 'claude') {
|
|
10
10
|
return config;
|
|
11
11
|
}
|
|
12
|
-
const agentChoices = choices[agent];
|
|
13
12
|
const fields = choiceFieldsFor(agent);
|
|
14
|
-
|
|
13
|
+
// dsh 的 choices 恒为空(host 是连接配置而非 per-run choice),无需恢复
|
|
14
|
+
if (!fields || fields.length === 0) {
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
const agentChoices = choices[agent];
|
|
18
|
+
if (!agentChoices) {
|
|
15
19
|
return config;
|
|
16
20
|
}
|
|
17
21
|
// Clone to avoid mutation
|
|
@@ -2,7 +2,7 @@ import { choiceFieldsFor } from './agent-choices-common.js';
|
|
|
2
2
|
export function syncAgentChoices(config, agent) {
|
|
3
3
|
// claude 的配置在顶层 config.claude,不在 agents 下,无需同步
|
|
4
4
|
const fields = choiceFieldsFor(agent);
|
|
5
|
-
if (!fields)
|
|
5
|
+
if (!fields || fields.length === 0)
|
|
6
6
|
return config;
|
|
7
7
|
const agents = config.agents;
|
|
8
8
|
const agentCfg = agents?.[agent];
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -215,6 +215,10 @@ interface ToolUseContent {
|
|
|
215
215
|
id: string;
|
|
216
216
|
name: string;
|
|
217
217
|
input: unknown;
|
|
218
|
+
/**
|
|
219
|
+
* 工具原始 title(ACP kind 映射后与 name 不同时保存;渲染层做面板副标题)。
|
|
220
|
+
*/
|
|
221
|
+
summary?: string;
|
|
218
222
|
}
|
|
219
223
|
interface ToolResultContent {
|
|
220
224
|
type: 'tool_result';
|
|
@@ -259,6 +263,8 @@ export interface ResultEvent {
|
|
|
259
263
|
/** 当前模型 context window 上限(codex app-server tokenUsage.modelContextWindow
|
|
260
264
|
* 透传)。仅 app-server 提供;缺省时卡片只显示绝对量、不显示百分比。 */
|
|
261
265
|
context_limit?: number;
|
|
266
|
+
/** 推理 token(pi 提供)。缺省不展示。 */
|
|
267
|
+
reasoning_tokens?: number;
|
|
262
268
|
};
|
|
263
269
|
total_cost_usd?: number;
|
|
264
270
|
errorMessage?: string;
|
|
@@ -275,6 +281,21 @@ export interface PlanEvent {
|
|
|
275
281
|
plan: string;
|
|
276
282
|
timestamp?: string;
|
|
277
283
|
}
|
|
284
|
+
/** 运行期通知(warning、模型重路由、压缩提示等)。translator 产出,公共层无分支渲染。 */
|
|
285
|
+
export interface NoticeEvent {
|
|
286
|
+
type: 'notice';
|
|
287
|
+
level: 'info' | 'warn' | 'error';
|
|
288
|
+
code?: string;
|
|
289
|
+
text: string;
|
|
290
|
+
timestamp?: string;
|
|
291
|
+
}
|
|
292
|
+
/** 会话元信息(ACP session_info_update / current_mode_update)。字段缺省表示本次未变。 */
|
|
293
|
+
export interface SessionInfoEvent {
|
|
294
|
+
type: 'session_info';
|
|
295
|
+
title?: string;
|
|
296
|
+
mode?: string;
|
|
297
|
+
timestamp?: string;
|
|
298
|
+
}
|
|
278
299
|
/** File change event — for agents that report file modifications (e.g. Codex). */
|
|
279
300
|
export interface FileChangeEvent {
|
|
280
301
|
type: 'file_change';
|
|
@@ -319,14 +340,30 @@ export interface TurnDiffEvent {
|
|
|
319
340
|
* tool status ok, completedAt stamped. Never reorders blocks.
|
|
320
341
|
*/
|
|
321
342
|
complete?: boolean;
|
|
343
|
+
/**
|
|
344
|
+
* Kimi ACP snapshot semantics: replace the existing block's anchor
|
|
345
|
+
* timestamp with THIS diff's timestamp on every update (text/reasoning).
|
|
346
|
+
* Kimi's `'text'`/`'thinking'` are whole-turn accumulators that keep
|
|
347
|
+
* receiving late deltas while tools run, so a first-sight timestamp makes
|
|
348
|
+
* the header (and its position after move-to-bottom) look stale. Other
|
|
349
|
+
* snapshot streams (codex app-server) keep the first-sight timestamp by
|
|
350
|
+
* contract — this flag stays false/undefined for them.
|
|
351
|
+
*/
|
|
352
|
+
refreshTimestamp?: boolean;
|
|
322
353
|
/** Authoritative tool status at completion (commandExecution item). */
|
|
323
354
|
toolStatus?: 'ok' | 'error';
|
|
355
|
+
/** 工具身份(commandExecution/mcpToolCall 等 item 首次发出时携带)。 */
|
|
356
|
+
toolName?: string;
|
|
357
|
+
toolInput?: Record<string, unknown>;
|
|
358
|
+
toolHint?: ToolHint;
|
|
324
359
|
threadId: string;
|
|
325
360
|
turnId: string;
|
|
326
361
|
timestamp?: string;
|
|
327
362
|
}
|
|
328
363
|
export type RunnerLifetime = 'turn' | 'workspace';
|
|
329
|
-
|
|
364
|
+
/** 工具渲染 hint:runner 自愿声明,渲染层只消费不推断。 */
|
|
365
|
+
export type ToolHint = 'shell' | 'file' | 'search' | 'web' | 'patch';
|
|
366
|
+
export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent;
|
|
330
367
|
export interface SpawnOptions {
|
|
331
368
|
cwd: string;
|
|
332
369
|
sessionId?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { scanJsonlLines } from '../common/jsonl.js';
|
|
4
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
4
5
|
import { extractContentBlocks } from '../common/content-blocks.js';
|
|
5
6
|
import { getLogger } from '../../logger/index.js';
|
|
6
7
|
// ─── Claude content block mapping (shared with sessions.ts) ─
|
|
@@ -175,7 +176,8 @@ export class SessionIndex {
|
|
|
175
176
|
byId.set(entry.sessionId, entry);
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
|
-
const sorted = [...byId.values()]
|
|
179
|
+
const sorted = [...byId.values()];
|
|
180
|
+
sortByRecencyDesc(sorted, (s) => s.mtimeMs, (s) => s.sessionId);
|
|
179
181
|
const offset = opts.offset ?? 0;
|
|
180
182
|
const limit = opts.limit;
|
|
181
183
|
const sliced = limit === undefined ? sorted.slice(offset) : sorted.slice(offset, offset + limit);
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import { findJsonlLine, readLastJsonlLine
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { findJsonlLine, readLastJsonlLine } from '../common/jsonl.js';
|
|
5
|
+
import { isStale } from '../common/constants.js';
|
|
6
|
+
import { samePath, encodeProjectDirName } from '../../platform/path.js';
|
|
7
7
|
import { extractContentBlocks } from '../common/content-blocks.js';
|
|
8
|
-
import {
|
|
8
|
+
import { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
|
|
9
|
+
import { UsageAccumulator, contextWindowOccupancy, cumulativeUsageFields, } from '../common/usage-accumulator.js';
|
|
9
10
|
import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } from './session-index.js';
|
|
10
11
|
/**
|
|
11
12
|
* Directory Claude Code uses to store session transcripts for a given cwd.
|
|
12
13
|
* Claude encodes the cwd by replacing `/` with `-` (e.g.
|
|
13
14
|
* `/Users/x/proj` -> `-Users-x-proj`). Underscores are also replaced with `-`.
|
|
15
|
+
* win32 上 `\` 同样是分隔符、`:` 是非法文件名字符,一并归一为 `-`
|
|
16
|
+
* (`C:\Users\x\proj` -> `C--Users-x-proj`);见 encodeProjectDirName。
|
|
14
17
|
*
|
|
15
18
|
* Note: this encoding is LOSSY (`disk_d/foo` and `disk-d/foo` both encode to
|
|
16
19
|
* `disk-d-foo`). It is only safe to use the result to **locate** files; never
|
|
@@ -18,8 +21,7 @@ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } fr
|
|
|
18
21
|
* from the JSONL content (regression 2026-06-21 /resume & /cd 路径错乱).
|
|
19
22
|
*/
|
|
20
23
|
function projectDirForCwd(cwd, projectsDir) {
|
|
21
|
-
|
|
22
|
-
return path.join(projectsDir, encoded);
|
|
24
|
+
return path.join(projectsDir, encodeProjectDirName(cwd));
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* cwd guard for the index-miss fallback (readSessionContent /
|
|
@@ -35,7 +37,8 @@ function fileContainsCwd(filePath, cwd) {
|
|
|
35
37
|
return (findJsonlLine(filePath, (l) => {
|
|
36
38
|
try {
|
|
37
39
|
const obj = JSON.parse(l);
|
|
38
|
-
|
|
40
|
+
// PathKit.samePath:darwin/win32 大小写不敏感 + 尾分隔符不敏感,linux 严格
|
|
41
|
+
return typeof obj.cwd === 'string' && samePath(obj.cwd, cwd);
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
41
44
|
return false; // skip malformed line
|
|
@@ -144,7 +147,7 @@ export function getNewestSession(cwd, opts = {}) {
|
|
|
144
147
|
*/
|
|
145
148
|
function isSessionActive(filePath, mtimeMs, now = Date.now()) {
|
|
146
149
|
// Hard signal: file untouched for over an hour → can't be from a live process
|
|
147
|
-
if (
|
|
150
|
+
if (isStale(mtimeMs, now))
|
|
148
151
|
return false;
|
|
149
152
|
const lastLine = readLastJsonlLine(filePath);
|
|
150
153
|
if (!lastLine)
|
|
@@ -239,14 +242,7 @@ function scalarScan(filePath) {
|
|
|
239
242
|
let lastAiTitle;
|
|
240
243
|
let lastRecap;
|
|
241
244
|
let lastUserMessage;
|
|
242
|
-
|
|
243
|
-
let obj;
|
|
244
|
-
try {
|
|
245
|
-
obj = JSON.parse(line);
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
245
|
+
scanJsonlOnce(filePath, (obj, line, offset) => {
|
|
250
246
|
// --- usage aggregation (was aggregateSessionUsage) ---
|
|
251
247
|
if (obj.type === 'assistant') {
|
|
252
248
|
const msg = obj.message;
|
|
@@ -273,12 +269,11 @@ function scalarScan(filePath) {
|
|
|
273
269
|
}
|
|
274
270
|
// --- last user message offset ---
|
|
275
271
|
// Record the byte offset where the tail should start: the beginning of
|
|
276
|
-
// the NEXT line after this user message
|
|
277
|
-
// start offset + its byte length + 1 (the trailing '\n').
|
|
272
|
+
// the NEXT line after this user message (see tailOffsetAfter).
|
|
278
273
|
if (obj.type === 'user') {
|
|
279
274
|
const msg = obj.message;
|
|
280
275
|
if (msg?.role === 'user') {
|
|
281
|
-
tailOffset =
|
|
276
|
+
tailOffset = tailOffsetAfter(line, offset);
|
|
282
277
|
}
|
|
283
278
|
}
|
|
284
279
|
// --- ai-title extraction ---
|
|
@@ -329,7 +324,7 @@ function scalarScan(filePath) {
|
|
|
329
324
|
// contract — excludes output/reasoning, which are generated, not part of
|
|
330
325
|
// the input window). Take the max so a pre-compact turn larger than the
|
|
331
326
|
// post-compact size is not under-reported.
|
|
332
|
-
const lastWindow = l ? l
|
|
327
|
+
const lastWindow = l ? contextWindowOccupancy(l) : 0;
|
|
333
328
|
let contextLength = lastPostTokens;
|
|
334
329
|
if (contextLength === 0 || lastWindow > contextLength) {
|
|
335
330
|
contextLength = lastWindow;
|
|
@@ -347,11 +342,7 @@ function scalarScan(filePath) {
|
|
|
347
342
|
cacheReadTokens: l && l.cacheRead > 0 ? l.cacheRead : undefined,
|
|
348
343
|
cacheCreationTokens: l && l.cacheCreation > 0 ? l.cacheCreation : undefined,
|
|
349
344
|
totalTokens: lastTotal,
|
|
350
|
-
|
|
351
|
-
cumulativeInputTokens: t.input,
|
|
352
|
-
cumulativeOutputTokens: t.output,
|
|
353
|
-
cumulativeCacheReadTokens: t.cacheRead,
|
|
354
|
-
cumulativeCacheCreationTokens: t.cacheCreation,
|
|
345
|
+
...cumulativeUsageFields(t),
|
|
355
346
|
};
|
|
356
347
|
return {
|
|
357
348
|
tailOffset,
|
|
@@ -362,47 +353,30 @@ function scalarScan(filePath) {
|
|
|
362
353
|
};
|
|
363
354
|
}
|
|
364
355
|
/**
|
|
365
|
-
* P2-2 + P2-5 second-pass:
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* object memory are O(tail), not O(whole file) — P3-1 + P2-5.
|
|
356
|
+
* P2-2 + P2-5 second-pass tail mapper: expand one parsed tail line into
|
|
357
|
+
* content-block events. Re-parses only tail lines (scanTailEvents does the
|
|
358
|
+
* offset re-read + line filtering), so both raw line-string memory and
|
|
359
|
+
* parsed-object memory are O(tail), not O(whole file) — P3-1 + P2-5.
|
|
370
360
|
*
|
|
371
|
-
* `maxEvents` (
|
|
372
|
-
*
|
|
373
|
-
* matching codex/opencode/pi's `slice(-maxEvents)`). The whole tail is parsed
|
|
374
|
-
* first (usage/title extraction above already scanned the file; this pass only
|
|
375
|
-
* re-parses the short tail), then `slice(-maxEvents)` keeps the most recent
|
|
376
|
-
* events. `maxEvents <= 0` returns `[]` (guards the `slice(-0) === slice(0)`
|
|
377
|
-
* full-array trap).
|
|
361
|
+
* `maxEvents` capping (review P2-7 — unified "last N events" contract across
|
|
362
|
+
* all readers) is applied by scanTailEvents.
|
|
378
363
|
*/
|
|
379
|
-
function
|
|
364
|
+
function mapClaudeTailLine(obj) {
|
|
365
|
+
const message = obj.message;
|
|
366
|
+
// Expand all content blocks from this message into separate events.
|
|
367
|
+
const blocks = extractContentBlocks(message?.content, CLAUDE_MAPPING);
|
|
368
|
+
const role = message?.role ?? obj.type ?? 'unknown';
|
|
369
|
+
const timestamp = obj.timestamp;
|
|
380
370
|
const events = [];
|
|
381
|
-
for (const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const message = obj.message;
|
|
392
|
-
// Expand all content blocks from this message into separate events.
|
|
393
|
-
const blocks = extractContentBlocks(message?.content, CLAUDE_MAPPING);
|
|
394
|
-
const role = message?.role ?? obj.type ?? 'unknown';
|
|
395
|
-
const timestamp = obj.timestamp;
|
|
396
|
-
for (const block of blocks) {
|
|
397
|
-
events.push({ type: block.type, content: block.content, timestamp });
|
|
398
|
-
}
|
|
399
|
-
// If message has no content blocks (e.g., metadata-only lines like last-prompt),
|
|
400
|
-
// still emit an event with the raw type so users see what happened
|
|
401
|
-
if (blocks.length === 0 && message?.role) {
|
|
402
|
-
events.push({ type: role, content: `(${role} event)`, timestamp });
|
|
403
|
-
}
|
|
371
|
+
for (const block of blocks) {
|
|
372
|
+
events.push({ type: block.type, content: block.content, timestamp });
|
|
373
|
+
}
|
|
374
|
+
// If message has no content blocks (e.g., metadata-only lines like last-prompt),
|
|
375
|
+
// still emit an event with the raw type so users see what happened
|
|
376
|
+
if (blocks.length === 0 && message?.role) {
|
|
377
|
+
events.push({ type: role, content: `(${role} event)`, timestamp });
|
|
404
378
|
}
|
|
405
|
-
return
|
|
379
|
+
return events;
|
|
406
380
|
}
|
|
407
381
|
export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
408
382
|
const { maxEvents } = opts;
|
|
@@ -462,8 +436,7 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
462
436
|
// only the tail. If no user message was found (tailOffset === -1), fall
|
|
463
437
|
// back to re-reading the whole file so the card still has something to
|
|
464
438
|
// show.
|
|
465
|
-
const
|
|
466
|
-
const events = extractEventsFromTail(tailLines, maxEvents);
|
|
439
|
+
const events = scanTailEvents(filePath, scan.tailOffset, mapClaudeTailLine, maxEvents);
|
|
467
440
|
// Use aiTitle if available, otherwise fallback to last user message.
|
|
468
441
|
// Truncation is deferred to the consumer (router card building).
|
|
469
442
|
const displayTitle = scan.aiTitle ?? scan.lastUserMessage ?? undefined;
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
* {"timestamp":"...","type":"response_item","payload":{"type":"message","role":"user|developer","content":[...]}}
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
|
+
* Event vocabulary drift (codex CLI ≥0.153.4, transitional from 0.147.0): the
|
|
14
|
+
* top-level `user_message` / `agent_message` events were removed; the same
|
|
15
|
+
* payloads now ride on `event_msg` → `item_completed` envelopes whose
|
|
16
|
+
* `item.type` is `UserMessage` / `AgentMessage` / `CommandExecution` / … .
|
|
17
|
+
* Both spellings stay readable — `extractRealUserText` adapts per file.
|
|
18
|
+
*
|
|
19
|
+
* Deprecation: if ~6 months pass (≈2027-03) with the legacy `user_message` /
|
|
20
|
+
* `agent_message` events still absent from every codex CLI and the old-codex
|
|
21
|
+
* session tail effectively gone, drop the legacy branch + its fixtures.
|
|
22
|
+
*
|
|
13
23
|
* Note: This handles the **rollout file format** (historical logging), NOT the
|
|
14
24
|
* `codex exec --json` stdout format(已随 exec 模式移除)。
|
|
15
25
|
*/
|
|
@@ -21,7 +31,6 @@ interface CodexRolloutEntry {
|
|
|
21
31
|
firstUserMessage: string;
|
|
22
32
|
/** Last real (human-typed) user message, for the "最近输入" card label. */
|
|
23
33
|
lastRealUserMessage: string;
|
|
24
|
-
createdAtMs: number;
|
|
25
34
|
updatedAtMs: number;
|
|
26
35
|
events: AgentSessionContentEvent[];
|
|
27
36
|
/** Last-turn usage extracted from token_count events (ccusage-aligned). */
|
|
@@ -38,19 +47,6 @@ interface ListCodexRolloutsOptions {
|
|
|
38
47
|
offset?: number;
|
|
39
48
|
}
|
|
40
49
|
export declare function readCodexRollout(filePath: string): CodexRolloutEntry | null;
|
|
41
|
-
/**
|
|
42
|
-
* Lightweight single-file scan that extracts only the display-relevant fields
|
|
43
|
-
* (last real user message + usage) — **without** constructing the `events`
|
|
44
|
-
* array. Backs `readCodexSessionSummary` so the `/resume` list prefetch does
|
|
45
|
-
* not pay the cost of JSON.parse-ing every response_item and building events
|
|
46
|
-
* that are immediately discarded.
|
|
47
|
-
*
|
|
48
|
-
* Returns null if the file doesn't exist, is corrupted, or has no session_meta.
|
|
49
|
-
*/
|
|
50
|
-
export declare function readCodexRolloutSummary(filePath: string): {
|
|
51
|
-
lastRealUserMessage: string;
|
|
52
|
-
usage?: AgentSessionUsage;
|
|
53
|
-
} | null;
|
|
54
50
|
/**
|
|
55
51
|
* List Codex sessions from rollout files.
|
|
56
52
|
*
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
* {"timestamp":"...","type":"response_item","payload":{"type":"message","role":"user|developer","content":[...]}}
|
|
11
11
|
* ```
|
|
12
12
|
*
|
|
13
|
+
* Event vocabulary drift (codex CLI ≥0.153.4, transitional from 0.147.0): the
|
|
14
|
+
* top-level `user_message` / `agent_message` events were removed; the same
|
|
15
|
+
* payloads now ride on `event_msg` → `item_completed` envelopes whose
|
|
16
|
+
* `item.type` is `UserMessage` / `AgentMessage` / `CommandExecution` / … .
|
|
17
|
+
* Both spellings stay readable — `extractRealUserText` adapts per file.
|
|
18
|
+
*
|
|
19
|
+
* Deprecation: if ~6 months pass (≈2027-03) with the legacy `user_message` /
|
|
20
|
+
* `agent_message` events still absent from every codex CLI and the old-codex
|
|
21
|
+
* session tail effectively gone, drop the legacy branch + its fixtures.
|
|
22
|
+
*
|
|
13
23
|
* Note: This handles the **rollout file format** (historical logging), NOT the
|
|
14
24
|
* `codex exec --json` stdout format(已随 exec 模式移除)。
|
|
15
25
|
*/
|
|
@@ -17,7 +27,9 @@ import fs from 'node:fs';
|
|
|
17
27
|
import path from 'node:path';
|
|
18
28
|
import { readJsonlLines, findJsonlLine } from '../common/jsonl.js';
|
|
19
29
|
import { STALE_MS } from '../common/constants.js';
|
|
30
|
+
import { samePath } from '../../platform/path.js';
|
|
20
31
|
import { paginate, capEvents } from '../common/pagination.js';
|
|
32
|
+
import { sortByRecencyDesc } from '../common/recency.js';
|
|
21
33
|
import { getLogger } from '../../logger/index.js';
|
|
22
34
|
import { isRecord, stringValue } from '../../common/guards.js';
|
|
23
35
|
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
@@ -45,11 +57,7 @@ export function readCodexRollout(filePath) {
|
|
|
45
57
|
if (!sessionId) {
|
|
46
58
|
return null;
|
|
47
59
|
}
|
|
48
|
-
// Use file mtime as updatedAtMs, or parse timestamp from session_meta
|
|
49
60
|
const stats = fs.statSync(filePath);
|
|
50
|
-
const createdAtMs = sessionMeta.timestamp
|
|
51
|
-
? new Date(String(sessionMeta.timestamp)).getTime()
|
|
52
|
-
: stats.birthtimeMs;
|
|
53
61
|
const updatedAtMs = stats.mtimeMs;
|
|
54
62
|
const firstUserMessage = scan.realUserMessages[0] ?? '';
|
|
55
63
|
const lastRealUserMessage = scan.realUserMessages[scan.realUserMessages.length - 1] ?? '';
|
|
@@ -59,7 +67,6 @@ export function readCodexRollout(filePath) {
|
|
|
59
67
|
cwd,
|
|
60
68
|
firstUserMessage: firstUserMessage || '(no user message)',
|
|
61
69
|
lastRealUserMessage,
|
|
62
|
-
createdAtMs,
|
|
63
70
|
updatedAtMs,
|
|
64
71
|
events: scan.events,
|
|
65
72
|
usage,
|
|
@@ -79,7 +86,7 @@ export function readCodexRollout(filePath) {
|
|
|
79
86
|
*
|
|
80
87
|
* Returns null if the file doesn't exist, is corrupted, or has no session_meta.
|
|
81
88
|
*/
|
|
82
|
-
|
|
89
|
+
function readCodexRolloutSummary(filePath) {
|
|
83
90
|
if (!fs.existsSync(filePath)) {
|
|
84
91
|
return null;
|
|
85
92
|
}
|
|
@@ -107,15 +114,23 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
107
114
|
const lines = readJsonlLines(filePath);
|
|
108
115
|
const events = [];
|
|
109
116
|
let sessionMeta = null;
|
|
110
|
-
// Real user input
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// <environment_context>, permissions) is
|
|
114
|
-
// response_items but
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// "最近输入".
|
|
117
|
+
// Real user input comes from the authoritative "human typed this" event —
|
|
118
|
+
// `user_message` on old codex, `item_completed`/UserMessage on new codex
|
|
119
|
+
// (see `extractRealUserText`). Injected scaffolding (project rules,
|
|
120
|
+
// <environment_context>, permissions) is written as `role:"user"`
|
|
121
|
+
// response_items but gets NEITHER event, so it stays excluded from
|
|
122
|
+
// displayTitle/recap/summary. Regression 2026-07-13: the first `role:user`
|
|
123
|
+
// message is the injected project rules, mistakenly shown as "最近输入".
|
|
118
124
|
const realUserMessages = [];
|
|
125
|
+
// Which channel produced the last pushed message — used only to collapse a
|
|
126
|
+
// hypothetical double-write (same turn logged by both the legacy and the
|
|
127
|
+
// item channel), never to drop real consecutive input.
|
|
128
|
+
let lastUserMessageSource = null;
|
|
129
|
+
// Drift probe: count of `role:"user"` response_items (injected scaffolding
|
|
130
|
+
// included). If a file has those but yields zero real user messages, the
|
|
131
|
+
// event vocabulary changed again and displayTitle silently went empty —
|
|
132
|
+
// exactly the 0.153 compatibility incident shape.
|
|
133
|
+
let userResponseItemCount = 0;
|
|
119
134
|
// token_count tracking: codex emits cumulative `total_token_usage` and an
|
|
120
135
|
// incremental `last_token_usage`. We want the LAST turn's usage (matches
|
|
121
136
|
// pi/opencode /resume "last turn" display semantics). When
|
|
@@ -152,13 +167,21 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
152
167
|
sessionMeta = parsed.payload;
|
|
153
168
|
}
|
|
154
169
|
else if (parsed.type === 'event_msg' && parsed.payload) {
|
|
155
|
-
// user_message events carry the authoritative human-typed text.
|
|
156
170
|
const payload = parsed.payload;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
const human = extractRealUserText(payload);
|
|
172
|
+
if (human) {
|
|
173
|
+
const text = human.text.slice(0, 200);
|
|
174
|
+
const prev = realUserMessages[realUserMessages.length - 1];
|
|
175
|
+
// 过渡期防御:同一 turn 若被旧版 user_message 与新版 item_completed
|
|
176
|
+
// 各写一次,会得到相邻重复。仅在「通道切换且文本与上一条完全相同」
|
|
177
|
+
// 时跳过——同通道连续相同输入(真人连发两条一样的话)不受影响。
|
|
178
|
+
const duplicateAcrossChannels = lastUserMessageSource !== null &&
|
|
179
|
+
lastUserMessageSource !== human.source &&
|
|
180
|
+
prev === text;
|
|
181
|
+
if (!duplicateAcrossChannels) {
|
|
182
|
+
realUserMessages.push(text);
|
|
161
183
|
}
|
|
184
|
+
lastUserMessageSource = human.source;
|
|
162
185
|
}
|
|
163
186
|
else if (payload.type === 'token_count') {
|
|
164
187
|
const info = payload.info;
|
|
@@ -209,6 +232,8 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
209
232
|
else if (includeEvents && parsed.type === 'response_item' && parsed.payload) {
|
|
210
233
|
const payload = parsed.payload;
|
|
211
234
|
if (payload.type === 'message' && Array.isArray(payload.content)) {
|
|
235
|
+
if (payload.role === 'user')
|
|
236
|
+
userResponseItemCount++;
|
|
212
237
|
// Extract text content for display
|
|
213
238
|
const messages = extractMessageContent(payload);
|
|
214
239
|
for (const msg of messages) {
|
|
@@ -226,6 +251,14 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
226
251
|
continue;
|
|
227
252
|
}
|
|
228
253
|
}
|
|
254
|
+
// 兼容性漂移探针:文件里有 `role:"user"` 注入,却识别不出任何真人输入 ——
|
|
255
|
+
// 说明 codex 又换了 event_msg 词表,displayTitle 会静默变空(0.153 事故形态)。
|
|
256
|
+
// 只告警不兜底:误用注入文本当标题正是 2026-07-13 修掉的缺陷。
|
|
257
|
+
if (realUserMessages.length === 0 && userResponseItemCount > 0) {
|
|
258
|
+
getLogger().warn(`[codex-rollout-reader] no human user message recognised in ${filePath} ` +
|
|
259
|
+
`(${userResponseItemCount} role:"user" response_items seen) — codex rollout ` +
|
|
260
|
+
`event vocabulary may have changed; session title will be empty`);
|
|
261
|
+
}
|
|
229
262
|
return {
|
|
230
263
|
sessionMeta,
|
|
231
264
|
events,
|
|
@@ -309,14 +342,14 @@ export function listCodexRollouts(opts = {}) {
|
|
|
309
342
|
// session (main file missing) never pollutes the list or total.
|
|
310
343
|
if (entry.isSubagent)
|
|
311
344
|
continue;
|
|
312
|
-
if (filterCwd && entry.cwd
|
|
345
|
+
if (filterCwd && !samePath(entry.cwd, filterCwd))
|
|
313
346
|
continue;
|
|
314
347
|
matched.push(entry);
|
|
315
348
|
}
|
|
316
349
|
// Establish the global order by mtime desc, then slice the page.
|
|
317
350
|
// Same-mtime ties use filePath as a deterministic secondary key so index
|
|
318
351
|
// rebuilds / walk order changes never reorder the page.
|
|
319
|
-
matched
|
|
352
|
+
sortByRecencyDesc(matched, (e) => e.mtimeMs, (e) => e.filePath);
|
|
320
353
|
const { items: page, total } = paginate(matched, { limit: opts.limit, offset: opts.offset });
|
|
321
354
|
// Full-parse only the page being returned (summary/usage).
|
|
322
355
|
const entries = [];
|
|
@@ -327,6 +360,24 @@ export function listCodexRollouts(opts = {}) {
|
|
|
327
360
|
}
|
|
328
361
|
return { entries, total };
|
|
329
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* W2.6 单源:三处(readCodexSessionContent / readCodexSessionSummary /
|
|
365
|
+
* isCodexSessionActive)共用的「索引查找 + miss 强制刷新重查」。
|
|
366
|
+
* cwd 守卫与 subagent 过滤语义各异,留在调用方。
|
|
367
|
+
*/
|
|
368
|
+
function resolveRolloutEntry(sessionId, codexHome) {
|
|
369
|
+
// P2-4: Use session index for direct file lookup
|
|
370
|
+
let index = getSessionIndex(codexHome);
|
|
371
|
+
let entry = index.get(sessionId);
|
|
372
|
+
// P3-2: On miss, the index may be stale (a rollout file created after the
|
|
373
|
+
// cache was built but within the 5s TTL). Force a refresh and recheck so a
|
|
374
|
+
// brand-new session is found without waiting for TTL expiry.
|
|
375
|
+
if (!entry) {
|
|
376
|
+
index = getSessionIndex(codexHome, true);
|
|
377
|
+
entry = index.get(sessionId);
|
|
378
|
+
}
|
|
379
|
+
return entry;
|
|
380
|
+
}
|
|
330
381
|
/**
|
|
331
382
|
* Read the full content of a specific session by its threadId.
|
|
332
383
|
*
|
|
@@ -342,16 +393,7 @@ export function listCodexRollouts(opts = {}) {
|
|
|
342
393
|
*/
|
|
343
394
|
export function readCodexSessionContent(sessionId, opts = {}) {
|
|
344
395
|
const codexHome = resolveCodexHome(opts.codexHome);
|
|
345
|
-
|
|
346
|
-
let index = getSessionIndex(codexHome);
|
|
347
|
-
let entry = index.get(sessionId);
|
|
348
|
-
// P3-2: On miss, the index may be stale (a rollout file created after the
|
|
349
|
-
// cache was built but within the 5s TTL). Force a refresh and recheck so a
|
|
350
|
-
// brand-new session is found without waiting for TTL expiry.
|
|
351
|
-
if (!entry) {
|
|
352
|
-
index = getSessionIndex(codexHome, true);
|
|
353
|
-
entry = index.get(sessionId);
|
|
354
|
-
}
|
|
396
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
355
397
|
if (!entry) {
|
|
356
398
|
return { events: [] };
|
|
357
399
|
}
|
|
@@ -361,7 +403,7 @@ export function readCodexSessionContent(sessionId, opts = {}) {
|
|
|
361
403
|
// Codex has no relocation (no EnterWorktree equivalent), so a simple equality
|
|
362
404
|
// check suffices — unlike claude which needs jsonlContainsCwd to handle
|
|
363
405
|
// relocated sessions with multiple cwd values.
|
|
364
|
-
if (opts.cwd && entry.cwd
|
|
406
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
365
407
|
return { events: [] };
|
|
366
408
|
}
|
|
367
409
|
const rollout = readCodexRollout(entry.filePath);
|
|
@@ -391,18 +433,13 @@ export function readCodexSessionContent(sessionId, opts = {}) {
|
|
|
391
433
|
*/
|
|
392
434
|
export function readCodexSessionSummary(sessionId, opts = {}) {
|
|
393
435
|
const codexHome = resolveCodexHome(opts.codexHome);
|
|
394
|
-
|
|
395
|
-
let entry = index.get(sessionId);
|
|
396
|
-
if (!entry) {
|
|
397
|
-
index = getSessionIndex(codexHome, true);
|
|
398
|
-
entry = index.get(sessionId);
|
|
399
|
-
}
|
|
436
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
400
437
|
if (!entry) {
|
|
401
438
|
return {};
|
|
402
439
|
}
|
|
403
440
|
// Cwd guard: same semantics as readCodexSessionContent — when a cwd is
|
|
404
441
|
// provided, the session's working directory must match.
|
|
405
|
-
if (opts.cwd && entry.cwd
|
|
442
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
406
443
|
return {};
|
|
407
444
|
}
|
|
408
445
|
const summary = readCodexRolloutSummary(entry.filePath);
|
|
@@ -431,14 +468,7 @@ export function isCodexSessionActive(sessionId, opts = {}) {
|
|
|
431
468
|
// all use STALE_MS). Previously codex used a divergent 10-minute default.
|
|
432
469
|
const threshold = opts.activeThresholdMs ?? STALE_MS;
|
|
433
470
|
// P2-4: Use session index for direct file lookup + mtime check
|
|
434
|
-
|
|
435
|
-
let entry = index.get(sessionId);
|
|
436
|
-
// P3-2: On miss, refresh the index in case the session file was created
|
|
437
|
-
// after the cache was built (within the 5s TTL). See readCodexSessionContent.
|
|
438
|
-
if (!entry) {
|
|
439
|
-
index = getSessionIndex(codexHome, true);
|
|
440
|
-
entry = index.get(sessionId);
|
|
441
|
-
}
|
|
471
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
442
472
|
if (!entry) {
|
|
443
473
|
return false;
|
|
444
474
|
}
|
|
@@ -450,7 +480,7 @@ export function isCodexSessionActive(sessionId, opts = {}) {
|
|
|
450
480
|
}
|
|
451
481
|
// Cwd guard: when a cwd is provided, the session's working directory must
|
|
452
482
|
// match (align with claude/pi/opencode which validate cwd).
|
|
453
|
-
if (opts.cwd && entry.cwd
|
|
483
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
454
484
|
return false;
|
|
455
485
|
}
|
|
456
486
|
// Re-stat to get fresh mtime (index may be up to INDEX_TTL_MS stale)
|
|
@@ -598,6 +628,54 @@ function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
|
|
|
598
628
|
export function clearSessionIndexCache() {
|
|
599
629
|
sessionIndexCache.clear();
|
|
600
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Extract the human-typed text from a rollout `event_msg` payload, adapting to
|
|
633
|
+
* codex's two rollout event vocabularies.
|
|
634
|
+
*
|
|
635
|
+
* - Legacy (CLI ≤0.150.1): a dedicated `{"type":"user_message","message":…}`
|
|
636
|
+
* event. Codex emitted it ONLY for text the human actually typed; injected
|
|
637
|
+
* scaffolding (project rules, `<environment_context>`, permissions) is
|
|
638
|
+
* written as `role:"user"` response_items but gets NO such event, so it stays
|
|
639
|
+
* out of displayTitle/recap/summary. (Regression 2026-07-13: the first
|
|
640
|
+
* `role:user` message is the injected project rules, once shown as 最近输入.)
|
|
641
|
+
* - Current (CLI ≥0.153.4, transitional from 0.147.0): the `user_message` /
|
|
642
|
+
* `agent_message` events were removed; the same authoritative text now rides
|
|
643
|
+
* on `{"type":"item_completed","item":{"type":"UserMessage","content":[…]}}`.
|
|
644
|
+
* A UserMessage may bundle several content parts — `text` parts are the human
|
|
645
|
+
* input, `skill` parts are injected skill files (e.g. a `$skill-name`
|
|
646
|
+
* invocation) that must not leak into the title.
|
|
647
|
+
*
|
|
648
|
+
* Real-data note (2026-09): no rollout file observed carries BOTH spellings;
|
|
649
|
+
* 0.147–0.149 are transitional (either spelling per file), 0.153.4+/0.154.0 are
|
|
650
|
+
* item_completed-only. `source` reports which spelling matched so the scanner
|
|
651
|
+
* can collapse a hypothetical double-write of the same turn.
|
|
652
|
+
*
|
|
653
|
+
* TODO(2027-03-11): drop the legacy branch (and its fixtures) once ~6 months
|
|
654
|
+
* pass with the event still absent from every CLI and old-codex tails gone —
|
|
655
|
+
* mirrors the deprecation note in the module header.
|
|
656
|
+
*/
|
|
657
|
+
function extractRealUserText(payload) {
|
|
658
|
+
if (payload.type === 'user_message') {
|
|
659
|
+
const text = stringValue(payload.message);
|
|
660
|
+
return text ? { text, source: 'legacy' } : null;
|
|
661
|
+
}
|
|
662
|
+
if (payload.type === 'item_completed') {
|
|
663
|
+
const item = payload.item;
|
|
664
|
+
if (!isRecord(item) || item.type !== 'UserMessage' || !Array.isArray(item.content)) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const texts = [];
|
|
668
|
+
for (const part of item.content) {
|
|
669
|
+
if (!isRecord(part) || part.type !== 'text')
|
|
670
|
+
continue;
|
|
671
|
+
const text = stringValue(part.text);
|
|
672
|
+
if (text)
|
|
673
|
+
texts.push(text);
|
|
674
|
+
}
|
|
675
|
+
return texts.length > 0 ? { text: texts.join('\n'), source: 'item' } : null;
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
601
679
|
function extractMessageContent(payload) {
|
|
602
680
|
const result = [];
|
|
603
681
|
const content = payload.content;
|