mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-10-26 01:50:40 +08:00
fix(codex): surface corrupt history at user entry points
This commit is contained in:
1 parent
48c4243cc3
commit
dac6160362
5 files changed
+129
-28
No files matched your search
+29
-24
@@ -140,7 +140,7 @@ func parseCodexSessionFile(path, filterCwd string) *core.AgentSessionInfo {
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -149,7 +149,8 @@ func parseCodexSessionFile(path, filterCwd string) *core.AgentSessionInfo {
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
||||
continue
|
||||
slog.Warn("codex: decode session file", "path", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch entry.Type {
|
||||
@@ -162,11 +163,13 @@ func parseCodexSessionFile(path, filterCwd string) *core.AgentSessionInfo {
|
||||
Cwd string `json:"cwd"`
|
||||
Source json.RawMessage `json:"source"`
|
||||
}
|
||||
if json.Unmarshal(entry.Payload, &meta) == nil {
|
||||
sessionID = meta.ID
|
||||
sessionCwd = meta.Cwd
|
||||
sessionSource = meta.Source
|
||||
if err := json.Unmarshal(entry.Payload, &meta); err != nil {
|
||||
slog.Warn("codex: decode session metadata", "path", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
sessionID = meta.ID
|
||||
sessionCwd = meta.Cwd
|
||||
sessionSource = meta.Source
|
||||
|
||||
case "response_item":
|
||||
var item struct {
|
||||
@@ -176,21 +179,23 @@ func parseCodexSessionFile(path, filterCwd string) *core.AgentSessionInfo {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if json.Unmarshal(entry.Payload, &item) == nil {
|
||||
if item.Role == "user" {
|
||||
userMsgSeen++
|
||||
msgCount++
|
||||
// The actual user prompt is the last user response_item
|
||||
// (earlier ones are system/AGENTS.md instructions).
|
||||
// Pick the last content block that looks like a real prompt.
|
||||
for _, c := range item.Content {
|
||||
if c.Type == "input_text" && c.Text != "" && isUserPrompt(c.Text) {
|
||||
summary = c.Text
|
||||
}
|
||||
if err := json.Unmarshal(entry.Payload, &item); err != nil {
|
||||
slog.Warn("codex: decode session response item", "path", path, "error", err)
|
||||
return nil
|
||||
}
|
||||
if item.Role == "user" {
|
||||
userMsgSeen++
|
||||
msgCount++
|
||||
// The actual user prompt is the last user response_item
|
||||
// (earlier ones are system/AGENTS.md instructions).
|
||||
// Pick the last content block that looks like a real prompt.
|
||||
for _, c := range item.Content {
|
||||
if c.Type == "input_text" && c.Text != "" && isUserPrompt(c.Text) {
|
||||
summary = c.Text
|
||||
}
|
||||
} else if item.Role == "assistant" {
|
||||
msgCount++
|
||||
}
|
||||
} else if item.Role == "assistant" {
|
||||
msgCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,7 +294,7 @@ func readCodexSessionHistory(r io.Reader, limit int) ([]core.HistoryEntry, error
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -298,8 +303,8 @@ func readCodexSessionHistory(r io.Reader, limit int) ([]core.HistoryEntry, error
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
if json.Unmarshal([]byte(line), &raw) != nil {
|
||||
continue
|
||||
if err := json.Unmarshal([]byte(line), &raw); err != nil {
|
||||
return nil, fmt.Errorf("decode session record: %w", err)
|
||||
}
|
||||
if raw.Type != "response_item" {
|
||||
continue
|
||||
@@ -314,8 +319,8 @@ func readCodexSessionHistory(r io.Reader, limit int) ([]core.HistoryEntry, error
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if json.Unmarshal(raw.Payload, &item) != nil {
|
||||
continue
|
||||
if err := json.Unmarshal(raw.Payload, &item); err != nil {
|
||||
return nil, fmt.Errorf("decode response item: %w", err)
|
||||
}
|
||||
|
||||
ts := parseCodexTimestamp(raw.Timestamp)
|
||||
|
||||
@@ -215,6 +215,51 @@ func TestReadCodexSessionHistory_ReportsScanError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCodexSessionHistory_RejectsCorruptJSONAfterValidPrefix(t *testing.T) {
|
||||
valid := `{"type":"response_item","payload":{"role":"user","content":[{"type":"input_text","text":"hello"}]}}`
|
||||
for _, bad := range []string{
|
||||
`{"type":"response_item",`,
|
||||
`{"type":"response_item","payload":{"role":`,
|
||||
} {
|
||||
entries, err := readCodexSessionHistory(strings.NewReader(valid+"\n"+bad+"\n"), 10)
|
||||
if err == nil || entries != nil {
|
||||
t.Fatalf("corrupt record %q: entries = %+v, error = %v; want nil entries and error", bad, entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := readCodexSessionHistory(strings.NewReader(valid+"\n\n \n"+`{"type":"unknown_event","payload":{}}`+"\n"), 10)
|
||||
if err != nil || len(entries) != 1 {
|
||||
t.Fatalf("blank and unknown records: entries = %+v, error = %v; want one entry", entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentListSessions_BadFileDoesNotHideHealthySession(t *testing.T) {
|
||||
workDir := t.TempDir()
|
||||
codexHome := t.TempDir()
|
||||
dir := filepath.Join(codexHome, "sessions")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cwd, err := json.Marshal(workDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
healthy := `{"type":"session_meta","payload":{"id":"healthy","cwd":` + string(cwd) + `}}` + "\n" +
|
||||
`{"type":"response_item","payload":{"role":"user","content":[{"type":"input_text","text":"hello"}]}}` + "\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "rollout-healthy.jsonl"), []byte(healthy), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bad := `{"type":"session_meta","payload":{"id":"bad","cwd":` + string(cwd) + `}}` + "\n" + `{"type":"response_item","payload":`
|
||||
if err := os.WriteFile(filepath.Join(dir, "rollout-bad.jsonl"), []byte(bad), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := &Agent{workDir: workDir, codexHome: codexHome}
|
||||
sessions, err := a.ListSessions(context.Background())
|
||||
if err != nil || len(sessions) != 1 || sessions[0].ID != "healthy" {
|
||||
t.Fatalf("ListSessions() = %+v, %v; want only healthy session", sessions, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSessionHistory_ConvertsTimestampToLocal(t *testing.T) {
|
||||
codexHome := t.TempDir()
|
||||
sessionID := "session-local-time"
|
||||
|
||||
+11
-4
@@ -9641,9 +9641,13 @@ func (e *Engine) cmdHistory(p Platform, msg *Message, args []string) {
|
||||
agentSID := s.GetAgentSessionID()
|
||||
if len(entries) == 0 && agentSID != "" {
|
||||
if hp, ok := agent.(HistoryProvider); ok {
|
||||
if agentEntries, err := hp.GetSessionHistory(e.ctx, agentSID, n); err == nil {
|
||||
entries = agentEntries
|
||||
agentEntries, err := hp.GetSessionHistory(e.ctx, agentSID, n)
|
||||
if err != nil {
|
||||
slog.Warn("history: agent transcript read failed", "session_id", agentSID, "error", err)
|
||||
e.reply(p, msg.ReplyCtx, e.i18n.T(MsgHistoryReadFailed))
|
||||
return
|
||||
}
|
||||
entries = agentEntries
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13810,9 +13814,12 @@ func (e *Engine) renderHistoryCard(sessionKey string) *Card {
|
||||
agentSID := s.GetAgentSessionID()
|
||||
if len(entries) == 0 && agentSID != "" {
|
||||
if hp, ok := agent.(HistoryProvider); ok {
|
||||
if agentEntries, err := hp.GetSessionHistory(e.ctx, agentSID, 10); err == nil {
|
||||
entries = agentEntries
|
||||
agentEntries, err := hp.GetSessionHistory(e.ctx, agentSID, 10)
|
||||
if err != nil {
|
||||
slog.Warn("history: agent transcript read failed", "session_id", agentSID, "error", err)
|
||||
return e.simpleCard(e.i18n.T(MsgCardTitleHistory), "turquoise", e.i18n.T(MsgHistoryReadFailed))
|
||||
}
|
||||
entries = agentEntries
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,15 @@ import (
|
||||
|
||||
type stubAgent struct{}
|
||||
|
||||
type failingHistoryAgent struct {
|
||||
stubAgent
|
||||
err error
|
||||
}
|
||||
|
||||
func (a *failingHistoryAgent) GetSessionHistory(context.Context, string, int) ([]HistoryEntry, error) {
|
||||
return []HistoryEntry{{Role: "user", Content: "partial history"}}, a.err
|
||||
}
|
||||
|
||||
func (a *stubAgent) Name() string { return "stub" }
|
||||
func (a *stubAgent) StartSession(_ context.Context, _ string) (AgentSession, error) {
|
||||
return &stubAgentSession{}, nil
|
||||
@@ -3830,6 +3839,33 @@ func TestCmdCurrent_UsesLegacyTextOnPlatformWithoutCardSupport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdHistory_ReadFailureIsVisibleWithoutLeakingPath(t *testing.T) {
|
||||
const secretPath = "/private/codex-home/sessions/secret.jsonl"
|
||||
agent := &failingHistoryAgent{err: fmt.Errorf("scan %s: token too long", secretPath)}
|
||||
msg := &Message{SessionKey: "test:history-failure", ReplyCtx: "ctx"}
|
||||
|
||||
plain := &stubPlatformEngine{n: "plain"}
|
||||
textEngine := NewEngine("test", agent, []Platform{plain}, "", LangEnglish)
|
||||
textEngine.sessions.GetOrCreateActive(msg.SessionKey).SetAgentSessionID("broken", "test")
|
||||
textEngine.cmdHistory(plain, msg, []string{"10"})
|
||||
sent := plain.getSent()
|
||||
if len(sent) != 1 || sent[0] != textEngine.i18n.T(MsgHistoryReadFailed) || strings.Contains(sent[0], secretPath) {
|
||||
t.Fatalf("text history response = %q; want generic localized failure without path", sent)
|
||||
}
|
||||
|
||||
cardPlatform := &stubCardPlatform{stubPlatformEngine: stubPlatformEngine{n: "cards"}}
|
||||
cardEngine := NewEngine("test", agent, []Platform{cardPlatform}, "", LangEnglish)
|
||||
cardEngine.sessions.GetOrCreateActive(msg.SessionKey).SetAgentSessionID("broken", "test")
|
||||
cardEngine.cmdHistory(cardPlatform, msg, nil)
|
||||
if len(cardPlatform.repliedCards) != 1 {
|
||||
t.Fatalf("card replies = %d, want 1", len(cardPlatform.repliedCards))
|
||||
}
|
||||
cardText := cardPlatform.repliedCards[0].RenderText()
|
||||
if !strings.Contains(cardText, cardEngine.i18n.T(MsgHistoryReadFailed)) || strings.Contains(cardText, secretPath) || strings.Contains(cardText, "partial history") {
|
||||
t.Fatalf("card history response = %q; want generic localized failure without path or partial history", cardText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCurrent_ShowsAgentSummaryWhenNoCustomName(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "plain"}
|
||||
agent := &stubListAgent{sessions: []AgentSessionInfo{
|
||||
|
||||
@@ -255,6 +255,7 @@ const (
|
||||
MsgListSwitchHint MsgKey = "list_switch_hint"
|
||||
MsgListError MsgKey = "list_error"
|
||||
MsgHistoryEmpty MsgKey = "history_empty"
|
||||
MsgHistoryReadFailed MsgKey = "history_read_failed"
|
||||
MsgNameUsage MsgKey = "name_usage"
|
||||
MsgNameSet MsgKey = "name_set"
|
||||
MsgNameNoSession MsgKey = "name_no_session"
|
||||
@@ -1510,6 +1511,13 @@ var messages = map[MsgKey]map[Language]string{
|
||||
LangJapanese: "現在のセッションに履歴がありません。",
|
||||
LangSpanish: "No hay historial en la sesión actual.",
|
||||
},
|
||||
MsgHistoryReadFailed: {
|
||||
LangEnglish: "Failed to read session history.",
|
||||
LangChinese: "读取会话历史失败。",
|
||||
LangTraditionalChinese: "讀取會話歷史失敗。",
|
||||
LangJapanese: "セッション履歴の読み取りに失敗しました。",
|
||||
LangSpanish: "No se pudo leer el historial de la sesión.",
|
||||
},
|
||||
MsgNameUsage: {
|
||||
LangEnglish: "Usage:\n`/name <text>` — name the current session\n`/name <number> <text>` — name a session by list number",
|
||||
LangChinese: "用法:\n`/name <名称>` — 命名当前会话\n`/name <序号> <名称>` — 按列表序号命名会话",
|
||||
|
||||
Reference in new issue
Block a user