mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Move SDK processing to async queue.
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
json "github.com/goccy/go-json"
|
json "github.com/goccy/go-json"
|
||||||
@@ -39,7 +38,8 @@ type Processor struct {
|
|||||||
broadcastFunc BroadcastFunc
|
broadcastFunc BroadcastFunc
|
||||||
syncObservationFunc SyncObservationFunc
|
syncObservationFunc SyncObservationFunc
|
||||||
syncSummaryFunc SyncSummaryFunc
|
syncSummaryFunc SyncSummaryFunc
|
||||||
mu sync.Mutex
|
// Semaphore to limit concurrent Claude CLI calls (prevents API overload)
|
||||||
|
sem chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBroadcastFunc sets the broadcast callback for SSE events.
|
// SetBroadcastFunc sets the broadcast callback for SSE events.
|
||||||
@@ -64,6 +64,10 @@ func (p *Processor) broadcast(event map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxConcurrentCLICalls is the maximum number of concurrent Claude CLI calls.
|
||||||
|
// This prevents overwhelming the API and manages resource usage.
|
||||||
|
const MaxConcurrentCLICalls = 4
|
||||||
|
|
||||||
// NewProcessor creates a new SDK processor.
|
// NewProcessor creates a new SDK processor.
|
||||||
func NewProcessor(observationStore *sqlite.ObservationStore, summaryStore *sqlite.SummaryStore) (*Processor, error) {
|
func NewProcessor(observationStore *sqlite.ObservationStore, summaryStore *sqlite.SummaryStore) (*Processor, error) {
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
@@ -89,6 +93,7 @@ func NewProcessor(observationStore *sqlite.ObservationStore, summaryStore *sqlit
|
|||||||
model: cfg.Model,
|
model: cfg.Model,
|
||||||
observationStore: observationStore,
|
observationStore: observationStore,
|
||||||
summaryStore: summaryStore,
|
summaryStore: summaryStore,
|
||||||
|
sem: make(chan struct{}, MaxConcurrentCLICalls),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +105,6 @@ func (p *Processor) IsAvailable() bool {
|
|||||||
|
|
||||||
// ProcessObservation processes a single tool observation and extracts insights.
|
// ProcessObservation processes a single tool observation and extracts insights.
|
||||||
func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, project string, toolName string, toolInput, toolResponse interface{}, promptNumber int, cwd string) error {
|
func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, project string, toolName string, toolInput, toolResponse interface{}, promptNumber int, cwd string) error {
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Skip certain tools that aren't worth processing
|
// Skip certain tools that aren't worth processing
|
||||||
if shouldSkipTool(toolName) {
|
if shouldSkipTool(toolName) {
|
||||||
log.Info().Str("tool", toolName).Msg("Skipping tool (not interesting for memory)")
|
log.Info().Str("tool", toolName).Msg("Skipping tool (not interesting for memory)")
|
||||||
@@ -136,6 +138,14 @@ func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, projec
|
|||||||
}
|
}
|
||||||
prompt := BuildObservationPrompt(exec)
|
prompt := BuildObservationPrompt(exec)
|
||||||
|
|
||||||
|
// Acquire semaphore slot (limits concurrent CLI calls)
|
||||||
|
select {
|
||||||
|
case p.sem <- struct{}{}:
|
||||||
|
defer func() { <-p.sem }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// Call Claude Code CLI
|
// Call Claude Code CLI
|
||||||
response, err := p.callClaudeCLI(ctx, prompt)
|
response, err := p.callClaudeCLI(ctx, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -226,9 +236,6 @@ func (p *Processor) ProcessObservation(ctx context.Context, sdkSessionID, projec
|
|||||||
|
|
||||||
// ProcessSummary processes a session summary request.
|
// ProcessSummary processes a session summary request.
|
||||||
func (p *Processor) ProcessSummary(ctx context.Context, sessionDBID int64, sdkSessionID, project, userPrompt, lastUserMsg, lastAssistantMsg string) error {
|
func (p *Processor) ProcessSummary(ctx context.Context, sessionDBID int64, sdkSessionID, project, userPrompt, lastUserMsg, lastAssistantMsg string) error {
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Skip summary generation if there's no meaningful assistant response
|
// Skip summary generation if there's no meaningful assistant response
|
||||||
// This prevents generic "initial session setup" summaries
|
// This prevents generic "initial session setup" summaries
|
||||||
if !hasMeaningfulContent(lastAssistantMsg) {
|
if !hasMeaningfulContent(lastAssistantMsg) {
|
||||||
@@ -249,6 +256,14 @@ func (p *Processor) ProcessSummary(ctx context.Context, sessionDBID int64, sdkSe
|
|||||||
}
|
}
|
||||||
prompt := BuildSummaryPrompt(req)
|
prompt := BuildSummaryPrompt(req)
|
||||||
|
|
||||||
|
// Acquire semaphore slot (limits concurrent CLI calls)
|
||||||
|
select {
|
||||||
|
case p.sem <- struct{}{}:
|
||||||
|
defer func() { <-p.sem }()
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// Call Claude Code CLI
|
// Call Claude Code CLI
|
||||||
response, err := p.callClaudeCLI(ctx, prompt)
|
response, err := p.callClaudeCLI(ctx, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+53
-40
@@ -736,10 +736,14 @@ func (s *Service) processQueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processAllSessions processes pending messages for all active sessions.
|
// processAllSessions processes pending messages for all active sessions.
|
||||||
|
// Messages are processed in parallel using goroutines, with concurrency
|
||||||
|
// limited by the processor's semaphore.
|
||||||
func (s *Service) processAllSessions() {
|
func (s *Service) processAllSessions() {
|
||||||
// Get all sessions with pending messages
|
// Get all sessions with pending messages
|
||||||
sessions := s.sessionManager.GetAllSessions()
|
sessions := s.sessionManager.GetAllSessions()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, sess := range sessions {
|
for _, sess := range sessions {
|
||||||
// Get pending messages
|
// Get pending messages
|
||||||
messages := s.sessionManager.DrainMessages(sess.SessionDBID)
|
messages := s.sessionManager.DrainMessages(sess.SessionDBID)
|
||||||
@@ -747,52 +751,61 @@ func (s *Service) processAllSessions() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each message
|
// Process each message in a goroutine
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
switch msg.Type {
|
wg.Add(1)
|
||||||
case session.MessageTypeObservation:
|
go func(sess *session.ActiveSession, msg session.PendingMessage) {
|
||||||
if msg.Observation != nil {
|
defer wg.Done()
|
||||||
err := s.processor.ProcessObservation(
|
|
||||||
s.ctx,
|
|
||||||
sess.SDKSessionID,
|
|
||||||
sess.Project,
|
|
||||||
msg.Observation.ToolName,
|
|
||||||
msg.Observation.ToolInput,
|
|
||||||
msg.Observation.ToolResponse,
|
|
||||||
msg.Observation.PromptNumber,
|
|
||||||
msg.Observation.CWD,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).
|
|
||||||
Str("tool", msg.Observation.ToolName).
|
|
||||||
Msg("Failed to process observation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case session.MessageTypeSummarize:
|
switch msg.Type {
|
||||||
if msg.Summarize != nil {
|
case session.MessageTypeObservation:
|
||||||
err := s.processor.ProcessSummary(
|
if msg.Observation != nil {
|
||||||
s.ctx,
|
err := s.processor.ProcessObservation(
|
||||||
sess.SessionDBID,
|
s.ctx,
|
||||||
sess.SDKSessionID,
|
sess.SDKSessionID,
|
||||||
sess.Project,
|
sess.Project,
|
||||||
sess.UserPrompt,
|
msg.Observation.ToolName,
|
||||||
msg.Summarize.LastUserMessage,
|
msg.Observation.ToolInput,
|
||||||
msg.Summarize.LastAssistantMessage,
|
msg.Observation.ToolResponse,
|
||||||
)
|
msg.Observation.PromptNumber,
|
||||||
if err != nil {
|
msg.Observation.CWD,
|
||||||
log.Error().Err(err).
|
)
|
||||||
Int64("sessionId", sess.SessionDBID).
|
if err != nil {
|
||||||
Msg("Failed to process summary")
|
log.Error().Err(err).
|
||||||
|
Str("tool", msg.Observation.ToolName).
|
||||||
|
Msg("Failed to process observation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case session.MessageTypeSummarize:
|
||||||
|
if msg.Summarize != nil {
|
||||||
|
err := s.processor.ProcessSummary(
|
||||||
|
s.ctx,
|
||||||
|
sess.SessionDBID,
|
||||||
|
sess.SDKSessionID,
|
||||||
|
sess.Project,
|
||||||
|
sess.UserPrompt,
|
||||||
|
msg.Summarize.LastUserMessage,
|
||||||
|
msg.Summarize.LastAssistantMessage,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Int64("sessionId", sess.SessionDBID).
|
||||||
|
Msg("Failed to process summary")
|
||||||
|
}
|
||||||
|
// Delete session after summary
|
||||||
|
s.sessionManager.DeleteSession(sess.SessionDBID)
|
||||||
}
|
}
|
||||||
// Delete session after summary
|
|
||||||
s.sessionManager.DeleteSession(sess.SessionDBID)
|
|
||||||
}
|
}
|
||||||
}
|
}(sess, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.broadcastProcessingStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for all goroutines to complete
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Broadcast status after processing
|
||||||
|
s.broadcastProcessingStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the service.
|
// Shutdown gracefully shuts down the service.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-4-g0a0dfc1-dirty",
|
"version": "v0.6.1-5-gcd14ba8-dirty",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-4-g0a0dfc1-dirty",
|
"version": "v0.6.1-5-gcd14ba8-dirty",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.13"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-mnemonic-dashboard",
|
"name": "claude-mnemonic-dashboard",
|
||||||
"version": "v0.6.1-4-g0a0dfc1-dirty",
|
"version": "v0.6.1-5-gcd14ba8-dirty",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user