package search import ( "context" "sync" "time" "github.com/anotherhadi/iknowyou/internal/tools" ) type Status string const ( StatusRunning Status = "running" StatusDone Status = "done" StatusCancelled Status = "cancelled" ) type ToolStatus struct { Name string `json:"name"` Skipped bool `json:"skipped"` Reason string `json:"reason,omitempty"` // non-empty only when Skipped is true ResultCount *int `json:"result_count,omitempty"` // nil = pending, 0 = no results } type Search struct { ID string Target string InputType tools.InputType Profile string StartedAt time.Time PlannedTools []ToolStatus cancelFn context.CancelFunc pinned bool // if true, never purged by the cleanup loop mu sync.RWMutex events []tools.Event status Status finishedAt time.Time } func (s *Search) Events() []tools.Event { s.mu.RLock() defer s.mu.RUnlock() out := make([]tools.Event, len(s.events)) copy(out, s.events) return out } func (s *Search) Status() Status { s.mu.RLock() defer s.mu.RUnlock() return s.status } func (s *Search) FinishedAt() time.Time { s.mu.RLock() defer s.mu.RUnlock() return s.finishedAt } func (s *Search) Cancel() { s.mu.Lock() if s.status == StatusRunning { s.status = StatusCancelled s.finishedAt = time.Now() } s.mu.Unlock() s.cancelFn() } func (s *Search) setToolResultCount(toolName string, count int) { s.mu.Lock() defer s.mu.Unlock() for i, t := range s.PlannedTools { if t.Name == toolName { s.PlannedTools[i].ResultCount = &count return } } } func (s *Search) append(e tools.Event) { s.mu.Lock() defer s.mu.Unlock() s.events = append(s.events, e) } func (s *Search) markDone() { s.mu.Lock() defer s.mu.Unlock() if s.status == StatusRunning { s.status = StatusDone s.finishedAt = time.Now() } }