Add the statusline. Fix the installation.

This commit is contained in:
2025-12-16 00:15:25 +00:00
parent be4a7c19d0
commit bc4e9e66ae
14 changed files with 582 additions and 60 deletions
+9 -5
View File
@@ -74,12 +74,16 @@ func (s *ObservationStore) StoreObservation(ctx context.Context, sdkSessionID, p
id, _ := result.LastInsertId()
// Cleanup old observations beyond the limit for this project
// Cleanup old observations beyond the limit for this project (async to not block handler)
if project != "" {
deletedIDs, _ := s.CleanupOldObservations(ctx, project)
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
s.cleanupFunc(ctx, deletedIDs)
}
go func(proj string) {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
deletedIDs, _ := s.CleanupOldObservations(cleanupCtx, proj)
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
s.cleanupFunc(cleanupCtx, deletedIDs)
}
}(project)
}
return id, nowEpoch, nil
+9 -5
View File
@@ -51,11 +51,15 @@ func (s *PromptStore) SaveUserPromptWithMatches(ctx context.Context, claudeSessi
id, _ := result.LastInsertId()
// Cleanup old prompts beyond the global limit
deletedIDs, _ := s.CleanupOldPrompts(ctx)
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
s.cleanupFunc(ctx, deletedIDs)
}
// Cleanup old prompts beyond the global limit (async to not block handler)
go func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
deletedIDs, _ := s.CleanupOldPrompts(cleanupCtx)
if len(deletedIDs) > 0 && s.cleanupFunc != nil {
s.cleanupFunc(cleanupCtx, deletedIDs)
}
}()
return id, nil
}
+14 -2
View File
@@ -514,7 +514,7 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
retrievalStats := s.GetRetrievalStats()
sessionsToday, _ := s.sessionStore.GetSessionsToday(r.Context())
writeJSON(w, map[string]interface{}{
response := map[string]interface{}{
"uptime": time.Since(s.startTime).String(),
"activeSessions": s.sessionManager.GetActiveSessionCount(),
"queueDepth": s.sessionManager.GetTotalQueueDepth(),
@@ -522,7 +522,19 @@ func (s *Service) handleGetStats(w http.ResponseWriter, r *http.Request) {
"connectedClients": s.sseBroadcaster.ClientCount(),
"sessionsToday": sessionsToday,
"retrieval": retrievalStats,
})
"ready": s.ready.Load(),
}
// Include project-specific observation count if project is specified
if project := r.URL.Query().Get("project"); project != "" {
count, err := s.observationStore.GetObservationCount(r.Context(), project)
if err == nil {
response["projectObservations"] = count
response["project"] = project
}
}
writeJSON(w, response)
}
// handleGetRetrievalStats returns detailed retrieval statistics.
+60 -15
View File
@@ -6,10 +6,17 @@ import (
"fmt"
"net/http"
"sync"
"time"
"github.com/rs/zerolog/log"
)
const (
// WriteTimeout is the timeout for writing to SSE clients.
// Prevents blocking on stale connections.
WriteTimeout = 2 * time.Second
)
// Client represents a connected SSE client.
type Client struct {
ID string
@@ -101,6 +108,7 @@ func (b *Broadcaster) removeClientByID(id string) {
}
// Broadcast sends a message to all connected clients.
// Uses non-blocking writes with timeout to prevent stale connections from blocking.
func (b *Broadcaster) Broadcast(data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
@@ -117,30 +125,67 @@ func (b *Broadcaster) Broadcast(data interface{}) {
}
b.mu.RUnlock()
// Track dead clients for removal
var deadClients []*Client
if len(clients) == 0 {
return
}
// Use a channel to collect dead clients from concurrent writes
deadClientsCh := make(chan string, len(clients))
var wg sync.WaitGroup
for _, client := range clients {
select {
case <-client.Done:
continue
default:
_, err := client.Writer.Write([]byte(message))
if err != nil {
log.Debug().
Str("clientId", client.ID).
Err(err).
Msg("Failed to write to SSE client, marking for removal")
deadClients = append(deadClients, client)
continue
}
client.Flusher.Flush()
wg.Add(1)
go func(c *Client) {
defer wg.Done()
b.writeToClient(c, message, deadClientsCh)
}(client)
}
}
// Remove dead clients outside the iteration
for _, client := range deadClients {
b.removeClientByID(client.ID)
// Wait for all writes to complete (with their individual timeouts)
wg.Wait()
close(deadClientsCh)
// Remove dead clients
for clientID := range deadClientsCh {
b.removeClientByID(clientID)
}
}
// writeToClient writes a message to a single client with timeout.
func (b *Broadcaster) writeToClient(client *Client, message string, deadCh chan<- string) {
// Use a timeout channel to prevent blocking on stale connections
done := make(chan struct{})
go func() {
defer close(done)
_, err := client.Writer.Write([]byte(message))
if err != nil {
log.Debug().
Str("clientId", client.ID).
Err(err).
Msg("Failed to write to SSE client, marking for removal")
deadCh <- client.ID
return
}
client.Flusher.Flush()
}()
select {
case <-done:
// Write completed successfully
case <-time.After(WriteTimeout):
log.Warn().
Str("clientId", client.ID).
Dur("timeout", WriteTimeout).
Msg("SSE write timed out, marking client for removal")
deadCh <- client.ID
case <-client.Done:
// Client disconnected during write
}
}