@threadcrumbs/common 0.1.7 → 0.1.9
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/harnesses/claude-code.mjs +578 -578
- package/harnesses/hermes.mjs +2 -1
- package/lib/claude-transcript.mjs +16 -0
- package/lib/opencode-store.mjs +734 -734
- package/package.json +7 -2
- package/telemetry/README.md +290 -290
- package/telemetry/claude-code.mjs +5 -2
- package/telemetry/codex.mjs +700 -700
- package/telemetry/hermes.mjs +441 -441
- package/ui/rows.css +5 -0
package/telemetry/codex.mjs
CHANGED
|
@@ -1,700 +1,700 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises'
|
|
2
|
-
import os from 'node:os'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { exists, jsonLines, listDirs, listFiles } from '../lib/fsutil.mjs'
|
|
5
|
-
import { contextBreakdown, emptyFixed, emptySizes, setFixed, sizeCall, sizeCompaction, sizePrompt, sizeReply, sizeRequest, sizeResult } from '../lib/context-breakdown.mjs'
|
|
6
|
-
import { jsonlFollower, recentReaders } from '../lib/jsonl-follower.mjs'
|
|
7
|
-
import { columnsOf, tablesOf, withReadOnly } from '../lib/sqlite.mjs'
|
|
8
|
-
import {
|
|
9
|
-
DETAIL_LIMITS,
|
|
10
|
-
RECENT_MS,
|
|
11
|
-
activityDetail,
|
|
12
|
-
addFileChange,
|
|
13
|
-
askOf,
|
|
14
|
-
askQuestions,
|
|
15
|
-
clip,
|
|
16
|
-
clipBlock,
|
|
17
|
-
diffHunk,
|
|
18
|
-
emptyActivity,
|
|
19
|
-
emptySample,
|
|
20
|
-
endCall,
|
|
21
|
-
fileList,
|
|
22
|
-
filePaths,
|
|
23
|
-
inputKind,
|
|
24
|
-
joinedText,
|
|
25
|
-
keepNewest,
|
|
26
|
-
keepText,
|
|
27
|
-
nextPlan,
|
|
28
|
-
number,
|
|
29
|
-
planItems,
|
|
30
|
-
planSummary,
|
|
31
|
-
relativePath,
|
|
32
|
-
sampleList,
|
|
33
|
-
samplePoint,
|
|
34
|
-
settleCalls,
|
|
35
|
-
startCall,
|
|
36
|
-
text,
|
|
37
|
-
textList,
|
|
38
|
-
wordsFull,
|
|
39
|
-
titleFromPrompt,
|
|
40
|
-
toolInput,
|
|
41
|
-
toolNow,
|
|
42
|
-
toolTarget,
|
|
43
|
-
} from '../lib/telemetry-fields.mjs'
|
|
44
|
-
|
|
45
|
-
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
46
|
-
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
47
|
-
const OPENED_KEPT = 3
|
|
48
|
-
const TAIL_BYTES = 512 * 1024
|
|
49
|
-
const COMPACT_SHARE = 0.9
|
|
50
|
-
const REPORTED_SHARE = 0.95
|
|
51
|
-
const ROLLOUT_FILE = /^rollout-.{19}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i
|
|
52
|
-
const STATE_DB = /^state_(\d+)\.sqlite$/
|
|
53
|
-
const CALLS = new Set(['function_call', 'custom_tool_call', 'local_shell_call', 'tool_search_call'])
|
|
54
|
-
const OUTPUTS = new Set(['function_call_output', 'custom_tool_call_output', 'tool_search_output'])
|
|
55
|
-
const AGENT_TOOLS = new Set(['spawn_agent', 'wait_agent', 'send_input'])
|
|
56
|
-
const QUESTION_TOOLS = new Set(['request_user_input', 'request_permissions'])
|
|
57
|
-
const INTERNAL_SOURCES = new Set(['guardian_review', 'memory_consolidation'])
|
|
58
|
-
const PATCH_APPLIED = /Success\. Updated the following files/
|
|
59
|
-
const PATCH_FILE = /^\*\*\* (?:Update|Add|Delete) File: (.+)$/
|
|
60
|
-
const NOISE = /<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g
|
|
61
|
-
|
|
62
|
-
function contentText(content) {
|
|
63
|
-
if (typeof content === 'string') return content
|
|
64
|
-
if (!Array.isArray(content)) return ''
|
|
65
|
-
return content.map((p) => (typeof p === 'string' ? p : text(p?.text) || text(p?.input_text))).filter(Boolean).join('\n')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function promptText(raw) {
|
|
69
|
-
const prompt = text(raw).replace(NOISE, ' ').trim()
|
|
70
|
-
return prompt && !prompt.startsWith('<') && !prompt.startsWith('# AGENTS.md') ? prompt : ''
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseArgs(raw) {
|
|
74
|
-
if (raw && typeof raw === 'object') return raw
|
|
75
|
-
try {
|
|
76
|
-
return JSON.parse(text(raw) || '{}')
|
|
77
|
-
} catch {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function outputText(output) {
|
|
83
|
-
if (Array.isArray(output)) return contentText(output)
|
|
84
|
-
const raw = text(output)
|
|
85
|
-
try {
|
|
86
|
-
const parsed = JSON.parse(raw)
|
|
87
|
-
return typeof parsed?.output === 'string' ? parsed.output : raw
|
|
88
|
-
} catch {
|
|
89
|
-
return raw
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function exitFailed(output) {
|
|
94
|
-
if (typeof output !== 'string') return null
|
|
95
|
-
try {
|
|
96
|
-
const code = JSON.parse(output)?.metadata?.exit_code
|
|
97
|
-
return Number.isFinite(code) ? code !== 0 : null
|
|
98
|
-
} catch {
|
|
99
|
-
return null
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const patchFile = (patch) => patch.split('\n').map((line) => PATCH_FILE.exec(line)?.[1]).find(Boolean)?.trim() || ''
|
|
104
|
-
|
|
105
|
-
function callTarget(item, args, patch, cwd) {
|
|
106
|
-
if (patch) {
|
|
107
|
-
const file = patchFile(patch)
|
|
108
|
-
return file ? toolTarget({ path: file }, cwd) : ''
|
|
109
|
-
}
|
|
110
|
-
if (item.type === 'local_shell_call') return toolTarget({ command: item.action?.command }, cwd)
|
|
111
|
-
if (item.type === 'custom_tool_call') return toolTarget(text(item.input), cwd)
|
|
112
|
-
return toolTarget(args ?? parseArgs(item.arguments), cwd)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function callInput(item, args, patch) {
|
|
116
|
-
if (patch) return null
|
|
117
|
-
if (item.type === 'local_shell_call') return toolInput({ command: item.action?.command })
|
|
118
|
-
if (item.type === 'custom_tool_call') return toolInput(text(item.input))
|
|
119
|
-
return toolInput(args ?? parseArgs(item.arguments))
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function latestCall(calls) {
|
|
123
|
-
let latest = null
|
|
124
|
-
for (const call of calls.values()) {
|
|
125
|
-
if (!latest || call.startedAt >= latest.startedAt) latest = call
|
|
126
|
-
}
|
|
127
|
-
return toolNow(latest)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function emptyRollout() {
|
|
131
|
-
return {
|
|
132
|
-
threadId: '',
|
|
133
|
-
parentId: '',
|
|
134
|
-
subagent: false,
|
|
135
|
-
internal: false,
|
|
136
|
-
cwd: '',
|
|
137
|
-
gitBranch: '',
|
|
138
|
-
model: '',
|
|
139
|
-
effort: '',
|
|
140
|
-
startedAt: 0,
|
|
141
|
-
lastAt: 0,
|
|
142
|
-
firstPrompt: '',
|
|
143
|
-
prompts: 0,
|
|
144
|
-
typed: false,
|
|
145
|
-
fallbackPrompt: '',
|
|
146
|
-
fallbackPrompts: 0,
|
|
147
|
-
open: false,
|
|
148
|
-
turnStartedAt: 0,
|
|
149
|
-
turnId: '',
|
|
150
|
-
usageLimits: null,
|
|
151
|
-
failed: false,
|
|
152
|
-
calls: new Map(),
|
|
153
|
-
patches: new Map(),
|
|
154
|
-
lines: { added: 0, removed: 0 },
|
|
155
|
-
tokens: null,
|
|
156
|
-
context: null,
|
|
157
|
-
contextSample: emptySample(),
|
|
158
|
-
sizes: emptySizes(),
|
|
159
|
-
sent: emptySent(),
|
|
160
|
-
plan: null,
|
|
161
|
-
history: [],
|
|
162
|
-
contextParts: null,
|
|
163
|
-
lastTotal: -1,
|
|
164
|
-
modelTokens: new Map(),
|
|
165
|
-
activity: emptyActivity(),
|
|
166
|
-
files: new Map(),
|
|
167
|
-
promptList: [],
|
|
168
|
-
fallbackPromptList: [],
|
|
169
|
-
errors: [],
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const AGENTS_FILE = /^# AGENTS\.md instructions for ([^\n]+)/
|
|
174
|
-
|
|
175
|
-
const emptyWords = () => ({ lastAt: 0, thoughts: [], replies: [] })
|
|
176
|
-
|
|
177
|
-
function applyWords(state, record) {
|
|
178
|
-
const at = Date.parse(record.timestamp) || state.lastAt
|
|
179
|
-
state.lastAt = at
|
|
180
|
-
const item = record.payload
|
|
181
|
-
if (record.type !== 'response_item' || !item || typeof item !== 'object') return
|
|
182
|
-
const id = text(item.id) || null
|
|
183
|
-
if (item.type === 'reasoning') keepText(state.thoughts, 'thoughts', { id, at, text: joinedText(item.summary, 'summary_text', 'text') })
|
|
184
|
-
else if (item.type === 'message' && item.role === 'assistant') keepText(state.replies, 'replies', { id, at, text: joinedText(item.content, 'output_text', 'text') })
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const emptySent = () => ({ base: 0, developer: [], other: 0, memory: new Map(), tools: new Map() })
|
|
188
|
-
|
|
189
|
-
function callName(item) {
|
|
190
|
-
if (item.type === 'local_shell_call') return 'shell'
|
|
191
|
-
if (item.type === 'tool_search_call') return 'tool_search'
|
|
192
|
-
return `${text(item.namespace)}${text(item.name)}`
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function codexFixed(sent) {
|
|
196
|
-
const fixed = emptyFixed()
|
|
197
|
-
const developer = sent.developer.reduce((n, chars) => n + chars, 0)
|
|
198
|
-
fixed.system = developer + (sent.developer.includes(sent.base) ? 0 : sent.base) + sent.other
|
|
199
|
-
fixed.mcp = new Map(sent.tools)
|
|
200
|
-
fixed.memory = new Map(sent.memory)
|
|
201
|
-
fixed.recorded = new Set(['system', 'mcpTools', 'memory'])
|
|
202
|
-
return fixed
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function applySizes(state, record, item, at) {
|
|
206
|
-
const { sizes, sent } = state
|
|
207
|
-
const before = sizes.first == null
|
|
208
|
-
if (record.type === 'session_meta') {
|
|
209
|
-
sent.base = text(item.base_instructions?.text).length
|
|
210
|
-
} else if (record.type === 'compacted') {
|
|
211
|
-
sizeCompaction(sizes)
|
|
212
|
-
return
|
|
213
|
-
} else if (record.type === 'event_msg') {
|
|
214
|
-
if (item.type === 'token_count' && item.info?.last_token_usage) sizeRequest(sizes, number(item.info.last_token_usage.input_tokens), at)
|
|
215
|
-
return
|
|
216
|
-
} else if (record.type !== 'response_item') {
|
|
217
|
-
return
|
|
218
|
-
} else if (item.type === 'reasoning') {
|
|
219
|
-
sizeReply(sizes, joinedText(item.summary, 'summary_text', 'text').length)
|
|
220
|
-
return
|
|
221
|
-
} else if (item.type === 'message' && item.role === 'assistant') {
|
|
222
|
-
sizeReply(sizes, joinedText(item.content, 'output_text', 'text').length)
|
|
223
|
-
return
|
|
224
|
-
} else if (CALLS.has(item.type)) {
|
|
225
|
-
sizeCall(sizes, text(item.call_id), callName(item), (text(item.arguments) || text(item.input) || JSON.stringify(item.action ?? '')).length)
|
|
226
|
-
return
|
|
227
|
-
} else if (OUTPUTS.has(item.type)) {
|
|
228
|
-
sizeResult(sizes, text(item.call_id), outputText(item.output).length)
|
|
229
|
-
return
|
|
230
|
-
} else if (item.type === 'additional_tools') {
|
|
231
|
-
for (const group of Array.isArray(item.tools) ? item.tools : []) {
|
|
232
|
-
const tools = Array.isArray(group?.tools) ? group.tools.length : 0
|
|
233
|
-
sent.tools.set(text(group?.name) || 'tools', { tools: tools || null, chars: JSON.stringify(group ?? null).length })
|
|
234
|
-
}
|
|
235
|
-
} else if (item.type === 'message') {
|
|
236
|
-
const raw = contentText(item.content)
|
|
237
|
-
const agents = item.role === 'user' ? AGENTS_FILE.exec(raw) : null
|
|
238
|
-
if (agents) sent.memory.set(`${agents[1].trim().replace(/[\\/]+$/, '')}/AGENTS.md`, raw.length)
|
|
239
|
-
else if (item.role === 'user' && promptText(raw)) {
|
|
240
|
-
sizePrompt(sizes, raw.length)
|
|
241
|
-
return
|
|
242
|
-
} else if (!before) {
|
|
243
|
-
sizePrompt(sizes, raw.length)
|
|
244
|
-
return
|
|
245
|
-
} else if (item.role === 'developer' || item.role === 'system') sent.developer.push(raw.length)
|
|
246
|
-
else sent.other += raw.length
|
|
247
|
-
} else {
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
setFixed(sizes, codexFixed(sent))
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function applyRecord(state, record) {
|
|
254
|
-
const at = Date.parse(record.timestamp)
|
|
255
|
-
if (at) {
|
|
256
|
-
if (!state.startedAt) state.startedAt = at
|
|
257
|
-
if (at > state.lastAt) state.lastAt = at
|
|
258
|
-
}
|
|
259
|
-
const payload = record.payload
|
|
260
|
-
if (!payload || typeof payload !== 'object') return
|
|
261
|
-
applySizes(state, record, payload, at || state.lastAt)
|
|
262
|
-
if (record.type === 'session_meta') applyMeta(state, payload)
|
|
263
|
-
else if (record.type === 'turn_context') applySettings(state, payload.model, payload.effort, payload.cwd)
|
|
264
|
-
else if (record.type === 'response_item') applyItem(state, payload, at || state.lastAt)
|
|
265
|
-
else if (record.type === 'event_msg') applyEvent(state, payload, at || state.lastAt)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function applyMeta(state, meta) {
|
|
269
|
-
const subagent = meta.source?.subagent
|
|
270
|
-
const spawn = subagent?.thread_spawn
|
|
271
|
-
state.threadId ||= text(meta.id).toLowerCase()
|
|
272
|
-
state.parentId ||= (text(meta.parent_thread_id) || text(spawn?.parent_thread_id)).toLowerCase()
|
|
273
|
-
state.subagent ||= meta.thread_source === 'subagent' || Boolean(spawn)
|
|
274
|
-
state.internal ||= INTERNAL_SOURCES.has(meta.thread_source) || (subagent != null && !spawn)
|
|
275
|
-
state.cwd ||= text(meta.cwd)
|
|
276
|
-
state.gitBranch ||= text(meta.git?.branch)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function applySettings(state, model, effort, cwd) {
|
|
280
|
-
if (model) state.model = text(model)
|
|
281
|
-
if (effort) state.effort = text(effort)
|
|
282
|
-
if (cwd) state.cwd = text(cwd)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function countPrompt(state, raw, at) {
|
|
286
|
-
const prompt = promptText(raw)
|
|
287
|
-
if (!prompt) return
|
|
288
|
-
state.prompts++
|
|
289
|
-
state.firstPrompt ||= prompt
|
|
290
|
-
keepNewest(state.promptList, { at, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function applyItem(state, item, at) {
|
|
294
|
-
if (item.type === 'message' && item.role === 'user') {
|
|
295
|
-
const prompt = promptText(contentText(item.content))
|
|
296
|
-
if (prompt) {
|
|
297
|
-
state.fallbackPrompts++
|
|
298
|
-
state.fallbackPrompt ||= prompt
|
|
299
|
-
keepNewest(state.fallbackPromptList, { at, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
|
|
300
|
-
}
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
const id = text(item.call_id)
|
|
304
|
-
if (!id) return
|
|
305
|
-
if (CALLS.has(item.type)) {
|
|
306
|
-
const name = callName(item)
|
|
307
|
-
const args = item.type === 'function_call' ? parseArgs(item.arguments) : null
|
|
308
|
-
if (item.type === 'function_call' && !item.namespace && item.name === 'update_plan') state.plan = nextPlan(state.plan, 'update_plan', args)
|
|
309
|
-
const patch = item.name === 'apply_patch' ? (item.type === 'custom_tool_call' ? text(item.input) : text(args?.input)) : ''
|
|
310
|
-
if (patch) state.patches.set(id, patch)
|
|
311
|
-
const file = patch ? patchFile(patch) : ''
|
|
312
|
-
const target = callTarget(item, args, patch, state.cwd)
|
|
313
|
-
const input = callInput(item, args, patch)
|
|
314
|
-
const entry = { name, startedAt: at, target, kind: inputKind(name, input).kind }
|
|
315
|
-
const questions = QUESTION_TOOLS.has(name) ? askQuestions(args) : null
|
|
316
|
-
if (questions) entry.questions = questions
|
|
317
|
-
state.calls.set(id, entry)
|
|
318
|
-
startCall(state.activity, { id, name, target, input, path: file ? relativePath(file, state.cwd) : null, startedAt: at })
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
if (!OUTPUTS.has(item.type)) return
|
|
322
|
-
state.calls.delete(id)
|
|
323
|
-
const patch = state.patches.get(id)
|
|
324
|
-
if (patch === undefined) {
|
|
325
|
-
const failed = exitFailed(item.output)
|
|
326
|
-
endCall(state.activity, id, at, failed, failed ? outputText(item.output) : null)
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
state.patches.delete(id)
|
|
330
|
-
const output = outputText(item.output)
|
|
331
|
-
const applied = PATCH_APPLIED.test(output)
|
|
332
|
-
endCall(state.activity, id, at, !applied, applied ? null : output)
|
|
333
|
-
if (applied) addLines(state, patch)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function addLines(state, patch) {
|
|
337
|
-
const counts = new Map()
|
|
338
|
-
let entry = null
|
|
339
|
-
let hunk = null
|
|
340
|
-
for (const line of patch.split('\n')) {
|
|
341
|
-
const header = PATCH_FILE.exec(line)
|
|
342
|
-
if (header) {
|
|
343
|
-
const file = header[1].trim()
|
|
344
|
-
entry = counts.get(file) || { added: 0, removed: 0, hunks: [] }
|
|
345
|
-
counts.set(file, entry)
|
|
346
|
-
hunk = null
|
|
347
|
-
continue
|
|
348
|
-
}
|
|
349
|
-
if (line.startsWith('@@')) {
|
|
350
|
-
hunk = null
|
|
351
|
-
continue
|
|
352
|
-
}
|
|
353
|
-
if (line.startsWith('***')) continue
|
|
354
|
-
if (line.startsWith('+')) {
|
|
355
|
-
state.lines.added++
|
|
356
|
-
if (entry) entry.added++
|
|
357
|
-
} else if (line.startsWith('-')) {
|
|
358
|
-
state.lines.removed++
|
|
359
|
-
if (entry) entry.removed++
|
|
360
|
-
} else if (!line.startsWith(' ')) continue
|
|
361
|
-
if (!entry) continue
|
|
362
|
-
if (!hunk) {
|
|
363
|
-
hunk = []
|
|
364
|
-
entry.hunks.push(hunk)
|
|
365
|
-
}
|
|
366
|
-
hunk.push(line)
|
|
367
|
-
}
|
|
368
|
-
for (const [name, count] of counts) {
|
|
369
|
-
addFileChange(state.files, name, count.added, count.removed, count.hunks.map((lines) => diffHunk({ lines })))
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function applyEvent(state, event, at) {
|
|
374
|
-
switch (event.type) {
|
|
375
|
-
case 'task_started':
|
|
376
|
-
case 'turn_started': {
|
|
377
|
-
const turn = text(event.turn_id)
|
|
378
|
-
if (!state.open || (turn && turn !== state.turnId)) state.turnStartedAt = at
|
|
379
|
-
if (turn) state.turnId = turn
|
|
380
|
-
state.open = true
|
|
381
|
-
state.failed = false
|
|
382
|
-
state.calls.clear()
|
|
383
|
-
settleCalls(state.activity)
|
|
384
|
-
break
|
|
385
|
-
}
|
|
386
|
-
case 'task_complete':
|
|
387
|
-
case 'turn_complete':
|
|
388
|
-
state.open = false
|
|
389
|
-
state.failed = event.error != null
|
|
390
|
-
state.calls.clear()
|
|
391
|
-
settleCalls(state.activity)
|
|
392
|
-
if (event.error != null) {
|
|
393
|
-
const message = text(event.error?.message) || text(event.error) || 'The turn failed'
|
|
394
|
-
keepNewest(state.errors, { at, message: clip(message, DETAIL_LIMITS.errorText) }, DETAIL_LIMITS.errors)
|
|
395
|
-
}
|
|
396
|
-
break
|
|
397
|
-
case 'turn_aborted':
|
|
398
|
-
state.open = false
|
|
399
|
-
state.failed = false
|
|
400
|
-
state.calls.clear()
|
|
401
|
-
settleCalls(state.activity)
|
|
402
|
-
break
|
|
403
|
-
case 'token_count':
|
|
404
|
-
applyTokens(state, event.info, at)
|
|
405
|
-
applyLimits(state, event.rate_limits, at)
|
|
406
|
-
break
|
|
407
|
-
case 'user_message':
|
|
408
|
-
state.typed = true
|
|
409
|
-
countPrompt(state, event.message, at)
|
|
410
|
-
break
|
|
411
|
-
case 'item_completed':
|
|
412
|
-
if (event.item?.type !== 'UserMessage') break
|
|
413
|
-
state.typed = true
|
|
414
|
-
countPrompt(state, contentText(event.item.content), at)
|
|
415
|
-
break
|
|
416
|
-
case 'thread_settings_applied': {
|
|
417
|
-
const settings = event.thread_settings
|
|
418
|
-
if (settings) applySettings(state, settings.model, settings.reasoning_effort, settings.cwd)
|
|
419
|
-
break
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const LIMIT_WINDOWS = ['primary', 'secondary']
|
|
425
|
-
|
|
426
|
-
function applyLimits(state, limits, at) {
|
|
427
|
-
if (!limits || typeof limits !== 'object') return
|
|
428
|
-
const windows = []
|
|
429
|
-
for (const id of LIMIT_WINDOWS) {
|
|
430
|
-
const w = limits[id]
|
|
431
|
-
if (!w || typeof w !== 'object' || w.used_percent == null) continue
|
|
432
|
-
const resetsAt = w.resets_at != null ? number(w.resets_at) * 1000 : w.resets_in_seconds != null ? at + number(w.resets_in_seconds) * 1000 : null
|
|
433
|
-
windows.push({ id, usedPercent: number(w.used_percent), windowMinutes: w.window_minutes == null ? null : number(w.window_minutes), resetsAt: resetsAt || null })
|
|
434
|
-
}
|
|
435
|
-
if (!windows.length) return
|
|
436
|
-
state.usageLimits = { at, subscription: text(limits.plan_type) || null, windows }
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function applyTokens(state, info, at) {
|
|
440
|
-
if (!info) return
|
|
441
|
-
const total = info.total_token_usage
|
|
442
|
-
if (total) {
|
|
443
|
-
const cached = number(total.cached_input_tokens)
|
|
444
|
-
const next = {
|
|
445
|
-
input: Math.max(0, number(total.input_tokens) - cached),
|
|
446
|
-
output: number(total.output_tokens),
|
|
447
|
-
cacheRead: cached,
|
|
448
|
-
cacheWrite: number(total.cache_write_input_tokens),
|
|
449
|
-
}
|
|
450
|
-
if (state.model) {
|
|
451
|
-
const sums = state.modelTokens.get(state.model) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
452
|
-
for (const key of Object.keys(next)) sums[key] += Math.max(0, next[key] - (state.tokens?.[key] || 0))
|
|
453
|
-
state.modelTokens.set(state.model, sums)
|
|
454
|
-
}
|
|
455
|
-
state.tokens = next
|
|
456
|
-
}
|
|
457
|
-
const last = info.last_token_usage
|
|
458
|
-
if (!last) return
|
|
459
|
-
state.context = { used: number(last.total_tokens), limit: number(info.model_context_window) }
|
|
460
|
-
if (last.input_tokens != null) {
|
|
461
|
-
const cached = number(last.cached_input_tokens)
|
|
462
|
-
state.contextParts = { input: Math.max(0, number(last.input_tokens) - cached), cacheRead: cached, cacheWrite: 0, output: number(last.output_tokens) }
|
|
463
|
-
}
|
|
464
|
-
const sum = number(total?.total_tokens)
|
|
465
|
-
if (sum === state.lastTotal) return
|
|
466
|
-
state.lastTotal = sum
|
|
467
|
-
samplePoint(state.contextSample, last.total_tokens)
|
|
468
|
-
keepNewest(state.history, { at, context: number(last.total_tokens), output: number(last.output_tokens) }, DETAIL_LIMITS.history)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export function codex({
|
|
472
|
-
home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
|
|
473
|
-
sqliteHome = process.env.CODEX_SQLITE_HOME || home,
|
|
474
|
-
now = Date.now,
|
|
475
|
-
} = {}) {
|
|
476
|
-
const sessionsDir = path.join(home, 'sessions')
|
|
477
|
-
const locksDir = path.join(home, 'thread-writer-locks')
|
|
478
|
-
const indexFile = path.join(home, 'session_index.jsonl')
|
|
479
|
-
const rollouts = jsonlFollower({ empty: emptyRollout, apply: applyRecord })
|
|
480
|
-
const openedFor = recentReaders(() => jsonlFollower({ empty: emptyWords, apply: applyWords, tail: { bytes: TAIL_BYTES, enough: wordsFull } }), OPENED_KEPT)
|
|
481
|
-
let index = { mtime: -1, names: new Map() }
|
|
482
|
-
let known = new Map()
|
|
483
|
-
let childrenOf = new Map()
|
|
484
|
-
|
|
485
|
-
async function findRollouts(locks) {
|
|
486
|
-
const cutoff = now() - RECENT_MS
|
|
487
|
-
const byId = new Map()
|
|
488
|
-
for (const year of await listDirs(sessionsDir)) {
|
|
489
|
-
for (const month of await listDirs(year)) {
|
|
490
|
-
for (const day of await listDirs(month)) {
|
|
491
|
-
for (const file of await listFiles(day, (n) => ROLLOUT_FILE.test(n))) {
|
|
492
|
-
const id = ROLLOUT_FILE.exec(path.basename(file))[1].toLowerCase()
|
|
493
|
-
const stat = await fsp.stat(file).catch(() => null)
|
|
494
|
-
if (!stat || (stat.mtimeMs < cutoff && !locks.has(id))) continue
|
|
495
|
-
const existing = byId.get(id)
|
|
496
|
-
if (existing && existing.stat.mtimeMs >= stat.mtimeMs) continue
|
|
497
|
-
byId.set(id, { id, file, stat })
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return [...byId.values()]
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async function latestStateDb() {
|
|
506
|
-
const files = await listFiles(sqliteHome, (n) => STATE_DB.test(n))
|
|
507
|
-
return files.sort((a, b) => Number(STATE_DB.exec(path.basename(b))[1]) - Number(STATE_DB.exec(path.basename(a))[1]))[0] || ''
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
async function threadRecords() {
|
|
511
|
-
const stat = await fsp.stat(indexFile).catch(() => null)
|
|
512
|
-
if (stat && stat.mtimeMs !== index.mtime) {
|
|
513
|
-
const names = new Map()
|
|
514
|
-
for (const row of jsonLines(await fsp.readFile(indexFile, 'utf8').catch(() => ''))) {
|
|
515
|
-
if (row?.id && row.thread_name) names.set(String(row.id).toLowerCase(), text(row.thread_name))
|
|
516
|
-
}
|
|
517
|
-
index = { mtime: stat.mtimeMs, names }
|
|
518
|
-
}
|
|
519
|
-
const names = new Map(index.names)
|
|
520
|
-
const archived = new Set()
|
|
521
|
-
const read = new Map()
|
|
522
|
-
await withReadOnly(await latestStateDb(), (db) => {
|
|
523
|
-
const tables = tablesOf(db)
|
|
524
|
-
if (!tables.has('threads')) return
|
|
525
|
-
const columns = columnsOf(db, 'threads')
|
|
526
|
-
if (columns.has('name')) {
|
|
527
|
-
for (const row of db.prepare("SELECT id, name FROM threads WHERE name IS NOT NULL AND name != ''").all()) {
|
|
528
|
-
names.set(String(row.id).toLowerCase(), text(row.name))
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if (columns.has('archived')) {
|
|
532
|
-
for (const row of db.prepare('SELECT id FROM threads WHERE archived = 1').all()) archived.add(String(row.id).toLowerCase())
|
|
533
|
-
}
|
|
534
|
-
if (tables.has('thread_read_receipts') && ['thread_id', 'first_unread_turn'].every((c) => columnsOf(db, 'thread_read_receipts').has(c))) {
|
|
535
|
-
for (const row of db.prepare('SELECT thread_id, first_unread_turn FROM thread_read_receipts').all()) {
|
|
536
|
-
read.set(String(row.thread_id).toLowerCase(), row.first_unread_turn != null)
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
})
|
|
540
|
-
return { names, archived, read }
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
async function scanSessions() {
|
|
544
|
-
const at = now()
|
|
545
|
-
const locking = await exists(locksDir)
|
|
546
|
-
const locks = new Set((await listFiles(locksDir, (n) => n.endsWith('.lock'))).map((f) => path.basename(f, '.lock').toLowerCase()))
|
|
547
|
-
const [found, { names, archived, read: unread }] = await Promise.all([findRollouts(locks), threadRecords()])
|
|
548
|
-
const read = []
|
|
549
|
-
for (const rollout of found) {
|
|
550
|
-
try {
|
|
551
|
-
read.push({ ...rollout, state: await rollouts.read(rollout.file, rollout.stat) })
|
|
552
|
-
} catch {}
|
|
553
|
-
}
|
|
554
|
-
rollouts.sweep()
|
|
555
|
-
known = new Map(read.map((r) => [r.id, r.file]))
|
|
556
|
-
childrenOf = new Map()
|
|
557
|
-
for (const { id, state } of read) {
|
|
558
|
-
if (state.subagent && state.parentId) childrenOf.set(state.parentId, [...(childrenOf.get(state.parentId) || []), id])
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const running = new Map()
|
|
562
|
-
for (const { id, stat, state } of read) {
|
|
563
|
-
if (!state.subagent || !state.parentId || !state.open) continue
|
|
564
|
-
if (!locks.has(id) && at - stat.mtimeMs > SUBAGENT_WINDOW_MS) continue
|
|
565
|
-
running.set(state.parentId, [...(running.get(state.parentId) || []), { id, name: childName(state), startedAt: state.startedAt }])
|
|
566
|
-
}
|
|
567
|
-
const workingOf = (id) => (running.get(id) || []).sort((a, b) => a.startedAt - b.startedAt).map(({ id: childId, name }) => ({ id: childId, name }))
|
|
568
|
-
|
|
569
|
-
return read
|
|
570
|
-
.filter(({ id, state }) => !state.subagent && !state.internal && !archived.has(id))
|
|
571
|
-
.map(({ id, stat, state }) => {
|
|
572
|
-
const lastActivityAt = Math.max(state.lastAt, Math.floor(stat.mtimeMs))
|
|
573
|
-
const fresh = at - lastActivityAt < ACTIVE_WINDOW_MS
|
|
574
|
-
const live = locking ? locks.has(id) : state.open && fresh
|
|
575
|
-
const workingSubagents = workingOf(id)
|
|
576
|
-
return toSession({ id, state, names, live, turnLive: state.open && (locking ? live : fresh), fresh, locking, unread: unread.get(id), subagents: workingSubagents.length, workingSubagents, lastActivityAt })
|
|
577
|
-
})
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
async function readKnown(id) {
|
|
581
|
-
const file = known.get(id)
|
|
582
|
-
const stat = file ? await fsp.stat(file).catch(() => null) : null
|
|
583
|
-
if (!stat) return null
|
|
584
|
-
const state = await rollouts.read(file, stat).catch(() => null)
|
|
585
|
-
return state ? { file, stat, state } : null
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
589
|
-
const id = String(key).replace(/^codex:/, '').toLowerCase()
|
|
590
|
-
const found = await readKnown(id)
|
|
591
|
-
if (!found || found.state.subagent || found.state.internal) return null
|
|
592
|
-
const { file, stat, state } = found
|
|
593
|
-
if (filesOnly) return { id: `codex:${id}`, sessionId: id, files: filePaths(state.files, state.cwd) }
|
|
594
|
-
const said = await openedFor(file).read(file, stat).catch(() => null)
|
|
595
|
-
const limit = state.context?.limit || null
|
|
596
|
-
const subagents = []
|
|
597
|
-
for (const childId of childrenOf.get(id) || []) {
|
|
598
|
-
const child = await readKnown(childId)
|
|
599
|
-
if (!child) continue
|
|
600
|
-
const c = child.state
|
|
601
|
-
const lastActivityAt = Math.max(c.lastAt, Math.floor(child.stat.mtimeMs))
|
|
602
|
-
const status = c.open && now() - lastActivityAt < ACTIVE_WINDOW_MS ? 'working' : c.failed ? 'error' : 'done'
|
|
603
|
-
subagents.push({
|
|
604
|
-
id: childId,
|
|
605
|
-
name: childName(c),
|
|
606
|
-
type: '',
|
|
607
|
-
status,
|
|
608
|
-
model: c.model,
|
|
609
|
-
tokens: c.tokens ? { ...c.tokens } : null,
|
|
610
|
-
tool: status === 'working' ? latestCall(c.calls) : null,
|
|
611
|
-
startedAt: c.startedAt,
|
|
612
|
-
lastActivityAt,
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
id: `codex:${id}`,
|
|
617
|
-
sessionId: id,
|
|
618
|
-
source: file,
|
|
619
|
-
models: [...state.modelTokens].map(([model, tokens]) => ({ model, tokens: { ...tokens }, costUsd: null })),
|
|
620
|
-
contextParts: state.contextParts ? { ...state.contextParts } : null,
|
|
621
|
-
history: state.history.map((p) => ({ ...p })),
|
|
622
|
-
compactions: null,
|
|
623
|
-
...activityDetail(state.activity),
|
|
624
|
-
plan: planItems(state.plan),
|
|
625
|
-
subagents: subagents.sort((a, b) => a.startedAt - b.startedAt),
|
|
626
|
-
files: fileList(state.files, state.cwd),
|
|
627
|
-
prompts: (state.typed ? state.promptList : state.fallbackPromptList).map((p) => ({ ...p })),
|
|
628
|
-
thoughts: textList(said?.thoughts),
|
|
629
|
-
replies: textList(said?.replies),
|
|
630
|
-
contextBreakdown: contextBreakdown(state.sizes, {
|
|
631
|
-
total: state.context?.used,
|
|
632
|
-
limit,
|
|
633
|
-
reserved: limit ? Math.round(limit * (1 - COMPACT_SHARE / REPORTED_SHARE)) : null,
|
|
634
|
-
}),
|
|
635
|
-
errors: state.errors.map((e) => ({ ...e })),
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return {
|
|
640
|
-
id: 'codex',
|
|
641
|
-
name: 'Codex',
|
|
642
|
-
detect: () => exists(sessionsDir),
|
|
643
|
-
watchRoots: () => [sessionsDir, locksDir],
|
|
644
|
-
scanSessions,
|
|
645
|
-
sessionDetail,
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const childName = (state) => titleFromPrompt(state.typed ? state.firstPrompt : state.fallbackPrompt) || 'Subagent'
|
|
650
|
-
|
|
651
|
-
function toSession({ id, state, names, live, turnLive, fresh, locking, unread, subagents, workingSubagents, lastActivityAt }) {
|
|
652
|
-
const calls = [...state.calls.values()].sort((a, b) => a.startedAt - b.startedAt)
|
|
653
|
-
const asked = calls.filter((c) => QUESTION_TOOLS.has(c.name))
|
|
654
|
-
let status
|
|
655
|
-
let statusReason
|
|
656
|
-
if (turnLive) {
|
|
657
|
-
if (asked.length) [status, statusReason] = ['asking', asked.some((c) => c.name === 'request_user_input') ? 'question' : 'approval']
|
|
658
|
-
else if (calls.length ? calls.every((c) => AGENT_TOOLS.has(c.name)) : subagents > 0) [status, statusReason] = ['delegating', 'subagents']
|
|
659
|
-
else [status, statusReason] = ['working', 'turn-open']
|
|
660
|
-
} else if (subagents) [status, statusReason] = ['delegating', 'subagents']
|
|
661
|
-
else if (state.failed) [status, statusReason] = ['error', 'turn-failed']
|
|
662
|
-
else if (unread !== undefined) [status, statusReason] = unread ? ['done', 'unread'] : ['idle', 'read']
|
|
663
|
-
else [status, statusReason] = (locking ? live : fresh) ? ['done', 'recent'] : ['idle', 'quiet']
|
|
664
|
-
|
|
665
|
-
const tool = turnLive && (status === 'working' || status === 'delegating') ? latestCall(state.calls) : null
|
|
666
|
-
const prompt = state.typed ? state.firstPrompt : state.fallbackPrompt
|
|
667
|
-
const cwd = state.cwd
|
|
668
|
-
return {
|
|
669
|
-
id: `codex:${id}`,
|
|
670
|
-
harness: 'codex',
|
|
671
|
-
title: names.get(id) ||
|
|
672
|
-
project: path.basename(cwd.replace(/\\/g, '/')) || cwd || 'unknown',
|
|
673
|
-
projectPath: cwd,
|
|
674
|
-
worktree: '',
|
|
675
|
-
cwd,
|
|
676
|
-
gitBranch: state.gitBranch,
|
|
677
|
-
model: state.model,
|
|
678
|
-
effort: state.effort,
|
|
679
|
-
status,
|
|
680
|
-
live: live || turnLive,
|
|
681
|
-
startedAt: state.startedAt,
|
|
682
|
-
lastActivityAt,
|
|
683
|
-
tool,
|
|
684
|
-
tokens: state.tokens ? { ...state.tokens } : null,
|
|
685
|
-
context: state.context && state.context.used ? { ...state.context } : null,
|
|
686
|
-
contextHistory: sampleList(state.contextSample, state.context?.used),
|
|
687
|
-
costUsd: null,
|
|
688
|
-
lines: { ...state.lines },
|
|
689
|
-
plan: planSummary(state.plan),
|
|
690
|
-
subagents,
|
|
691
|
-
workingSubagents,
|
|
692
|
-
prompts: state.typed ? state.prompts : state.fallbackPrompts,
|
|
693
|
-
turnStartedAt: turnLive ? state.turnStartedAt || null : null,
|
|
694
|
-
statusReason,
|
|
695
|
-
ask: askOf(status, { calls: asked, errorText: state.errors.at(-1)?.message }),
|
|
696
|
-
usageLimits: state.usageLimits ? { ...state.usageLimits, windows: state.usageLimits.windows.map((w) => ({ ...w })) } : null,
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
export default codex()
|
|
1
|
+
import fsp from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { exists, jsonLines, listDirs, listFiles } from '../lib/fsutil.mjs'
|
|
5
|
+
import { contextBreakdown, emptyFixed, emptySizes, setFixed, sizeCall, sizeCompaction, sizePrompt, sizeReply, sizeRequest, sizeResult } from '../lib/context-breakdown.mjs'
|
|
6
|
+
import { jsonlFollower, recentReaders } from '../lib/jsonl-follower.mjs'
|
|
7
|
+
import { columnsOf, tablesOf, withReadOnly } from '../lib/sqlite.mjs'
|
|
8
|
+
import {
|
|
9
|
+
DETAIL_LIMITS,
|
|
10
|
+
RECENT_MS,
|
|
11
|
+
activityDetail,
|
|
12
|
+
addFileChange,
|
|
13
|
+
askOf,
|
|
14
|
+
askQuestions,
|
|
15
|
+
clip,
|
|
16
|
+
clipBlock,
|
|
17
|
+
diffHunk,
|
|
18
|
+
emptyActivity,
|
|
19
|
+
emptySample,
|
|
20
|
+
endCall,
|
|
21
|
+
fileList,
|
|
22
|
+
filePaths,
|
|
23
|
+
inputKind,
|
|
24
|
+
joinedText,
|
|
25
|
+
keepNewest,
|
|
26
|
+
keepText,
|
|
27
|
+
nextPlan,
|
|
28
|
+
number,
|
|
29
|
+
planItems,
|
|
30
|
+
planSummary,
|
|
31
|
+
relativePath,
|
|
32
|
+
sampleList,
|
|
33
|
+
samplePoint,
|
|
34
|
+
settleCalls,
|
|
35
|
+
startCall,
|
|
36
|
+
text,
|
|
37
|
+
textList,
|
|
38
|
+
wordsFull,
|
|
39
|
+
titleFromPrompt,
|
|
40
|
+
toolInput,
|
|
41
|
+
toolNow,
|
|
42
|
+
toolTarget,
|
|
43
|
+
} from '../lib/telemetry-fields.mjs'
|
|
44
|
+
|
|
45
|
+
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
46
|
+
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
47
|
+
const OPENED_KEPT = 3
|
|
48
|
+
const TAIL_BYTES = 512 * 1024
|
|
49
|
+
const COMPACT_SHARE = 0.9
|
|
50
|
+
const REPORTED_SHARE = 0.95
|
|
51
|
+
const ROLLOUT_FILE = /^rollout-.{19}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i
|
|
52
|
+
const STATE_DB = /^state_(\d+)\.sqlite$/
|
|
53
|
+
const CALLS = new Set(['function_call', 'custom_tool_call', 'local_shell_call', 'tool_search_call'])
|
|
54
|
+
const OUTPUTS = new Set(['function_call_output', 'custom_tool_call_output', 'tool_search_output'])
|
|
55
|
+
const AGENT_TOOLS = new Set(['spawn_agent', 'wait_agent', 'send_input'])
|
|
56
|
+
const QUESTION_TOOLS = new Set(['request_user_input', 'request_permissions'])
|
|
57
|
+
const INTERNAL_SOURCES = new Set(['guardian_review', 'memory_consolidation'])
|
|
58
|
+
const PATCH_APPLIED = /Success\. Updated the following files/
|
|
59
|
+
const PATCH_FILE = /^\*\*\* (?:Update|Add|Delete) File: (.+)$/
|
|
60
|
+
const NOISE = /<([a-z][\w-]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g
|
|
61
|
+
|
|
62
|
+
function contentText(content) {
|
|
63
|
+
if (typeof content === 'string') return content
|
|
64
|
+
if (!Array.isArray(content)) return ''
|
|
65
|
+
return content.map((p) => (typeof p === 'string' ? p : text(p?.text) || text(p?.input_text))).filter(Boolean).join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function promptText(raw) {
|
|
69
|
+
const prompt = text(raw).replace(NOISE, ' ').trim()
|
|
70
|
+
return prompt && !prompt.startsWith('<') && !prompt.startsWith('# AGENTS.md') ? prompt : ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseArgs(raw) {
|
|
74
|
+
if (raw && typeof raw === 'object') return raw
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text(raw) || '{}')
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function outputText(output) {
|
|
83
|
+
if (Array.isArray(output)) return contentText(output)
|
|
84
|
+
const raw = text(output)
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(raw)
|
|
87
|
+
return typeof parsed?.output === 'string' ? parsed.output : raw
|
|
88
|
+
} catch {
|
|
89
|
+
return raw
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function exitFailed(output) {
|
|
94
|
+
if (typeof output !== 'string') return null
|
|
95
|
+
try {
|
|
96
|
+
const code = JSON.parse(output)?.metadata?.exit_code
|
|
97
|
+
return Number.isFinite(code) ? code !== 0 : null
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const patchFile = (patch) => patch.split('\n').map((line) => PATCH_FILE.exec(line)?.[1]).find(Boolean)?.trim() || ''
|
|
104
|
+
|
|
105
|
+
function callTarget(item, args, patch, cwd) {
|
|
106
|
+
if (patch) {
|
|
107
|
+
const file = patchFile(patch)
|
|
108
|
+
return file ? toolTarget({ path: file }, cwd) : ''
|
|
109
|
+
}
|
|
110
|
+
if (item.type === 'local_shell_call') return toolTarget({ command: item.action?.command }, cwd)
|
|
111
|
+
if (item.type === 'custom_tool_call') return toolTarget(text(item.input), cwd)
|
|
112
|
+
return toolTarget(args ?? parseArgs(item.arguments), cwd)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function callInput(item, args, patch) {
|
|
116
|
+
if (patch) return null
|
|
117
|
+
if (item.type === 'local_shell_call') return toolInput({ command: item.action?.command })
|
|
118
|
+
if (item.type === 'custom_tool_call') return toolInput(text(item.input))
|
|
119
|
+
return toolInput(args ?? parseArgs(item.arguments))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function latestCall(calls) {
|
|
123
|
+
let latest = null
|
|
124
|
+
for (const call of calls.values()) {
|
|
125
|
+
if (!latest || call.startedAt >= latest.startedAt) latest = call
|
|
126
|
+
}
|
|
127
|
+
return toolNow(latest)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function emptyRollout() {
|
|
131
|
+
return {
|
|
132
|
+
threadId: '',
|
|
133
|
+
parentId: '',
|
|
134
|
+
subagent: false,
|
|
135
|
+
internal: false,
|
|
136
|
+
cwd: '',
|
|
137
|
+
gitBranch: '',
|
|
138
|
+
model: '',
|
|
139
|
+
effort: '',
|
|
140
|
+
startedAt: 0,
|
|
141
|
+
lastAt: 0,
|
|
142
|
+
firstPrompt: '',
|
|
143
|
+
prompts: 0,
|
|
144
|
+
typed: false,
|
|
145
|
+
fallbackPrompt: '',
|
|
146
|
+
fallbackPrompts: 0,
|
|
147
|
+
open: false,
|
|
148
|
+
turnStartedAt: 0,
|
|
149
|
+
turnId: '',
|
|
150
|
+
usageLimits: null,
|
|
151
|
+
failed: false,
|
|
152
|
+
calls: new Map(),
|
|
153
|
+
patches: new Map(),
|
|
154
|
+
lines: { added: 0, removed: 0 },
|
|
155
|
+
tokens: null,
|
|
156
|
+
context: null,
|
|
157
|
+
contextSample: emptySample(),
|
|
158
|
+
sizes: emptySizes(),
|
|
159
|
+
sent: emptySent(),
|
|
160
|
+
plan: null,
|
|
161
|
+
history: [],
|
|
162
|
+
contextParts: null,
|
|
163
|
+
lastTotal: -1,
|
|
164
|
+
modelTokens: new Map(),
|
|
165
|
+
activity: emptyActivity(),
|
|
166
|
+
files: new Map(),
|
|
167
|
+
promptList: [],
|
|
168
|
+
fallbackPromptList: [],
|
|
169
|
+
errors: [],
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const AGENTS_FILE = /^# AGENTS\.md instructions for ([^\n]+)/
|
|
174
|
+
|
|
175
|
+
const emptyWords = () => ({ lastAt: 0, thoughts: [], replies: [] })
|
|
176
|
+
|
|
177
|
+
function applyWords(state, record) {
|
|
178
|
+
const at = Date.parse(record.timestamp) || state.lastAt
|
|
179
|
+
state.lastAt = at
|
|
180
|
+
const item = record.payload
|
|
181
|
+
if (record.type !== 'response_item' || !item || typeof item !== 'object') return
|
|
182
|
+
const id = text(item.id) || null
|
|
183
|
+
if (item.type === 'reasoning') keepText(state.thoughts, 'thoughts', { id, at, text: joinedText(item.summary, 'summary_text', 'text') })
|
|
184
|
+
else if (item.type === 'message' && item.role === 'assistant') keepText(state.replies, 'replies', { id, at, text: joinedText(item.content, 'output_text', 'text') })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const emptySent = () => ({ base: 0, developer: [], other: 0, memory: new Map(), tools: new Map() })
|
|
188
|
+
|
|
189
|
+
function callName(item) {
|
|
190
|
+
if (item.type === 'local_shell_call') return 'shell'
|
|
191
|
+
if (item.type === 'tool_search_call') return 'tool_search'
|
|
192
|
+
return `${text(item.namespace)}${text(item.name)}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function codexFixed(sent) {
|
|
196
|
+
const fixed = emptyFixed()
|
|
197
|
+
const developer = sent.developer.reduce((n, chars) => n + chars, 0)
|
|
198
|
+
fixed.system = developer + (sent.developer.includes(sent.base) ? 0 : sent.base) + sent.other
|
|
199
|
+
fixed.mcp = new Map(sent.tools)
|
|
200
|
+
fixed.memory = new Map(sent.memory)
|
|
201
|
+
fixed.recorded = new Set(['system', 'mcpTools', 'memory'])
|
|
202
|
+
return fixed
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function applySizes(state, record, item, at) {
|
|
206
|
+
const { sizes, sent } = state
|
|
207
|
+
const before = sizes.first == null
|
|
208
|
+
if (record.type === 'session_meta') {
|
|
209
|
+
sent.base = text(item.base_instructions?.text).length
|
|
210
|
+
} else if (record.type === 'compacted') {
|
|
211
|
+
sizeCompaction(sizes)
|
|
212
|
+
return
|
|
213
|
+
} else if (record.type === 'event_msg') {
|
|
214
|
+
if (item.type === 'token_count' && item.info?.last_token_usage) sizeRequest(sizes, number(item.info.last_token_usage.input_tokens), at)
|
|
215
|
+
return
|
|
216
|
+
} else if (record.type !== 'response_item') {
|
|
217
|
+
return
|
|
218
|
+
} else if (item.type === 'reasoning') {
|
|
219
|
+
sizeReply(sizes, joinedText(item.summary, 'summary_text', 'text').length)
|
|
220
|
+
return
|
|
221
|
+
} else if (item.type === 'message' && item.role === 'assistant') {
|
|
222
|
+
sizeReply(sizes, joinedText(item.content, 'output_text', 'text').length)
|
|
223
|
+
return
|
|
224
|
+
} else if (CALLS.has(item.type)) {
|
|
225
|
+
sizeCall(sizes, text(item.call_id), callName(item), (text(item.arguments) || text(item.input) || JSON.stringify(item.action ?? '')).length)
|
|
226
|
+
return
|
|
227
|
+
} else if (OUTPUTS.has(item.type)) {
|
|
228
|
+
sizeResult(sizes, text(item.call_id), outputText(item.output).length)
|
|
229
|
+
return
|
|
230
|
+
} else if (item.type === 'additional_tools') {
|
|
231
|
+
for (const group of Array.isArray(item.tools) ? item.tools : []) {
|
|
232
|
+
const tools = Array.isArray(group?.tools) ? group.tools.length : 0
|
|
233
|
+
sent.tools.set(text(group?.name) || 'tools', { tools: tools || null, chars: JSON.stringify(group ?? null).length })
|
|
234
|
+
}
|
|
235
|
+
} else if (item.type === 'message') {
|
|
236
|
+
const raw = contentText(item.content)
|
|
237
|
+
const agents = item.role === 'user' ? AGENTS_FILE.exec(raw) : null
|
|
238
|
+
if (agents) sent.memory.set(`${agents[1].trim().replace(/[\\/]+$/, '')}/AGENTS.md`, raw.length)
|
|
239
|
+
else if (item.role === 'user' && promptText(raw)) {
|
|
240
|
+
sizePrompt(sizes, raw.length)
|
|
241
|
+
return
|
|
242
|
+
} else if (!before) {
|
|
243
|
+
sizePrompt(sizes, raw.length)
|
|
244
|
+
return
|
|
245
|
+
} else if (item.role === 'developer' || item.role === 'system') sent.developer.push(raw.length)
|
|
246
|
+
else sent.other += raw.length
|
|
247
|
+
} else {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
setFixed(sizes, codexFixed(sent))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function applyRecord(state, record) {
|
|
254
|
+
const at = Date.parse(record.timestamp)
|
|
255
|
+
if (at) {
|
|
256
|
+
if (!state.startedAt) state.startedAt = at
|
|
257
|
+
if (at > state.lastAt) state.lastAt = at
|
|
258
|
+
}
|
|
259
|
+
const payload = record.payload
|
|
260
|
+
if (!payload || typeof payload !== 'object') return
|
|
261
|
+
applySizes(state, record, payload, at || state.lastAt)
|
|
262
|
+
if (record.type === 'session_meta') applyMeta(state, payload)
|
|
263
|
+
else if (record.type === 'turn_context') applySettings(state, payload.model, payload.effort, payload.cwd)
|
|
264
|
+
else if (record.type === 'response_item') applyItem(state, payload, at || state.lastAt)
|
|
265
|
+
else if (record.type === 'event_msg') applyEvent(state, payload, at || state.lastAt)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applyMeta(state, meta) {
|
|
269
|
+
const subagent = meta.source?.subagent
|
|
270
|
+
const spawn = subagent?.thread_spawn
|
|
271
|
+
state.threadId ||= text(meta.id).toLowerCase()
|
|
272
|
+
state.parentId ||= (text(meta.parent_thread_id) || text(spawn?.parent_thread_id)).toLowerCase()
|
|
273
|
+
state.subagent ||= meta.thread_source === 'subagent' || Boolean(spawn)
|
|
274
|
+
state.internal ||= INTERNAL_SOURCES.has(meta.thread_source) || (subagent != null && !spawn)
|
|
275
|
+
state.cwd ||= text(meta.cwd)
|
|
276
|
+
state.gitBranch ||= text(meta.git?.branch)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function applySettings(state, model, effort, cwd) {
|
|
280
|
+
if (model) state.model = text(model)
|
|
281
|
+
if (effort) state.effort = text(effort)
|
|
282
|
+
if (cwd) state.cwd = text(cwd)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function countPrompt(state, raw, at) {
|
|
286
|
+
const prompt = promptText(raw)
|
|
287
|
+
if (!prompt) return
|
|
288
|
+
state.prompts++
|
|
289
|
+
state.firstPrompt ||= prompt
|
|
290
|
+
keepNewest(state.promptList, { at, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function applyItem(state, item, at) {
|
|
294
|
+
if (item.type === 'message' && item.role === 'user') {
|
|
295
|
+
const prompt = promptText(contentText(item.content))
|
|
296
|
+
if (prompt) {
|
|
297
|
+
state.fallbackPrompts++
|
|
298
|
+
state.fallbackPrompt ||= prompt
|
|
299
|
+
keepNewest(state.fallbackPromptList, { at, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
|
|
300
|
+
}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
const id = text(item.call_id)
|
|
304
|
+
if (!id) return
|
|
305
|
+
if (CALLS.has(item.type)) {
|
|
306
|
+
const name = callName(item)
|
|
307
|
+
const args = item.type === 'function_call' ? parseArgs(item.arguments) : null
|
|
308
|
+
if (item.type === 'function_call' && !item.namespace && item.name === 'update_plan') state.plan = nextPlan(state.plan, 'update_plan', args)
|
|
309
|
+
const patch = item.name === 'apply_patch' ? (item.type === 'custom_tool_call' ? text(item.input) : text(args?.input)) : ''
|
|
310
|
+
if (patch) state.patches.set(id, patch)
|
|
311
|
+
const file = patch ? patchFile(patch) : ''
|
|
312
|
+
const target = callTarget(item, args, patch, state.cwd)
|
|
313
|
+
const input = callInput(item, args, patch)
|
|
314
|
+
const entry = { name, startedAt: at, target, kind: inputKind(name, input).kind }
|
|
315
|
+
const questions = QUESTION_TOOLS.has(name) ? askQuestions(args) : null
|
|
316
|
+
if (questions) entry.questions = questions
|
|
317
|
+
state.calls.set(id, entry)
|
|
318
|
+
startCall(state.activity, { id, name, target, input, path: file ? relativePath(file, state.cwd) : null, startedAt: at })
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
if (!OUTPUTS.has(item.type)) return
|
|
322
|
+
state.calls.delete(id)
|
|
323
|
+
const patch = state.patches.get(id)
|
|
324
|
+
if (patch === undefined) {
|
|
325
|
+
const failed = exitFailed(item.output)
|
|
326
|
+
endCall(state.activity, id, at, failed, failed ? outputText(item.output) : null)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
state.patches.delete(id)
|
|
330
|
+
const output = outputText(item.output)
|
|
331
|
+
const applied = PATCH_APPLIED.test(output)
|
|
332
|
+
endCall(state.activity, id, at, !applied, applied ? null : output)
|
|
333
|
+
if (applied) addLines(state, patch)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function addLines(state, patch) {
|
|
337
|
+
const counts = new Map()
|
|
338
|
+
let entry = null
|
|
339
|
+
let hunk = null
|
|
340
|
+
for (const line of patch.split('\n')) {
|
|
341
|
+
const header = PATCH_FILE.exec(line)
|
|
342
|
+
if (header) {
|
|
343
|
+
const file = header[1].trim()
|
|
344
|
+
entry = counts.get(file) || { added: 0, removed: 0, hunks: [] }
|
|
345
|
+
counts.set(file, entry)
|
|
346
|
+
hunk = null
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (line.startsWith('@@')) {
|
|
350
|
+
hunk = null
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
if (line.startsWith('***')) continue
|
|
354
|
+
if (line.startsWith('+')) {
|
|
355
|
+
state.lines.added++
|
|
356
|
+
if (entry) entry.added++
|
|
357
|
+
} else if (line.startsWith('-')) {
|
|
358
|
+
state.lines.removed++
|
|
359
|
+
if (entry) entry.removed++
|
|
360
|
+
} else if (!line.startsWith(' ')) continue
|
|
361
|
+
if (!entry) continue
|
|
362
|
+
if (!hunk) {
|
|
363
|
+
hunk = []
|
|
364
|
+
entry.hunks.push(hunk)
|
|
365
|
+
}
|
|
366
|
+
hunk.push(line)
|
|
367
|
+
}
|
|
368
|
+
for (const [name, count] of counts) {
|
|
369
|
+
addFileChange(state.files, name, count.added, count.removed, count.hunks.map((lines) => diffHunk({ lines })))
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyEvent(state, event, at) {
|
|
374
|
+
switch (event.type) {
|
|
375
|
+
case 'task_started':
|
|
376
|
+
case 'turn_started': {
|
|
377
|
+
const turn = text(event.turn_id)
|
|
378
|
+
if (!state.open || (turn && turn !== state.turnId)) state.turnStartedAt = at
|
|
379
|
+
if (turn) state.turnId = turn
|
|
380
|
+
state.open = true
|
|
381
|
+
state.failed = false
|
|
382
|
+
state.calls.clear()
|
|
383
|
+
settleCalls(state.activity)
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
case 'task_complete':
|
|
387
|
+
case 'turn_complete':
|
|
388
|
+
state.open = false
|
|
389
|
+
state.failed = event.error != null
|
|
390
|
+
state.calls.clear()
|
|
391
|
+
settleCalls(state.activity)
|
|
392
|
+
if (event.error != null) {
|
|
393
|
+
const message = text(event.error?.message) || text(event.error) || 'The turn failed'
|
|
394
|
+
keepNewest(state.errors, { at, message: clip(message, DETAIL_LIMITS.errorText) }, DETAIL_LIMITS.errors)
|
|
395
|
+
}
|
|
396
|
+
break
|
|
397
|
+
case 'turn_aborted':
|
|
398
|
+
state.open = false
|
|
399
|
+
state.failed = false
|
|
400
|
+
state.calls.clear()
|
|
401
|
+
settleCalls(state.activity)
|
|
402
|
+
break
|
|
403
|
+
case 'token_count':
|
|
404
|
+
applyTokens(state, event.info, at)
|
|
405
|
+
applyLimits(state, event.rate_limits, at)
|
|
406
|
+
break
|
|
407
|
+
case 'user_message':
|
|
408
|
+
state.typed = true
|
|
409
|
+
countPrompt(state, event.message, at)
|
|
410
|
+
break
|
|
411
|
+
case 'item_completed':
|
|
412
|
+
if (event.item?.type !== 'UserMessage') break
|
|
413
|
+
state.typed = true
|
|
414
|
+
countPrompt(state, contentText(event.item.content), at)
|
|
415
|
+
break
|
|
416
|
+
case 'thread_settings_applied': {
|
|
417
|
+
const settings = event.thread_settings
|
|
418
|
+
if (settings) applySettings(state, settings.model, settings.reasoning_effort, settings.cwd)
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const LIMIT_WINDOWS = ['primary', 'secondary']
|
|
425
|
+
|
|
426
|
+
function applyLimits(state, limits, at) {
|
|
427
|
+
if (!limits || typeof limits !== 'object') return
|
|
428
|
+
const windows = []
|
|
429
|
+
for (const id of LIMIT_WINDOWS) {
|
|
430
|
+
const w = limits[id]
|
|
431
|
+
if (!w || typeof w !== 'object' || w.used_percent == null) continue
|
|
432
|
+
const resetsAt = w.resets_at != null ? number(w.resets_at) * 1000 : w.resets_in_seconds != null ? at + number(w.resets_in_seconds) * 1000 : null
|
|
433
|
+
windows.push({ id, usedPercent: number(w.used_percent), windowMinutes: w.window_minutes == null ? null : number(w.window_minutes), resetsAt: resetsAt || null })
|
|
434
|
+
}
|
|
435
|
+
if (!windows.length) return
|
|
436
|
+
state.usageLimits = { at, subscription: text(limits.plan_type) || null, windows }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function applyTokens(state, info, at) {
|
|
440
|
+
if (!info) return
|
|
441
|
+
const total = info.total_token_usage
|
|
442
|
+
if (total) {
|
|
443
|
+
const cached = number(total.cached_input_tokens)
|
|
444
|
+
const next = {
|
|
445
|
+
input: Math.max(0, number(total.input_tokens) - cached),
|
|
446
|
+
output: number(total.output_tokens),
|
|
447
|
+
cacheRead: cached,
|
|
448
|
+
cacheWrite: number(total.cache_write_input_tokens),
|
|
449
|
+
}
|
|
450
|
+
if (state.model) {
|
|
451
|
+
const sums = state.modelTokens.get(state.model) || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
452
|
+
for (const key of Object.keys(next)) sums[key] += Math.max(0, next[key] - (state.tokens?.[key] || 0))
|
|
453
|
+
state.modelTokens.set(state.model, sums)
|
|
454
|
+
}
|
|
455
|
+
state.tokens = next
|
|
456
|
+
}
|
|
457
|
+
const last = info.last_token_usage
|
|
458
|
+
if (!last) return
|
|
459
|
+
state.context = { used: number(last.total_tokens), limit: number(info.model_context_window) }
|
|
460
|
+
if (last.input_tokens != null) {
|
|
461
|
+
const cached = number(last.cached_input_tokens)
|
|
462
|
+
state.contextParts = { input: Math.max(0, number(last.input_tokens) - cached), cacheRead: cached, cacheWrite: 0, output: number(last.output_tokens) }
|
|
463
|
+
}
|
|
464
|
+
const sum = number(total?.total_tokens)
|
|
465
|
+
if (sum === state.lastTotal) return
|
|
466
|
+
state.lastTotal = sum
|
|
467
|
+
samplePoint(state.contextSample, last.total_tokens)
|
|
468
|
+
keepNewest(state.history, { at, context: number(last.total_tokens), output: number(last.output_tokens) }, DETAIL_LIMITS.history)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function codex({
|
|
472
|
+
home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
|
|
473
|
+
sqliteHome = process.env.CODEX_SQLITE_HOME || home,
|
|
474
|
+
now = Date.now,
|
|
475
|
+
} = {}) {
|
|
476
|
+
const sessionsDir = path.join(home, 'sessions')
|
|
477
|
+
const locksDir = path.join(home, 'thread-writer-locks')
|
|
478
|
+
const indexFile = path.join(home, 'session_index.jsonl')
|
|
479
|
+
const rollouts = jsonlFollower({ empty: emptyRollout, apply: applyRecord })
|
|
480
|
+
const openedFor = recentReaders(() => jsonlFollower({ empty: emptyWords, apply: applyWords, tail: { bytes: TAIL_BYTES, enough: wordsFull } }), OPENED_KEPT)
|
|
481
|
+
let index = { mtime: -1, names: new Map() }
|
|
482
|
+
let known = new Map()
|
|
483
|
+
let childrenOf = new Map()
|
|
484
|
+
|
|
485
|
+
async function findRollouts(locks) {
|
|
486
|
+
const cutoff = now() - RECENT_MS
|
|
487
|
+
const byId = new Map()
|
|
488
|
+
for (const year of await listDirs(sessionsDir)) {
|
|
489
|
+
for (const month of await listDirs(year)) {
|
|
490
|
+
for (const day of await listDirs(month)) {
|
|
491
|
+
for (const file of await listFiles(day, (n) => ROLLOUT_FILE.test(n))) {
|
|
492
|
+
const id = ROLLOUT_FILE.exec(path.basename(file))[1].toLowerCase()
|
|
493
|
+
const stat = await fsp.stat(file).catch(() => null)
|
|
494
|
+
if (!stat || (stat.mtimeMs < cutoff && !locks.has(id))) continue
|
|
495
|
+
const existing = byId.get(id)
|
|
496
|
+
if (existing && existing.stat.mtimeMs >= stat.mtimeMs) continue
|
|
497
|
+
byId.set(id, { id, file, stat })
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return [...byId.values()]
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function latestStateDb() {
|
|
506
|
+
const files = await listFiles(sqliteHome, (n) => STATE_DB.test(n))
|
|
507
|
+
return files.sort((a, b) => Number(STATE_DB.exec(path.basename(b))[1]) - Number(STATE_DB.exec(path.basename(a))[1]))[0] || ''
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function threadRecords() {
|
|
511
|
+
const stat = await fsp.stat(indexFile).catch(() => null)
|
|
512
|
+
if (stat && stat.mtimeMs !== index.mtime) {
|
|
513
|
+
const names = new Map()
|
|
514
|
+
for (const row of jsonLines(await fsp.readFile(indexFile, 'utf8').catch(() => ''))) {
|
|
515
|
+
if (row?.id && row.thread_name) names.set(String(row.id).toLowerCase(), text(row.thread_name))
|
|
516
|
+
}
|
|
517
|
+
index = { mtime: stat.mtimeMs, names }
|
|
518
|
+
}
|
|
519
|
+
const names = new Map(index.names)
|
|
520
|
+
const archived = new Set()
|
|
521
|
+
const read = new Map()
|
|
522
|
+
await withReadOnly(await latestStateDb(), (db) => {
|
|
523
|
+
const tables = tablesOf(db)
|
|
524
|
+
if (!tables.has('threads')) return
|
|
525
|
+
const columns = columnsOf(db, 'threads')
|
|
526
|
+
if (columns.has('name')) {
|
|
527
|
+
for (const row of db.prepare("SELECT id, name FROM threads WHERE name IS NOT NULL AND name != ''").all()) {
|
|
528
|
+
names.set(String(row.id).toLowerCase(), text(row.name))
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (columns.has('archived')) {
|
|
532
|
+
for (const row of db.prepare('SELECT id FROM threads WHERE archived = 1').all()) archived.add(String(row.id).toLowerCase())
|
|
533
|
+
}
|
|
534
|
+
if (tables.has('thread_read_receipts') && ['thread_id', 'first_unread_turn'].every((c) => columnsOf(db, 'thread_read_receipts').has(c))) {
|
|
535
|
+
for (const row of db.prepare('SELECT thread_id, first_unread_turn FROM thread_read_receipts').all()) {
|
|
536
|
+
read.set(String(row.thread_id).toLowerCase(), row.first_unread_turn != null)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
return { names, archived, read }
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function scanSessions() {
|
|
544
|
+
const at = now()
|
|
545
|
+
const locking = await exists(locksDir)
|
|
546
|
+
const locks = new Set((await listFiles(locksDir, (n) => n.endsWith('.lock'))).map((f) => path.basename(f, '.lock').toLowerCase()))
|
|
547
|
+
const [found, { names, archived, read: unread }] = await Promise.all([findRollouts(locks), threadRecords()])
|
|
548
|
+
const read = []
|
|
549
|
+
for (const rollout of found) {
|
|
550
|
+
try {
|
|
551
|
+
read.push({ ...rollout, state: await rollouts.read(rollout.file, rollout.stat) })
|
|
552
|
+
} catch {}
|
|
553
|
+
}
|
|
554
|
+
rollouts.sweep()
|
|
555
|
+
known = new Map(read.map((r) => [r.id, r.file]))
|
|
556
|
+
childrenOf = new Map()
|
|
557
|
+
for (const { id, state } of read) {
|
|
558
|
+
if (state.subagent && state.parentId) childrenOf.set(state.parentId, [...(childrenOf.get(state.parentId) || []), id])
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const running = new Map()
|
|
562
|
+
for (const { id, stat, state } of read) {
|
|
563
|
+
if (!state.subagent || !state.parentId || !state.open) continue
|
|
564
|
+
if (!locks.has(id) && at - stat.mtimeMs > SUBAGENT_WINDOW_MS) continue
|
|
565
|
+
running.set(state.parentId, [...(running.get(state.parentId) || []), { id, name: childName(state), startedAt: state.startedAt }])
|
|
566
|
+
}
|
|
567
|
+
const workingOf = (id) => (running.get(id) || []).sort((a, b) => a.startedAt - b.startedAt).map(({ id: childId, name }) => ({ id: childId, name }))
|
|
568
|
+
|
|
569
|
+
return read
|
|
570
|
+
.filter(({ id, state }) => !state.subagent && !state.internal && !archived.has(id))
|
|
571
|
+
.map(({ id, stat, state }) => {
|
|
572
|
+
const lastActivityAt = Math.max(state.lastAt, Math.floor(stat.mtimeMs))
|
|
573
|
+
const fresh = at - lastActivityAt < ACTIVE_WINDOW_MS
|
|
574
|
+
const live = locking ? locks.has(id) : state.open && fresh
|
|
575
|
+
const workingSubagents = workingOf(id)
|
|
576
|
+
return toSession({ id, state, names, live, turnLive: state.open && (locking ? live : fresh), fresh, locking, unread: unread.get(id), subagents: workingSubagents.length, workingSubagents, lastActivityAt })
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function readKnown(id) {
|
|
581
|
+
const file = known.get(id)
|
|
582
|
+
const stat = file ? await fsp.stat(file).catch(() => null) : null
|
|
583
|
+
if (!stat) return null
|
|
584
|
+
const state = await rollouts.read(file, stat).catch(() => null)
|
|
585
|
+
return state ? { file, stat, state } : null
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function sessionDetail(key, { filesOnly = false } = {}) {
|
|
589
|
+
const id = String(key).replace(/^codex:/, '').toLowerCase()
|
|
590
|
+
const found = await readKnown(id)
|
|
591
|
+
if (!found || found.state.subagent || found.state.internal) return null
|
|
592
|
+
const { file, stat, state } = found
|
|
593
|
+
if (filesOnly) return { id: `codex:${id}`, sessionId: id, files: filePaths(state.files, state.cwd) }
|
|
594
|
+
const said = await openedFor(file).read(file, stat).catch(() => null)
|
|
595
|
+
const limit = state.context?.limit || null
|
|
596
|
+
const subagents = []
|
|
597
|
+
for (const childId of childrenOf.get(id) || []) {
|
|
598
|
+
const child = await readKnown(childId)
|
|
599
|
+
if (!child) continue
|
|
600
|
+
const c = child.state
|
|
601
|
+
const lastActivityAt = Math.max(c.lastAt, Math.floor(child.stat.mtimeMs))
|
|
602
|
+
const status = c.open && now() - lastActivityAt < ACTIVE_WINDOW_MS ? 'working' : c.failed ? 'error' : 'done'
|
|
603
|
+
subagents.push({
|
|
604
|
+
id: childId,
|
|
605
|
+
name: childName(c),
|
|
606
|
+
type: '',
|
|
607
|
+
status,
|
|
608
|
+
model: c.model,
|
|
609
|
+
tokens: c.tokens ? { ...c.tokens } : null,
|
|
610
|
+
tool: status === 'working' ? latestCall(c.calls) : null,
|
|
611
|
+
startedAt: c.startedAt,
|
|
612
|
+
lastActivityAt,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
id: `codex:${id}`,
|
|
617
|
+
sessionId: id,
|
|
618
|
+
source: file,
|
|
619
|
+
models: [...state.modelTokens].map(([model, tokens]) => ({ model, tokens: { ...tokens }, costUsd: null })),
|
|
620
|
+
contextParts: state.contextParts ? { ...state.contextParts } : null,
|
|
621
|
+
history: state.history.map((p) => ({ ...p })),
|
|
622
|
+
compactions: null,
|
|
623
|
+
...activityDetail(state.activity),
|
|
624
|
+
plan: planItems(state.plan),
|
|
625
|
+
subagents: subagents.sort((a, b) => a.startedAt - b.startedAt),
|
|
626
|
+
files: fileList(state.files, state.cwd),
|
|
627
|
+
prompts: (state.typed ? state.promptList : state.fallbackPromptList).map((p) => ({ ...p })),
|
|
628
|
+
thoughts: textList(said?.thoughts),
|
|
629
|
+
replies: textList(said?.replies),
|
|
630
|
+
contextBreakdown: contextBreakdown(state.sizes, {
|
|
631
|
+
total: state.context?.used,
|
|
632
|
+
limit,
|
|
633
|
+
reserved: limit ? Math.round(limit * (1 - COMPACT_SHARE / REPORTED_SHARE)) : null,
|
|
634
|
+
}),
|
|
635
|
+
errors: state.errors.map((e) => ({ ...e })),
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
id: 'codex',
|
|
641
|
+
name: 'Codex',
|
|
642
|
+
detect: () => exists(sessionsDir),
|
|
643
|
+
watchRoots: () => [sessionsDir, locksDir],
|
|
644
|
+
scanSessions,
|
|
645
|
+
sessionDetail,
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const childName = (state) => titleFromPrompt(state.typed ? state.firstPrompt : state.fallbackPrompt) || 'Subagent'
|
|
650
|
+
|
|
651
|
+
function toSession({ id, state, names, live, turnLive, fresh, locking, unread, subagents, workingSubagents, lastActivityAt }) {
|
|
652
|
+
const calls = [...state.calls.values()].sort((a, b) => a.startedAt - b.startedAt)
|
|
653
|
+
const asked = calls.filter((c) => QUESTION_TOOLS.has(c.name))
|
|
654
|
+
let status
|
|
655
|
+
let statusReason
|
|
656
|
+
if (turnLive) {
|
|
657
|
+
if (asked.length) [status, statusReason] = ['asking', asked.some((c) => c.name === 'request_user_input') ? 'question' : 'approval']
|
|
658
|
+
else if (calls.length ? calls.every((c) => AGENT_TOOLS.has(c.name)) : subagents > 0) [status, statusReason] = ['delegating', 'subagents']
|
|
659
|
+
else [status, statusReason] = ['working', 'turn-open']
|
|
660
|
+
} else if (subagents) [status, statusReason] = ['delegating', 'subagents']
|
|
661
|
+
else if (state.failed) [status, statusReason] = ['error', 'turn-failed']
|
|
662
|
+
else if (unread !== undefined) [status, statusReason] = unread ? ['done', 'unread'] : ['idle', 'read']
|
|
663
|
+
else [status, statusReason] = (locking ? live : fresh) ? ['done', 'recent'] : ['idle', 'quiet']
|
|
664
|
+
|
|
665
|
+
const tool = turnLive && (status === 'working' || status === 'delegating') ? latestCall(state.calls) : null
|
|
666
|
+
const prompt = state.typed ? state.firstPrompt : state.fallbackPrompt
|
|
667
|
+
const cwd = state.cwd
|
|
668
|
+
return {
|
|
669
|
+
id: `codex:${id}`,
|
|
670
|
+
harness: 'codex',
|
|
671
|
+
title: titleFromPrompt(names.get(id) || prompt || '') || 'Untitled session',
|
|
672
|
+
project: path.basename(cwd.replace(/\\/g, '/')) || cwd || 'unknown',
|
|
673
|
+
projectPath: cwd,
|
|
674
|
+
worktree: '',
|
|
675
|
+
cwd,
|
|
676
|
+
gitBranch: state.gitBranch,
|
|
677
|
+
model: state.model,
|
|
678
|
+
effort: state.effort,
|
|
679
|
+
status,
|
|
680
|
+
live: live || turnLive,
|
|
681
|
+
startedAt: state.startedAt,
|
|
682
|
+
lastActivityAt,
|
|
683
|
+
tool,
|
|
684
|
+
tokens: state.tokens ? { ...state.tokens } : null,
|
|
685
|
+
context: state.context && state.context.used ? { ...state.context } : null,
|
|
686
|
+
contextHistory: sampleList(state.contextSample, state.context?.used),
|
|
687
|
+
costUsd: null,
|
|
688
|
+
lines: { ...state.lines },
|
|
689
|
+
plan: planSummary(state.plan),
|
|
690
|
+
subagents,
|
|
691
|
+
workingSubagents,
|
|
692
|
+
prompts: state.typed ? state.prompts : state.fallbackPrompts,
|
|
693
|
+
turnStartedAt: turnLive ? state.turnStartedAt || null : null,
|
|
694
|
+
statusReason,
|
|
695
|
+
ask: askOf(status, { calls: asked, errorText: state.errors.at(-1)?.message }),
|
|
696
|
+
usageLimits: state.usageLimits ? { ...state.usageLimits, windows: state.usageLimits.windows.map((w) => ({ ...w })) } : null,
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export default codex()
|