lark-remote 0.1.13 → 0.2.1
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 +36 -3
- package/README.en.md +7 -11
- package/README.md +7 -11
- package/dist/bridge/final-usage.d.ts +24 -0
- package/dist/bridge/final-usage.js +55 -0
- package/dist/bridge/index.d.ts +0 -7
- package/dist/bridge/index.js +22 -64
- 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 +13 -1
- package/dist/card/card-shared.js +21 -30
- 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/config/codex-config.js +2 -0
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.js +0 -11
- package/dist/config/kimi-config.js +1 -0
- package/dist/config/opencode-config.js +13 -2
- 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 -4
- package/dist/index.js +15 -42
- 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 +66 -6
- package/dist/router/index.js +451 -316
- 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 -1
- package/dist/runner/claude/session.js +18 -4
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -1
- 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 +39 -0
- package/dist/runner/common/acp/protocol-helpers.js +34 -0
- 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/index.d.ts +1 -1
- package/dist/runner/kimi/acp/runner.d.ts +45 -42
- package/dist/runner/kimi/acp/runner.js +140 -293
- package/dist/runner/kimi/acp/translator.d.ts +18 -100
- package/dist/runner/kimi/acp/translator.js +56 -242
- package/dist/runner/opencode/acp/runner.d.ts +22 -46
- package/dist/runner/opencode/acp/runner.js +73 -256
- package/dist/runner/opencode/acp/translator.d.ts +28 -103
- package/dist/runner/opencode/acp/translator.js +51 -236
- 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/types.d.ts +38 -1
- package/dist/session/claude/sessions.js +35 -58
- package/dist/session/codex/rollout-reader.d.ts +10 -0
- package/dist/session/codex/rollout-reader.js +123 -41
- package/dist/session/common/constants.d.ts +7 -0
- package/dist/session/common/constants.js +9 -0
- package/dist/session/common/two-pass.d.ts +22 -0
- package/dist/session/common/two-pass.js +70 -0
- package/dist/session/dsh/sessions.js +7 -6
- package/dist/session/kimi/sessions.d.ts +6 -0
- package/dist/session/kimi/sessions.js +50 -73
- package/dist/session/opencode/sessions.js +26 -8
- package/dist/session/pi/sessions.js +54 -73
- package/dist/update/index.d.ts +1 -2
- 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 -6
- package/dist/update/startup-hint.d.ts +0 -5
- package/dist/update/startup-hint.js +0 -11
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,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 { scanJsonlOnce, tailOffsetAfter, scanTailEvents } from '../common/two-pass.js';
|
|
8
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
|
|
@@ -111,7 +114,7 @@ export function listClaudeSessions(cwd, opts = {}) {
|
|
|
111
114
|
const limit = opts.limit;
|
|
112
115
|
const projectsDir = opts.projectsDir ?? defaultProjectsDir();
|
|
113
116
|
const index = ensureSessionIndex(projectsDir);
|
|
114
|
-
// 首次调用延迟全量建索引;之后 5s
|
|
117
|
+
// 首次调用延迟全量建索引;之后 5s 按需节流增量刷新(拍板 3)。
|
|
115
118
|
index.refresh();
|
|
116
119
|
// 统一 parser 的完整 cwd 集合 → A→B→C 任意中间 cwd 都可列出(核心 bug 修复)。
|
|
117
120
|
// readdir 失败时索引保留旧快照(首次失败则为空),与旧扫描的 readdir 兜底等价。
|
|
@@ -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 ---
|
|
@@ -358,47 +353,30 @@ function scalarScan(filePath) {
|
|
|
358
353
|
};
|
|
359
354
|
}
|
|
360
355
|
/**
|
|
361
|
-
* P2-2 + P2-5 second-pass:
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* 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.
|
|
366
360
|
*
|
|
367
|
-
* `maxEvents` (
|
|
368
|
-
*
|
|
369
|
-
* matching codex/opencode/pi's `slice(-maxEvents)`). The whole tail is parsed
|
|
370
|
-
* first (usage/title extraction above already scanned the file; this pass only
|
|
371
|
-
* re-parses the short tail), then `slice(-maxEvents)` keeps the most recent
|
|
372
|
-
* events. `maxEvents <= 0` returns `[]` (guards the `slice(-0) === slice(0)`
|
|
373
|
-
* full-array trap).
|
|
361
|
+
* `maxEvents` capping (review P2-7 — unified "last N events" contract across
|
|
362
|
+
* all readers) is applied by scanTailEvents.
|
|
374
363
|
*/
|
|
375
|
-
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;
|
|
376
370
|
const events = [];
|
|
377
|
-
for (const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const message = obj.message;
|
|
388
|
-
// Expand all content blocks from this message into separate events.
|
|
389
|
-
const blocks = extractContentBlocks(message?.content, CLAUDE_MAPPING);
|
|
390
|
-
const role = message?.role ?? obj.type ?? 'unknown';
|
|
391
|
-
const timestamp = obj.timestamp;
|
|
392
|
-
for (const block of blocks) {
|
|
393
|
-
events.push({ type: block.type, content: block.content, timestamp });
|
|
394
|
-
}
|
|
395
|
-
// If message has no content blocks (e.g., metadata-only lines like last-prompt),
|
|
396
|
-
// still emit an event with the raw type so users see what happened
|
|
397
|
-
if (blocks.length === 0 && message?.role) {
|
|
398
|
-
events.push({ type: role, content: `(${role} event)`, timestamp });
|
|
399
|
-
}
|
|
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 });
|
|
400
378
|
}
|
|
401
|
-
return
|
|
379
|
+
return events;
|
|
402
380
|
}
|
|
403
381
|
export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
404
382
|
const { maxEvents } = opts;
|
|
@@ -458,8 +436,7 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
458
436
|
// only the tail. If no user message was found (tailOffset === -1), fall
|
|
459
437
|
// back to re-reading the whole file so the card still has something to
|
|
460
438
|
// show.
|
|
461
|
-
const
|
|
462
|
-
const events = extractEventsFromTail(tailLines, maxEvents);
|
|
439
|
+
const events = scanTailEvents(filePath, scan.tailOffset, mapClaudeTailLine, maxEvents);
|
|
463
440
|
// Use aiTitle if available, otherwise fallback to last user message.
|
|
464
441
|
// Truncation is deferred to the consumer (router card building).
|
|
465
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
|
*/
|
|
@@ -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,6 +27,7 @@ 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';
|
|
21
32
|
import { sortByRecencyDesc } from '../common/recency.js';
|
|
22
33
|
import { getLogger } from '../../logger/index.js';
|
|
@@ -103,15 +114,23 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
103
114
|
const lines = readJsonlLines(filePath);
|
|
104
115
|
const events = [];
|
|
105
116
|
let sessionMeta = null;
|
|
106
|
-
// Real user input
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// <environment_context>, permissions) is
|
|
110
|
-
// response_items but
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// "最近输入".
|
|
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 "最近输入".
|
|
114
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;
|
|
115
134
|
// token_count tracking: codex emits cumulative `total_token_usage` and an
|
|
116
135
|
// incremental `last_token_usage`. We want the LAST turn's usage (matches
|
|
117
136
|
// pi/opencode /resume "last turn" display semantics). When
|
|
@@ -148,13 +167,21 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
148
167
|
sessionMeta = parsed.payload;
|
|
149
168
|
}
|
|
150
169
|
else if (parsed.type === 'event_msg' && parsed.payload) {
|
|
151
|
-
// user_message events carry the authoritative human-typed text.
|
|
152
170
|
const payload = parsed.payload;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
157
183
|
}
|
|
184
|
+
lastUserMessageSource = human.source;
|
|
158
185
|
}
|
|
159
186
|
else if (payload.type === 'token_count') {
|
|
160
187
|
const info = payload.info;
|
|
@@ -205,6 +232,8 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
205
232
|
else if (includeEvents && parsed.type === 'response_item' && parsed.payload) {
|
|
206
233
|
const payload = parsed.payload;
|
|
207
234
|
if (payload.type === 'message' && Array.isArray(payload.content)) {
|
|
235
|
+
if (payload.role === 'user')
|
|
236
|
+
userResponseItemCount++;
|
|
208
237
|
// Extract text content for display
|
|
209
238
|
const messages = extractMessageContent(payload);
|
|
210
239
|
for (const msg of messages) {
|
|
@@ -222,6 +251,14 @@ function scanCodexRollout(filePath, includeEvents) {
|
|
|
222
251
|
continue;
|
|
223
252
|
}
|
|
224
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
|
+
}
|
|
225
262
|
return {
|
|
226
263
|
sessionMeta,
|
|
227
264
|
events,
|
|
@@ -305,7 +342,7 @@ export function listCodexRollouts(opts = {}) {
|
|
|
305
342
|
// session (main file missing) never pollutes the list or total.
|
|
306
343
|
if (entry.isSubagent)
|
|
307
344
|
continue;
|
|
308
|
-
if (filterCwd && entry.cwd
|
|
345
|
+
if (filterCwd && !samePath(entry.cwd, filterCwd))
|
|
309
346
|
continue;
|
|
310
347
|
matched.push(entry);
|
|
311
348
|
}
|
|
@@ -323,6 +360,24 @@ export function listCodexRollouts(opts = {}) {
|
|
|
323
360
|
}
|
|
324
361
|
return { entries, total };
|
|
325
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
|
+
}
|
|
326
381
|
/**
|
|
327
382
|
* Read the full content of a specific session by its threadId.
|
|
328
383
|
*
|
|
@@ -338,16 +393,7 @@ export function listCodexRollouts(opts = {}) {
|
|
|
338
393
|
*/
|
|
339
394
|
export function readCodexSessionContent(sessionId, opts = {}) {
|
|
340
395
|
const codexHome = resolveCodexHome(opts.codexHome);
|
|
341
|
-
|
|
342
|
-
let index = getSessionIndex(codexHome);
|
|
343
|
-
let entry = index.get(sessionId);
|
|
344
|
-
// P3-2: On miss, the index may be stale (a rollout file created after the
|
|
345
|
-
// cache was built but within the 5s TTL). Force a refresh and recheck so a
|
|
346
|
-
// brand-new session is found without waiting for TTL expiry.
|
|
347
|
-
if (!entry) {
|
|
348
|
-
index = getSessionIndex(codexHome, true);
|
|
349
|
-
entry = index.get(sessionId);
|
|
350
|
-
}
|
|
396
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
351
397
|
if (!entry) {
|
|
352
398
|
return { events: [] };
|
|
353
399
|
}
|
|
@@ -357,7 +403,7 @@ export function readCodexSessionContent(sessionId, opts = {}) {
|
|
|
357
403
|
// Codex has no relocation (no EnterWorktree equivalent), so a simple equality
|
|
358
404
|
// check suffices — unlike claude which needs jsonlContainsCwd to handle
|
|
359
405
|
// relocated sessions with multiple cwd values.
|
|
360
|
-
if (opts.cwd && entry.cwd
|
|
406
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
361
407
|
return { events: [] };
|
|
362
408
|
}
|
|
363
409
|
const rollout = readCodexRollout(entry.filePath);
|
|
@@ -387,18 +433,13 @@ export function readCodexSessionContent(sessionId, opts = {}) {
|
|
|
387
433
|
*/
|
|
388
434
|
export function readCodexSessionSummary(sessionId, opts = {}) {
|
|
389
435
|
const codexHome = resolveCodexHome(opts.codexHome);
|
|
390
|
-
|
|
391
|
-
let entry = index.get(sessionId);
|
|
392
|
-
if (!entry) {
|
|
393
|
-
index = getSessionIndex(codexHome, true);
|
|
394
|
-
entry = index.get(sessionId);
|
|
395
|
-
}
|
|
436
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
396
437
|
if (!entry) {
|
|
397
438
|
return {};
|
|
398
439
|
}
|
|
399
440
|
// Cwd guard: same semantics as readCodexSessionContent — when a cwd is
|
|
400
441
|
// provided, the session's working directory must match.
|
|
401
|
-
if (opts.cwd && entry.cwd
|
|
442
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
402
443
|
return {};
|
|
403
444
|
}
|
|
404
445
|
const summary = readCodexRolloutSummary(entry.filePath);
|
|
@@ -427,14 +468,7 @@ export function isCodexSessionActive(sessionId, opts = {}) {
|
|
|
427
468
|
// all use STALE_MS). Previously codex used a divergent 10-minute default.
|
|
428
469
|
const threshold = opts.activeThresholdMs ?? STALE_MS;
|
|
429
470
|
// P2-4: Use session index for direct file lookup + mtime check
|
|
430
|
-
|
|
431
|
-
let entry = index.get(sessionId);
|
|
432
|
-
// P3-2: On miss, refresh the index in case the session file was created
|
|
433
|
-
// after the cache was built (within the 5s TTL). See readCodexSessionContent.
|
|
434
|
-
if (!entry) {
|
|
435
|
-
index = getSessionIndex(codexHome, true);
|
|
436
|
-
entry = index.get(sessionId);
|
|
437
|
-
}
|
|
471
|
+
const entry = resolveRolloutEntry(sessionId, codexHome);
|
|
438
472
|
if (!entry) {
|
|
439
473
|
return false;
|
|
440
474
|
}
|
|
@@ -446,7 +480,7 @@ export function isCodexSessionActive(sessionId, opts = {}) {
|
|
|
446
480
|
}
|
|
447
481
|
// Cwd guard: when a cwd is provided, the session's working directory must
|
|
448
482
|
// match (align with claude/pi/opencode which validate cwd).
|
|
449
|
-
if (opts.cwd && entry.cwd
|
|
483
|
+
if (opts.cwd && !samePath(entry.cwd, opts.cwd)) {
|
|
450
484
|
return false;
|
|
451
485
|
}
|
|
452
486
|
// Re-stat to get fresh mtime (index may be up to INDEX_TTL_MS stale)
|
|
@@ -594,6 +628,54 @@ function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
|
|
|
594
628
|
export function clearSessionIndexCache() {
|
|
595
629
|
sessionIndexCache.clear();
|
|
596
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
|
+
}
|
|
597
679
|
function extractMessageContent(payload) {
|
|
598
680
|
const result = [];
|
|
599
681
|
const content = payload.content;
|
|
@@ -9,3 +9,10 @@
|
|
|
9
9
|
* mtime is older than this threshold cannot be from a running process.
|
|
10
10
|
*/
|
|
11
11
|
export declare const STALE_MS: number;
|
|
12
|
+
/**
|
|
13
|
+
* A session file is stale (cannot be from a running process) when its
|
|
14
|
+
* timestamp is older than STALE_MS. All readers share this single
|
|
15
|
+
* comparison — previously it was re-derived per reader as
|
|
16
|
+
* `Date.now() - mtimeMs > STALE_MS` (or its `< STALE_MS` negation).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isStale(timestampMs: number, now?: number): boolean;
|
|
@@ -9,4 +9,13 @@
|
|
|
9
9
|
* mtime is older than this threshold cannot be from a running process.
|
|
10
10
|
*/
|
|
11
11
|
export const STALE_MS = 60 * 60 * 1000;
|
|
12
|
+
/**
|
|
13
|
+
* A session file is stale (cannot be from a running process) when its
|
|
14
|
+
* timestamp is older than STALE_MS. All readers share this single
|
|
15
|
+
* comparison — previously it was re-derived per reader as
|
|
16
|
+
* `Date.now() - mtimeMs > STALE_MS` (or its `< STALE_MS` negation).
|
|
17
|
+
*/
|
|
18
|
+
export function isStale(timestampMs, now = Date.now()) {
|
|
19
|
+
return now - timestampMs > STALE_MS;
|
|
20
|
+
}
|
|
12
21
|
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentSessionContentEvent } from '../../runner/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Byte offset where the tail begins: the start of the line AFTER `line`.
|
|
4
|
+
* `offset` is the byte offset of `line` itself; +1 accounts for the trailing
|
|
5
|
+
* '\n'.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tailOffsetAfter(line: string, offset: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Pass 1: stream the file once, parse each line exactly once (broken lines
|
|
10
|
+
* skipped), and feed the parsed object + raw line + byte offset to `handle`.
|
|
11
|
+
* `handle` may mutate closure state to collect scalars and track the tail
|
|
12
|
+
* offset (via tailOffsetAfter on user lines).
|
|
13
|
+
*/
|
|
14
|
+
export declare function scanJsonlOnce(filePath: string, handle: (obj: Record<string, unknown>, line: string, offset: number) => void): void;
|
|
15
|
+
/**
|
|
16
|
+
* Pass 2: re-read only the tail (lines after `tailOffset`; the whole file
|
|
17
|
+
* when tailOffset is -1) and map each parsed line to content events via
|
|
18
|
+
* `mapLine` (return an array — possibly empty — of events; returning null
|
|
19
|
+
* skips the line entirely). `maxEvents` keeps the LAST N events
|
|
20
|
+
* (`maxEvents <= 0` → [], guarding the `slice(-0)` full-array trap).
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanTailEvents(filePath: string, tailOffset: number, mapLine: (obj: Record<string, unknown>) => AgentSessionContentEvent[] | null, maxEvents?: number): AgentSessionContentEvent[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-pass scan skeleton shared by the claude / pi session readers (P2-2 +
|
|
3
|
+
* P2-5/P2-6 lineage): pass 1 STREAMS the whole file once via `scanJsonlLines`
|
|
4
|
+
* (no `string[]` materialized), parsing each line exactly once to collect
|
|
5
|
+
* scalars (usage, tail offset, title) while retaining NO parsed objects; pass
|
|
6
|
+
* 2 re-reads ONLY the tail from the recorded offset and re-parses it for
|
|
7
|
+
* content events — O(tail) memory for both raw line strings and parsed
|
|
8
|
+
* objects, instead of O(whole file).
|
|
9
|
+
*
|
|
10
|
+
* Parse ratio: ≈1.0–1.5× line count when a user message exists (streaming
|
|
11
|
+
* full-file scan + tail re-parse of the usually-short post-user lines). When
|
|
12
|
+
* the session has NO user message, tailOffset stays -1 and the tail IS the
|
|
13
|
+
* whole file → scan(N) + tail(N) = 2.0× — the known asymptotic upper bound of
|
|
14
|
+
* the two-phase design.
|
|
15
|
+
*/
|
|
16
|
+
import { scanJsonlLines, readJsonlLinesFromOffset } from './jsonl.js';
|
|
17
|
+
import { capEvents } from './pagination.js';
|
|
18
|
+
/**
|
|
19
|
+
* Byte offset where the tail begins: the start of the line AFTER `line`.
|
|
20
|
+
* `offset` is the byte offset of `line` itself; +1 accounts for the trailing
|
|
21
|
+
* '\n'.
|
|
22
|
+
*/
|
|
23
|
+
export function tailOffsetAfter(line, offset) {
|
|
24
|
+
return offset + Buffer.byteLength(line, 'utf-8') + 1;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pass 1: stream the file once, parse each line exactly once (broken lines
|
|
28
|
+
* skipped), and feed the parsed object + raw line + byte offset to `handle`.
|
|
29
|
+
* `handle` may mutate closure state to collect scalars and track the tail
|
|
30
|
+
* offset (via tailOffsetAfter on user lines).
|
|
31
|
+
*/
|
|
32
|
+
export function scanJsonlOnce(filePath, handle) {
|
|
33
|
+
scanJsonlLines(filePath, (line, offset) => {
|
|
34
|
+
let obj;
|
|
35
|
+
try {
|
|
36
|
+
obj = JSON.parse(line);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
handle(obj, line, offset);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pass 2: re-read only the tail (lines after `tailOffset`; the whole file
|
|
46
|
+
* when tailOffset is -1) and map each parsed line to content events via
|
|
47
|
+
* `mapLine` (return an array — possibly empty — of events; returning null
|
|
48
|
+
* skips the line entirely). `maxEvents` keeps the LAST N events
|
|
49
|
+
* (`maxEvents <= 0` → [], guarding the `slice(-0)` full-array trap).
|
|
50
|
+
*/
|
|
51
|
+
export function scanTailEvents(filePath, tailOffset, mapLine, maxEvents) {
|
|
52
|
+
const tailLines = readJsonlLinesFromOffset(filePath, tailOffset >= 0 ? tailOffset : 0);
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const line of tailLines) {
|
|
55
|
+
if (!line.trim())
|
|
56
|
+
continue;
|
|
57
|
+
let obj;
|
|
58
|
+
try {
|
|
59
|
+
obj = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const produced = mapLine(obj);
|
|
65
|
+
if (produced)
|
|
66
|
+
events.push(...produced);
|
|
67
|
+
}
|
|
68
|
+
return capEvents(events, maxEvents);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=two-pass.js.map
|