Files
claude-mnemonic/internal/worker/handlers.go
T
2026-03-06 15:39:52 +00:00

286 lines
8.1 KiB
Go

// Package worker provides the main worker service for claude-mnemonic.
// This file contains shared handler utilities and health/status endpoints.
// Domain-specific handlers are split into:
// - handlers_sessions.go: Session lifecycle (init, start, observation, summarize)
// - handlers_context.go: Context/search (search by prompt, file context, inject)
// - handlers_data.go: Data retrieval (observations, summaries, prompts, stats)
// - handlers_update.go: Updates and self-check (update check/apply, self-check)
// - handlers_import_export.go: Import/export/archive operations
package worker
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/rs/zerolog/log"
)
// Handler configuration constants
const (
// DefaultObservationsLimit is the default number of observations to return.
DefaultObservationsLimit = 100
// DefaultSummariesLimit is the default number of summaries to return.
DefaultSummariesLimit = 50
// DefaultPromptsLimit is the default number of prompts to return.
DefaultPromptsLimit = 100
// DefaultSearchLimit is the default number of search results to return.
DefaultSearchLimit = 50
// DefaultContextLimit is the default number of context observations to return.
DefaultContextLimit = 50
)
// ObservationTypes is the canonical list of observation types.
// Used by both Go backend and served to frontend.
var ObservationTypes = []string{
"bugfix",
"feature",
"refactor",
"discovery",
"decision",
"change",
}
// observationTypeSet is a pre-computed map for O(1) type validation.
// Initialized at package load time.
var observationTypeSet = func() map[string]struct{} {
m := make(map[string]struct{}, len(ObservationTypes))
for _, t := range ObservationTypes {
m[t] = struct{}{}
}
return m
}()
// IsValidObservationType returns true if the type is valid (O(1) lookup).
func IsValidObservationType(t string) bool {
_, ok := observationTypeSet[t]
return ok
}
// ConceptTypes is the canonical list of valid concept types.
// Used by both Go backend and served to frontend.
var ConceptTypes = []string{
// Semantic concepts
"how-it-works",
"why-it-exists",
"what-changed",
"problem-solution",
"gotcha",
"pattern",
"trade-off",
// Globalizable concepts (from models.GlobalizableConcepts)
"best-practice",
"anti-pattern",
"architecture",
"security",
"performance",
"testing",
"debugging",
"workflow",
"tooling",
// Additional useful concepts
"refactoring",
"api",
"database",
"configuration",
"error-handling",
"caching",
"logging",
"auth",
"validation",
}
// conceptTypeSet is a pre-computed map for O(1) concept validation.
var conceptTypeSet = func() map[string]struct{} {
m := make(map[string]struct{}, len(ConceptTypes))
for _, t := range ConceptTypes {
m[t] = struct{}{}
}
return m
}()
// IsValidConceptType returns true if the concept type is valid (O(1) lookup).
func IsValidConceptType(t string) bool {
_, ok := conceptTypeSet[t]
return ok
}
// writeJSON writes a JSON response with proper error handling.
func writeJSON(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Error().Err(err).Msg("Failed to encode JSON response")
}
}
// parseIDParam parses an ID parameter from a string.
// Returns the parsed ID and true on success, or writes an error response and returns false.
// The entityName is used in error messages (e.g., "observation", "session", "pattern").
func parseIDParam(w http.ResponseWriter, idStr, entityName string) (int64, bool) {
if idStr == "" {
http.Error(w, entityName+" id required", http.StatusBadRequest)
return 0, false
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "invalid "+entityName+" id", http.StatusBadRequest)
return 0, false
}
return id, true
}
// formatWarning formats a warning message for use in health responses.
func formatWarning(format string, args ...any) string {
return fmt.Sprintf(format, args...)
}
// handleHealth handles health check requests.
// Returns 200 when ready, 503 when initializing or degraded.
func (s *Service) handleHealth(w http.ResponseWriter, r *http.Request) {
status := "ready"
dbStatus := "ok"
embeddingStatus := "ok"
if !s.ready.Load() {
status = "initializing"
if err := s.GetInitError(); err != nil {
status = "error"
}
}
// Check embedding service
if s.embedSvc == nil {
embeddingStatus = "unavailable"
if status == "ready" {
status = "degraded"
}
}
// Check DB
if s.store == nil {
dbStatus = "unavailable"
if status == "ready" {
status = "degraded"
}
}
activeSessions := 0
if s.sessionManager != nil {
activeSessions = s.sessionManager.GetActiveSessionCount()
}
resp := map[string]any{
"status": status,
"ready": s.ready.Load(),
"uptime_seconds": int(time.Since(s.startTime).Seconds()),
"active_sessions": activeSessions,
"db_status": dbStatus,
"embedding_status": embeddingStatus,
"version": s.version,
}
w.Header().Set("Content-Type", "application/json")
if status != "ready" {
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(resp)
}
// handleVersion returns the worker version for version checking.
func (s *Service) handleVersion(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{
"version": s.version,
})
}
// handleRebuildStatus returns the current status of vector rebuild operations.
// This provides visibility into long-running rebuild operations.
func (s *Service) handleRebuildStatus(w http.ResponseWriter, _ *http.Request) {
s.rebuildStatusMu.RLock()
status := s.rebuildStatus
s.rebuildStatusMu.RUnlock()
if status == nil {
writeJSON(w, map[string]any{
"in_progress": false,
"message": "No rebuild operation has been started",
})
return
}
writeJSON(w, status)
}
// handleTriggerVectorRebuild triggers a full vector rebuild operation.
// This rebuilds all vectors from observations, summaries, and prompts.
// Returns 409 Conflict if a rebuild is already in progress.
// Returns 429 Too Many Requests if called too frequently (5 minute cooldown).
func (s *Service) handleTriggerVectorRebuild(w http.ResponseWriter, _ *http.Request) {
// Check rate limiting for expensive operations
if s.expensiveOpLimiter != nil && !s.expensiveOpLimiter.CanRebuild() {
http.Error(w, "rebuild requested too recently, please wait 5 minutes", http.StatusTooManyRequests)
return
}
// Check if rebuild is already in progress
s.rebuildStatusMu.RLock()
if s.rebuildStatus != nil && s.rebuildStatus.InProgress {
s.rebuildStatusMu.RUnlock()
http.Error(w, "rebuild already in progress", http.StatusConflict)
return
}
s.rebuildStatusMu.RUnlock()
// Verify we have the necessary components
if s.vectorSync == nil || s.observationStore == nil || s.summaryStore == nil || s.promptStore == nil {
http.Error(w, "vector sync not initialized", http.StatusServiceUnavailable)
return
}
// Start rebuild in background
s.wg.Add(1)
go s.rebuildAllVectors(s.observationStore, s.summaryStore, s.promptStore, s.vectorSync)
writeJSON(w, map[string]any{
"status": "started",
"message": "Vector rebuild started. Check /api/rebuild-status for progress.",
})
}
// handleReady handles readiness check requests.
// Returns 200 only when fully initialized, 503 otherwise.
func (s *Service) handleReady(w http.ResponseWriter, r *http.Request) {
if !s.ready.Load() {
if err := s.GetInitError(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Error(w, "service initializing", http.StatusServiceUnavailable)
return
}
writeJSON(w, map[string]string{"status": "ready"})
}
// requireReady is middleware that returns 503 if service isn't ready.
func (s *Service) requireReady(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !s.ready.Load() {
if err := s.GetInitError(); err != nil {
http.Error(w, "service initialization failed: "+err.Error(), http.StatusInternalServerError)
return
}
http.Error(w, "service initializing", http.StatusServiceUnavailable)
return
}
next.ServeHTTP(w, r)
})
}