fix(herdr): rewrite CodexActivity against the real live rollout shape (S11)
The previous function_call/function_call_output shape was never verified and doesn't exist in any real Codex rollout. Confirmed the real shape against this machine's own ~/.codex/sessions files: file edits arrive as event_msg/patch_apply_end (changes+success, no pairing needed), and shell commands arrive as a freeform custom_tool_call named "exec" whose input is a JS snippet embedding cmd:"..." rather than a flat arguments object, with failure signaled by a literal "Script error:" prefix in the output text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
+66
-21
@@ -127,17 +127,28 @@ func ClaudeActivity(path string) ([]ToolCall, error) {
|
||||
return calls, s.Err()
|
||||
}
|
||||
|
||||
// CodexActivity is best-effort and unverified against a live Codex rollout —
|
||||
// unlike ClaudeActivity, which reads the same transcript shape ClaudeUsage
|
||||
// already confirmed. Codex's Responses-API-style rollout is expected to log
|
||||
// a "function_call" payload (name, arguments, call_id) followed later by a
|
||||
// "function_call_output" payload (call_id, output), mirroring the
|
||||
// "payload.type" wrapper CodexUsage already reads for "token_count". Success
|
||||
// is inferred heuristically from the output text (no confirmed structured
|
||||
// exit-code field), which is coarser than Claude's explicit is_error flag.
|
||||
// Treat any thrash/milestone signal derived from this as advisory until
|
||||
// checked against a real rollout, same caveat this file's other Codex-facing
|
||||
// code already carries (AUDIT.md Phase 0's own bar: verify before trusting).
|
||||
// CodexActivity parses the real Codex rollout shape, confirmed live against
|
||||
// this machine's own ~/.codex/sessions on 2026-07-28 — the previous version
|
||||
// of this function assumed an unverified "function_call"/"function_call_output"
|
||||
// payload shape (flat name/arguments/call_id, mirroring CodexUsage's
|
||||
// token_count wrapper) that turned out not to exist in any real rollout file.
|
||||
// The real shape has two independent signals:
|
||||
//
|
||||
// 1. File edits: an `event_msg` with `payload.type == "patch_apply_end"`
|
||||
// carries `changes` (map of absolute path -> diff) and a top-level
|
||||
// `success` bool directly — no pairing needed, and a much more reliable
|
||||
// "same file edited M times" source than trying to parse it out of a
|
||||
// tool call's input.
|
||||
// 2. Shell commands: Codex's actual tool surface is a single freeform
|
||||
// `custom_tool_call` named "exec" whose `input` is a JS snippet calling
|
||||
// `tools.exec_command({cmd:"...", ...})` — not a flat arguments object.
|
||||
// codexExecCommand best-effort-extracts the first embedded cmd string.
|
||||
// Success is read from the paired `custom_tool_call_output`'s text
|
||||
// blocks: a failed script's output observably starts with "Script
|
||||
// error:" on this machine's real transcripts (both a JS syntax error and
|
||||
// an apply_patch verification failure took this form) — there is no
|
||||
// structured exit-code field, so this remains a text heuristic, just a
|
||||
// confirmed one rather than a guessed one.
|
||||
func CodexActivity(path string) ([]ToolCall, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -145,14 +156,20 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
type outputBlock struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type payload struct {
|
||||
Type string `json:"type"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Arguments json.RawMessage `json:"arguments"`
|
||||
Output string `json:"output"`
|
||||
Type string `json:"type"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Input string `json:"input"`
|
||||
Output []outputBlock `json:"output"`
|
||||
Success bool `json:"success"`
|
||||
Changes map[string]json.RawMessage `json:"changes"`
|
||||
}
|
||||
type entry struct {
|
||||
Type string `json:"type"`
|
||||
Payload payload `json:"payload"`
|
||||
}
|
||||
|
||||
@@ -165,13 +182,27 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
if json.Unmarshal(s.Bytes(), &e) != nil {
|
||||
continue
|
||||
}
|
||||
switch e.Payload.Type {
|
||||
case "function_call":
|
||||
kind, key := toolCallMeta(e.Payload.Arguments)
|
||||
switch {
|
||||
case e.Type == "event_msg" && e.Payload.Type == "patch_apply_end":
|
||||
for path := range e.Payload.Changes {
|
||||
calls = append(calls, ToolCall{Name: "apply_patch", Kind: "file", Key: path, Success: e.Payload.Success})
|
||||
}
|
||||
case e.Type == "response_item" && e.Payload.Type == "custom_tool_call":
|
||||
kind, key := "", ""
|
||||
if cmd := codexExecCommand(e.Payload.Input); cmd != "" {
|
||||
kind, key = "command", cmd
|
||||
}
|
||||
pending[e.Payload.CallID] = ToolCall{Name: e.Payload.Name, Kind: kind, Key: key, IsTest: isTestCommand(kind, key)}
|
||||
case "function_call_output":
|
||||
case e.Type == "response_item" && e.Payload.Type == "custom_tool_call_output":
|
||||
if tc, ok := pending[e.Payload.CallID]; ok {
|
||||
tc.Success = !strings.Contains(strings.ToLower(e.Payload.Output), "error")
|
||||
failed := false
|
||||
for _, o := range e.Payload.Output {
|
||||
if strings.HasPrefix(strings.TrimSpace(o.Text), "Script error:") {
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
tc.Success = !failed
|
||||
calls = append(calls, tc)
|
||||
delete(pending, e.Payload.CallID)
|
||||
}
|
||||
@@ -180,6 +211,20 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
return calls, s.Err()
|
||||
}
|
||||
|
||||
// codexExecCmdRe extracts the first `cmd:"..."` argument out of an "exec"
|
||||
// custom-tool-call's JS-scripted input. Only the first embedded command in a
|
||||
// multi-call script is captured — a documented limitation, not an oversight.
|
||||
var codexExecCmdRe = regexp.MustCompile(`cmd\s*:\s*"((?:[^"\\]|\\.)*)"`)
|
||||
|
||||
func codexExecCommand(input string) string {
|
||||
m := codexExecCmdRe.FindStringSubmatch(input)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
cmd := strings.NewReplacer(`\"`, `"`, `\n`, "\n", `\t`, "\t", `\\`, `\`).Replace(m[1])
|
||||
return strings.TrimSpace(cmd)
|
||||
}
|
||||
|
||||
// OpenCodeActivity has no verified source. OpenCodeUsage already only reads
|
||||
// aggregate token counts per message file (~/.local/share/opencode/storage/
|
||||
// message/), not per-tool-call records, and nobody has confirmed opencode's
|
||||
|
||||
Reference in New Issue
Block a user