Resolve data races in channels, logger, spawn, and subagent

This commit is contained in:
Yasuhiro Matsumoto 2026-02-16 00:51:44 +09:00
parent 8d757fbb6f
commit c176d1faab
No known key found for this signature in database
GPG key ID: F2EA90DF2C146D1E
5 changed files with 41 additions and 19 deletions

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"sync/atomic"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
) )
@ -20,7 +21,7 @@ type Channel interface {
type BaseChannel struct { type BaseChannel struct {
config interface{} config interface{}
bus *bus.MessageBus bus *bus.MessageBus
running bool running atomic.Bool
name string name string
allowList []string allowList []string
} }
@ -31,7 +32,6 @@ func NewBaseChannel(name string, config interface{}, bus *bus.MessageBus, allowL
bus: bus, bus: bus,
name: name, name: name,
allowList: allowList, allowList: allowList,
running: false,
} }
} }
@ -40,7 +40,7 @@ func (c *BaseChannel) Name() string {
} }
func (c *BaseChannel) IsRunning() bool { func (c *BaseChannel) IsRunning() bool {
return c.running return c.running.Load()
} }
func (c *BaseChannel) IsAllowed(senderID string) bool { func (c *BaseChannel) IsAllowed(senderID string) bool {
@ -104,5 +104,5 @@ func (c *BaseChannel) HandleMessage(senderID, chatID, content string, media []st
} }
func (c *BaseChannel) setRunning(running bool) { func (c *BaseChannel) setRunning(running bool) {
c.running = running c.running.Store(running)
} }

View file

@ -28,8 +28,8 @@ type TelegramChannel struct {
*BaseChannel *BaseChannel
bot *telego.Bot bot *telego.Bot
commands TelegramCommander commands TelegramCommander
config *config.Config config config.TelegramConfig
chatIDs map[string]int64 chatIDs sync.Map
transcriber *voice.GroqTranscriber transcriber *voice.GroqTranscriber
placeholders sync.Map // chatID -> messageID placeholders sync.Map // chatID -> messageID
stopThinking sync.Map // chatID -> thinkingCancel stopThinking sync.Map // chatID -> thinkingCancel
@ -72,8 +72,8 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
BaseChannel: base, BaseChannel: base,
commands: NewTelegramCommands(bot, cfg), commands: NewTelegramCommands(bot, cfg),
bot: bot, bot: bot,
config: cfg, config: telegramCfg,
chatIDs: make(map[string]int64), chatIDs: sync.Map{},
transcriber: nil, transcriber: nil,
placeholders: sync.Map{}, placeholders: sync.Map{},
stopThinking: sync.Map{}, stopThinking: sync.Map{},
@ -210,7 +210,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
} }
chatID := message.Chat.ID chatID := message.Chat.ID
c.chatIDs[senderID] = chatID c.chatIDs.Store(senderID, chatID)
content := "" content := ""
mediaPaths := []string{} mediaPaths := []string{}

View file

@ -97,7 +97,12 @@ func DisableFileLogging() {
} }
func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) { func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) {
if level < currentLevel { mu.RLock()
lvl := currentLevel
file := logger.file
mu.RUnlock()
if level < lvl {
return return
} }
@ -109,17 +114,17 @@ func logMessage(level LogLevel, component string, message string, fields map[str
Fields: fields, Fields: fields,
} }
if pc, file, line, ok := runtime.Caller(2); ok { if pc, f, line, ok := runtime.Caller(2); ok {
fn := runtime.FuncForPC(pc) fn := runtime.FuncForPC(pc)
if fn != nil { if fn != nil {
entry.Caller = fmt.Sprintf("%s:%d (%s)", file, line, fn.Name()) entry.Caller = fmt.Sprintf("%s:%d (%s)", f, line, fn.Name())
} }
} }
if logger.file != nil { if file != nil {
jsonData, err := json.Marshal(entry) jsonData, err := json.Marshal(entry)
if err == nil { if err == nil {
logger.file.WriteString(string(jsonData) + "\n") file.WriteString(string(jsonData) + "\n")
} }
} }

View file

@ -3,9 +3,11 @@ package tools
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
) )
type SpawnTool struct { type SpawnTool struct {
mu sync.Mutex
manager *SubagentManager manager *SubagentManager
originChannel string originChannel string
originChatID string originChatID string
@ -22,7 +24,9 @@ func NewSpawnTool(manager *SubagentManager) *SpawnTool {
// SetCallback implements AsyncTool interface for async completion notification // SetCallback implements AsyncTool interface for async completion notification
func (t *SpawnTool) SetCallback(cb AsyncCallback) { func (t *SpawnTool) SetCallback(cb AsyncCallback) {
t.mu.Lock()
t.callback = cb t.callback = cb
t.mu.Unlock()
} }
func (t *SpawnTool) Name() string { func (t *SpawnTool) Name() string {
@ -51,8 +55,10 @@ func (t *SpawnTool) Parameters() map[string]interface{} {
} }
func (t *SpawnTool) SetContext(channel, chatID string) { func (t *SpawnTool) SetContext(channel, chatID string) {
t.mu.Lock()
t.originChannel = channel t.originChannel = channel
t.originChatID = chatID t.originChatID = chatID
t.mu.Unlock()
} }
func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
@ -67,8 +73,14 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *T
return ErrorResult("Subagent manager not configured") return ErrorResult("Subagent manager not configured")
} }
t.mu.Lock()
originChannel := t.originChannel
originChatID := t.originChatID
callback := t.callback
t.mu.Unlock()
// Pass callback to manager for async completion notification // Pass callback to manager for async completion notification
result, err := t.manager.Spawn(ctx, task, label, t.originChannel, t.originChatID, t.callback) result, err := t.manager.Spawn(ctx, task, label, originChannel, originChatID, callback)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err)) return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err))
} }

View file

@ -89,9 +89,6 @@ func (sm *SubagentManager) Spawn(ctx context.Context, task, label, originChannel
} }
func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, callback AsyncCallback) { func (sm *SubagentManager) runTask(ctx context.Context, task *SubagentTask, callback AsyncCallback) {
task.Status = "running"
task.Created = time.Now().UnixMilli()
// Build system prompt for subagent // Build system prompt for subagent
systemPrompt := `You are a subagent. Complete the given task independently and report the result. systemPrompt := `You are a subagent. Complete the given task independently and report the result.
You have access to tools - use them as needed to complete your task. You have access to tools - use them as needed to complete your task.
@ -209,6 +206,7 @@ func (sm *SubagentManager) ListTasks() []*SubagentTask {
// Unlike SpawnTool which runs tasks asynchronously, SubagentTool waits for completion // Unlike SpawnTool which runs tasks asynchronously, SubagentTool waits for completion
// and returns the result directly in the ToolResult. // and returns the result directly in the ToolResult.
type SubagentTool struct { type SubagentTool struct {
mu sync.Mutex
manager *SubagentManager manager *SubagentManager
originChannel string originChannel string
originChatID string originChatID string
@ -248,11 +246,18 @@ func (t *SubagentTool) Parameters() map[string]interface{} {
} }
func (t *SubagentTool) SetContext(channel, chatID string) { func (t *SubagentTool) SetContext(channel, chatID string) {
t.mu.Lock()
defer t.mu.Unlock()
t.originChannel = channel t.originChannel = channel
t.originChatID = chatID t.originChatID = chatID
} }
func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
t.mu.Lock()
originChannel := t.originChannel
originChatID := t.originChatID
t.mu.Unlock()
task, ok := args["task"].(string) task, ok := args["task"].(string)
if !ok { if !ok {
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required")) return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
@ -292,7 +297,7 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{})
"max_tokens": 4096, "max_tokens": 4096,
"temperature": 0.7, "temperature": 0.7,
}, },
}, messages, t.originChannel, t.originChatID) }, messages, originChannel, originChatID)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err) return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)