Skip to content

Commit 63dc056

Browse files
committed
fix: reparse late codex wait bindings
1 parent 63bae3b commit 63dc056

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

internal/parser/codex.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ func (b *codexSessionBuilder) handleSubagentNotification(
278278
b.agentResults[agentID] = true
279279
return true
280280
}
281+
if _, ok := b.pendingAgentResults[agentID]; ok {
282+
return true
283+
}
281284
b.pendingAgentResults[agentID] = codexPendingResult{
282285
text: text,
283286
timestamp: ts,
@@ -1057,6 +1060,8 @@ func codexIncrementalNeedsFullParse(line string) bool {
10571060

10581061
payload := gjson.Get(line, "payload")
10591062
switch payload.Get("type").Str {
1063+
case "function_call":
1064+
return payload.Get("name").Str == "wait"
10601065
case "function_call_output":
10611066
output, _ := parseCodexFunctionOutput(payload)
10621067
if !output.Exists() {

internal/parser/codex_parser_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,35 @@ func TestParseCodexSession_FunctionCalls(t *testing.T) {
238238
assert.Equal(t, "continuing", msgs[3].Content)
239239
})
240240

241+
t.Run("duplicate pending notification preserves earliest chronology", func(t *testing.T) {
242+
childID := "019c9c96-6ee7-77c0-ba4c-380f844289d5"
243+
summary := "Exit code: `1`\n\nFull output:\n```text\nTraceback...\n```"
244+
notification := "<subagent_notification>\n" +
245+
"{\"agent_id\":\"" + childID + "\",\"status\":{\"completed\":\"Exit code: `1`\\n\\nFull output:\\n```text\\nTraceback...\\n```\"}}\n" +
246+
"</subagent_notification>"
247+
content := testjsonl.JoinJSONL(
248+
testjsonl.CodexSessionMetaJSON("fc-subagent-notify-dupe-order", "/tmp", "user", tsEarly),
249+
testjsonl.CodexMsgJSON("user", "run a child agent", tsEarlyS1),
250+
testjsonl.CodexFunctionCallWithCallIDJSON("spawn_agent", "call_spawn", map[string]any{
251+
"agent_type": "awaiter",
252+
"message": "Run the compile smoke test",
253+
}, tsEarlyS5),
254+
testjsonl.CodexFunctionCallOutputJSON("call_spawn", `{"agent_id":"`+childID+`","nickname":"Fennel"}`, tsLate),
255+
testjsonl.CodexMsgJSON("user", notification, tsLateS5),
256+
testjsonl.CodexMsgJSON("assistant", "continuing", "2024-01-01T10:01:06Z"),
257+
testjsonl.CodexMsgJSON("user", notification, "2024-01-01T10:01:07Z"),
258+
)
259+
sess, msgs := runCodexParserTest(t, "test.jsonl", content, false)
260+
261+
require.NotNil(t, sess)
262+
assert.Equal(t, 4, len(msgs))
263+
require.Len(t, msgs[2].ToolResults, 1)
264+
assert.Equal(t, "call_spawn", msgs[2].ToolResults[0].ToolUseID)
265+
assert.Equal(t, summary, DecodeContent(msgs[2].ToolResults[0].ContentRaw))
266+
assert.Equal(t, RoleAssistant, msgs[3].Role)
267+
assert.Equal(t, "continuing", msgs[3].Content)
268+
})
269+
241270
t.Run("running subagent notification does not suppress later completion", func(t *testing.T) {
242271
childID := "019c9c96-6ee7-77c0-ba4c-380f844289d5"
243272
running := "<subagent_notification>\n" +
@@ -824,6 +853,44 @@ func TestParseCodexSessionFrom_SubagentOutputRequiresFullParse(t *testing.T) {
824853
assert.Contains(t, err.Error(), "full parse")
825854
}
826855

856+
func TestParseCodexSessionFrom_WaitCallRequiresFullParse(t *testing.T) {
857+
t.Parallel()
858+
859+
childID := "019c9c96-6ee7-77c0-ba4c-380f844289d5"
860+
notification := "<subagent_notification>\n" +
861+
"{\"agent_id\":\"" + childID + "\",\"status\":{\"completed\":\"Finished successfully\"}}\n" +
862+
"</subagent_notification>"
863+
initial := testjsonl.JoinJSONL(
864+
testjsonl.CodexSessionMetaJSON("inc-wait", "/tmp", "codex_cli_rs", tsEarly),
865+
testjsonl.CodexMsgJSON("user", "run child", tsEarlyS1),
866+
testjsonl.CodexFunctionCallWithCallIDJSON("spawn_agent", "call_spawn", map[string]any{
867+
"agent_type": "awaiter",
868+
"message": "run it",
869+
}, tsEarlyS5),
870+
testjsonl.CodexFunctionCallOutputJSON("call_spawn", `{"agent_id":"`+childID+`","nickname":"Fennel"}`, tsLate),
871+
testjsonl.CodexMsgJSON("user", notification, tsLateS5),
872+
)
873+
path := createTestFile(t, "codex-wait-inc.jsonl", initial)
874+
875+
info, err := os.Stat(path)
876+
require.NoError(t, err)
877+
offset := info.Size()
878+
879+
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
880+
require.NoError(t, err)
881+
_, err = f.WriteString(testjsonl.JoinJSONL(
882+
testjsonl.CodexFunctionCallWithCallIDJSON("wait", "call_wait", map[string]any{
883+
"ids": []string{childID},
884+
}, "2024-01-01T10:01:06Z"),
885+
))
886+
require.NoError(t, err)
887+
require.NoError(t, f.Close())
888+
889+
_, _, _, err = ParseCodexSessionFrom(path, offset, 4, false)
890+
require.Error(t, err)
891+
assert.Contains(t, err.Error(), "full parse")
892+
}
893+
827894
func TestParseCodexSessionFrom_SystemMessageDoesNotRequireFullParse(t *testing.T) {
828895
t.Parallel()
829896

0 commit comments

Comments
 (0)