feat(cli): add verbose logging option for CLI commands
Some checks failed
CI / build-and-test (push) Failing after -32m43s

* Introduced a new flag `--verbose` to enable detailed logging.
* Implemented logging for connection events in SSE and stdio commands.
* Added a utility function to handle verbose logging.
This commit is contained in:
2026-04-21 22:24:57 +02:00
parent 979afc909e
commit 9a9fa4f384
16 changed files with 317 additions and 87 deletions

View File

@@ -55,24 +55,41 @@ func NewBackfillTool(db *store.DB, embeddings *ai.EmbeddingRunner, sessions *ses
// It is used by capture when the embedding provider is temporarily unavailable.
func (t *BackfillTool) QueueThought(ctx context.Context, id uuid.UUID, content string) {
go func() {
started := time.Now()
t.logger.Info("background embedding started",
slog.String("thought_id", id.String()),
slog.String("provider", t.embeddings.PrimaryProvider()),
slog.String("model", t.embeddings.PrimaryModel()),
)
result, err := t.embeddings.Embed(ctx, content)
if err != nil {
t.logger.Warn("background embedding retry failed",
t.logger.Warn("background embedding error",
slog.String("thought_id", id.String()),
slog.String("provider", t.embeddings.PrimaryProvider()),
slog.String("model", t.embeddings.PrimaryModel()),
slog.String("stage", "embed"),
slog.Duration("duration", time.Since(started)),
slog.String("error", err.Error()),
)
return
}
if err := t.store.UpsertEmbedding(ctx, id, result.Model, result.Vector); err != nil {
t.logger.Warn("background embedding upsert failed",
t.logger.Warn("background embedding error",
slog.String("thought_id", id.String()),
slog.String("provider", t.embeddings.PrimaryProvider()),
slog.String("model", result.Model),
slog.String("stage", "upsert"),
slog.Duration("duration", time.Since(started)),
slog.String("error", err.Error()),
)
return
}
t.logger.Info("background embedding retry succeeded",
t.logger.Info("background embedding complete",
slog.String("thought_id", id.String()),
slog.String("provider", t.embeddings.PrimaryProvider()),
slog.String("model", result.Model),
slog.Duration("duration", time.Since(started)),
)
}()
}

View File

@@ -2,9 +2,7 @@ package tools
import (
"context"
"log/slog"
"strings"
"time"
"github.com/google/uuid"
"github.com/modelcontextprotocol/go-sdk/mcp"
@@ -29,15 +27,12 @@ type MetadataQueuer interface {
}
type CaptureTool struct {
store *store.DB
embeddings *ai.EmbeddingRunner
metadata *ai.MetadataRunner
capture config.CaptureConfig
sessions *session.ActiveProjects
metadataTimeout time.Duration
retryer MetadataQueuer
embedRetryer EmbeddingQueuer
log *slog.Logger
store *store.DB
embeddings *ai.EmbeddingRunner
capture config.CaptureConfig
sessions *session.ActiveProjects
retryer MetadataQueuer
embedRetryer EmbeddingQueuer
}
type CaptureInput struct {
@@ -49,8 +44,8 @@ type CaptureOutput struct {
Thought thoughttypes.Thought `json:"thought"`
}
func NewCaptureTool(db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, capture config.CaptureConfig, metadataTimeout time.Duration, sessions *session.ActiveProjects, retryer MetadataQueuer, embedRetryer EmbeddingQueuer, log *slog.Logger) *CaptureTool {
return &CaptureTool{store: db, embeddings: embeddings, metadata: metadata, capture: capture, sessions: sessions, metadataTimeout: metadataTimeout, retryer: retryer, embedRetryer: embedRetryer, log: log}
func NewCaptureTool(db *store.DB, embeddings *ai.EmbeddingRunner, capture config.CaptureConfig, sessions *session.ActiveProjects, retryer MetadataQueuer, embedRetryer EmbeddingQueuer) *CaptureTool {
return &CaptureTool{store: db, embeddings: embeddings, capture: capture, sessions: sessions, retryer: retryer, embedRetryer: embedRetryer}
}
func (t *CaptureTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in CaptureInput) (*mcp.CallToolResult, CaptureOutput, error) {
@@ -65,6 +60,7 @@ func (t *CaptureTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in C
}
rawMetadata := metadata.Fallback(t.capture)
rawMetadata.MetadataStatus = metadata.MetadataStatusPending
thought := thoughttypes.Thought{
Content: content,
Metadata: rawMetadata,
@@ -81,56 +77,12 @@ func (t *CaptureTool) Handle(ctx context.Context, req *mcp.CallToolRequest, in C
_ = t.store.TouchProject(ctx, project.ID)
}
if t.retryer != nil || t.embedRetryer != nil {
t.launchEnrichment(created.ID, content)
if t.retryer != nil {
t.retryer.QueueThought(created.ID)
}
if t.embedRetryer != nil {
t.embedRetryer.QueueThought(ctx, created.ID, content)
}
return nil, CaptureOutput{Thought: created}, nil
}
func (t *CaptureTool) launchEnrichment(id uuid.UUID, content string) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if t.retryer != nil {
attemptedAt := time.Now().UTC()
rawMetadata := metadata.Fallback(t.capture)
extracted, err := t.metadata.ExtractMetadata(ctx, content)
if err != nil {
failed := metadata.MarkMetadataFailed(rawMetadata, t.capture, attemptedAt, err)
if _, updateErr := t.store.UpdateThoughtMetadata(ctx, id, failed); updateErr != nil {
t.log.Warn("deferred metadata failure could not be persisted",
slog.String("thought_id", id.String()),
slog.String("error", updateErr.Error()),
)
}
t.log.Warn("deferred metadata extraction failed",
slog.String("thought_id", id.String()),
slog.String("provider", t.metadata.PrimaryProvider()),
slog.String("error", err.Error()),
)
t.retryer.QueueThought(id)
} else {
completed := metadata.MarkMetadataComplete(extracted, t.capture, attemptedAt)
if _, updateErr := t.store.UpdateThoughtMetadata(ctx, id, completed); updateErr != nil {
t.log.Warn("deferred metadata completion could not be persisted",
slog.String("thought_id", id.String()),
slog.String("error", updateErr.Error()),
)
}
}
}
if t.embedRetryer != nil {
if _, err := t.embeddings.Embed(ctx, content); err != nil {
t.log.Warn("deferred embedding failed",
slog.String("thought_id", id.String()),
slog.String("provider", t.embeddings.PrimaryProvider()),
slog.String("error", err.Error()),
)
}
t.embedRetryer.QueueThought(ctx, id, content)
}
}()
}

View File

@@ -91,12 +91,30 @@ func (t *RetryEnrichmentTool) Handle(ctx context.Context, req *mcp.CallToolReque
func (r *EnrichmentRetryer) QueueThought(id uuid.UUID) {
go func() {
if _, err := r.retryOne(r.backgroundCtx, id); err != nil {
r.logger.Warn("background metadata retry failed",
started := time.Now()
r.logger.Info("background metadata started",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
)
updated, err := r.retryOne(r.backgroundCtx, id)
if err != nil {
r.logger.Warn("background metadata error",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
slog.Duration("duration", time.Since(started)),
slog.String("error", err.Error()),
)
return
}
r.logger.Info("background metadata complete",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
slog.Bool("updated", updated),
slog.Duration("duration", time.Since(started)),
)
}()
}

View File

@@ -113,13 +113,35 @@ func (t *RetryMetadataTool) Handle(ctx context.Context, req *mcp.CallToolRequest
func (r *MetadataRetryer) QueueThought(id uuid.UUID) {
go func() {
started := time.Now()
if !r.lock.Acquire(id, 15*time.Minute) {
return
}
defer r.lock.Release(id)
if _, err := r.retryOne(r.backgroundCtx, id); err != nil {
r.logger.Warn("background metadata retry failed", slog.String("thought_id", id.String()), slog.String("error", err.Error()))
r.logger.Info("background metadata started",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
)
updated, err := r.retryOne(r.backgroundCtx, id)
if err != nil {
r.logger.Warn("background metadata error",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
slog.Duration("duration", time.Since(started)),
slog.String("error", err.Error()),
)
return
}
r.logger.Info("background metadata complete",
slog.String("thought_id", id.String()),
slog.String("provider", r.metadata.PrimaryProvider()),
slog.String("model", r.metadata.PrimaryModel()),
slog.Bool("updated", updated),
slog.Duration("duration", time.Since(started)),
)
}()
}