Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

2195 lines
56 KiB
Go

package parser
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/gjson"
)
// Codex JSONL entry types.
const (
codexTypeSessionMeta = "session_meta"
codexTypeResponseItem = "response_item"
codexTypeTurnContext = "turn_context"
codexTypeEventMsg = "event_msg"
codexOriginatorExec = "codex_exec"
)
var errCodexIncrementalNeedsFullParse = errors.New(
"codex incremental event requires full parse",
)
const codexGoalContextSourceAttr = `source="goal"`
var codexGoalContextSourceAttrRe = regexp.MustCompile(`(?:^|\s)` +
regexp.QuoteMeta(codexGoalContextSourceAttr) + `(?:\s|/|$)`)
var codexSessionIndexCache = struct {
mu sync.Mutex
entries map[string]codexSessionIndexEntry
}{
entries: make(map[string]codexSessionIndexEntry),
}
type codexSessionIndexEntry struct {
mtime int64
size int64
changeTime int64
changeTimeOkay bool
titles map[string]string
}
// codexSessionBuilder accumulates state while scanning a Codex
// JSONL session file line by line.
type codexSessionBuilder struct {
codexCursorState
messages []ParsedMessage
firstMessage string
startedAt time.Time
endedAt time.Time
sessionID string
project string
ordinal int
callNames map[string]string
callRefs map[string]codexToolCallRef
agentSpawnCalls map[string]string
agentWaitCalls map[string]string
pendingAgentEvents map[string][]codexPendingEvent
orphanNotificationIx map[string]int
unattachedTokenUsage bool
}
// codexForkGate drops the replayed parent history at the top of a
// forked Codex rollout (#643).
//
// `codex fork` copies the parent's lines — its session_meta, turns,
// messages and token_count events — into the new file with re-stamped
// envelope timestamps, so the same usage exists in two session files
// and gets counted twice. Envelope timestamps cannot locate the
// boundary (the replay is re-stamped at fork creation), but turn ids
// are UUIDv7 values minted when the turn originally ran: every
// replayed turn predates the fork instant, and the first genuine turn
// is minted at or after it. The gate stays closed until the first
// turn_context whose turn_id timestamp is >= the fork's own creation
// time, then everything flows normally.
//
// Replayed turn_context entries from parents recorded before Codex
// stamped turn ids carry no turn_id at all; a CLI new enough to write
// forked_from_id always stamps genuine turns, so a missing turn_id
// while gated means replayed history. An unparseable turn_id fails
// open (pre-#643 behaviour) rather than risk dropping live data.
type codexForkGate struct {
active bool
createdMs int64
}
// armFromMeta activates the gate when the session_meta belongs to a
// forked session and its creation instant can be anchored: from the
// fork's UUIDv7 id, the payload timestamp, or the JSONL envelope
// timestamp, in that order.
func (g *codexForkGate) armFromMeta(payload gjson.Result, envelopeTS time.Time) {
if payload.Get("forked_from_id").Str == "" {
return
}
ms := uuidV7Millis(payload.Get("id").Str)
if ms == 0 {
if t := parseTimestamp(payload.Get("timestamp").Str); !t.IsZero() {
ms = t.UnixMilli()
}
}
if ms == 0 && !envelopeTS.IsZero() {
ms = envelopeTS.UnixMilli()
}
if ms == 0 {
return // no anchor for the boundary — fail open
}
g.active = true
g.createdMs = ms
}
// suppresses reports whether the line is replayed parent history.
// turn_context lines open the gate when their turn id was minted at
// or after the fork instant.
func (g *codexForkGate) suppresses(lineType string, payload gjson.Result) bool {
if !g.active {
return false
}
if lineType != codexTypeTurnContext {
return true
}
tid := payload.Get("turn_id").Str
if tid == "" {
return true // pre-turn_id parent history
}
if ms := uuidV7Millis(tid); ms != 0 && ms < g.createdMs {
return true
}
g.active = false
return false
}
// uuidV7Millis extracts the millisecond timestamp embedded in a
// UUIDv7, returning 0 for anything that is not a v7 UUID.
func uuidV7Millis(id string) int64 {
hex := strings.ReplaceAll(id, "-", "")
if len(hex) != 32 || hex[12] != '7' {
return 0
}
ms, err := strconv.ParseInt(hex[:12], 16, 64)
if err != nil {
return 0
}
return ms
}
type codexToolCallRef struct {
messageIndex int
callIndex int
}
type codexPendingEvent struct {
agentID string
source string
status string
text string
timestamp time.Time
ordinal int
}
func newCodexSessionBuilder(
_ bool,
) *codexSessionBuilder {
return &codexSessionBuilder{
project: "unknown",
callNames: make(map[string]string),
callRefs: make(map[string]codexToolCallRef),
agentSpawnCalls: make(map[string]string),
agentWaitCalls: make(map[string]string),
pendingAgentEvents: make(map[string][]codexPendingEvent),
orphanNotificationIx: make(map[string]int),
}
}
func (b *codexSessionBuilder) incrementalSeed() codexIncrementalSeed {
return b.codexCursorState
}
// processLine handles a single non-empty, valid JSON line.
func (b *codexSessionBuilder) processLine(
line string,
) (skip bool) {
tsStr := gjson.Get(line, "timestamp").Str
ts := parseTimestamp(tsStr)
if ts.IsZero() {
if tsStr != "" {
logParseError(tsStr)
}
} else {
if b.startedAt.IsZero() {
b.startedAt = ts
}
b.endedAt = ts
}
payload := gjson.Get(line, "payload")
switch gjson.Get(line, "type").Str {
case codexTypeSessionMeta:
if b.forkGate.active {
// A forked rollout replays the parent's session_meta
// too — the fork's own meta came first and wins.
return false
}
return b.handleSessionMeta(payload, ts)
case codexTypeTurnContext:
if b.forkGate.suppresses(codexTypeTurnContext, payload) {
return false
}
b.model = payload.Get("model").Str
case codexTypeResponseItem:
if b.forkGate.suppresses(codexTypeResponseItem, payload) {
return false
}
b.handleResponseItem(payload, ts)
case codexTypeEventMsg:
if b.forkGate.suppresses(codexTypeEventMsg, payload) {
return false
}
b.handleEventMsg(payload)
}
return false
}
func (b *codexSessionBuilder) handleSessionMeta(
payload gjson.Result, envelopeTS time.Time,
) (skip bool) {
b.sessionID = payload.Get("id").Str
if cwd := payload.Get("cwd").Str; cwd != "" {
b.cwd = cwd
branch := payload.Get("git.branch").Str
if proj := ExtractProjectFromCwdWithBranch(cwd, branch); proj != "" {
b.project = proj
} else {
b.project = "unknown"
}
}
b.forkGate.armFromMeta(payload, envelopeTS)
return false
}
func (b *codexSessionBuilder) handleResponseItem(
payload gjson.Result, ts time.Time,
) {
switch payload.Get("type").Str {
case "function_call", "custom_tool_call":
b.handleFunctionCall(payload, ts)
return
case "function_call_output", "custom_tool_call_output":
b.handleFunctionCallOutput(payload, ts)
return
}
role := payload.Get("role").Str
if role != "user" && role != "assistant" {
return
}
content := extractCodexContent(payload)
if role == "user" && !b.firstUserSeen {
content = extractCodexInitialUserContent(payload)
}
if strings.TrimSpace(content) == "" {
return
}
if role == "user" && b.handleSubagentNotification(content, ts) {
return
}
if role == "user" {
if isCodexTurnAbortedMessage(content) {
b.markFirstUserReplayPossible()
}
if isCodexSystemMessage(content) {
return
}
}
if role == "user" {
first, replay := b.observeUserPrompt(content)
if first {
b.firstMessage = truncate(
strings.ReplaceAll(content, "\n", " "), 300,
)
}
if replay {
// Codex can re-emit the initial prompt verbatim after a
// turn_aborted continuation signal. Drop only that positively
// identified replay; otherwise an identical second prompt is
// real transcript content. The digest covers the full content,
// not the truncated first-message preview.
return
}
}
b.messages = append(b.messages, ParsedMessage{
Ordinal: b.ordinal,
Role: RoleType(role),
Content: content,
Timestamp: ts,
ContentLength: len(content),
Model: b.model,
})
b.ordinal++
}
func (b *codexSessionBuilder) handleEventMsg(payload gjson.Result) {
eventType := payload.Get("type").Str
switch eventType {
case "task_started", "task_complete", "turn_aborted":
b.observeTaskEvent(eventType)
case "token_count":
b.handleTokenCountEvent(payload)
case "collab_agent_spawn_end":
b.handleCollabAgentSpawnEnd(payload)
}
}
func (b *codexSessionBuilder) markFirstUserReplayPossible() {
b.codexCursorState.markFirstUserReplayPossible()
}
func (b *codexSessionBuilder) handleTokenCountEvent(
payload gjson.Result,
) {
raw := payload.Get("info.last_token_usage").Raw
if raw == "" || b.observeTokenUsage(raw) {
return
}
// Find last assistant message without usage in the current
// turn. Stop at user message boundary so we don't cross
// turns.
for i, v := range slices.Backward(b.messages) {
if v.Role == RoleUser {
break
}
if v.Role == RoleAssistant &&
v.TokenUsage == nil {
b.applyCodexTokenUsage(&b.messages[i], raw)
return
}
}
b.unattachedTokenUsage = true
}
func (b *codexSessionBuilder) handleCollabAgentSpawnEnd(
payload gjson.Result,
) {
callID := payload.Get("call_id").Str
agentID := strings.TrimSpace(payload.Get("new_thread_id").Str)
if callID == "" || agentID == "" {
return
}
b.agentSpawnCalls[agentID] = callID
b.setCallSubagentSessionID(callID, codexSubagentSessionID(agentID))
}
// applyCodexTokenUsage normalizes Codex token usage fields
// into the Anthropic-style shape expected by the usage and cost
// queries. Codex reports input_tokens as the full input count
// (cached portion included), while the downstream cost formula
// treats input_tokens as the uncached remainder and bills
// cache_read_input_tokens separately. Subtracting cached here
// prevents double-counting the cached portion at the full input
// rate.
//
// input_tokens - cached_input_tokens → input_tokens (uncached)
// output_tokens → output_tokens
// cached_input_tokens → cache_read_input_tokens
func (b *codexSessionBuilder) applyCodexTokenUsage(
msg *ParsedMessage, raw string,
) {
usage := gjson.Parse(raw)
totalInput := int(usage.Get("input_tokens").Int())
cached := int(usage.Get("cached_input_tokens").Int())
output := int(usage.Get("output_tokens").Int())
uncached := max(totalInput-cached, 0)
normalized := map[string]int{
"input_tokens": uncached,
"output_tokens": output,
"cache_read_input_tokens": cached,
}
j, err := json.Marshal(normalized)
if err != nil {
return
}
msg.TokenUsage = j
msg.OutputTokens = output
msg.HasOutputTokens = output > 0
msg.ContextTokens = uncached + cached
msg.HasContextTokens = totalInput > 0 || cached > 0
}
func (b *codexSessionBuilder) handleFunctionCall(
payload gjson.Result, ts time.Time,
) {
name := payload.Get("name").Str
if name == "" {
return
}
callID := payload.Get("call_id").Str
if callID != "" {
b.callNames[callID] = name
}
content := formatCodexFunctionCall(name, payload)
inputJSON := extractCodexInputJSON(payload)
skillName := inferCodexSkillNameWithBase(name, inputJSON, b.cwd)
waitAgentIDs := []string(nil)
if isCodexWaitAgentCall(name) && callID != "" {
args, _ := parseCodexFunctionArgs(payload)
waitAgentIDs = codexWaitAgentIDs(args)
}
b.messages = append(b.messages, ParsedMessage{
Ordinal: b.ordinal,
Role: RoleAssistant,
Content: content,
Timestamp: ts,
HasToolUse: true,
ContentLength: len(content),
Model: b.model,
ToolCalls: []ParsedToolCall{{
ToolUseID: callID,
ToolName: name,
Category: NormalizeToolCategory(name),
InputJSON: inputJSON,
SkillName: skillName,
}},
})
if callID != "" {
b.callRefs[callID] = codexToolCallRef{
messageIndex: len(b.messages) - 1,
callIndex: 0,
}
}
b.ordinal++
if isCodexWaitAgentCall(name) && callID != "" {
for _, agentID := range waitAgentIDs {
b.agentWaitCalls[agentID] = callID
b.claimPendingAgentEvents(callID, agentID)
}
}
}
func (b *codexSessionBuilder) handleFunctionCallOutput(
payload gjson.Result, ts time.Time,
) {
callID := payload.Get("call_id").Str
if callID == "" {
return
}
output, raw := parseCodexFunctionOutput(payload)
if !output.Exists() {
if strings.TrimSpace(raw) == "" {
return
}
}
switch b.callNames[callID] {
case "spawn_agent":
agentID := strings.TrimSpace(output.Get("agent_id").Str)
if agentID == "" {
return
}
b.agentSpawnCalls[agentID] = callID
b.setCallSubagentSessionID(callID, codexSubagentSessionID(agentID))
case "wait", "wait_agent":
status := output.Get("status")
if !status.Exists() || !status.IsObject() {
return
}
status.ForEach(func(key, entry gjson.Result) bool {
agentID := key.Str
statusName, text := codexTerminalSubagentEvent(entry)
if text == "" {
return true
}
b.appendCallResultEvent(callID, ParsedToolResultEvent{
ToolUseID: callID,
AgentID: agentID,
SubagentSessionID: codexSubagentSessionID(agentID),
Source: "wait_output",
Status: statusName,
Content: text,
Timestamp: ts,
})
return true
})
default:
if text := strings.TrimSpace(raw); text != "" {
source := "function_call_output"
status := ""
if payload.Get("type").Str == "custom_tool_call_output" {
source = "custom_tool_call_output"
status = payload.Get("status").Str
if status == "" {
status = "completed"
}
}
b.appendCallResultEvent(callID, ParsedToolResultEvent{
ToolUseID: callID,
Source: source,
Status: status,
Content: text,
Timestamp: ts,
})
}
}
}
// setCallSubagentSessionID links a tool call to the session of
// the subagent it spawned. Callers must invoke this only after
// the originating function_call has been processed (which
// populates b.callRefs[callID]); otherwise the link is silently
// dropped. In real codex session files the spawn function_call
// always precedes both its function_call_output and the
// collab_agent_spawn_end event_msg.
func (b *codexSessionBuilder) setCallSubagentSessionID(
callID, sessionID string,
) {
if callID == "" || sessionID == "" {
return
}
ref, ok := b.callRefs[callID]
if !ok || ref.messageIndex < 0 || ref.messageIndex >= len(b.messages) {
return
}
if ref.callIndex < 0 || ref.callIndex >= len(b.messages[ref.messageIndex].ToolCalls) {
return
}
b.messages[ref.messageIndex].ToolCalls[ref.callIndex].SubagentSessionID = sessionID
}
func (b *codexSessionBuilder) handleSubagentNotification(
content string, ts time.Time,
) bool {
agentID, statusName, text := parseCodexSubagentNotification(content)
if agentID == "" || text == "" {
return false
}
if callID := b.agentWaitCalls[agentID]; callID != "" {
b.appendCallResultEvent(callID, ParsedToolResultEvent{
AgentID: agentID,
SubagentSessionID: codexSubagentSessionID(agentID),
Source: "subagent_notification",
Status: statusName,
Content: text,
Timestamp: ts,
})
return true
}
b.pendingAgentEvents[agentID] = append(
b.pendingAgentEvents[agentID], codexPendingEvent{
agentID: agentID,
source: "subagent_notification",
status: statusName,
text: text,
timestamp: ts,
ordinal: b.ordinal,
},
)
b.ordinal++
return true
}
func (b *codexSessionBuilder) appendCallResultEvent(
callID string, ev ParsedToolResultEvent,
) {
if callID == "" {
return
}
ref, ok := b.callRefs[callID]
if !ok || ref.messageIndex < 0 || ref.messageIndex >= len(b.messages) {
return
}
if ref.callIndex < 0 || ref.callIndex >= len(b.messages[ref.messageIndex].ToolCalls) {
return
}
tc := &b.messages[ref.messageIndex].ToolCalls[ref.callIndex]
if ev.ToolUseID == "" {
ev.ToolUseID = tc.ToolUseID
}
if ev.SubagentSessionID == "" && ev.AgentID != "" {
ev.SubagentSessionID = codexSubagentSessionID(ev.AgentID)
}
if b.hasEquivalentCallResultEvent(tc.ResultEvents, ev) {
return
}
tc.ResultEvents = append(tc.ResultEvents, ev)
}
func (b *codexSessionBuilder) hasEquivalentCallResultEvent(
events []ParsedToolResultEvent, candidate ParsedToolResultEvent,
) bool {
for _, existing := range events {
if existing.AgentID == candidate.AgentID &&
existing.Status == candidate.Status &&
existing.Content == candidate.Content {
return true
}
}
return false
}
func (b *codexSessionBuilder) claimPendingAgentEvents(
callID, agentID string,
) {
pending := b.pendingAgentEvents[agentID]
if len(pending) == 0 {
return
}
for _, ev := range pending {
b.appendCallResultEvent(callID, ParsedToolResultEvent{
AgentID: ev.agentID,
SubagentSessionID: codexSubagentSessionID(ev.agentID),
Source: ev.source,
Status: ev.status,
Content: ev.text,
Timestamp: ev.timestamp,
})
}
delete(b.pendingAgentEvents, agentID)
}
func (b *codexSessionBuilder) flushPendingAgentResults() {
if len(b.pendingAgentEvents) == 0 {
return
}
agentIDs := make([]string, 0, len(b.pendingAgentEvents))
for agentID := range b.pendingAgentEvents {
agentIDs = append(agentIDs, agentID)
}
sort.Strings(agentIDs)
for _, agentID := range agentIDs {
pending := b.pendingAgentEvents[agentID]
switch {
case b.agentWaitCalls[agentID] != "":
b.claimPendingAgentEvents(b.agentWaitCalls[agentID], agentID)
case b.agentSpawnCalls[agentID] != "":
b.claimPendingAgentEvents(b.agentSpawnCalls[agentID], agentID)
default:
for _, ev := range pending {
key := agentID + "\x00" + ev.status + "\x00" + ev.text
if _, ok := b.orphanNotificationIx[key]; ok {
continue
}
idx := b.insertMessage(ParsedMessage{
Ordinal: ev.ordinal,
Role: RoleUser,
Content: ev.text,
Timestamp: ev.timestamp,
Model: b.model,
ContentLength: len(ev.text),
})
b.orphanNotificationIx[key] = idx
}
delete(b.pendingAgentEvents, agentID)
}
}
}
func codexSubagentSessionID(agentID string) string {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return ""
}
if strings.HasPrefix(agentID, "codex:") {
return agentID
}
return "codex:" + agentID
}
func (b *codexSessionBuilder) normalizeOrdinals() {
sort.SliceStable(b.messages, func(i, j int) bool {
if b.messages[i].Ordinal == b.messages[j].Ordinal {
return i < j
}
return b.messages[i].Ordinal < b.messages[j].Ordinal
})
for i := range b.messages {
b.messages[i].Ordinal = i
}
}
func (b *codexSessionBuilder) insertMessage(msg ParsedMessage) int {
idx := len(b.messages)
for i, existing := range b.messages {
if existing.Ordinal > msg.Ordinal ||
(existing.Ordinal == msg.Ordinal &&
!msg.Timestamp.IsZero() &&
(existing.Timestamp.IsZero() ||
msg.Timestamp.Before(existing.Timestamp))) {
idx = i
break
}
}
b.messages = append(b.messages, ParsedMessage{})
copy(b.messages[idx+1:], b.messages[idx:])
b.messages[idx] = msg
for callID, ref := range b.callRefs {
if ref.messageIndex >= idx {
ref.messageIndex++
b.callRefs[callID] = ref
}
}
return idx
}
func formatCodexFunctionCall(
name string, payload gjson.Result,
) string {
summary := sanitizeToolLabel(payload.Get("summary").Str)
args, rawArgs := parseCodexFunctionArgs(payload)
switch name {
case "exec_command", "shell_command", "shell":
return formatCodexBashCall(summary, args, rawArgs)
case "write_stdin":
return formatCodexWriteStdinCall(summary, args, rawArgs)
case "apply_patch":
return formatCodexApplyPatchCall(summary, args, rawArgs)
case "spawn_agent":
return formatCodexSpawnAgentCall(summary, args, rawArgs)
}
category := NormalizeToolCategory(name)
if category == "Other" {
header := formatToolHeader("Tool", name)
if summary != "" {
return header + "\n" + summary
}
if preview := codexArgPreview(args, rawArgs); preview != "" {
return header + "\n" + preview
}
return header
}
detail := firstNonEmpty(summary,
codexCategoryDetail(category, args))
header := formatToolHeader(category, detail)
if preview := codexArgPreview(args, rawArgs); preview != "" {
return header + "\n" + preview
}
return header
}
func parseCodexFunctionArgs(
payload gjson.Result,
) (gjson.Result, string) {
for _, key := range []string{"arguments", "input"} {
arg := payload.Get(key)
if !arg.Exists() {
continue
}
switch arg.Type {
case gjson.String:
s := strings.TrimSpace(arg.Str)
if s == "" {
continue
}
if gjson.Valid(s) {
return gjson.Parse(s), ""
}
return gjson.Result{}, s
default:
if arg.IsObject() {
if len(arg.Map()) == 0 {
continue
}
return arg, ""
}
if arg.IsArray() {
if len(arg.Array()) == 0 {
continue
}
return arg, ""
}
raw := strings.TrimSpace(arg.Raw)
if raw == "" {
continue
}
if gjson.Valid(raw) {
return gjson.Parse(raw), ""
}
return gjson.Result{}, raw
}
}
return gjson.Result{}, ""
}
// extractCodexInputJSON returns the raw JSON string of the
// function call arguments from the payload. It checks
// "arguments" then "input", normalizing string-encoded JSON
// to an object string.
func extractCodexInputJSON(payload gjson.Result) string {
for _, key := range []string{"arguments", "input"} {
arg := payload.Get(key)
if !arg.Exists() {
continue
}
switch arg.Type {
case gjson.String:
s := strings.TrimSpace(arg.Str)
if s == "" {
continue
}
if gjson.Valid(s) {
if s == "{}" || s == "[]" {
continue
}
return s
}
return arg.Str
default:
raw := strings.TrimSpace(arg.Raw)
if raw == "" || raw == "{}" || raw == "[]" {
continue
}
return arg.Raw
}
}
return ""
}
func formatCodexBashCall(
summary string, args gjson.Result, rawArgs string,
) string {
cmd := codexArgValue(args, "cmd", "command")
if cmd == "" && rawArgs != "" && !gjson.Valid(rawArgs) {
cmd = rawArgs
}
if cmd == "" && args.Type == gjson.String {
cmd = strings.TrimSpace(args.Str)
}
header := formatToolHeader("Bash", summary)
if cmd != "" {
firstLine, _, hasMore := strings.Cut(cmd, "\n")
if hasMore {
return header + "\n$ " + firstLine
}
return header + "\n$ " + cmd
}
if preview := codexArgPreview(args, rawArgs); preview != "" {
return header + "\n" + preview
}
return header
}
func formatCodexWriteStdinCall(
summary string, args gjson.Result, rawArgs string,
) string {
if summary == "" {
if sid := codexArgValue(args, "session_id"); sid != "" {
summary = "stdin -> " + sid
} else {
summary = "stdin"
}
}
header := formatToolHeader("Bash", summary)
chars := codexArgString(args, "chars")
if chars != "" {
quoted := strings.Trim(
strconv.QuoteToASCII(chars), "\"",
)
return header + "\n" + truncate(quoted, 220)
}
if preview := codexArgPreview(args, rawArgs); preview != "" {
return header + "\n" + preview
}
return header
}
func formatCodexApplyPatchCall(
summary string, args gjson.Result, rawArgs string,
) string {
patch := codexArgString(args, "patch")
if patch == "" && strings.Contains(rawArgs, "*** Begin Patch") {
patch = rawArgs
}
files := extractPatchedFiles(patch)
if summary == "" {
summary = summarizePatchedFiles(files)
}
header := formatToolHeader("Edit", summary)
if len(files) > 1 {
limit := min(len(files), 6)
body := strings.Join(files[:limit], "\n")
if len(files) > limit {
body += fmt.Sprintf("\n+%d more files", len(files)-limit)
}
return header + "\n" + body
}
if preview := codexArgPreview(args, rawArgs); preview != "" &&
len(files) == 0 {
return header + "\n" + preview
}
return header
}
func formatCodexSpawnAgentCall(
summary string, args gjson.Result, rawArgs string,
) string {
if summary == "" {
summary = firstNonEmpty(
codexArgValue(args, "agent_type"),
codexArgValue(args, "subagent_type"),
"spawn_agent",
)
}
header := formatToolHeader("Task", summary)
prompt := firstNonEmpty(
codexArgValue(args, "description"),
codexArgValue(args, "message"),
codexArgValue(args, "prompt"),
)
if prompt != "" {
firstLine, _, _ := strings.Cut(prompt, "\n")
return header + "\n" + truncate(firstLine, 220)
}
if preview := codexArgPreview(args, rawArgs); preview != "" {
return header + "\n" + preview
}
return header
}
func extractPatchedFiles(patch string) []string {
if patch == "" {
return nil
}
var files []string
seen := make(map[string]struct{})
for line := range strings.SplitSeq(patch, "\n") {
for _, prefix := range []string{
"*** Add File: ",
"*** Update File: ",
"*** Delete File: ",
"*** Move to: ",
} {
if !strings.HasPrefix(line, prefix) {
continue
}
file := strings.TrimSpace(
strings.TrimPrefix(line, prefix),
)
if file == "" {
continue
}
if _, ok := seen[file]; ok {
continue
}
seen[file] = struct{}{}
files = append(files, file)
break
}
}
return files
}
func summarizePatchedFiles(files []string) string {
switch len(files) {
case 0:
return ""
case 1:
return files[0]
default:
return fmt.Sprintf(
"%s (+%d more)",
files[0], len(files)-1,
)
}
}
func codexCategoryDetail(
category string, args gjson.Result,
) string {
switch category {
case "Read", "Write", "Edit":
return codexArgValue(args, "file_path", "path")
case "Grep":
return codexArgValue(args, "pattern")
case "Glob":
pattern := codexArgValue(args, "pattern")
path := codexArgValue(args, "path")
if pattern != "" && path != "" {
return fmt.Sprintf("%s in %s", pattern, path)
}
return firstNonEmpty(pattern, path)
case "Task", "Agent":
desc := codexArgValue(args, "description")
agent := codexArgValue(args, "subagent_type")
if desc != "" && agent != "" {
return fmt.Sprintf("%s (%s)", desc, agent)
}
return firstNonEmpty(desc, agent)
default:
return ""
}
}
func codexArgString(
args gjson.Result, path string,
) string {
v := args.Get(path)
if !v.Exists() {
return ""
}
if v.Type == gjson.String {
return v.Str
}
raw := strings.TrimSpace(v.Raw)
if raw == "" || raw == "null" {
return ""
}
return raw
}
func codexArgValue(
args gjson.Result, paths ...string,
) string {
for _, path := range paths {
v := strings.TrimSpace(codexArgString(args, path))
if v != "" {
return v
}
}
return ""
}
func codexArgPreview(
args gjson.Result, rawArgs string,
) string {
if rawArgs != "" {
flat := strings.Join(
strings.Fields(rawArgs), " ",
)
return truncate(flat, 220)
}
if args.Exists() {
flat := strings.Join(
strings.Fields(args.Raw), " ",
)
if flat != "" {
return truncate(flat, 220)
}
}
return ""
}
func formatToolHeader(
label, detail string,
) string {
label = sanitizeToolLabel(label)
if label == "" {
label = "Tool"
}
detail = sanitizeToolLabel(detail)
if detail != "" {
return fmt.Sprintf("[%s: %s]", label, detail)
}
return fmt.Sprintf("[%s]", label)
}
func sanitizeToolLabel(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
s = strings.ReplaceAll(s, "]", ")")
return strings.Join(strings.Fields(s), " ")
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
v = strings.TrimSpace(v)
if v != "" {
return v
}
}
return ""
}
func parseCodexFunctionOutput(
payload gjson.Result,
) (gjson.Result, string) {
out := payload.Get("output")
if !out.Exists() {
return gjson.Result{}, ""
}
switch out.Type {
case gjson.String:
s := strings.TrimSpace(out.Str)
if s == "" {
return gjson.Result{}, ""
}
if gjson.Valid(s) {
return gjson.Parse(s), s
}
return gjson.Result{}, s
default:
raw := strings.TrimSpace(out.Raw)
if raw == "" {
return gjson.Result{}, ""
}
if gjson.Valid(raw) {
return gjson.Parse(raw), raw
}
return gjson.Result{}, raw
}
}
func codexWaitAgentIDs(args gjson.Result) []string {
if !args.Exists() {
return nil
}
ids := args.Get("ids")
if !ids.Exists() {
ids = args.Get("targets")
}
if !ids.Exists() || !ids.IsArray() {
return nil
}
var out []string
for _, item := range ids.Array() {
id := strings.TrimSpace(item.Str)
if id == "" {
continue
}
out = append(out, id)
}
return out
}
func isCodexWaitAgentCall(name string) bool {
return name == "wait" || name == "wait_agent"
}
func parseCodexSubagentNotification(
content string,
) (agentID, statusName, text string) {
if !isCodexSubagentNotification(content) {
return "", "", ""
}
body := strings.TrimSpace(content)
body = strings.TrimPrefix(body, "<subagent_notification>")
body = strings.TrimSuffix(body, "</subagent_notification>")
body = strings.TrimSpace(body)
if !gjson.Valid(body) {
return "", "", ""
}
parsed := gjson.Parse(body)
agentID = firstNonEmpty(
parsed.Get("agent_id").Str,
parsed.Get("agent_path").Str,
)
status := parsed.Get("status")
statusName, text = codexTerminalSubagentEvent(status)
return agentID, statusName, text
}
func codexTerminalSubagentEvent(status gjson.Result) (string, string) {
if text := strings.TrimSpace(status.Get("completed").Str); text != "" {
return "completed", text
}
if text := strings.TrimSpace(status.Get("errored").Str); text != "" {
return "errored", text
}
if text := strings.TrimSpace(status.Get("running").Str); text != "" {
return "running", text
}
return "", ""
}
func codexTerminalSubagentStatus(status gjson.Result) string {
_, text := codexTerminalSubagentEvent(status)
return text
}
func isCodexSubagentFunctionOutput(output gjson.Result) bool {
if !output.Exists() {
return false
}
if strings.TrimSpace(output.Get("agent_id").Str) != "" {
return true
}
status := output.Get("status")
if !status.Exists() || !status.IsObject() {
return false
}
entries := status.Map()
if len(entries) == 0 {
return false
}
for agentID, entry := range entries {
if strings.TrimSpace(agentID) == "" || !entry.IsObject() {
return false
}
if codexTerminalSubagentStatus(entry) != "" {
continue
}
if strings.TrimSpace(entry.Get("running").Str) != "" {
continue
}
return false
}
return true
}
func extractCodexTextBlocks(payload gjson.Result) []string {
var texts []string
payload.Get("content").ForEach(
func(_, block gjson.Result) bool {
switch block.Get("type").Str {
case "input_text", "output_text", "text":
if t := block.Get("text").Str; t != "" {
texts = append(texts, t)
}
}
return true
},
)
return texts
}
// extractCodexContent joins all text blocks from a Codex
// response item's content array.
func extractCodexContent(payload gjson.Result) string {
return strings.Join(extractCodexTextBlocks(payload), "\n")
}
// extractCodexInitialUserContent filters the synthetic blocks bundled with
// Codex's recommended-plugins injection while retaining user-authored blocks
// from the same response item.
func extractCodexInitialUserContent(payload gjson.Result) string {
texts := extractCodexTextBlocks(payload)
if len(texts) == 0 {
return strings.Join(texts, "\n")
}
stripped := stripCodexRecommendedPlugins(texts[0])
if stripped == texts[0] {
return strings.Join(texts, "\n")
}
texts[0] = stripped
kept := texts[:0]
for _, text := range texts {
text = stripCodexInitialSystemPrefix(text)
if strings.TrimSpace(text) == "" || isCodexSystemMessage(text) {
continue
}
kept = append(kept, text)
}
return strings.Join(kept, "\n")
}
// IsCodexExecSessionFile reports whether any session_meta
// line in a Codex JSONL file has originator=="codex_exec".
// The pre-bulk-sync parser called handleSessionMeta on every
// session_meta line and flagged the whole session as exec if
// any of them carried that originator, so a one-shot check
// of only the first session_meta would miss files that were
// originally skipped because a later session_meta set the
// originator. Scan all session_meta lines to match the old
// skip condition exactly.
func IsCodexExecSessionFile(path string) bool {
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
s := bufio.NewScanner(f)
s.Buffer(make([]byte, 0, 64*1024), maxLineSize)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || !gjson.Valid(line) {
continue
}
if gjson.Get(line, "type").Str != codexTypeSessionMeta {
continue
}
if gjson.Get(line, "payload.originator").Str ==
codexOriginatorExec {
return true
}
}
return false
}
// parseSession parses a Codex JSONL session file into a session and its
// messages. The includeExec parameter is retained for backward compatibility;
// exec-originated sessions are now always parsed and imported. This is the
// provider-owned parse entrypoint; the package-level free function was folded
// onto the provider.
func (p *codexProvider) parseSession(
path, machine string, includeExec bool,
) (*ParsedSession, []ParsedMessage, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, nil, fmt.Errorf("stat %s: %w", path, err)
}
return p.parseSessionSnapshot(
path, machine, includeExec, f, info,
)
}
// parseSessionSnapshot parses exactly the raw-size snapshot captured from f.
// Limiting the reader prevents an append racing the scan from being folded into
// a cursor keyed by the earlier size.
func (p *codexProvider) parseSessionSnapshot(
path, machine string,
includeExec bool,
f *os.File,
info os.FileInfo,
) (*ParsedSession, []ParsedMessage, error) {
lr := newLineReader(io.LimitReader(f, info.Size()), maxLineSize)
defer releaseLineReader(lr)
b := newCodexSessionBuilder(includeExec)
for {
line, ok := lr.next()
if !ok {
break
}
if !gjson.Valid(line) {
continue
}
if b.processLine(line) {
return nil, nil, nil
}
}
if err := lr.Err(); err != nil {
return nil, nil,
fmt.Errorf("reading codex %s: %w", path, err)
}
b.flushPendingAgentResults()
b.normalizeOrdinals()
inode, device := sourceFileIdentity(info)
if safe, safeErr := codexSafeResumeOffsetFile(f, info.Size()); safeErr == nil && safe {
p.cursorCache.Put(
path,
info.Size(),
inode,
device,
b.incrementalSeed(),
)
}
sessionID := b.sessionID
if sessionID == "" {
sessionID = strings.TrimSuffix(
filepath.Base(path), ".jsonl",
)
}
sessionID = "codex:" + sessionID
userCount := 0
for _, m := range b.messages {
if m.Role == RoleUser && m.Content != "" {
userCount++
}
}
mtime := info.ModTime().UnixNano()
// Include session_index.jsonl mtime so renames trigger a re-parse.
if idxPath := codexSessionIndexPath(path); idxPath != "" {
if idxInfo, err := os.Stat(idxPath); err == nil {
if idxMtime := idxInfo.ModTime().UnixNano(); idxMtime > mtime {
mtime = idxMtime
}
}
}
sess := &ParsedSession{
ID: sessionID,
Project: b.project,
Machine: machine,
Agent: AgentCodex,
Cwd: b.cwd,
FirstMessage: b.firstMessage,
SessionName: LookupCodexThreadName(path, b.sessionID),
StartedAt: b.startedAt,
EndedAt: b.endedAt,
MessageCount: len(b.messages),
UserMessageCount: userCount,
TerminationStatus: classifyCodexTermination(b.lastTaskEvent),
File: FileInfo{
Path: path,
Size: info.Size(),
Mtime: mtime,
Inode: int64(inode),
Device: int64(device),
},
}
accumulateMessageTokenUsage(sess, b.messages)
return sess, b.messages, nil
}
// CodexSessionIndexFilename is the name of the Codex index file that maps
// session UUIDs to their (renameable) thread titles. It sits next to the
// sessions/ and archived_sessions/ directories.
const CodexSessionIndexFilename = "session_index.jsonl"
// CodexSessionIndexTitles returns the session UUID to thread-title map from
// a Codex session_index.jsonl file, or nil when it cannot be read. The
// underlying read is cached by path, mtime, and size.
func CodexSessionIndexTitles(indexPath string) map[string]string {
titles, err := loadCodexSessionIndex(indexPath)
if err != nil {
return nil
}
return titles
}
// EvictCodexSessionIndex removes one cached session_index.jsonl entry. Callers
// use it when an explicit change event makes the sidecar stat tuple insufficient
// and when transient hydrated indexes should not outlive their parse.
func EvictCodexSessionIndex(indexPath string) {
codexSessionIndexCache.mu.Lock()
delete(codexSessionIndexCache.entries, indexPath)
codexSessionIndexCache.mu.Unlock()
}
// EvictAllCodexSessionIndexes removes every cached session_index.jsonl entry.
// Watcher overflow recovery uses this before a force-reverification pass,
// because the individual index paths that changed were deliberately coalesced.
func EvictAllCodexSessionIndexes() {
codexSessionIndexCache.mu.Lock()
clear(codexSessionIndexCache.entries)
codexSessionIndexCache.mu.Unlock()
}
// EvictCodexSessionIndexForSession removes the cached sidecar associated with
// one Codex transcript. Explicit full-parse callers use this when an external
// event says the sidecar changed even if its stat tuple did not.
func EvictCodexSessionIndexForSession(sessionPath string) {
if indexPath := codexSessionIndexPath(sessionPath); indexPath != "" {
EvictCodexSessionIndex(indexPath)
}
}
// LookupCodexThreadName returns the current Codex thread name for a session
// from the session_index.jsonl file next to the session root.
func LookupCodexThreadName(sessionPath, sessionID string) string {
if strings.TrimSpace(sessionID) == "" {
return ""
}
indexPath := codexSessionIndexPath(sessionPath)
if indexPath == "" {
return ""
}
titles, err := loadCodexSessionIndex(indexPath)
if err != nil {
return ""
}
return strings.TrimSpace(titles[sessionID])
}
// CodexEffectiveMtime returns the effective mtime for a Codex session file,
// incorporating session_index.jsonl so renames invalidate the cache.
func CodexEffectiveMtime(sessionPath string, fileMtime int64) int64 {
if idxPath := codexSessionIndexPath(sessionPath); idxPath != "" {
if si, err := os.Stat(idxPath); err == nil {
if idxMtime := si.ModTime().UnixNano(); idxMtime > fileMtime {
return idxMtime
}
}
}
return fileMtime
}
// CodexSessionIndexPath returns the local session_index.jsonl path associated
// with a Codex transcript, or an empty string when the transcript is outside a
// recognized Codex session directory.
func CodexSessionIndexPath(sessionPath string) string {
return codexSessionIndexPath(sessionPath)
}
func codexSessionIndexPath(sessionPath string) string {
dir := filepath.Dir(sessionPath)
for dir != "" {
base := filepath.Base(dir)
if base == "sessions" || base == "archived_sessions" {
return filepath.Join(
filepath.Dir(dir), CodexSessionIndexFilename,
)
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
func loadCodexSessionIndex(indexPath string) (map[string]string, error) {
info, err := os.Stat(indexPath)
if err != nil {
return nil, err
}
mtime := info.ModTime().UnixNano()
size := info.Size()
changeTime, changeTimeOkay := codexIndexChangeTime(indexPath, info)
codexSessionIndexCache.mu.Lock()
if entry, ok := codexSessionIndexCache.entries[indexPath]; ok &&
entry.mtime == mtime && entry.size == size &&
(!changeTimeOkay ||
(entry.changeTimeOkay && entry.changeTime == changeTime)) {
codexSessionIndexCache.mu.Unlock()
return entry.titles, nil
}
codexSessionIndexCache.mu.Unlock()
f, err := os.Open(indexPath)
if err != nil {
return nil, err
}
defer f.Close()
titles, err := ParseCodexSessionIndexTitles(f)
if err != nil {
return nil, err
}
codexSessionIndexCache.mu.Lock()
codexSessionIndexCache.entries[indexPath] = codexSessionIndexEntry{
mtime: mtime,
size: size,
changeTime: changeTime,
changeTimeOkay: changeTimeOkay,
titles: titles,
}
codexSessionIndexCache.mu.Unlock()
return titles, nil
}
// ParseCodexSessionIndexTitles reads a Codex session_index.jsonl stream and
// returns session UUIDs mapped to non-empty thread titles.
func ParseCodexSessionIndexTitles(r io.Reader) (map[string]string, error) {
titles := make(map[string]string)
s := bufio.NewScanner(r)
s.Buffer(make([]byte, 0, 64*1024), maxLineSize)
for s.Scan() {
line := s.Text()
if !gjson.Valid(line) {
continue
}
id := gjson.Get(line, "id").Str
title := strings.TrimSpace(gjson.Get(line, "thread_name").Str)
if id == "" || title == "" {
continue
}
titles[id] = title
}
if err := s.Err(); err != nil {
return nil, err
}
return titles, nil
}
// classifyCodexTermination maps the most recent task lifecycle
// event seen on a Codex session file to a TerminationStatus.
// Codex emits explicit task_started / task_complete / turn_aborted
// events, so the classification is unambiguous when any are
// present. Returns "" (unknown) for files where no task event
// was seen — typically very short or malformed sessions.
func classifyCodexTermination(lastTaskEvent string) TerminationStatus {
switch lastTaskEvent {
case "task_complete":
return TerminationAwaitingUser
case "task_started", "turn_aborted":
// task_started without a matching task_complete after
// means the agent was mid-turn when the file last
// flushed — treat the same as an orphan tool call.
// turn_aborted means the user interrupted; same shape.
return TerminationToolCallPending
}
return ""
}
// codexIncrementalSeed carries the builder state recovered from the
// already-parsed prefix [0, offset) of a Codex JSONL file so an
// incremental parse resumes with the same view a full parse would
// have at that offset: the current model, the re-emitted-prompt
// dedup state, task lifecycle marker, and the fork replay gate (#643).
type codexIncrementalSeed = codexCursorState
// seedCodexIncrementalState scans a Codex JSONL prefix [0, offset)
// and mirrors processLine's dispatch: every turn_context overwrites
// the model (including empty strings), user messages feed the
// re-emitted-prompt dedup exactly as handleResponseItem would, and
// the fork gate arms/opens on the same lines as a full parse. A gate
// still active at the end of the scan means the stored offset landed
// inside the replayed parent history of a forked rollout, so the
// incremental parse must keep suppressing appended replay lines.
func seedCodexIncrementalState(
path string, offset int64,
) (codexIncrementalSeed, error) {
f, err := os.Open(path)
if err != nil {
return codexIncrementalSeed{}, fmt.Errorf(
"open codex prefix %s: %w", path, err,
)
}
defer f.Close()
seed, err := seedCodexIncrementalStateFromReader(
io.LimitReader(f, offset),
)
if err != nil {
return codexIncrementalSeed{}, fmt.Errorf(
"read codex prefix %s: %w", path, err,
)
}
return seed, nil
}
func seedCodexIncrementalStateFromReader(
r io.Reader,
) (codexIncrementalSeed, error) {
var seed codexIncrementalSeed
lr := newLineReader(r, maxLineSize)
defer releaseLineReader(lr)
for {
line, ok := lr.next()
if !ok {
break
}
if !gjson.Valid(line) {
continue
}
lineType := gjson.Get(line, "type").Str
payload := gjson.Get(line, "payload")
if lineType == codexTypeSessionMeta {
// Mirror processLine: the fork's own meta arms the
// gate and supplies cwd, and replayed parent metas are
// dropped while it is active.
if !seed.forkGate.active {
if cwd := payload.Get("cwd").Str; cwd != "" {
seed.cwd = cwd
}
seed.forkGate.armFromMeta(
payload,
parseTimestamp(gjson.Get(line, "timestamp").Str),
)
}
continue
}
if seed.forkGate.suppresses(lineType, payload) {
continue
}
switch lineType {
case codexTypeTurnContext:
seed.model = payload.Get("model").Str
case codexTypeEventMsg:
eventType := payload.Get("type").Str
seed.observeTaskEvent(eventType)
if eventType == "token_count" {
raw := payload.Get("info.last_token_usage").Raw
if raw != "" {
seed.observeTokenUsage(raw)
}
}
case codexTypeResponseItem:
observeCodexIncrementalUserMessage(&seed, payload)
}
}
if err := lr.Err(); err != nil {
return codexIncrementalSeed{}, err
}
return seed, nil
}
// observeUserMessage feeds one response_item into the
// re-emitted-prompt dedup state, mirroring handleResponseItem's
// user-message filtering and full-content matching.
func observeCodexIncrementalUserMessage(
s *codexIncrementalSeed,
payload gjson.Result,
) {
if payload.Get("role").Str != "user" {
return
}
content := extractCodexContent(payload)
if !s.firstUserSeen {
content = extractCodexInitialUserContent(payload)
}
if strings.TrimSpace(content) == "" {
return
}
if isCodexTurnAbortedMessage(content) {
s.markFirstUserReplayPossible()
}
if isCodexSystemMessage(content) {
return
}
s.observeUserPrompt(content)
}
// CodexTranscriptConsumedSize returns the byte offset after the last complete,
// valid JSON line in a Codex transcript. Bytes after this offset are ignored by
// the Codex JSONL parser, so partial trailing writes are not part of the parsed
// source snapshot.
func CodexTranscriptConsumedSize(path string) (int64, error) {
consumed, err := readCodexJSONLFrom(path, 0, func(line string) {})
if errors.Is(err, errCodexIncrementalNeedsFullParse) {
return consumed, nil
}
return consumed, err
}
// readCodexJSONLFrom is the Codex-specific conservative tail reader. Codex may
// expose syntactically valid JSON before the writer appends its record newline;
// such an EOF record requires a full-parse fallback and is never staged as a
// safe continuation cursor.
func readCodexJSONLFrom(
path string,
offset int64,
fn func(line string),
) (consumed int64, err error) {
f, err := os.Open(path)
if err != nil {
return 0, fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return 0, fmt.Errorf("stat %s: %w", path, err)
}
return readCodexJSONLSection(f, offset, info.Size(), fn)
}
// readCodexJSONLSection applies the conservative Codex JSONL rules to the
// exact byte range [offset, limit). A SectionReader makes the captured limit a
// real EOF even if the underlying descriptor has since grown.
func readCodexJSONLSection(
f *os.File,
offset int64,
limit int64,
fn func(line string),
) (consumed int64, err error) {
if offset < 0 || limit < offset {
return 0, fmt.Errorf(
"invalid codex JSONL section [%d,%d)", offset, limit,
)
}
section := io.NewSectionReader(f, offset, limit-offset)
return readCodexJSONLReader(section, section, fn)
}
func readCodexJSONLReader(
r io.Reader,
at io.ReaderAt,
fn func(line string),
) (consumed int64, err error) {
lr := newLineReader(r, maxLineSize)
defer releaseLineReader(lr)
for {
line, ok := lr.next()
if !ok {
break
}
var terminator [1]byte
if _, err := at.ReadAt(terminator[:], lr.bytesRead-1); err != nil {
return consumed, err
}
if terminator[0] != '\n' {
if gjson.Valid(line) {
return consumed, errCodexIncrementalNeedsFullParse
}
break
}
if gjson.Valid(line) {
fn(line)
consumed = lr.bytesRead
}
}
return consumed, lr.Err()
}
// codexSafeResumeOffset checks in O(1) that offset starts at a physical line
// boundary. Offset zero is always safe; every nonzero offset must immediately
// follow a newline byte.
func codexSafeResumeOffset(path string, offset int64) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, err
}
defer f.Close()
return codexSafeResumeOffsetFile(f, offset)
}
func codexSafeResumeOffsetFile(f *os.File, offset int64) (bool, error) {
if offset == 0 {
return true, nil
}
if offset < 0 {
return false, nil
}
var previous [1]byte
if _, err := f.ReadAt(previous[:], offset-1); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return false, nil
}
return false, err
}
return previous[0] == '\n', nil
}
type codexIncrementalParseResult struct {
messages []ParsedMessage
endedAt time.Time
consumedBytes int64
initialCursor codexCursorState
cursor codexCursorState
inode uint64
device uint64
}
// parseSessionFromDetailed parses only new lines from a Codex JSONL file. It
// resumes from an exact cached cursor when one exists and otherwise reconstructs
// the same state by scanning the prefix. A successful result carries the exact
// cursor the provider may stage at offset+consumed; the prior offset remains
// eligible until the caller persists the new offset.
func (p *codexProvider) parseSessionFromDetailed(
path string,
offset int64,
startOrdinal int,
includeExec bool,
) (codexIncrementalParseResult, error) {
f, err := os.Open(path)
if err != nil {
return codexIncrementalParseResult{}, fmt.Errorf(
"open codex %s: %w", path, err,
)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return codexIncrementalParseResult{}, fmt.Errorf(
"stat codex %s: %w", path, err,
)
}
return p.parseSessionFromSnapshot(
path, offset, startOrdinal, includeExec, f, info, info.Size(),
)
}
func (p *codexProvider) parseSessionFromSnapshot(
path string,
offset int64,
startOrdinal int,
includeExec bool,
f *os.File,
info os.FileInfo,
limit int64,
) (codexIncrementalParseResult, error) {
return p.parseSessionFromWithSources(
path,
offset,
startOrdinal,
includeExec,
info,
func(fn func(string)) (int64, error) {
return readCodexJSONLSection(f, offset, limit, fn)
},
func() (codexIncrementalSeed, error) {
return seedCodexIncrementalStateFromReader(
io.NewSectionReader(f, 0, offset),
)
},
)
}
func (p *codexProvider) parseSessionFromWithReader(
path string,
offset int64,
startOrdinal int,
includeExec bool,
readLines func(string, int64, func(string)) (int64, error),
) (codexIncrementalParseResult, error) {
return p.parseSessionFromWithReaders(
path,
offset,
startOrdinal,
includeExec,
readLines,
seedCodexIncrementalState,
)
}
func (p *codexProvider) parseSessionFromWithReaders(
path string,
offset int64,
startOrdinal int,
includeExec bool,
readLines func(string, int64, func(string)) (int64, error),
readSeed func(string, int64) (codexIncrementalSeed, error),
) (codexIncrementalParseResult, error) {
info, err := os.Stat(path)
if err != nil {
return codexIncrementalParseResult{}, fmt.Errorf(
"stat codex %s: %w", path, err,
)
}
return p.parseSessionFromWithSources(
path,
offset,
startOrdinal,
includeExec,
info,
func(fn func(string)) (int64, error) {
return readLines(path, offset, fn)
},
func() (codexIncrementalSeed, error) {
return readSeed(path, offset)
},
)
}
func (p *codexProvider) parseSessionFromWithSources(
path string,
offset int64,
startOrdinal int,
includeExec bool,
info os.FileInfo,
readLines func(func(string)) (int64, error),
readSeed func() (codexIncrementalSeed, error),
) (codexIncrementalParseResult, error) {
inode, device := sourceFileIdentity(info)
seed, cacheHit := p.cursorCache.Get(path, offset, inode, device)
if !cacheHit {
var err error
seed, err = readSeed()
if err != nil {
return codexIncrementalParseResult{}, fmt.Errorf(
"seed codex %s at offset %d: %w",
path, offset, err,
)
}
}
b := newCodexSessionBuilder(includeExec)
b.ordinal = startOrdinal
b.codexCursorState = seed
var fallbackErr error
consumed, err := readLines(
func(line string) {
if fallbackErr != nil {
return
}
// Skip session_meta — already processed in
// the initial full parse.
if gjson.Get(line, "type").Str ==
codexTypeSessionMeta {
return
}
if codexIncrementalNeedsFullParse(line) {
fallbackErr = errCodexIncrementalNeedsFullParse
return
}
b.processLine(line)
if b.unattachedTokenUsage {
fallbackErr = errCodexIncrementalNeedsFullParse
return
}
},
)
if err != nil {
return codexIncrementalParseResult{}, fmt.Errorf(
"reading codex %s from offset %d: %w",
path, offset, err,
)
}
if fallbackErr != nil {
return codexIncrementalParseResult{}, fallbackErr
}
b.flushPendingAgentResults()
result := codexIncrementalParseResult{
messages: b.messages,
endedAt: b.endedAt,
consumedBytes: consumed,
initialCursor: seed,
cursor: b.incrementalSeed(),
inode: inode,
device: device,
}
return result, nil
}
// parseSessionFrom preserves the legacy test-helper and parser signature while
// the provider facade consumes the detailed cursor result internally.
func (p *codexProvider) parseSessionFrom(
path string,
offset int64,
startOrdinal int,
includeExec bool,
) ([]ParsedMessage, time.Time, int64, error) {
result, err := p.parseSessionFromWithReader(
path,
offset,
startOrdinal,
includeExec,
readJSONLFrom,
)
if err != nil {
return nil, time.Time{}, 0, err
}
return result.messages, result.endedAt, result.consumedBytes, nil
}
// IsIncrementalFullParseFallback reports whether an incremental
// parse error requires the caller to fall back to a full parse.
func IsIncrementalFullParseFallback(err error) bool {
return errors.Is(err, errCodexIncrementalNeedsFullParse) ||
errors.Is(err, ErrClaudeIncrementalNeedsFullParse)
}
func isCodexSystemMessage(content string) bool {
trimmed := strings.TrimSpace(content)
return strings.HasPrefix(content, "# AGENTS.md") ||
strings.HasPrefix(content, "<environment_context>") ||
strings.HasPrefix(content, "<INSTRUCTIONS>") ||
isCodexTurnAbortedMessage(content) ||
strings.HasPrefix(trimmed, "<skill>") ||
isCodexSubagentNotification(content) ||
isCodexGoalContext(content)
}
// stripCodexRecommendedPlugins removes the plugin-discovery envelope
// that recent Codex versions prepend to the synthetic context item at the
// start of a session. It is called only while looking for the first genuine
// user turn, so a later user message that quotes the envelope is preserved.
func stripCodexRecommendedPlugins(content string) string {
const (
openTag = "<recommended_plugins>"
closeTag = "</recommended_plugins>"
)
start := strings.Index(content, openTag)
if start < 0 {
return content
}
relativeEnd := strings.Index(content[start+len(openTag):], closeTag)
if relativeEnd < 0 {
return content
}
end := start + len(openTag) + relativeEnd + len(closeTag)
prefix := content[:start]
suffix := strings.TrimLeft(content[end:], "\r\n")
if strings.TrimSpace(prefix) == "" {
return suffix
}
return prefix + suffix
}
// stripCodexInitialSystemPrefix removes complete synthetic envelopes from the
// start of an initial text block. A genuine prompt may follow an injected
// envelope in the same block, so only text through the first matching close
// tag is removed.
func stripCodexInitialSystemPrefix(content string) string {
for {
trimmed := strings.TrimLeft(content, "\r\n")
var closeTag string
switch {
case strings.HasPrefix(trimmed, "# AGENTS.md"):
closeTag = "</INSTRUCTIONS>"
case strings.HasPrefix(trimmed, "<environment_context>"):
closeTag = "</environment_context>"
case strings.HasPrefix(trimmed, "<INSTRUCTIONS>"):
closeTag = "</INSTRUCTIONS>"
default:
return content
}
_, after, ok := strings.Cut(trimmed, closeTag)
if !ok {
return content
}
content = strings.TrimLeft(after, "\r\n")
}
}
// isCodexGoalContext reports whether content is a Codex /goal
// continuation envelope. These are harness-injected as role=user
// records to keep the model working toward an active thread goal, but
// they are not user-authored turns and should be treated as system
// content. Current sessions wrap the body in
// <codex_internal_context source="goal">; older sessions used
// <goal_context>. Detection is scoped to the structured wrapper (and,
// for the modern form, the goal source specifically) so that other
// internal-context envelopes and real user messages quoting the goal
// text are left untouched.
func isCodexGoalContext(content string) bool {
trimmed := strings.TrimSpace(content)
if strings.HasPrefix(trimmed, "<goal_context>") {
return true
}
if strings.HasPrefix(trimmed, "<codex_internal_context") {
openTag, _, ok := strings.Cut(trimmed, ">")
return ok && codexGoalContextSourceAttrRe.MatchString(openTag)
}
return false
}
func isCodexTurnAbortedMessage(content string) bool {
return strings.HasPrefix(
strings.TrimSpace(content),
"<turn_aborted>",
)
}
func isCodexSubagentNotification(content string) bool {
return strings.HasPrefix(
strings.TrimSpace(content),
"<subagent_notification>",
)
}
func codexIncrementalNeedsFullParse(line string) bool {
switch gjson.Get(line, "type").Str {
case codexTypeEventMsg:
return gjson.Get(line, "payload.type").Str ==
"collab_agent_spawn_end"
case codexTypeResponseItem:
default:
return false
}
payload := gjson.Get(line, "payload")
switch payload.Get("type").Str {
case "function_call", "custom_tool_call":
return isCodexWaitAgentCall(payload.Get("name").Str)
case "function_call_output", "custom_tool_call_output":
output, raw := parseCodexFunctionOutput(payload)
return isCodexSubagentFunctionOutput(output) ||
strings.TrimSpace(raw) != ""
default:
role := payload.Get("role").Str
if role != "user" {
return false
}
agentID, _, text := parseCodexSubagentNotification(
extractCodexContent(payload),
)
return agentID != "" && text != ""
}
}