lark-remote 0.1.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 +52 -0
- package/LICENSE +21 -0
- package/README.en.md +162 -0
- package/README.md +161 -0
- package/dist/binder.d.ts +57 -0
- package/dist/binder.js +105 -0
- package/dist/bridge/index.d.ts +338 -0
- package/dist/bridge/index.js +1281 -0
- package/dist/bridge/queue-manager.d.ts +277 -0
- package/dist/bridge/queue-manager.js +729 -0
- package/dist/card/bash-card-session.d.ts +78 -0
- package/dist/card/bash-card-session.js +174 -0
- package/dist/card/bash-renderer.d.ts +24 -0
- package/dist/card/bash-renderer.js +187 -0
- package/dist/card/card-budget.d.ts +31 -0
- package/dist/card/card-budget.js +280 -0
- package/dist/card/card-session.d.ts +83 -0
- package/dist/card/card-session.js +185 -0
- package/dist/card/card-shared.d.ts +28 -0
- package/dist/card/card-shared.js +99 -0
- package/dist/card/collapsible.d.ts +26 -0
- package/dist/card/collapsible.js +87 -0
- package/dist/card/run-card-session.d.ts +78 -0
- package/dist/card/run-card-session.js +165 -0
- package/dist/card/run-renderer.d.ts +66 -0
- package/dist/card/run-renderer.js +841 -0
- package/dist/card/run-state.d.ts +103 -0
- package/dist/card/run-state.js +313 -0
- package/dist/card/text-truncate.d.ts +56 -0
- package/dist/card/text-truncate.js +184 -0
- package/dist/card/time.d.ts +1 -0
- package/dist/card/time.js +14 -0
- package/dist/card/tool-render.d.ts +25 -0
- package/dist/card/tool-render.js +176 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.js +89 -0
- package/dist/common/guards.d.ts +37 -0
- package/dist/common/guards.js +65 -0
- package/dist/config/codex-config.d.ts +113 -0
- package/dist/config/codex-config.js +428 -0
- package/dist/config/dir.d.ts +17 -0
- package/dist/config/dir.js +81 -0
- package/dist/config/index.d.ts +219 -0
- package/dist/config/index.js +388 -0
- package/dist/config/kimi-config.d.ts +33 -0
- package/dist/config/kimi-config.js +111 -0
- package/dist/config/opencode-config.d.ts +20 -0
- package/dist/config/opencode-config.js +124 -0
- package/dist/config/pi-config.d.ts +49 -0
- package/dist/config/pi-config.js +151 -0
- package/dist/config/wizard.d.ts +12 -0
- package/dist/config/wizard.js +84 -0
- package/dist/connector/file-limits.d.ts +7 -0
- package/dist/connector/file-limits.js +8 -0
- package/dist/connector/index.d.ts +86 -0
- package/dist/connector/index.js +340 -0
- package/dist/error-classification.d.ts +15 -0
- package/dist/error-classification.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +580 -0
- package/dist/instance-lock.d.ts +30 -0
- package/dist/instance-lock.js +184 -0
- package/dist/logger/index.d.ts +57 -0
- package/dist/logger/index.js +178 -0
- package/dist/order/index.d.ts +27 -0
- package/dist/order/index.js +90 -0
- package/dist/persistence/atomic-write.d.ts +10 -0
- package/dist/persistence/atomic-write.js +65 -0
- package/dist/persistence/load-json-file.d.ts +9 -0
- package/dist/persistence/load-json-file.js +23 -0
- package/dist/restart.d.ts +24 -0
- package/dist/restart.js +73 -0
- package/dist/router/card-helpers.d.ts +28 -0
- package/dist/router/card-helpers.js +65 -0
- package/dist/router/config/claude.d.ts +14 -0
- package/dist/router/config/claude.js +72 -0
- package/dist/router/config/codex.d.ts +22 -0
- package/dist/router/config/codex.js +100 -0
- package/dist/router/config/common/render.d.ts +16 -0
- package/dist/router/config/common/render.js +158 -0
- package/dist/router/config/index.d.ts +18 -0
- package/dist/router/config/index.js +38 -0
- package/dist/router/config/kimi.d.ts +21 -0
- package/dist/router/config/kimi.js +63 -0
- package/dist/router/config/opencode.d.ts +12 -0
- package/dist/router/config/opencode.js +44 -0
- package/dist/router/config/pi.d.ts +12 -0
- package/dist/router/config/pi.js +50 -0
- package/dist/router/config/types.d.ts +43 -0
- package/dist/router/config/types.js +8 -0
- package/dist/router/index.d.ts +318 -0
- package/dist/router/index.js +2715 -0
- package/dist/router/order-exec-dispatch.d.ts +30 -0
- package/dist/router/order-exec-dispatch.js +48 -0
- package/dist/router/utils.d.ts +44 -0
- package/dist/router/utils.js +116 -0
- package/dist/runner/bash/index.d.ts +4 -0
- package/dist/runner/bash/index.js +5 -0
- package/dist/runner/bash/runner.d.ts +55 -0
- package/dist/runner/bash/runner.js +213 -0
- package/dist/runner/claude/index.d.ts +4 -0
- package/dist/runner/claude/index.js +5 -0
- package/dist/runner/claude/runner.d.ts +29 -0
- package/dist/runner/claude/runner.js +83 -0
- package/dist/runner/codex/argv.d.ts +33 -0
- package/dist/runner/codex/argv.js +40 -0
- package/dist/runner/codex/index.d.ts +1 -0
- package/dist/runner/codex/index.js +2 -0
- package/dist/runner/codex/jsonl.d.ts +37 -0
- package/dist/runner/codex/jsonl.js +221 -0
- package/dist/runner/codex/runner.d.ts +65 -0
- package/dist/runner/codex/runner.js +124 -0
- package/dist/runner/common/exec-translator.d.ts +73 -0
- package/dist/runner/common/exec-translator.js +105 -0
- package/dist/runner/common/jsonl-stream.d.ts +39 -0
- package/dist/runner/common/jsonl-stream.js +203 -0
- package/dist/runner/common/process-stopper.d.ts +28 -0
- package/dist/runner/common/process-stopper.js +79 -0
- package/dist/runner/common/runner-utils.d.ts +21 -0
- package/dist/runner/common/runner-utils.js +65 -0
- package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
- package/dist/runner/common/spawn-heartbeat.js +51 -0
- package/dist/runner/common/spawning-runner.d.ts +321 -0
- package/dist/runner/common/spawning-runner.js +634 -0
- package/dist/runner/common/usage.d.ts +31 -0
- package/dist/runner/common/usage.js +29 -0
- package/dist/runner/index.d.ts +19 -0
- package/dist/runner/index.js +24 -0
- package/dist/runner/kimi/index.d.ts +1 -0
- package/dist/runner/kimi/index.js +2 -0
- package/dist/runner/kimi/jsonl.d.ts +28 -0
- package/dist/runner/kimi/jsonl.js +143 -0
- package/dist/runner/kimi/runner.d.ts +50 -0
- package/dist/runner/kimi/runner.js +105 -0
- package/dist/runner/opencode/argv.d.ts +32 -0
- package/dist/runner/opencode/argv.js +35 -0
- package/dist/runner/opencode/index.d.ts +1 -0
- package/dist/runner/opencode/index.js +2 -0
- package/dist/runner/opencode/jsonl.d.ts +68 -0
- package/dist/runner/opencode/jsonl.js +295 -0
- package/dist/runner/opencode/runner.d.ts +63 -0
- package/dist/runner/opencode/runner.js +93 -0
- package/dist/runner/pi/index.d.ts +1 -0
- package/dist/runner/pi/index.js +2 -0
- package/dist/runner/pi/jsonl.d.ts +128 -0
- package/dist/runner/pi/jsonl.js +249 -0
- package/dist/runner/pi/runner.d.ts +37 -0
- package/dist/runner/pi/runner.js +76 -0
- package/dist/runner/registry.d.ts +54 -0
- package/dist/runner/registry.js +75 -0
- package/dist/runner/resolve-agent-choices.d.ts +7 -0
- package/dist/runner/resolve-agent-choices.js +76 -0
- package/dist/runner/sync-agent-choices.d.ts +6 -0
- package/dist/runner/sync-agent-choices.js +73 -0
- package/dist/runner/types.d.ts +250 -0
- package/dist/runner/types.js +8 -0
- package/dist/session/claude/index.d.ts +1 -0
- package/dist/session/claude/index.js +2 -0
- package/dist/session/claude/session-reader.d.ts +34 -0
- package/dist/session/claude/session-reader.js +46 -0
- package/dist/session/claude/sessions.d.ts +53 -0
- package/dist/session/claude/sessions.js +528 -0
- package/dist/session/codex/index.d.ts +1 -0
- package/dist/session/codex/index.js +2 -0
- package/dist/session/codex/rollout-reader.d.ts +88 -0
- package/dist/session/codex/rollout-reader.js +473 -0
- package/dist/session/codex/sessions.d.ts +21 -0
- package/dist/session/codex/sessions.js +40 -0
- package/dist/session/common/catch-up.d.ts +12 -0
- package/dist/session/common/catch-up.js +31 -0
- package/dist/session/common/constants.d.ts +11 -0
- package/dist/session/common/constants.js +12 -0
- package/dist/session/common/content-blocks.d.ts +37 -0
- package/dist/session/common/content-blocks.js +68 -0
- package/dist/session/common/jsonl.d.ts +77 -0
- package/dist/session/common/jsonl.js +351 -0
- package/dist/session/common/usage-accumulator.d.ts +58 -0
- package/dist/session/common/usage-accumulator.js +72 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +11 -0
- package/dist/session/kimi/index.d.ts +1 -0
- package/dist/session/kimi/index.js +2 -0
- package/dist/session/kimi/sessions.d.ts +81 -0
- package/dist/session/kimi/sessions.js +527 -0
- package/dist/session/opencode/index.d.ts +1 -0
- package/dist/session/opencode/index.js +2 -0
- package/dist/session/opencode/sessions.d.ts +81 -0
- package/dist/session/opencode/sessions.js +388 -0
- package/dist/session/pi/index.d.ts +1 -0
- package/dist/session/pi/index.js +2 -0
- package/dist/session/pi/sessions.d.ts +27 -0
- package/dist/session/pi/sessions.js +399 -0
- package/dist/session/registry.d.ts +12 -0
- package/dist/session/registry.js +20 -0
- package/dist/session/session-store.d.ts +86 -0
- package/dist/session/session-store.js +450 -0
- package/dist/startup-contact.d.ts +18 -0
- package/dist/startup-contact.js +51 -0
- package/dist/workspace/index.d.ts +19 -0
- package/dist/workspace/index.js +53 -0
- package/package.json +93 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Rollout File Reader.
|
|
3
|
+
*
|
|
4
|
+
* Reads Codex session history from rollout JSONL files in `~/.codex/sessions/`.
|
|
5
|
+
*
|
|
6
|
+
* Rollout file format (appendix C in design doc):
|
|
7
|
+
* ```
|
|
8
|
+
* {"timestamp":"...","type":"session_meta","payload":{"session_id":"...","cwd":"...","originator":"lark-remote",...}}
|
|
9
|
+
* {"timestamp":"...","type":"event_msg","payload":{"type":"task_started","turn_id":"..."}}
|
|
10
|
+
* {"timestamp":"...","type":"response_item","payload":{"type":"message","role":"user|developer","content":[...]}}
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Note: This handles the **rollout file format** (historical logging), NOT the
|
|
14
|
+
* `codex exec --json` stdout format. Those are two different JSONL formats
|
|
15
|
+
* (see design doc appendix B vs C).
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { readJsonlLines, findJsonlLine } from '../common/jsonl.js';
|
|
20
|
+
import { getLogger } from '../../logger/index.js';
|
|
21
|
+
import { isRecord, stringValue } from '../../common/guards.js';
|
|
22
|
+
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
23
|
+
/** Field-wise difference (total - previous) for cumulative->incremental. */
|
|
24
|
+
function subtractRawUsage(a, b) {
|
|
25
|
+
return {
|
|
26
|
+
input_tokens: a.input_tokens - b.input_tokens,
|
|
27
|
+
cached_input_tokens: a.cached_input_tokens - b.cached_input_tokens,
|
|
28
|
+
output_tokens: a.output_tokens - b.output_tokens,
|
|
29
|
+
total_tokens: a.total_tokens - b.total_tokens,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function readCodexRollout(filePath) {
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const lines = readJsonlLines(filePath);
|
|
38
|
+
const events = [];
|
|
39
|
+
let sessionMeta = null;
|
|
40
|
+
// Real user input is identified by a paired `event_msg` whose
|
|
41
|
+
// payload.type === "user_message" - codex emits this ONLY for text the
|
|
42
|
+
// human actually typed. Injected scaffolding (AGENTS.md project rules,
|
|
43
|
+
// <environment_context>, permissions) is also written as `role:"user"`
|
|
44
|
+
// response_items but has NO user_message event, so it must be excluded
|
|
45
|
+
// from displayTitle/recap/summary. Regression 2026-07-13: the first
|
|
46
|
+
// `role:user` message is the injected AGENTS.md, mistakenly shown as
|
|
47
|
+
// "最近输入".
|
|
48
|
+
const realUserMessages = [];
|
|
49
|
+
// token_count tracking: codex emits cumulative `total_token_usage` and an
|
|
50
|
+
// incremental `last_token_usage`. We want the LAST turn's usage (matches
|
|
51
|
+
// pi/opencode /resume "last turn" display semantics). When
|
|
52
|
+
// last_token_usage is absent we derive it as total - previous_total.
|
|
53
|
+
let lastRawUsage;
|
|
54
|
+
// Final total_token_usage (cumulative across the whole session) for the
|
|
55
|
+
// Run card's "累计" display. Codex emits this alongside last_token_usage.
|
|
56
|
+
let lastTotalUsage;
|
|
57
|
+
let previousTotals;
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (!trimmed)
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(trimmed);
|
|
64
|
+
if (!parsed || typeof parsed.type !== 'string')
|
|
65
|
+
continue;
|
|
66
|
+
if (parsed.type === 'session_meta' && parsed.payload) {
|
|
67
|
+
sessionMeta = parsed.payload;
|
|
68
|
+
}
|
|
69
|
+
else if (parsed.type === 'event_msg' && parsed.payload) {
|
|
70
|
+
// user_message events carry the authoritative human-typed text.
|
|
71
|
+
const payload = parsed.payload;
|
|
72
|
+
if (payload.type === 'user_message') {
|
|
73
|
+
const text = stringValue(payload.message);
|
|
74
|
+
if (text) {
|
|
75
|
+
realUserMessages.push(text.slice(0, 200));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (payload.type === 'token_count') {
|
|
79
|
+
const info = payload.info;
|
|
80
|
+
const last = info?.last_token_usage;
|
|
81
|
+
const total = info?.total_token_usage;
|
|
82
|
+
let raw;
|
|
83
|
+
if (last) {
|
|
84
|
+
raw = last;
|
|
85
|
+
}
|
|
86
|
+
else if (total && previousTotals) {
|
|
87
|
+
raw = subtractRawUsage(total, previousTotals);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
raw = total;
|
|
91
|
+
}
|
|
92
|
+
if (total) {
|
|
93
|
+
previousTotals = total;
|
|
94
|
+
lastTotalUsage = total;
|
|
95
|
+
}
|
|
96
|
+
if (raw && (raw.input_tokens || raw.cached_input_tokens || raw.output_tokens)) {
|
|
97
|
+
lastRawUsage = raw;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (parsed.type === 'response_item' && parsed.payload) {
|
|
102
|
+
const payload = parsed.payload;
|
|
103
|
+
if (payload.type === 'message' && Array.isArray(payload.content)) {
|
|
104
|
+
// Extract text content for display
|
|
105
|
+
const messages = extractMessageContent(payload);
|
|
106
|
+
for (const msg of messages) {
|
|
107
|
+
events.push({
|
|
108
|
+
type: msg.role === 'user' ? 'user' : 'assistant',
|
|
109
|
+
content: msg.text,
|
|
110
|
+
timestamp: parsed.timestamp,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Skip malformed lines
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const firstUserMessage = realUserMessages[0] ?? '';
|
|
122
|
+
const lastRealUserMessage = realUserMessages[realUserMessages.length - 1] ?? '';
|
|
123
|
+
if (!sessionMeta) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const sessionId = stringValue(sessionMeta.session_id) ?? stringValue(sessionMeta.id) ?? '';
|
|
127
|
+
const cwd = stringValue(sessionMeta.cwd) ?? '';
|
|
128
|
+
if (!sessionId) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// Use file mtime as updatedAtMs, or parse timestamp from session_meta
|
|
132
|
+
const stats = fs.statSync(filePath);
|
|
133
|
+
const createdAtMs = sessionMeta.timestamp
|
|
134
|
+
? new Date(String(sessionMeta.timestamp)).getTime()
|
|
135
|
+
: stats.birthtimeMs;
|
|
136
|
+
const updatedAtMs = stats.mtimeMs;
|
|
137
|
+
// Build ccusage-aligned usage from the last turn's raw tokens.
|
|
138
|
+
// input = raw - cached (codex reports cached input separately), cache
|
|
139
|
+
// creation is never reported by codex, reasoning is a subset of output
|
|
140
|
+
// (display-only, never added to total).
|
|
141
|
+
// contextLength = input_tokens (= (input_tokens-cached) + cached + 0),
|
|
142
|
+
// i.e. input + cacheRead + cacheCreation — the unified context-window
|
|
143
|
+
// occupancy contract across all five readers (review P2-8, excludes
|
|
144
|
+
// output/reasoning).
|
|
145
|
+
let usage;
|
|
146
|
+
if (lastRawUsage) {
|
|
147
|
+
const r = lastRawUsage;
|
|
148
|
+
const cached = Math.min(r.cached_input_tokens, r.input_tokens);
|
|
149
|
+
usage = {
|
|
150
|
+
inputTokens: r.input_tokens - cached,
|
|
151
|
+
outputTokens: r.output_tokens,
|
|
152
|
+
contextLength: r.input_tokens,
|
|
153
|
+
cacheReadTokens: cached,
|
|
154
|
+
cacheCreationTokens: 0,
|
|
155
|
+
totalTokens: r.total_tokens,
|
|
156
|
+
};
|
|
157
|
+
// Cumulative from the final total_token_usage (non-cached input +
|
|
158
|
+
// output), summed across all turns in the session.
|
|
159
|
+
if (lastTotalUsage) {
|
|
160
|
+
const cumCached = Math.min(lastTotalUsage.cached_input_tokens, lastTotalUsage.input_tokens);
|
|
161
|
+
usage.cumulativeInputTokens = lastTotalUsage.input_tokens - cumCached;
|
|
162
|
+
usage.cumulativeOutputTokens = lastTotalUsage.output_tokens;
|
|
163
|
+
usage.cumulativeTotalTokens = lastTotalUsage.total_tokens;
|
|
164
|
+
usage.cumulativeCacheReadTokens = cumCached;
|
|
165
|
+
usage.cumulativeCacheCreationTokens = 0; // Codex 不报告 cache creation
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
threadId: sessionId,
|
|
170
|
+
cwd,
|
|
171
|
+
firstUserMessage: firstUserMessage || '(no user message)',
|
|
172
|
+
lastRealUserMessage,
|
|
173
|
+
createdAtMs,
|
|
174
|
+
updatedAtMs,
|
|
175
|
+
events,
|
|
176
|
+
usage,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
getLogger().warn(`[codex-rollout-reader] failed to read ${filePath}: ${err}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* List Codex sessions from rollout files.
|
|
186
|
+
*
|
|
187
|
+
* Returns `{ entries, total }`: `total` is the size of the full cwd-matched
|
|
188
|
+
* set before pagination; `entries` are the newest `limit` entries by mtime.
|
|
189
|
+
*
|
|
190
|
+
* A global order over the full set is established first (plan §1.4: no early
|
|
191
|
+
* termination before ordering) using the lightweight session index — full
|
|
192
|
+
* walk + first-line session_meta parse + stat mtime, 5s TTL cache. Only the
|
|
193
|
+
* returned page is fully parsed for summary/usage (plan §2.2).
|
|
194
|
+
*/
|
|
195
|
+
export function listCodexRollouts(opts = {}) {
|
|
196
|
+
const codexHome = resolveCodexHome(opts.codexHome);
|
|
197
|
+
const limit = opts.limit ?? 20;
|
|
198
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
199
|
+
const filterCwd = opts.cwd;
|
|
200
|
+
// Full index: every rollout file's session_meta + mtime, no early break.
|
|
201
|
+
const index = getSessionIndex(codexHome);
|
|
202
|
+
const matched = [];
|
|
203
|
+
for (const entry of index.values()) {
|
|
204
|
+
// Subagent thread rollouts are part of a thread tree, not standalone
|
|
205
|
+
// sessions: exclude them explicitly (plan §2.1) so a pure-subagent
|
|
206
|
+
// session (main file missing) never pollutes the list or total.
|
|
207
|
+
if (entry.isSubagent)
|
|
208
|
+
continue;
|
|
209
|
+
if (filterCwd && entry.cwd !== filterCwd)
|
|
210
|
+
continue;
|
|
211
|
+
matched.push(entry);
|
|
212
|
+
}
|
|
213
|
+
// Establish the global order by mtime desc, then slice the page.
|
|
214
|
+
// Same-mtime ties use filePath as a deterministic secondary key so index
|
|
215
|
+
// rebuilds / walk order changes never reorder the page.
|
|
216
|
+
matched.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
217
|
+
const total = matched.length;
|
|
218
|
+
const page = matched.slice(offset, offset + limit);
|
|
219
|
+
// Full-parse only the page being returned (summary/usage).
|
|
220
|
+
const entries = [];
|
|
221
|
+
for (const item of page) {
|
|
222
|
+
const entry = readCodexRollout(item.filePath);
|
|
223
|
+
if (entry)
|
|
224
|
+
entries.push(entry);
|
|
225
|
+
}
|
|
226
|
+
return { entries, total };
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Read the full content of a specific session by its threadId.
|
|
230
|
+
*
|
|
231
|
+
* P2-4: Uses session index for O(1) file lookup instead of walking the
|
|
232
|
+
* entire directory tree and fully parsing every file. Only the matching
|
|
233
|
+
* file is fully parsed.
|
|
234
|
+
*
|
|
235
|
+
* `maxEvents` (optional): when set, only the LAST `maxEvents` events are
|
|
236
|
+
* returned (most recent activity). This matches the auto-resume use case
|
|
237
|
+
* ("show recent history") and keeps the card payload small. Unlike claude,
|
|
238
|
+
* codex events are already at message granularity (no multi-block split),
|
|
239
|
+
* so a hard `slice(-maxEvents)` is sufficient — no soft-cap complication.
|
|
240
|
+
*/
|
|
241
|
+
export function readCodexSessionContent(sessionId, opts = {}) {
|
|
242
|
+
const codexHome = resolveCodexHome(opts.codexHome);
|
|
243
|
+
// P2-4: Use session index for direct file lookup
|
|
244
|
+
let index = getSessionIndex(codexHome);
|
|
245
|
+
let entry = index.get(sessionId);
|
|
246
|
+
// P3-2: On miss, the index may be stale (a rollout file created after the
|
|
247
|
+
// cache was built but within the 5s TTL). Force a refresh and recheck so a
|
|
248
|
+
// brand-new session is found without waiting for TTL expiry.
|
|
249
|
+
if (!entry) {
|
|
250
|
+
index = getSessionIndex(codexHome, true);
|
|
251
|
+
entry = index.get(sessionId);
|
|
252
|
+
}
|
|
253
|
+
if (!entry) {
|
|
254
|
+
return { events: [] };
|
|
255
|
+
}
|
|
256
|
+
const rollout = readCodexRollout(entry.filePath);
|
|
257
|
+
if (!rollout || rollout.threadId !== sessionId) {
|
|
258
|
+
return { events: [] };
|
|
259
|
+
}
|
|
260
|
+
// Apply maxEvents cap: keep the LAST N events (most recent).
|
|
261
|
+
// See function docstring for rationale.
|
|
262
|
+
// P2-7: maxEvents <= 0 returns [] — guards the `slice(-0) === slice(0)`
|
|
263
|
+
// full-array trap (no caller passes 0 today, but the contract must hold).
|
|
264
|
+
const events = opts.maxEvents !== undefined && opts.maxEvents > 0 && rollout.events.length > opts.maxEvents
|
|
265
|
+
? rollout.events.slice(-opts.maxEvents)
|
|
266
|
+
: opts.maxEvents !== undefined && opts.maxEvents <= 0
|
|
267
|
+
? []
|
|
268
|
+
: rollout.events;
|
|
269
|
+
return {
|
|
270
|
+
events,
|
|
271
|
+
// displayTitle = LAST real user message (matches the
|
|
272
|
+
// "最近输入" label). recap is undefined: codex has no
|
|
273
|
+
// compact-summary concept, so never fake one from a user
|
|
274
|
+
// message (was: firstUserMessage = injected AGENTS.md).
|
|
275
|
+
displayTitle: rollout.lastRealUserMessage
|
|
276
|
+
? rollout.lastRealUserMessage.slice(0, 50)
|
|
277
|
+
: undefined,
|
|
278
|
+
recap: undefined,
|
|
279
|
+
usage: rollout.usage,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if a session is still active (recently updated).
|
|
284
|
+
*
|
|
285
|
+
* P2-4: Uses session index for direct file lookup, then checks mtime
|
|
286
|
+
* via statSync — no full rollout parse required. This reduces the
|
|
287
|
+
* cost from "walk entire directory + full-parse every file" to a single
|
|
288
|
+
* statSync call for the matching file.
|
|
289
|
+
*/
|
|
290
|
+
export function isCodexSessionActive(sessionId, opts = {}) {
|
|
291
|
+
const codexHome = resolveCodexHome(opts.codexHome);
|
|
292
|
+
const threshold = opts.activeThresholdMs ?? 10 * 60 * 1000; // 10 minutes default
|
|
293
|
+
// P2-4: Use session index for direct file lookup + mtime check
|
|
294
|
+
let index = getSessionIndex(codexHome);
|
|
295
|
+
let entry = index.get(sessionId);
|
|
296
|
+
// P3-2: On miss, refresh the index in case the session file was created
|
|
297
|
+
// after the cache was built (within the 5s TTL). See readCodexSessionContent.
|
|
298
|
+
if (!entry) {
|
|
299
|
+
index = getSessionIndex(codexHome, true);
|
|
300
|
+
entry = index.get(sessionId);
|
|
301
|
+
}
|
|
302
|
+
if (!entry) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
// Subagent thread rollouts are never "active sessions": their mtime
|
|
306
|
+
// reflects thread-tree activity, not the parent session's liveness
|
|
307
|
+
// (plan §2.1). A pure-subagent session must report inactive.
|
|
308
|
+
if (entry.isSubagent) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
// Re-stat to get fresh mtime (index may be up to INDEX_TTL_MS stale)
|
|
312
|
+
try {
|
|
313
|
+
const stat = fs.statSync(entry.filePath);
|
|
314
|
+
return Date.now() - stat.mtimeMs < threshold;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Walk the YYYY/MM/DD directory tree under `sessionsDir` and return all
|
|
322
|
+
* rollout-*.jsonl file paths. Silently skips non-directory entries and
|
|
323
|
+
* malformed names; returns an empty array if `sessionsDir` doesn't exist
|
|
324
|
+
* or is unreadable.
|
|
325
|
+
*/
|
|
326
|
+
function walkRolloutFiles(sessionsDir) {
|
|
327
|
+
if (!fs.existsSync(sessionsDir))
|
|
328
|
+
return [];
|
|
329
|
+
const result = [];
|
|
330
|
+
try {
|
|
331
|
+
const years = fs.readdirSync(sessionsDir);
|
|
332
|
+
for (const year of years) {
|
|
333
|
+
if (!/^\d{4}$/.test(year))
|
|
334
|
+
continue;
|
|
335
|
+
const yearPath = path.join(sessionsDir, year);
|
|
336
|
+
if (!fs.statSync(yearPath).isDirectory())
|
|
337
|
+
continue;
|
|
338
|
+
const months = fs.readdirSync(yearPath);
|
|
339
|
+
for (const month of months) {
|
|
340
|
+
if (!/^\d{2}$/.test(month))
|
|
341
|
+
continue;
|
|
342
|
+
const monthPath = path.join(yearPath, month);
|
|
343
|
+
if (!fs.statSync(monthPath).isDirectory())
|
|
344
|
+
continue;
|
|
345
|
+
const days = fs.readdirSync(monthPath);
|
|
346
|
+
for (const day of days) {
|
|
347
|
+
if (!/^\d{2}$/.test(day))
|
|
348
|
+
continue;
|
|
349
|
+
const dayPath = path.join(monthPath, day);
|
|
350
|
+
if (!fs.statSync(dayPath).isDirectory())
|
|
351
|
+
continue;
|
|
352
|
+
const files = fs
|
|
353
|
+
.readdirSync(dayPath)
|
|
354
|
+
.filter((f) => f.startsWith('rollout-') && f.endsWith('.jsonl'));
|
|
355
|
+
for (const file of files) {
|
|
356
|
+
result.push(path.join(dayPath, file));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
getLogger().warn(`[codex-rollout-reader] failed to walk sessions dir: ${err}`);
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
const INDEX_TTL_MS = 5_000; // 5 seconds — covers a single /resume flow
|
|
368
|
+
const sessionIndexCache = new Map();
|
|
369
|
+
/**
|
|
370
|
+
* Build or return cached session index for a given resolved codexHome.
|
|
371
|
+
* The index maps sessionId → {filePath, cwd, mtimeMs}.
|
|
372
|
+
*
|
|
373
|
+
* @param resolvedCodexHome Already-resolved codex home path (from resolveCodexHome).
|
|
374
|
+
* @param forceRefresh When true, bypass the TTL cache and rebuild. Used
|
|
375
|
+
* on an index MISS (readCodexSessionContent /
|
|
376
|
+
* isCodexSessionActive) so a rollout file created
|
|
377
|
+
* AFTER the cache was built — but still within the
|
|
378
|
+
* 5s TTL — is found without waiting for expiry.
|
|
379
|
+
*
|
|
380
|
+
* Cache shape note: `sessionIndexCache` is module-level and keyed by the
|
|
381
|
+
* resolved codexHome string. In production `CodexSessionReader` resolves
|
|
382
|
+
* codexHome once in its constructor and reuses it for every call, so a single
|
|
383
|
+
* bridge instance produces exactly one cache entry — the Map does NOT grow
|
|
384
|
+
* unbounded. The TTL refreshes the value on each rebuild; entries are never
|
|
385
|
+
* evicted, but there is effectively just one.
|
|
386
|
+
*/
|
|
387
|
+
function getSessionIndex(resolvedCodexHome, forceRefresh = false) {
|
|
388
|
+
const cacheKey = resolvedCodexHome;
|
|
389
|
+
const cached = sessionIndexCache.get(cacheKey);
|
|
390
|
+
if (!forceRefresh && cached && Date.now() - cached.builtAt < INDEX_TTL_MS) {
|
|
391
|
+
return cached.index;
|
|
392
|
+
}
|
|
393
|
+
const sessionsDir = path.join(resolvedCodexHome, 'sessions');
|
|
394
|
+
const files = walkRolloutFiles(sessionsDir);
|
|
395
|
+
const index = new Map();
|
|
396
|
+
for (const filePath of files) {
|
|
397
|
+
// Use findJsonlLine to extract session_meta without full file parse.
|
|
398
|
+
// session_meta is always the first line of a rollout file.
|
|
399
|
+
const metaLine = findJsonlLine(filePath, (line) => {
|
|
400
|
+
try {
|
|
401
|
+
const obj = JSON.parse(line);
|
|
402
|
+
return obj.type === 'session_meta';
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
if (!metaLine)
|
|
409
|
+
continue;
|
|
410
|
+
try {
|
|
411
|
+
const meta = JSON.parse(metaLine);
|
|
412
|
+
const sessionId = meta.payload?.session_id;
|
|
413
|
+
if (!sessionId)
|
|
414
|
+
continue;
|
|
415
|
+
let mtimeMs;
|
|
416
|
+
try {
|
|
417
|
+
mtimeMs = fs.statSync(filePath).mtimeMs;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Codex multi-agent threads: subagent rollout files share the parent
|
|
423
|
+
// session's session_id but carry thread_source === 'subagent' (or a
|
|
424
|
+
// source.subagent marker). Keep only the main-thread file for a given
|
|
425
|
+
// session id; among files of the same kind pick the newest mtime, with
|
|
426
|
+
// filePath as a deterministic tie-breaker — resolution never depends on
|
|
427
|
+
// fs.readdirSync order (APFS hash order is not filename order).
|
|
428
|
+
const isSubagent = meta.payload?.thread_source === 'subagent' || meta.payload?.source?.subagent !== undefined;
|
|
429
|
+
const candidate = {
|
|
430
|
+
filePath,
|
|
431
|
+
cwd: meta.payload?.cwd ?? '',
|
|
432
|
+
mtimeMs,
|
|
433
|
+
isSubagent,
|
|
434
|
+
};
|
|
435
|
+
const existing = index.get(sessionId);
|
|
436
|
+
if (!existing ||
|
|
437
|
+
(existing.isSubagent && !isSubagent) ||
|
|
438
|
+
(existing.isSubagent === isSubagent && mtimeMs > existing.mtimeMs) ||
|
|
439
|
+
(existing.isSubagent === isSubagent &&
|
|
440
|
+
mtimeMs === existing.mtimeMs &&
|
|
441
|
+
filePath.localeCompare(existing.filePath) < 0)) {
|
|
442
|
+
index.set(sessionId, candidate);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
sessionIndexCache.set(cacheKey, { index, builtAt: Date.now() });
|
|
450
|
+
return index;
|
|
451
|
+
}
|
|
452
|
+
/** Clear the session index cache. Exposed for tests. */
|
|
453
|
+
export function clearSessionIndexCache() {
|
|
454
|
+
sessionIndexCache.clear();
|
|
455
|
+
}
|
|
456
|
+
function extractMessageContent(payload) {
|
|
457
|
+
const result = [];
|
|
458
|
+
const content = payload.content;
|
|
459
|
+
if (!Array.isArray(content)) {
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
const role = stringValue(payload.role) ?? 'assistant';
|
|
463
|
+
for (const item of content) {
|
|
464
|
+
if (!isRecord(item))
|
|
465
|
+
continue;
|
|
466
|
+
const text = stringValue(item.text) ?? stringValue(item.input_text);
|
|
467
|
+
if (text) {
|
|
468
|
+
result.push({ role, text });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
//# sourceMappingURL=rollout-reader.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentSession, SessionContent, AgentSessionReader } from '../../runner/index.js';
|
|
2
|
+
interface CodexSessionReaderOptions {
|
|
3
|
+
codexHome?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class CodexSessionReader implements AgentSessionReader {
|
|
6
|
+
private readonly codexHome;
|
|
7
|
+
constructor(opts?: CodexSessionReaderOptions);
|
|
8
|
+
listSessions(cwd: string, opts?: {
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
}): {
|
|
12
|
+
sessions: AgentSession[];
|
|
13
|
+
total: number;
|
|
14
|
+
};
|
|
15
|
+
getNewestSession(cwd: string): AgentSession | null;
|
|
16
|
+
readSessionContent(sessionId: string, _cwd: string, opts?: {
|
|
17
|
+
maxEvents?: number;
|
|
18
|
+
}): SessionContent;
|
|
19
|
+
isSessionActive(sessionId: string, _cwd: string): boolean;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolveCodexHome } from '../../config/codex-config.js';
|
|
2
|
+
import { listCodexRollouts, readCodexSessionContent, isCodexSessionActive, } from './rollout-reader.js';
|
|
3
|
+
export class CodexSessionReader {
|
|
4
|
+
codexHome;
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
this.codexHome = resolveCodexHome(opts.codexHome);
|
|
7
|
+
}
|
|
8
|
+
listSessions(cwd, opts) {
|
|
9
|
+
const result = listCodexRollouts({
|
|
10
|
+
codexHome: this.codexHome,
|
|
11
|
+
cwd,
|
|
12
|
+
limit: opts?.limit ?? 20,
|
|
13
|
+
offset: opts?.offset,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
sessions: result.entries.map((entry) => ({
|
|
17
|
+
sessionId: entry.threadId,
|
|
18
|
+
// '(no user message)' 是 reader 的占位符,不是真实输入;给空串由
|
|
19
|
+
// router 的占位符集合统一过滤。`|| entry.cwd` 永不达(rollout-reader
|
|
20
|
+
// 已保证 firstUserMessage 非空字符串),去掉以免把 cwd 误当摘要。
|
|
21
|
+
summary: entry.firstUserMessage === '(no user message)' ? '' : entry.firstUserMessage,
|
|
22
|
+
mtime: entry.updatedAtMs,
|
|
23
|
+
})),
|
|
24
|
+
total: result.total,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
getNewestSession(cwd) {
|
|
28
|
+
return this.listSessions(cwd, { limit: 1 }).sessions[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
readSessionContent(sessionId, _cwd, opts) {
|
|
31
|
+
return readCodexSessionContent(sessionId, {
|
|
32
|
+
codexHome: this.codexHome,
|
|
33
|
+
maxEvents: opts?.maxEvents,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
isSessionActive(sessionId, _cwd) {
|
|
37
|
+
return isCodexSessionActive(sessionId, { codexHome: this.codexHome });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the index of the last line in a JSONL line array whose parsed object
|
|
3
|
+
* satisfies the given predicate.
|
|
4
|
+
*
|
|
5
|
+
* Invalid JSON, empty, and whitespace-only lines are silently skipped.
|
|
6
|
+
*
|
|
7
|
+
* @param lines Array of raw JSONL line strings.
|
|
8
|
+
* @param isUserMessage Predicate that receives the parsed object and returns
|
|
9
|
+
* true for lines considered "user" messages.
|
|
10
|
+
* @returns The array index of the last matching line, or -1 if none match.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findLastUserIndex(lines: string[], isUserMessage: (obj: Record<string, unknown>) => boolean): number;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the index of the last line in a JSONL line array whose parsed object
|
|
3
|
+
* satisfies the given predicate.
|
|
4
|
+
*
|
|
5
|
+
* Invalid JSON, empty, and whitespace-only lines are silently skipped.
|
|
6
|
+
*
|
|
7
|
+
* @param lines Array of raw JSONL line strings.
|
|
8
|
+
* @param isUserMessage Predicate that receives the parsed object and returns
|
|
9
|
+
* true for lines considered "user" messages.
|
|
10
|
+
* @returns The array index of the last matching line, or -1 if none match.
|
|
11
|
+
*/
|
|
12
|
+
export function findLastUserIndex(lines, isUserMessage) {
|
|
13
|
+
let lastIdx = -1;
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
if (!line.trim())
|
|
17
|
+
continue;
|
|
18
|
+
let obj;
|
|
19
|
+
try {
|
|
20
|
+
obj = JSON.parse(line);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (isUserMessage(obj)) {
|
|
26
|
+
lastIdx = i;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return lastIdx;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=catch-up.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-related constants shared across agent session readers.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Threshold (in milliseconds) after which a session is considered inactive.
|
|
6
|
+
*
|
|
7
|
+
* Set to 1 hour — well above the typical 15-minute idle watchdog, so a
|
|
8
|
+
* legitimately paused session is not prematurely marked stale. A file whose
|
|
9
|
+
* mtime is older than this threshold cannot be from a running process.
|
|
10
|
+
*/
|
|
11
|
+
export declare const STALE_MS: number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-related constants shared across agent session readers.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Threshold (in milliseconds) after which a session is considered inactive.
|
|
6
|
+
*
|
|
7
|
+
* Set to 1 hour — well above the typical 15-minute idle watchdog, so a
|
|
8
|
+
* legitimately paused session is not prematurely marked stale. A file whose
|
|
9
|
+
* mtime is older than this threshold cannot be from a running process.
|
|
10
|
+
*/
|
|
11
|
+
export const STALE_MS = 60 * 60 * 1000;
|
|
12
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameterized content-block extraction shared across session readers.
|
|
3
|
+
*
|
|
4
|
+
* Different agents (Claude, pi, …) use different field names for tool_use /
|
|
5
|
+
* tool_result blocks. The {@link ContentBlockMapping} type captures those
|
|
6
|
+
* differences so a single implementation can serve all readers.
|
|
7
|
+
*/
|
|
8
|
+
/** A single extracted content block for rendering. */
|
|
9
|
+
interface ContentBlock {
|
|
10
|
+
type: 'text' | 'thinking' | 'tool_use' | 'tool_result';
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/** Maps agent-specific field names to the generic extraction logic. */
|
|
14
|
+
export interface ContentBlockMapping {
|
|
15
|
+
/** The `type` value that identifies a tool-use block (e.g. 'tool_use' or 'toolCall'). */
|
|
16
|
+
toolUseType: string;
|
|
17
|
+
/** The `type` value that identifies a tool-result block (e.g. 'tool_result' or 'toolResult'). */
|
|
18
|
+
toolResultType: string;
|
|
19
|
+
/** Field name holding the tool input/arguments (e.g. 'input' or 'arguments'). */
|
|
20
|
+
toolInputField: string;
|
|
21
|
+
/** Field name holding the error flag (e.g. 'is_error' or 'isError'). */
|
|
22
|
+
toolErrorField: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract content blocks from a message content array.
|
|
26
|
+
*
|
|
27
|
+
* Handles text, thinking, tool_use, and tool_result blocks. Blank text and
|
|
28
|
+
* whitespace-only tool_result content are skipped. Tool input is serialized
|
|
29
|
+
* to JSON and truncated at 200 characters.
|
|
30
|
+
*
|
|
31
|
+
* @param content The raw `content` value from a message (array expected;
|
|
32
|
+
* non-array values return an empty array).
|
|
33
|
+
* @param mapping Agent-specific field-name mapping.
|
|
34
|
+
* @returns Array of {@link ContentBlock} objects.
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractContentBlocks(content: unknown, mapping: ContentBlockMapping): ContentBlock[];
|
|
37
|
+
export {};
|