fix: add sleep to signal loop and change temperature to pointer for zero-value handling
- Prevent busy-wait in signal polling goroutine
- Add `time.Sleep(time.Millisecond)` inside the loop that checks
`sh.IsExiting()`
- Import `time` package in `cmd/llm_aggregator.go`
- Change temperature field from `float64` to `*float64` across the
codebase
- Update `Runtime.Temperature` in `internal/runtime/runtime.go` to
pointer
- Update `LLMClient.temperature` and `NewLLMClient()` signature in
`internal/llm/llm.go`
- Modify config loading (`internal/config/config.go`) to assign
pointer from viper
- Distinguish between unset temperature (use default 0.7) and
explicitly set 0.0
- Dereference pointer when calling API and logging
- Adjust tests in `internal/llm/llm_test.go` to pass `nil` instead of
`0` for default
- Fix comment camelCase for `WasInterrupted()` method
- Modify `CHANGELOG.md` accordingly for v1.0.0.
Maxwell Jensen 85795372+maxwelljens@users.noreply.github.com
Mon, 27 Apr 2026 13:11:57 +0200
9 files changed,
35 insertions(+),
29 deletions(-)
M
CHANGELOG.md
→
CHANGELOG.md
@@ -6,16 +6,20 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0] - 2026-04-27 ### Added -- Linting using `golangci-lint`, mostly for readability and maintainability. +- Linting and code quality tooling via `golangci-lint`. ### Fixed -- Fixed all lint issues across the codebase, including unchecked error returns, - string formatting performance, dead code, and documentation style. +- Tight CPU-spinning polling loop in signal handler (added `time.Sleep` to + avoid busy-waiting). +- `--temperature 0` was silently overwritten by the default (0.7); temperature + is now a pointer so `nil` means "not provided" and a non-nil value — including + `0.0` — means "explicitly set". +- `WasInterrupted` godoc comment capitalised to satisfy `golint`. ## [0.15.0] - 2026-04-27
M
README.md
→
README.md
@@ -25,9 +25,6 @@
## Quick start ```bash -# Install -go install github.com/maxwelljensen/llm_aggregator/cmd/llm_aggregator.go@latest - # Create a feeds file cat > feeds.txt << 'EOF' https://news.ycombinator.com/rss@@ -35,7 +32,8 @@ https://lwn.net/headlines/newrss
EOF # Run -llm_aggregator -f feeds.txt -p "What are the top tech stories today?" +llm_aggregator --api-key <YOUR_KEY> --base-url <URL> \ +-f feeds.txt -p "What are the top tech stories today?" ``` **First run?** See [docs/USAGE.md](docs/USAGE.md) for installation, configuration,
M
cmd/llm_aggregator.go
→
cmd/llm_aggregator.go
@@ -5,6 +5,7 @@ "context"
"fmt" "os" "strconv" + "time" tea "github.com/charmbracelet/bubbletea" "llm_aggregator/internal/aggregator"@@ -96,12 +97,13 @@ // Monitor for signals and propagate into context cancellation.
// This goroutine lives until the Watch() goroutine exits (signalled via sh). go func() { // Poll IsExiting() — returns true as soon as the signal is received. - // No busy-waiting: the select in Watch() unblocks immediately on signal. + // Sleep briefly to avoid a tight busy-wait loop. for { if sh.IsExiting() { cancel() return } + time.Sleep(time.Millisecond) } }()
M
docs/llm_aggregator.1
→
docs/llm_aggregator.1
@@ -1,5 +1,5 @@
.\" Man page for llm_aggregator -.TH LLM_AGGREGATOR 1 "27 April 2026" "0.15.0" "User Commands" +.TH LLM_AGGREGATOR 1 "27 April 2026" "1.0.0" "User Commands" .SH NAME llm_aggregator \- aggregate RSS feeds and summarise them with LLMs .SH SYNOPSIS
M
docs/llm_aggregator.5
→
docs/llm_aggregator.5
@@ -1,5 +1,5 @@
.\" Man page for llm_aggregator configuration file -.TH LLM_AGGREGATOR 5 "27 April 2026" "0.15.0" "File Formats" +.TH LLM_AGGREGATOR 5 "27 April 2026" "1.0.0" "File Formats" .SH NAME llm_aggregator.conf \- configuration file for llm_aggregator .SH DESCRIPTION
M
internal/config/config.go
→
internal/config/config.go
@@ -196,7 +196,8 @@ rt.APIKey = v.GetString("api_key")
rt.BaseURL = v.GetString("base_url") rt.Model = v.GetString("model") rt.MaxTokens = v.GetInt("max_tokens") - rt.Temperature = v.GetFloat64("temperature") + t := v.GetFloat64("temperature") + rt.Temperature = &t rt.LLMTimeout = v.GetInt("timeout") // System prompt rt.SystemPrompt = v.GetString("system_prompt")
M
internal/llm/llm.go
→
internal/llm/llm.go
@@ -17,17 +17,17 @@ )
// LLMClient is a client for interacting with LLM API. type LLMClient struct { - client openai.Client - model string - maxTokens int - temperature float64 + client openai.Client + model string + maxTokens int + temperature *float64 llmTimeout int // seconds; 0 means no timeout - logger *progress.Context + logger *progress.Context } // NewLLMClient creates an LLM API client. // Set apiKey to "" to read from LLM_AGGREGATOR_API_KEY. -func NewLLMClient(apiKey, baseURL, model string, maxTokens int, temperature float64, timeoutSeconds int) (*LLMClient, error) { +func NewLLMClient(apiKey, baseURL, model string, maxTokens int, temperature *float64, timeoutSeconds int) (*LLMClient, error) { // Get API key from parameter or environment variable if apiKey == "" { apiKey = os.Getenv("LLM_AGGREGATOR_API_KEY")@@ -49,8 +49,9 @@ }
if maxTokens == 0 { maxTokens = defaults.DefaultMaxTokens } - if temperature == 0 { - temperature = defaults.DefaultTemperature + if temperature == nil { + t := defaults.DefaultTemperature + temperature = &t } if timeoutSeconds == 0 { timeoutSeconds = defaults.DefaultLLMTimeout@@ -185,7 +186,7 @@
func (dc *LLMClient) callAPIWithMessages(ctx context.Context, messages []openai.ChatCompletionMessageParamUnion) (string, *TokenUsage, error) { if dc.logger != nil { dc.logger.Logf("Calling LLM API with model: %s", dc.model) - dc.logger.Logf("Max tokens: %d, Temperature: %.2f", dc.maxTokens, dc.temperature) + dc.logger.Logf("Max tokens: %d, Temperature: %.2f", dc.maxTokens, *dc.temperature) dc.logger.Logf("Messages count: %d", len(messages)) }@@ -193,7 +194,7 @@ response, err := dc.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Model: dc.model, Messages: messages, MaxTokens: openai.Int(int64(dc.maxTokens)), - Temperature: openai.Float(dc.temperature), + Temperature: openai.Float(*dc.temperature), }) if err != nil {
M
internal/llm/llm_test.go
→
internal/llm/llm_test.go
@@ -36,7 +36,7 @@ "test-api-key",
"", "", 0, - 0, + nil, tt.timeout, ) if err != nil {@@ -51,7 +51,7 @@ }
func TestNewLLMClientDefaults(t *testing.T) { // Verify all defaults are applied when only API key is provided - client, err := NewLLMClient("test-api-key", "", "", 0, 0, 0) + client, err := NewLLMClient("test-api-key", "", "", 0, nil, 0) if err != nil { t.Fatalf("NewLLMClient returned unexpected error: %v", err) }@@ -62,8 +62,8 @@ }
if client.maxTokens != 4000 { t.Errorf("maxTokens = %d, want %d", client.maxTokens, 4000) } - if client.temperature != 0.7 { - t.Errorf("temperature = %f, want %f", client.temperature, 0.7) + if *client.temperature != 0.7 { + t.Errorf("temperature = %f, want %f", *client.temperature, 0.7) } if client.llmTimeout != 300 { t.Errorf("llmTimeout = %d, want %d", client.llmTimeout, 300)@@ -71,7 +71,7 @@ }
} func TestNewLLMClientRequiresAPIKey(t *testing.T) { - _, err := NewLLMClient("", "", "", 0, 0, 0) + _, err := NewLLMClient("", "", "", 0, nil, 0) if err == nil { t.Error("NewLLMClient expected error for missing API key, got nil") }
M
internal/runtime/runtime.go
→
internal/runtime/runtime.go
@@ -31,7 +31,7 @@ APIKey string
BaseURL string Model string MaxTokens int - Temperature float64 + Temperature *float64 LLMTimeout int // seconds; 0 means no timeout Prompt string SystemPrompt string@@ -183,7 +183,7 @@ }
return nil } -// was interrupted by a signal during execution. +// WasInterrupted reports whether the runtime was interrupted by a signal during execution. func (r *Runtime) WasInterrupted() bool { return r.Interrupted }