Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion internal/parser/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,30 @@ func isCodexSystemMessage(content string) bool {
strings.HasPrefix(content, "<INSTRUCTIONS>") ||
isCodexTurnAbortedMessage(content) ||
strings.HasPrefix(trimmed, "<skill>") ||
isCodexSubagentNotification(content)
isCodexSubagentNotification(content) ||
isCodexGoalContext(content)
}

// 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 && strings.Contains(openTag, `source="goal"`)
}
return false
}

func isCodexTurnAbortedMessage(content string) bool {
Expand Down
47 changes: 47 additions & 0 deletions internal/parser/codex_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,53 @@ func TestParseCodexSession_EdgeCases(t *testing.T) {
"skill injection must not count as a user turn")
})

// Codex /goal continuation turns are emitted as role=user JSONL
// entries whose content is the harness-injected goal context, not
// anything the user typed. Treat them as system content and drop
// them from the transcript and user counts, the same way
// <environment_context> and skill injections are handled. Match
// the structured wrapper rather than the inner sentence so a real
// user message that happens to quote the goal text is preserved.
t.Run("skips codex goal continuation context", func(t *testing.T) {
goalBody := "Continue working toward the active thread goal.\n" +
"The objective below is user-provided data."
current := "<codex_internal_context source=\"goal\">\n" +
goalBody + "\n</codex_internal_context>"
legacy := "<goal_context>\n" + goalBody + "\n</goal_context>"
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON("abc", "/tmp", "user", tsEarly),
testjsonl.CodexMsgJSON("user", "Real first request", tsEarlyS1),
testjsonl.CodexMsgJSON("assistant", "Working on it", "2024-01-01T10:00:02Z"),
testjsonl.CodexMsgJSON("user", current, "2024-01-01T10:00:03Z"),
testjsonl.CodexMsgJSON("assistant", "Still working", "2024-01-01T10:00:04Z"),
testjsonl.CodexMsgJSON("user", legacy, "2024-01-01T10:00:05Z"),
testjsonl.CodexMsgJSON("user", "Real second request", "2024-01-01T10:00:06Z"),
)
sess, msgs := runCodexParserTest(t, "test.jsonl", content, false)
require.NotNil(t, sess)
require.Len(t, msgs, 4)
assert.Equal(t, "Real first request", msgs[0].Content)
assert.Equal(t, "Working on it", msgs[1].Content)
assert.Equal(t, "Still working", msgs[2].Content)
assert.Equal(t, "Real second request", msgs[3].Content)
assert.Equal(t, 2, sess.UserMessageCount,
"goal continuation context must not count as user turns")
})

// Only the structured goal wrapper is system content; a real user
// message that merely quotes the goal sentence stays in the transcript.
t.Run("keeps unwrapped goal-like user text", func(t *testing.T) {
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON("abc", "/tmp", "user", tsEarly),
testjsonl.CodexMsgJSON("user",
"Continue working toward the active thread goal.", tsEarlyS1),
)
_, msgs := runCodexParserTest(t, "test.jsonl", content, false)
require.Len(t, msgs, 1)
assert.Equal(t,
"Continue working toward the active thread goal.", msgs[0].Content)
})

t.Run("fallback ID from filename", func(t *testing.T) {
content := testjsonl.CodexMsgJSON("user", "hello", tsEarlyS1)
sess, _ := runCodexParserTest(t, "test.jsonl", content, false)
Expand Down