Debounce and caching

This commit is contained in:
HugeFrog24
2026-07-24 23:41:28 +02:00
parent 0543283b8a
commit c36e1846f5
16 changed files with 1332 additions and 81 deletions
+39 -3
View File
@@ -29,6 +29,9 @@ type Bot struct {
botID uint
albumBuffers map[string]*pendingAlbum
albumBuffersMu sync.Mutex
intakeBuffers map[int64]*pendingIntake
intakeBuffersMu sync.Mutex
intakeSeq uint64
}
func messageType(msg *models.Message) string {
@@ -90,6 +93,7 @@ func NewBot(db *gorm.DB, config BotConfig, clock Clock, tgClient TelegramClient)
botID: botEntry.ID,
tgBot: tgClient,
albumBuffers: make(map[string]*pendingAlbum),
intakeBuffers: make(map[int64]*pendingIntake),
}
if tgClient == nil {
@@ -298,9 +302,39 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.BetaMes
}
contextMessages = append(contextMessages, param)
}
if b.config.CacheHistoryEnabled() {
markTrailingCacheBreakpoint(contextMessages)
}
return contextMessages
}
// markTrailingCacheBreakpoint puts a cache_control breakpoint on the final
// content block of the conversation, so the next turn reads the whole prefix
// from cache instead of reprocessing it. The system prompt keeps its own
// breakpoint; tools and system render ahead of messages, so the two compose.
//
// Caveat worth knowing when reading [usage] lines: chat memory is a sliding
// window. Once it is full, each new turn evicts the oldest message, which
// changes the prefix and forces a miss. Until then, and for any chat shorter
// than the window, this converts a full-price reread into a cache read.
func markTrailingCacheBreakpoint(messages []anthropic.BetaMessageParam) {
if len(messages) == 0 {
return
}
blocks := messages[len(messages)-1].Content
if len(blocks) == 0 {
return
}
switch last := &blocks[len(blocks)-1]; {
case last.OfText != nil:
last.OfText.CacheControl = anthropic.NewBetaCacheControlEphemeralParam()
case last.OfImage != nil:
last.OfImage.CacheControl = anthropic.NewBetaCacheControlEphemeralParam()
}
}
func contentBlocksForMessage(msg Message) []anthropic.BetaContentBlockParamUnion {
var blocks []anthropic.BetaContentBlockParamUnion
if msg.IsUser && len(msg.ImageFileIDs) > 0 {
@@ -706,16 +740,18 @@ func (b *Bot) screenOutgoingMessage(chatID int64, response string) (Message, err
return Message{}, err
}
// Mark every outstanding user message in the chat, not just the newest one.
// A coalesced turn answers the whole batch, so a single-row update would
// leave the earlier messages permanently unanswered. This also drops an
// UPDATE ... ORDER BY ... LIMIT, which stock SQLite builds do not support.
now := time.Now()
err := b.db.Model(&Message{}).
Where("chat_id = ? AND bot_id = ? AND is_user = ? AND answered_on IS NULL",
chatID, b.botID, true).
Order("timestamp DESC").
Limit(1).
Update("answered_on", now).Error
if err != nil {
ErrorLogger.Printf("Error marking user message as answered: %v", err)
ErrorLogger.Printf("Error marking user messages as answered: %v", err)
}
chatMemory := b.getOrCreateChatMemory(chatID)