diff --git a/cmd/entire/cli/agent/opencode/transcript.go b/cmd/entire/cli/agent/opencode/transcript.go index fabe2bd1d..b7b5f43f2 100644 --- a/cmd/entire/cli/agent/opencode/transcript.go +++ b/cmd/entire/cli/agent/opencode/transcript.go @@ -209,8 +209,48 @@ func ExtractTextFromParts(parts []Part) string { return strings.Join(texts, "\n") } +// Tags used by oh-my-opencode and similar orchestration tools to inject +// context into the conversation with role "user" — not actual user prompts. +const ( + systemReminderOpen = "" + systemReminderClose = "" +) + +// isSystemReminderOnly reports whether content consists entirely of +// ... blocks (after trimming whitespace). +// Delegates to stripSystemReminders to correctly handle multiple blocks +// (HasPrefix+HasSuffix would false-positive on "arealb"). +func isSystemReminderOnly(content string) bool { + if strings.TrimSpace(content) == "" { + return false + } + return stripSystemReminders(content) == "" +} + +// stripSystemReminders removes all ... +// sections from content and returns the remaining text. If nothing remains +// after stripping (or the content was system-reminder-only), it returns "". +func stripSystemReminders(content string) string { + result := content + for { + start := strings.Index(result, systemReminderOpen) + if start == -1 { + break + } + end := strings.Index(result[start:], systemReminderClose) + if end == -1 { + break + } + end += start + len(systemReminderClose) + result = result[:start] + result[end:] + } + return strings.TrimSpace(result) +} + // ExtractAllUserPrompts extracts all user prompts from raw export JSON transcript bytes. // This is a package-level function used by the condensation path. +// Messages that consist entirely of tags (e.g. from oh-my-opencode) +// are excluded. Mixed messages have their system-reminder sections stripped. func ExtractAllUserPrompts(data []byte) ([]string, error) { session, err := ParseExportSession(data) if err != nil { @@ -226,6 +266,13 @@ func ExtractAllUserPrompts(data []byte) ([]string, error) { continue } content := ExtractTextFromParts(msg.Parts) + if content == "" { + continue + } + if isSystemReminderOnly(content) { + continue + } + content = stripSystemReminders(content) if content != "" { prompts = append(prompts, content) } diff --git a/cmd/entire/cli/agent/opencode/transcript_test.go b/cmd/entire/cli/agent/opencode/transcript_test.go index 5bcd6e57e..1395226b2 100644 --- a/cmd/entire/cli/agent/opencode/transcript_test.go +++ b/cmd/entire/cli/agent/opencode/transcript_test.go @@ -668,6 +668,226 @@ func TestExtractModifiedFiles(t *testing.T) { } } +func TestExtractAllUserPrompts(t *testing.T) { + t.Parallel() + + prompts, err := ExtractAllUserPrompts([]byte(testExportJSON)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(prompts) != 2 { + t.Fatalf("expected 2 prompts, got %d: %v", len(prompts), prompts) + } + if prompts[0] != "Fix the bug in main.go" { + t.Errorf("expected first prompt 'Fix the bug in main.go', got %q", prompts[0]) + } + if prompts[1] != "Also fix util.go" { + t.Errorf("expected second prompt 'Also fix util.go', got %q", prompts[1]) + } +} + +func TestExtractAllUserPrompts_SystemReminderOnly(t *testing.T) { + t.Parallel() + + session := ExportSession{ + Info: SessionInfo{ID: "test-sysreminder"}, + Messages: []ExportMessage{ + { + Info: MessageInfo{ID: "msg-1", Role: "user"}, + Parts: []Part{ + {Type: "text", Text: "Fix the bug"}, + }, + }, + { + Info: MessageInfo{ID: "msg-2", Role: "user"}, + Parts: []Part{ + {Type: "text", Text: "\nAs you answer the user's questions, you can use the following context:\nContents of CLAUDE.md...\n"}, + }, + }, + { + Info: MessageInfo{ID: "msg-3", Role: "user"}, + Parts: []Part{ + {Type: "text", Text: "Now fix util.go"}, + }, + }, + }, + } + data, err := json.Marshal(session) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + prompts, err := ExtractAllUserPrompts(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(prompts) != 2 { + t.Fatalf("expected 2 prompts (system-reminder excluded), got %d: %v", len(prompts), prompts) + } + if prompts[0] != "Fix the bug" { + t.Errorf("expected 'Fix the bug', got %q", prompts[0]) + } + if prompts[1] != "Now fix util.go" { + t.Errorf("expected 'Now fix util.go', got %q", prompts[1]) + } +} + +func TestExtractAllUserPrompts_SystemReminderMixed(t *testing.T) { + t.Parallel() + + session := ExportSession{ + Info: SessionInfo{ID: "test-mixed"}, + Messages: []ExportMessage{ + { + Info: MessageInfo{ID: "msg-1", Role: "user"}, + Parts: []Part{ + {Type: "text", Text: "Fix the bug\n\nCLAUDE.md contents here\n"}, + }, + }, + }, + } + data, err := json.Marshal(session) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + prompts, err := ExtractAllUserPrompts(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(prompts) != 1 { + t.Fatalf("expected 1 prompt, got %d: %v", len(prompts), prompts) + } + if prompts[0] != "Fix the bug" { + t.Errorf("expected 'Fix the bug', got %q", prompts[0]) + } +} + +func TestExtractAllUserPrompts_SystemReminderWithWhitespace(t *testing.T) { + t.Parallel() + + session := ExportSession{ + Info: SessionInfo{ID: "test-whitespace"}, + Messages: []ExportMessage{ + { + Info: MessageInfo{ID: "msg-1", Role: "user"}, + Parts: []Part{ + {Type: "text", Text: " \n\nsome context\n\n "}, + }, + }, + }, + } + data, err := json.Marshal(session) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + prompts, err := ExtractAllUserPrompts(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(prompts) != 0 { + t.Fatalf("expected 0 prompts (whitespace + system-reminder), got %d: %v", len(prompts), prompts) + } +} + +func TestIsSystemReminderOnly(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + want bool + }{ + { + name: "exact system-reminder", + content: "context here", + want: true, + }, + { + name: "with surrounding whitespace", + content: " \ncontext\n ", + want: true, + }, + { + name: "not system-reminder", + content: "Fix the bug", + want: false, + }, + { + name: "mixed content", + content: "Fix the bug\ncontext", + want: false, + }, + { + name: "empty", + content: "", + want: false, + }, + { + // Multiple blocks with real content between them — starts with open tag + // and ends with close tag, but is NOT system-reminder-only. + name: "real content between multiple blocks", + content: "aFix thisb", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := isSystemReminderOnly(tt.content); got != tt.want { + t.Errorf("isSystemReminderOnly(%q) = %v, want %v", tt.content, got, tt.want) + } + }) + } +} + +func TestStripSystemReminders(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + want string + }{ + { + name: "no system-reminder", + content: "Fix the bug", + want: "Fix the bug", + }, + { + name: "only system-reminder", + content: "context", + want: "", + }, + { + name: "mixed content", + content: "Fix the bug\ncontext", + want: "Fix the bug", + }, + { + name: "system-reminder in middle", + content: "First part\ncontext\nSecond part", + want: "First part\n\nSecond part", + }, + { + name: "multiple system-reminders", + content: "aFix thisb", + want: "Fix this", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := stripSystemReminders(tt.content); got != tt.want { + t.Errorf("stripSystemReminders(%q) = %q, want %q", tt.content, got, tt.want) + } + }) + } +} + // Compile-time interface checks are in transcript.go. // Verify the unused import guard by referencing the agent package. var _ = agent.AgentNameOpenCode