fix(cli): keep chats readable with O(n) trace decoding
Skip broken display details so saved chats still open. Trace decoding benchmarks take 48-61% less time and use 68 allocations instead of 73.
264 unmodified linesExpand all265266267268269269270271272273274275276277278161 unmodified linesExpand all440441442443443444445446447444445446447448449450262 unmodified linesExpand all264 unmodified linesExpand all Phase: phase, }, nil}
// wireChatEvent keeps arbitrary tool arguments separate from protocol fields.// wireChatEvent decodes tool arguments and optional display traces separately.type wireChatEvent struct { CallID string `json:"callId"` Args json.RawMessage `json:"args"` Elapsed json.RawMessage `json:"elapsedMs"` Trace json.RawMessage `json:"trace"` StreamEvent}
func decodeChatEvent(data json.RawMessage) (*wireChatEvent, error) {161 unmodified linesExpand all if event.Image == nil { return nil, errors.New("invalid image metadata") } } if event.ToolTrace != nil { event.ToolTrace = ParseToolTrace(event.ToolTrace) if event.ToolTrace == nil { return nil, errors.New("invalid tool trace") } if len(event.Trace) != 0 && string(event.Trace) != "null" { // Display-only evidence must not prevent loading the saved conversation. event.ToolTrace = ParseToolTrace(event.Trace) } return &event, nil}
262 unmodified linesExpand all34 unmodified lines353637383940394041414243444546474849185 unmodified linesExpand all34 unmodified lines if raw == nil { return nil }
data, ok := raw.(json.RawMessage) if !ok { data, err := json.Marshal(raw) if err != nil { return nil encoded, err := json.Marshal(raw) if err != nil { return nil } data = encoded }
var trace ToolTrace if err := json.Unmarshal(data, &trace); err != nil {185 unmodified linesExpand all9 unmodified lines101112131415161718341 unmodified linesExpand all3603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164179 unmodified lines "net" "net/http" "net/http/httptest" "os" "strings" "sync/atomic" "testing")
341 unmodified linesExpand all } }) }}
func TestStreamLoadsHistoryDespiteInvalidDisplayTraces(t *testing.T) { cases := []struct { name string trace string wantTrace bool }{ {"missing", "", false}, {"null", "null", false}, {"wrong type", `"unusable"`, false}, {"wrong nested type", `{"activities":false}`, false}, {"unknown activity", `{"activities":[{"kind":"unknown","target":"file"}]}`, false}, {"oversized target", `{"activities":[{"kind":"read","target":"` + strings.Repeat("界", 1001) + `"}]}`, false}, {"oversized diff", `{"changes":[{"kind":"diff","path":"file","diff":"` + strings.Repeat("🙂", 16001) + `"}]}`, false}, {"negative line count", `{"changes":[{"kind":"diff","path":"file","added":-1}]}`, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { traceField := "" if tc.trace != "" { traceField = `,"trace":` + tc.trace } batch := `{"generation":"generation-a","cursor":12,"events":[{"type":"reset"},{"type":"tool","callId":"call","name":"python","args":"{}","result":"saved output"` + traceField + `},{"type":"text","text":"after tool"}]}` var requests atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") if requests.Add(1) == 1 { _, _ = fmt.Fprintf(w, "data: %s\n\n", batch) } else if got := r.URL.Query().Get("after_seq"); got != "12" { t.Errorf("history batch lost cursor: %s", got) } })) defer server.Close() client := NewChatClient(NewConnection(ConnectionSnapshot{Port: server.Listener.Addr().(*net.TCPAddr).Port}, ""), "session") var events []StreamEvent consumed := 0 for range 2 { if err := client.StreamWithProgress(t.Context(), 0, func(event StreamEvent) error { events = append(events, event) return nil }, func() { consumed++ }); err != nil { t.Fatal(err) } } if consumed != 1 || len(events) != 3 || events[1].Type != EventTool || events[1].ToolResult != "saved output" || events[2].Text != "after tool" { t.Fatalf("history did not load completely: consumed=%d events=%+v", consumed, events) } if got := events[1].ToolTrace; (got != nil) != tc.wantTrace { t.Fatalf("unexpected display trace: %+v", got)
} }) }}