@threadcrumbs/common 0.1.8 → 0.1.10

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.
@@ -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) || (prompt ? titleFromPrompt(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()
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()