Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

Pommel is a local-first semantic code search system designed to reduce context window consumption for AI coding agents. It maintains an always-current vector database of code embeddings, enabling targeted semantic searches instead of reading numerous files into context.

**Status:** v0.7.2 - Dynamic embedding dimensions and Windows path fixes
**Status:** v0.7.3 - Configurable timeouts for cold starts and slow connections

## Code Search Priority

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Local-first semantic code search for AI coding agents.
[![Go Version](https://img.shields.io/github/go-mod/go-version/dbinky/Pommel)](https://go.dev/)
[![License](https://img.shields.io/github/license/dbinky/Pommel)](LICENSE)

**v0.7.2** - Dynamic embedding dimensions and Windows path fixes!
**v0.7.3** - Configurable timeouts for cold starts and slow connections!

Pommel maintains a vector database of your code, enabling fast semantic search without loading files into context. Designed to complement AI coding assistants by providing targeted code discovery.

Expand Down
2 changes: 1 addition & 1 deletion cmd/pm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// Set at build time via ldflags
var (
version = "0.7.2"
version = "0.7.3"
commit = "unknown"
date = "unknown"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/pommeld/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

var (
version = "0.7.2"
version = "0.7.3"
commit = "unknown"
date = "unknown"
)
Expand Down
3 changes: 1 addition & 2 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package api
import (
"log/slog"
"net/http"
"time"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
Expand All @@ -25,7 +24,7 @@ func NewRouter(indexer *daemon.Indexer, cfg *config.Config, searcher Searcher) *
r := chi.NewRouter()

// Apply middleware
r.Use(middleware.Timeout(30 * time.Second))
r.Use(middleware.Timeout(cfg.Timeouts.APIRequestTimeout()))
r.Use(middleware.Recoverer)

// Register routes
Expand Down
5 changes: 2 additions & 3 deletions internal/cli/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"time"

"github.com/pommel-dev/pommel/internal/api"
"github.com/pommel-dev/pommel/internal/config"
Expand Down Expand Up @@ -43,7 +42,7 @@ func NewClient(cfg *config.Config) *Client {
return &Client{
baseURL: fmt.Sprintf("http://%s", cfg.Daemon.Address()),
httpClient: &http.Client{
Timeout: 30 * time.Second,
Timeout: cfg.Timeouts.ClientRequestTimeout(),
},
}
}
Expand All @@ -65,7 +64,7 @@ func NewClientFromProjectRoot(projectRoot string) (*Client, error) {
return &Client{
baseURL: fmt.Sprintf("http://%s", cfg.Daemon.AddressWithPort(port)),
httpClient: &http.Client{
Timeout: 30 * time.Second,
Timeout: cfg.Timeouts.ClientRequestTimeout(),
},
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

var (
Version = "0.7.2"
Version = "0.7.3"
BuildCommit = "unknown"
BuildDate = "unknown"

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func runStart(cmd *cobra.Command, args []string) error {
address := cfg.Daemon.AddressWithPort(port)
healthURL := fmt.Sprintf("http://%s/health", address)

timeout := time.After(10 * time.Second)
timeout := time.After(cfg.Timeouts.DaemonStartTimeout())
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()

Expand Down
8 changes: 7 additions & 1 deletion internal/cli/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ func runStop(cmd *cobra.Command, args []string) error {
return ErrNotInitialized()
}

// Load config for timeout settings
cfg, err := loader.Load()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

// Check if running
stateManager := daemon.NewStateManager(projectRoot)
running, pid := stateManager.IsRunning()
Expand All @@ -50,7 +56,7 @@ func runStop(cmd *cobra.Command, args []string) error {
}

// Wait for process to exit with timeout
timeout := time.After(5 * time.Second)
timeout := time.After(cfg.Timeouts.DaemonStopTimeout())
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()

Expand Down
88 changes: 88 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Config struct {
Embedding EmbeddingConfig `yaml:"embedding" json:"embedding" mapstructure:"embedding"`
Search SearchConfig `yaml:"search" json:"search" mapstructure:"search"`
Subprojects SubprojectsConfig `yaml:"subprojects" json:"subprojects" mapstructure:"subprojects"`
Timeouts TimeoutsConfig `yaml:"timeouts" json:"timeouts" mapstructure:"timeouts"`
}

// WatcherConfig contains file watcher settings
Expand Down Expand Up @@ -164,6 +165,93 @@ type SubprojectsConfig struct {
Exclude []string `yaml:"exclude" json:"exclude,omitempty" mapstructure:"exclude"`
}

// TimeoutsConfig contains timeout settings for various operations.
// All timeout values are in milliseconds.
// Longer timeouts are useful when embedding models need to be loaded from disk
// (cold start), which can take significantly longer than when the model is already in memory.
type TimeoutsConfig struct {
// EmbeddingRequestMs is the timeout for embedding API requests (default: 120000 = 2 minutes).
// This should be long enough to allow for model loading on cold starts.
EmbeddingRequestMs int `yaml:"embedding_request_ms" json:"embedding_request_ms" mapstructure:"embedding_request_ms"`

// DaemonStartMs is the timeout for waiting for the daemon to start (default: 30000 = 30 seconds).
DaemonStartMs int `yaml:"daemon_start_ms" json:"daemon_start_ms" mapstructure:"daemon_start_ms"`

// DaemonStopMs is the timeout for waiting for the daemon to stop (default: 10000 = 10 seconds).
DaemonStopMs int `yaml:"daemon_stop_ms" json:"daemon_stop_ms" mapstructure:"daemon_stop_ms"`

// ClientRequestMs is the timeout for CLI client requests to the daemon (default: 120000 = 2 minutes).
// This should be long enough to handle search requests that trigger embedding generation.
ClientRequestMs int `yaml:"client_request_ms" json:"client_request_ms" mapstructure:"client_request_ms"`

// APIRequestMs is the timeout for API middleware (default: 120000 = 2 minutes).
APIRequestMs int `yaml:"api_request_ms" json:"api_request_ms" mapstructure:"api_request_ms"`

// ShutdownMs is the timeout for graceful daemon shutdown (default: 10000 = 10 seconds).
ShutdownMs int `yaml:"shutdown_ms" json:"shutdown_ms" mapstructure:"shutdown_ms"`
}

// DefaultTimeoutsConfig returns the default timeout configuration.
// Defaults are set high enough to handle cold starts where models need to be loaded.
func DefaultTimeoutsConfig() TimeoutsConfig {
return TimeoutsConfig{
EmbeddingRequestMs: 120000, // 2 minutes - allows for model loading
DaemonStartMs: 30000, // 30 seconds
DaemonStopMs: 10000, // 10 seconds
ClientRequestMs: 120000, // 2 minutes - allows for embedding generation
APIRequestMs: 120000, // 2 minutes
ShutdownMs: 10000, // 10 seconds
}
}

// EmbeddingRequestTimeout returns the embedding request timeout as time.Duration.
func (t TimeoutsConfig) EmbeddingRequestTimeout() time.Duration {
if t.EmbeddingRequestMs <= 0 {
return DefaultTimeoutsConfig().EmbeddingRequestTimeout()
}
return time.Duration(t.EmbeddingRequestMs) * time.Millisecond
}

// DaemonStartTimeout returns the daemon start timeout as time.Duration.
func (t TimeoutsConfig) DaemonStartTimeout() time.Duration {
if t.DaemonStartMs <= 0 {
return DefaultTimeoutsConfig().DaemonStartTimeout()
}
return time.Duration(t.DaemonStartMs) * time.Millisecond
}

// DaemonStopTimeout returns the daemon stop timeout as time.Duration.
func (t TimeoutsConfig) DaemonStopTimeout() time.Duration {
if t.DaemonStopMs <= 0 {
return DefaultTimeoutsConfig().DaemonStopTimeout()
}
return time.Duration(t.DaemonStopMs) * time.Millisecond
}

// ClientRequestTimeout returns the client request timeout as time.Duration.
func (t TimeoutsConfig) ClientRequestTimeout() time.Duration {
if t.ClientRequestMs <= 0 {
return DefaultTimeoutsConfig().ClientRequestTimeout()
}
return time.Duration(t.ClientRequestMs) * time.Millisecond
}

// APIRequestTimeout returns the API request timeout as time.Duration.
func (t TimeoutsConfig) APIRequestTimeout() time.Duration {
if t.APIRequestMs <= 0 {
return DefaultTimeoutsConfig().APIRequestTimeout()
}
return time.Duration(t.APIRequestMs) * time.Millisecond
}

// ShutdownTimeout returns the shutdown timeout as time.Duration.
func (t TimeoutsConfig) ShutdownTimeout() time.Duration {
if t.ShutdownMs <= 0 {
return DefaultTimeoutsConfig().ShutdownTimeout()
}
return time.Duration(t.ShutdownMs) * time.Millisecond
}

// ProjectOverride defines a manual sub-project configuration
type ProjectOverride struct {
ID string `yaml:"id" json:"id,omitempty" mapstructure:"id"`
Expand Down
1 change: 1 addition & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@ func Default() *Config {
Projects: nil,
Exclude: nil,
},
Timeouts: DefaultTimeoutsConfig(),
}
}
3 changes: 2 additions & 1 deletion internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func New(projectRoot string, cfg *config.Config, logger *slog.Logger) (*Daemon,
// Build provider config from embedding settings (needed before db.Open for dimensions)
providerCfg := &embedder.ProviderConfig{
Provider: cfg.Embedding.Provider,
Timeout: cfg.Timeouts.EmbeddingRequestTimeout(),
Ollama: embedder.OllamaProviderSettings{
URL: cfg.Embedding.GetOllamaURL(),
Model: cfg.Embedding.Ollama.Model,
Expand Down Expand Up @@ -504,7 +505,7 @@ func (d *Daemon) shutdown() error {

// Shutdown API server
if d.server != nil {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
shutdownCtx, cancel := context.WithTimeout(context.Background(), d.config.Timeouts.ShutdownTimeout())
defer cancel()
if err := d.server.Shutdown(shutdownCtx); err != nil {
d.logger.Warn("server shutdown error", "error", err)
Expand Down
17 changes: 12 additions & 5 deletions internal/embedder/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package embedder

import "fmt"
import (
"fmt"
"time"
)

// ProviderType represents the type of embedding provider
type ProviderType string
Expand Down Expand Up @@ -100,6 +103,7 @@ func APIProviders() []ProviderType {
// ProviderConfig holds the configuration for creating an embedder
type ProviderConfig struct {
Provider string
Timeout time.Duration // Timeout for embedding requests
Ollama OllamaProviderSettings
OpenAI OpenAIProviderSettings
Voyage VoyageProviderSettings
Expand Down Expand Up @@ -135,6 +139,7 @@ func NewFromConfig(cfg *ProviderConfig) (Embedder, error) {
return NewOllamaClient(OllamaConfig{
BaseURL: cfg.Ollama.URL,
Model: cfg.Ollama.Model,
Timeout: cfg.Timeout,
}), nil

case ProviderOpenAI:
Expand All @@ -146,8 +151,9 @@ func NewFromConfig(cfg *ProviderConfig) (Embedder, error) {
}
}
return NewOpenAIClient(OpenAIConfig{
APIKey: cfg.OpenAI.APIKey,
Model: cfg.OpenAI.Model,
APIKey: cfg.OpenAI.APIKey,
Model: cfg.OpenAI.Model,
Timeout: cfg.Timeout,
}), nil

case ProviderVoyage:
Expand All @@ -159,8 +165,9 @@ func NewFromConfig(cfg *ProviderConfig) (Embedder, error) {
}
}
return NewVoyageClient(VoyageConfig{
APIKey: cfg.Voyage.APIKey,
Model: cfg.Voyage.Model,
APIKey: cfg.Voyage.APIKey,
Model: cfg.Voyage.Model,
Timeout: cfg.Timeout,
}), nil

default:
Expand Down
Loading