fix: handle slash commands instantly during long LLM processing

Split Run() into a dispatcher (main goroutine) and an LLM worker
(background goroutine). The dispatcher always consumes messages and
handles commands like /todo, /skills, /session immediately, even
while the LLM worker is busy with a long tool-call chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-20 18:21:16 +09:00
parent b00ca6e8f0
commit a90e0da32e

View file

@ -156,25 +156,68 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A
func (al *AgentLoop) Run(ctx context.Context) error { func (al *AgentLoop) Run(ctx context.Context) error {
al.running.Store(true) al.running.Store(true)
// LLM work is dispatched to a background worker so the main loop
// stays free to handle slash commands (/todo, /skills, …) instantly,
// even while a long tool-call chain is running.
llmQueue := make(chan bus.InboundMessage, 10)
workerDone := make(chan struct{})
go func() {
defer close(workerDone)
al.llmWorker(ctx, llmQueue)
}()
defer func() {
close(llmQueue)
<-workerDone
}()
for al.running.Load() { for al.running.Load() {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
default: default:
}
msg, ok := al.bus.ConsumeInbound(ctx) msg, ok := al.bus.ConsumeInbound(ctx)
if !ok { if !ok {
continue continue
} }
// Fast path: handle slash commands immediately without blocking the LLM worker.
if response, handled := al.handleCommand(ctx, msg); handled {
if response != "" {
al.bus.PublishOutbound(bus.OutboundMessage{
Channel: msg.Channel,
ChatID: msg.ChatID,
Content: response,
})
}
continue
}
// Dispatch to LLM worker
select {
case llmQueue <- msg:
case <-ctx.Done():
return nil
}
}
return nil
}
// llmWorker processes LLM messages sequentially in a background goroutine.
func (al *AgentLoop) llmWorker(ctx context.Context, queue <-chan bus.InboundMessage) {
for msg := range queue {
if ctx.Err() != nil {
return
}
response, err := al.processMessage(ctx, msg) response, err := al.processMessage(ctx, msg)
if err != nil { if err != nil {
response = fmt.Sprintf("Error processing message: %v", err) response = fmt.Sprintf("Error processing message: %v", err)
} }
if response != "" { if response != "" {
// Check if the message tool already sent a response during this round.
// If so, skip publishing to avoid duplicate messages to the user.
// Use default agent's tools to check (message tool is shared).
alreadySent := false alreadySent := false
defaultAgent := al.registry.GetDefaultAgent() defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent != nil { if defaultAgent != nil {
@ -196,9 +239,6 @@ func (al *AgentLoop) Run(ctx context.Context) error {
} }
} }
return nil
}
func (al *AgentLoop) Stop() { func (al *AgentLoop) Stop() {
al.running.Store(false) al.running.Store(false)
} }