1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-07 00:33:04 +00:00

Fix duplicate content in git notes by filtering since last commit

- Add ExtractContextSince method to filter conversation by timestamp
- Get previous commit time and only include activity since then
- Change labels to 'User prompts since last commit' and 'Tool interactions since last commit'
- Each commit now only captures its own unique conversation context
- Prevents accumulation of duplicate content across commits in same session
This commit is contained in:
Jason Hall 2025-07-21 13:38:30 -04:00
parent 6b338a76be
commit a6e6602bf8
Failed to extract signature
2 changed files with 55 additions and 16 deletions

View file

@ -7,6 +7,7 @@ import (
"io"
"log/slog"
"os"
"os/exec"
"strings"
"time"
@ -141,9 +142,12 @@ func processGitCommit(ctx context.Context, input HookInput, bashInput BashToolIn
return nil
}
// Extract conversation context
// Get the timestamp of the previous commit in this session
previousCommitTime := getLastCommitTimeForSession(ctx, notesManager, input.SessionID)
// Extract conversation context since the last commit
contextExtractor := conv.NewContextExtractor()
conversationContext, err := contextExtractor.ExtractRecentContext(input.TranscriptPath, input.SessionID)
conversationContext, err := contextExtractor.ExtractContextSince(input.TranscriptPath, input.SessionID, previousCommitTime)
if err != nil {
return fmt.Errorf("failed to extract conversation context: %w", err)
}
@ -238,6 +242,27 @@ func contains(slice []string, item string) bool {
return false
}
// getLastCommitTimeForSession finds the most recent commit time for this session
func getLastCommitTimeForSession(ctx context.Context, notesManager *notes.NotesManager, sessionID string) time.Time {
// For now, let's use a simpler approach - get the time of the previous commit
// This works well when commits are made sequentially in a session
cmd := exec.Command("git", "log", "-1", "--format=%cI", "HEAD~1")
output, err := cmd.Output()
if err != nil {
// No previous commit or error, return zero time
return time.Time{}
}
timeStr := strings.TrimSpace(string(output))
commitTime, err := time.Parse(time.RFC3339, timeStr)
if err != nil {
return time.Time{}
}
// Add a small buffer to ensure we don't miss events that happened right at commit time
return commitTime.Add(-5 * time.Second)
}
// readStdinWithTimeout reads from stdin with a timeout
func readStdinWithTimeout(timeout time.Duration) ([]byte, error) {
type result struct {

View file

@ -7,6 +7,7 @@ import (
"os"
"regexp"
"strings"
"time"
)
// ConversationContext represents relevant conversation context for a commit
@ -48,6 +49,11 @@ func NewContextExtractor() *ContextExtractor {
// ExtractRecentContext extracts recent conversation context from a transcript file
func (ce *ContextExtractor) ExtractRecentContext(transcriptPath string, sessionID string) (*ConversationContext, error) {
return ce.ExtractContextSince(transcriptPath, sessionID, time.Time{})
}
// ExtractContextSince extracts conversation context since a given timestamp
func (ce *ContextExtractor) ExtractContextSince(transcriptPath string, sessionID string, since time.Time) (*ConversationContext, error) {
if transcriptPath == "" {
return &ConversationContext{}, nil
}
@ -65,7 +71,7 @@ func (ce *ContextExtractor) ExtractRecentContext(transcriptPath string, sessionI
}
// Parse the transcript content
context := ce.parseTranscriptContent(string(content), sessionID)
context := ce.parseTranscriptContent(string(content), sessionID, since)
// Apply privacy filters
context = ce.filterSensitiveContent(context)
@ -74,7 +80,7 @@ func (ce *ContextExtractor) ExtractRecentContext(transcriptPath string, sessionI
}
// parseTranscriptContent parses transcript content and extracts conversation elements
func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string) *ConversationContext {
func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string, since time.Time) *ConversationContext {
context := &ConversationContext{
UserPrompts: []string{},
ClaudeResponses: []string{},
@ -98,11 +104,20 @@ func (ce *ContextExtractor) parseTranscriptContent(content, sessionID string) *C
// Only process entries for the current session
entrySessionID, _ := entry["sessionId"].(string)
// Debug: log session matching
if entrySessionID != "" && entrySessionID != sessionID {
continue
}
// Filter by timestamp if provided
if !since.IsZero() {
if timestampStr, ok := entry["timestamp"].(string); ok {
entryTime, err := time.Parse(time.RFC3339, timestampStr)
if err == nil && entryTime.Before(since) {
continue // Skip entries before the cutoff
}
}
}
// Extract based on type
entryType, _ := entry["type"].(string)
@ -211,15 +226,10 @@ func (ce *ContextExtractor) sanitizeText(text string) string {
func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string {
var parts []string
// Include recent user prompts (last 3 to capture more context)
// Include all user prompts (they're already filtered by time)
if len(context.UserPrompts) > 0 {
start := len(context.UserPrompts) - 3
if start < 0 {
start = 0
}
parts = append(parts, "Recent user prompts:")
for i := start; i < len(context.UserPrompts); i++ {
prompt := context.UserPrompts[i]
parts = append(parts, "User prompts since last commit:")
for _, prompt := range context.UserPrompts {
if len(prompt) > 300 {
prompt = prompt[:297] + "..."
}
@ -227,11 +237,15 @@ func (ce *ContextExtractor) CreateExcerpt(context *ConversationContext) string {
}
}
// Include recent tool interactions
// Include all tool interactions (they're already filtered by time)
if len(context.ToolInteractions) > 0 {
parts = append(parts, "\nTool interactions:")
parts = append(parts, "\nTool interactions since last commit:")
for _, interaction := range context.ToolInteractions {
parts = append(parts, fmt.Sprintf("- %s: %s", interaction.Tool, interaction.Input))
input := interaction.Input
if len(input) > 100 {
input = input[:97] + "..."
}
parts = append(parts, fmt.Sprintf("- %s: %s", interaction.Tool, input))
}
}