Fix (race): mutex chatbody

This commit is contained in:
Grail Finder
2026-03-07 10:46:18 +03:00
parent 014e297ae3
commit a842b00e96
9 changed files with 422 additions and 142 deletions

116
bot.go
View File

@@ -37,7 +37,7 @@ var (
chunkChan = make(chan string, 10)
openAIToolChan = make(chan string, 10)
streamDone = make(chan bool, 1)
chatBody *models.ChatBody
chatBody *models.SafeChatBody
store storage.FullRepo
defaultFirstMsg = "Hello! What can I do for you?"
defaultStarter = []models.RoleMsg{}
@@ -262,13 +262,13 @@ func warmUpModel() {
return
}
// Check if model is already loaded
loaded, err := isModelLoaded(chatBody.Model)
loaded, err := isModelLoaded(chatBody.GetModel())
if err != nil {
logger.Debug("failed to check model status", "model", chatBody.Model, "error", err)
logger.Debug("failed to check model status", "model", chatBody.GetModel(), "error", err)
// Continue with warmup attempt anyway
}
if loaded {
showToast("model already loaded", "Model "+chatBody.Model+" is already loaded.")
showToast("model already loaded", "Model "+chatBody.GetModel()+" is already loaded.")
return
}
go func() {
@@ -277,7 +277,7 @@ func warmUpModel() {
switch {
case strings.HasSuffix(cfg.CurrentAPI, "/completion"):
// Old completion endpoint
req := models.NewLCPReq(".", chatBody.Model, nil, map[string]float32{
req := models.NewLCPReq(".", chatBody.GetModel(), nil, map[string]float32{
"temperature": 0.8,
"dry_multiplier": 0.0,
"min_p": 0.05,
@@ -289,7 +289,7 @@ func warmUpModel() {
// OpenAI-compatible chat endpoint
req := models.OpenAIReq{
ChatBody: &models.ChatBody{
Model: chatBody.Model,
Model: chatBody.GetModel(),
Messages: []models.RoleMsg{
{Role: "system", Content: "."},
},
@@ -313,7 +313,7 @@ func warmUpModel() {
}
resp.Body.Close()
// Start monitoring for model load completion
monitorModelLoad(chatBody.Model)
monitorModelLoad(chatBody.GetModel())
}()
}
@@ -418,7 +418,9 @@ func fetchLCPModelsWithStatus() (*models.LCPModels, error) {
if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
return nil, err
}
localModelsMu.Lock()
localModelsData = data
localModelsMu.Unlock()
return data, nil
}
@@ -821,10 +823,10 @@ func chatRound(r *models.ChatRoundReq) error {
}
go sendMsgToLLM(reader)
logger.Debug("looking at vars in chatRound", "msg", r.UserMsg, "regen", r.Regen, "resume", r.Resume)
msgIdx := len(chatBody.Messages)
msgIdx := chatBody.GetMessageCount()
if !r.Resume {
// Add empty message to chatBody immediately so it persists during Alt+T toggle
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
chatBody.AppendMessage(models.RoleMsg{
Role: botPersona, Content: "",
})
nl := "\n\n"
@@ -836,7 +838,7 @@ func chatRound(r *models.ChatRoundReq) error {
}
fmt.Fprintf(textView, "%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
} else {
msgIdx = len(chatBody.Messages) - 1
msgIdx = chatBody.GetMessageCount() - 1
}
respText := strings.Builder{}
toolResp := strings.Builder{}
@@ -893,7 +895,10 @@ out:
fmt.Fprint(textView, chunk)
respText.WriteString(chunk)
// Update the message in chatBody.Messages so it persists during Alt+T
chatBody.Messages[msgIdx].Content = respText.String()
chatBody.UpdateMessageFunc(msgIdx, func(msg models.RoleMsg) models.RoleMsg {
msg.Content = respText.String()
return msg
})
if scrollToEndEnabled {
textView.ScrollToEnd()
}
@@ -936,29 +941,32 @@ out:
}
botRespMode = false
if r.Resume {
chatBody.Messages[len(chatBody.Messages)-1].Content += respText.String()
updatedMsg := chatBody.Messages[len(chatBody.Messages)-1]
processedMsg := processMessageTag(&updatedMsg)
chatBody.Messages[len(chatBody.Messages)-1] = *processedMsg
if msgStats != nil && chatBody.Messages[len(chatBody.Messages)-1].Role != cfg.ToolRole {
chatBody.Messages[len(chatBody.Messages)-1].Stats = msgStats
}
chatBody.UpdateMessageFunc(chatBody.GetMessageCount()-1, func(msg models.RoleMsg) models.RoleMsg {
msg.Content += respText.String()
processedMsg := processMessageTag(&msg)
if msgStats != nil && processedMsg.Role != cfg.ToolRole {
processedMsg.Stats = msgStats
}
return *processedMsg
})
} else {
chatBody.Messages[msgIdx].Content = respText.String()
processedMsg := processMessageTag(&chatBody.Messages[msgIdx])
chatBody.Messages[msgIdx] = *processedMsg
if msgStats != nil && chatBody.Messages[msgIdx].Role != cfg.ToolRole {
chatBody.Messages[msgIdx].Stats = msgStats
}
stopTTSIfNotForUser(&chatBody.Messages[msgIdx])
chatBody.UpdateMessageFunc(msgIdx, func(msg models.RoleMsg) models.RoleMsg {
msg.Content = respText.String()
processedMsg := processMessageTag(&msg)
if msgStats != nil && processedMsg.Role != cfg.ToolRole {
processedMsg.Stats = msgStats
}
return *processedMsg
})
stopTTSIfNotForUser(&chatBody.GetMessages()[msgIdx])
}
cleanChatBody()
refreshChatDisplay()
updateStatusLine()
// bot msg is done;
// now check it for func call
// logChat(activeChatName, chatBody.Messages)
if err := updateStorageChat(activeChatName, chatBody.Messages); err != nil {
// logChat(activeChatName, chatBody.GetMessages())
if err := updateStorageChat(activeChatName, chatBody.GetMessages()); err != nil {
logger.Warn("failed to update storage", "error", err, "name", activeChatName)
}
// Strip think blocks before parsing for tool calls
@@ -973,8 +981,8 @@ out:
// If so, trigger those characters to respond if that char is not controlled by user
// perhaps we should have narrator role to determine which char is next to act
if cfg.AutoTurn {
lastMsg := chatBody.Messages[len(chatBody.Messages)-1]
if len(lastMsg.KnownTo) > 0 {
lastMsg, ok := chatBody.GetLastMessage()
if ok && len(lastMsg.KnownTo) > 0 {
triggerPrivateMessageResponses(&lastMsg)
}
}
@@ -983,13 +991,15 @@ out:
// cleanChatBody removes messages with null or empty content to prevent API issues
func cleanChatBody() {
if chatBody == nil || chatBody.Messages == nil {
if chatBody == nil || chatBody.GetMessageCount() == 0 {
return
}
// Tool request cleaning is now configurable via AutoCleanToolCallsFromCtx (default false)
// /completion msg where part meant for user and other part tool call
// chatBody.Messages = cleanToolCalls(chatBody.Messages)
chatBody.Messages = consolidateAssistantMessages(chatBody.Messages)
chatBody.WithLock(func(cb *models.ChatBody) {
cb.Messages = consolidateAssistantMessages(cb.Messages)
})
}
// convertJSONToMapStringString unmarshals JSON into map[string]interface{} and converts all values to strings.
@@ -1089,7 +1099,7 @@ func findCall(msg, toolCall string) bool {
Content: fmt.Sprintf("Error processing tool call: %v. Please check the JSON format and try again.", err),
ToolCallID: lastToolCall.ID, // Use the stored tool call ID
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
chatBody.AppendMessage(toolResponseMsg)
// Clear the stored tool call ID after using it (no longer needed)
// Trigger the assistant to continue processing with the error message
crr := &models.ChatRoundReq{
@@ -1126,7 +1136,7 @@ func findCall(msg, toolCall string) bool {
Role: cfg.ToolRole,
Content: "Error processing tool call: no valid JSON found. Please check the JSON format.",
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
chatBody.AppendMessage(toolResponseMsg)
crr := &models.ChatRoundReq{
Role: cfg.AssistantRole,
}
@@ -1143,8 +1153,8 @@ func findCall(msg, toolCall string) bool {
Role: cfg.ToolRole,
Content: fmt.Sprintf("Error processing tool call: %v. Please check the JSON format and try again.", err),
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
logger.Debug("findCall: added tool error response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "message_count_after_add", len(chatBody.Messages))
chatBody.AppendMessage(toolResponseMsg)
logger.Debug("findCall: added tool error response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "message_count_after_add", chatBody.GetMessageCount())
// Trigger the assistant to continue processing with the error message
// chatRound("", cfg.AssistantRole, tv, false, false)
crr := &models.ChatRoundReq{
@@ -1162,17 +1172,23 @@ func findCall(msg, toolCall string) bool {
// we got here => last msg recognized as a tool call (correct or not)
// Use the tool call ID from streaming response (lastToolCall.ID)
// Don't generate random ID - the ID should match between assistant message and tool response
lastMsgIdx := len(chatBody.Messages) - 1
lastMsgIdx := chatBody.GetMessageCount() - 1
if lastToolCall.ID != "" {
chatBody.Messages[lastMsgIdx].ToolCallID = lastToolCall.ID
chatBody.UpdateMessageFunc(lastMsgIdx, func(msg models.RoleMsg) models.RoleMsg {
msg.ToolCallID = lastToolCall.ID
return msg
})
}
// Store tool call info in the assistant message
// Convert Args map to JSON string for storage
chatBody.Messages[lastMsgIdx].ToolCall = &models.ToolCall{
ID: lastToolCall.ID,
Name: lastToolCall.Name,
Args: mapToString(lastToolCall.Args),
}
chatBody.UpdateMessageFunc(lastMsgIdx, func(msg models.RoleMsg) models.RoleMsg {
msg.ToolCall = &models.ToolCall{
ID: lastToolCall.ID,
Name: lastToolCall.Name,
Args: mapToString(lastToolCall.Args),
}
return msg
})
// call a func
_, ok := fnMap[fc.Name]
if !ok {
@@ -1183,8 +1199,8 @@ func findCall(msg, toolCall string) bool {
Content: m,
ToolCallID: lastToolCall.ID, // Use the stored tool call ID
}
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages))
chatBody.AppendMessage(toolResponseMsg)
logger.Debug("findCall: added tool not implemented response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", chatBody.GetMessageCount())
// Clear the stored tool call ID after using it
lastToolCall.ID = ""
// Trigger the assistant to continue processing with the new tool response
@@ -1255,9 +1271,9 @@ func findCall(msg, toolCall string) bool {
}
}
fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
"\n\n", len(chatBody.Messages), cfg.ToolRole, toolResponseMsg.GetText())
chatBody.Messages = append(chatBody.Messages, toolResponseMsg)
logger.Debug("findCall: added actual tool response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", len(chatBody.Messages))
"\n\n", chatBody.GetMessageCount(), cfg.ToolRole, toolResponseMsg.GetText())
chatBody.AppendMessage(toolResponseMsg)
logger.Debug("findCall: added actual tool response", "role", toolResponseMsg.Role, "content_len", len(toolResponseMsg.Content), "tool_call_id", toolResponseMsg.ToolCallID, "message_count_after_add", chatBody.GetMessageCount())
// Clear the stored tool call ID after using it
lastToolCall.ID = ""
// Trigger the assistant to continue processing with the new tool response
@@ -1497,7 +1513,7 @@ func init() {
// load cards
basicCard.Role = cfg.AssistantRole
logLevel.Set(slog.LevelInfo)
logger = slog.New(slog.NewTextHandler(logfile, &slog.HandlerOptions{Level: logLevel}))
logger = slog.New(slog.NewTextHandler(logfile, &slog.HandlerOptions{Level: logLevel, AddSource: true}))
store = storage.NewProviderSQL(cfg.DBPATH, logger)
if store == nil {
cancel()
@@ -1521,11 +1537,11 @@ func init() {
}
lastToolCall = &models.FuncCall{}
lastChat := loadOldChatOrGetNew()
chatBody = &models.ChatBody{
chatBody = models.NewSafeChatBody(&models.ChatBody{
Model: "modelname",
Stream: true,
Messages: lastChat,
}
})
choseChunkParser()
httpClient = createClient(time.Second * 90)
if cfg.TTS_ENABLED {