Reduce Telegram tool message noise
This commit is contained in:
@@ -42,9 +42,20 @@ type Bot struct {
|
||||
}
|
||||
|
||||
type outputState struct {
|
||||
chatID int64
|
||||
assistant strings.Builder
|
||||
sentAny bool
|
||||
chatID int64
|
||||
assistant strings.Builder
|
||||
sentAny bool
|
||||
tools map[string]toolMessageState
|
||||
workingIndicatorOff context.CancelFunc
|
||||
}
|
||||
|
||||
type toolMessageState struct {
|
||||
chatID int64
|
||||
messageID int
|
||||
toolHTML string
|
||||
approvalHTML string
|
||||
approvalMarkup *InlineKeyboardMarkup
|
||||
editedAt string
|
||||
}
|
||||
|
||||
type codexThreadItemView struct {
|
||||
@@ -1039,9 +1050,9 @@ func (b *Bot) handleApprovalCallback(ctx context.Context, callback *CallbackQuer
|
||||
if err := b.tg.AnswerCallbackQuery(ctx, callback.ID, "Sent to Codex."); err != nil {
|
||||
return err
|
||||
}
|
||||
updated := renderApprovalHTML(approval.Kind, json.RawMessage(approval.PayloadJSON), approvalStatusLine(decision))
|
||||
updated := b.resolveApprovalMessageHTML(approval, decision)
|
||||
_, err = b.tg.EditMessageText(ctx, callback.Message.Chat.ID, callback.Message.MessageID, updated, EditMessageTextOptions{ParseMode: "HTML"})
|
||||
return err
|
||||
return ignoreTelegramMessageNotModified(err)
|
||||
}
|
||||
|
||||
func (b *Bot) handleCodexEvents(ctx context.Context) {
|
||||
@@ -1288,7 +1299,7 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
||||
return b.flushAssistantMessage(ctx, params.ThreadID)
|
||||
}
|
||||
if params.ThreadID != "" {
|
||||
return b.sendOutputHTMLBlock(ctx, params.ThreadID, renderCodexItemStarted(item))
|
||||
return b.upsertToolMessage(ctx, params.ThreadID, item.ID, renderCodexItemStarted(item))
|
||||
}
|
||||
case "item/agentMessage/delta":
|
||||
var params struct {
|
||||
@@ -1322,7 +1333,7 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event)
|
||||
return b.flushAssistantMessage(ctx, params.ThreadID)
|
||||
}
|
||||
if params.ThreadID != "" {
|
||||
return b.sendOutputHTMLBlock(ctx, params.ThreadID, renderCodexItemCompleted(item))
|
||||
return b.upsertToolMessage(ctx, params.ThreadID, item.ID, renderCodexItemCompleted(item))
|
||||
}
|
||||
case "turn/diff/updated":
|
||||
var params struct {
|
||||
@@ -1424,9 +1435,16 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event
|
||||
return err
|
||||
}
|
||||
text := renderApprovalHTML(kind, event.Params, "")
|
||||
markup := approvalMarkup(approval.ID)
|
||||
if msg, ok, err := b.attachApprovalToToolMessage(ctx, params.ThreadID, params.ItemID, text, markup); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
return b.store.UpdatePendingApprovalMessage(ctx, approval.ID, msg.Chat.ID, msg.MessageID)
|
||||
}
|
||||
|
||||
msg, err := b.tg.SendMessage(ctx, thread.TelegramUserID, text, SendMessageOptions{
|
||||
ParseMode: "HTML",
|
||||
ReplyMarkup: approvalMarkup(approval.ID),
|
||||
ReplyMarkup: markup,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1434,16 +1452,73 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event
|
||||
return b.store.UpdatePendingApprovalMessage(ctx, approval.ID, msg.Chat.ID, msg.MessageID)
|
||||
}
|
||||
|
||||
func (b *Bot) newOutputState(chatID int64) *outputState {
|
||||
return &outputState{
|
||||
chatID: chatID,
|
||||
tools: make(map[string]toolMessageState),
|
||||
workingIndicatorOff: b.startWorkingIndicator(chatID),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) registerOutput(threadID string, chatID int64) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.outputs[threadID] = &outputState{chatID: chatID}
|
||||
if state := b.outputs[threadID]; state != nil && state.workingIndicatorOff != nil {
|
||||
state.workingIndicatorOff()
|
||||
}
|
||||
b.outputs[threadID] = b.newOutputState(chatID)
|
||||
}
|
||||
|
||||
func (b *Bot) clearOutput(threadID string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
state := b.outputs[threadID]
|
||||
delete(b.outputs, threadID)
|
||||
b.mu.Unlock()
|
||||
if state != nil && state.workingIndicatorOff != nil {
|
||||
state.workingIndicatorOff()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) startWorkingIndicator(chatID int64) context.CancelFunc {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
draftID := time.Now().UnixNano()
|
||||
go func() {
|
||||
useDraft := true
|
||||
sendDraft := func() bool {
|
||||
return b.tg.SendMessageDraft(ctx, chatID, draftID, "") == nil
|
||||
}
|
||||
sendTyping := func() {
|
||||
if err := b.tg.SendChatAction(ctx, chatID, "typing"); err != nil && ctx.Err() == nil {
|
||||
b.logger.Printf("send typing action: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !sendDraft() {
|
||||
useDraft = false
|
||||
sendTyping()
|
||||
}
|
||||
|
||||
draftTicker := time.NewTicker(25 * time.Second)
|
||||
typingTicker := time.NewTicker(4 * time.Second)
|
||||
defer draftTicker.Stop()
|
||||
defer typingTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-draftTicker.C:
|
||||
if useDraft && !sendDraft() {
|
||||
useDraft = false
|
||||
sendTyping()
|
||||
}
|
||||
case <-typingTicker.C:
|
||||
if !useDraft {
|
||||
sendTyping()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return cancel
|
||||
}
|
||||
|
||||
func (b *Bot) hasAssistantText(threadID string) bool {
|
||||
@@ -1513,6 +1588,182 @@ func (b *Bot) sendOutputHTMLBlock(ctx context.Context, threadID, htmlText string
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s toolMessageState) html() string {
|
||||
return FitHTMLMessage(addEditedAtLine(combineToolApprovalHTML(s.toolHTML, s.approvalHTML), s.editedAt), TelegramHTMLMessageLimit)
|
||||
}
|
||||
|
||||
func combineToolApprovalHTML(toolHTML, approvalHTML string) string {
|
||||
toolHTML = strings.TrimSpace(toolHTML)
|
||||
approvalHTML = strings.TrimSpace(approvalHTML)
|
||||
switch {
|
||||
case toolHTML == "":
|
||||
return approvalHTML
|
||||
case approvalHTML == "":
|
||||
return toolHTML
|
||||
default:
|
||||
return toolHTML + "\n\n" + approvalHTML
|
||||
}
|
||||
}
|
||||
|
||||
func addEditedAtLine(htmlText, editedAt string) string {
|
||||
htmlText = strings.TrimSpace(htmlText)
|
||||
if htmlText == "" || editedAt == "" {
|
||||
return htmlText
|
||||
}
|
||||
line := EscapeHTML("Edited at: " + editedAt)
|
||||
quoteIndex := strings.Index(htmlText, "<blockquote expandable>")
|
||||
if quoteIndex < 0 {
|
||||
return htmlText + "\n" + line
|
||||
}
|
||||
summary := strings.TrimRight(htmlText[:quoteIndex], "\n")
|
||||
details := strings.TrimLeft(htmlText[quoteIndex:], "\n")
|
||||
if summary == "" {
|
||||
return line + "\n" + details
|
||||
}
|
||||
return summary + "\n" + line + "\n" + details
|
||||
}
|
||||
|
||||
func editedAtTimestamp() string {
|
||||
return time.Now().UTC().Format("2006-01-02 15:04:05 MST")
|
||||
}
|
||||
|
||||
func (b *Bot) upsertToolMessage(ctx context.Context, threadID, itemID, htmlText string) error {
|
||||
htmlText = strings.TrimSpace(htmlText)
|
||||
if htmlText == "" {
|
||||
return nil
|
||||
}
|
||||
if itemID == "" {
|
||||
return b.sendOutputHTMLBlock(ctx, threadID, htmlText)
|
||||
}
|
||||
if err := b.flushAssistantMessage(ctx, threadID); err != nil {
|
||||
return err
|
||||
}
|
||||
chatID, err := b.outputChatID(ctx, threadID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
state := b.outputs[threadID]
|
||||
if state != nil && state.tools == nil {
|
||||
state.tools = make(map[string]toolMessageState)
|
||||
}
|
||||
if state != nil {
|
||||
tool, ok := state.tools[itemID]
|
||||
if ok && tool.messageID != 0 {
|
||||
tool.toolHTML = htmlText
|
||||
tool.editedAt = editedAtTimestamp()
|
||||
state.tools[itemID] = tool
|
||||
combined := tool.html()
|
||||
msgChatID := tool.chatID
|
||||
msgID := tool.messageID
|
||||
markup := tool.approvalMarkup
|
||||
b.mu.Unlock()
|
||||
_, err := b.tg.EditMessageText(ctx, msgChatID, msgID, combined, EditMessageTextOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||
if err := ignoreTelegramMessageNotModified(err); err != nil {
|
||||
return err
|
||||
}
|
||||
b.markOutputSent(threadID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
msg, err := b.sendHTMLMessage(ctx, chatID, htmlText, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.mu.Lock()
|
||||
state = b.outputs[threadID]
|
||||
if state != nil {
|
||||
if state.tools == nil {
|
||||
state.tools = make(map[string]toolMessageState)
|
||||
}
|
||||
state.tools[itemID] = toolMessageState{chatID: msg.Chat.ID, messageID: msg.MessageID, toolHTML: htmlText}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.markOutputSent(threadID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bot) attachApprovalToToolMessage(ctx context.Context, threadID, itemID, approvalHTML string, markup *InlineKeyboardMarkup) (Message, bool, error) {
|
||||
approvalHTML = strings.TrimSpace(approvalHTML)
|
||||
if threadID == "" || itemID == "" || approvalHTML == "" {
|
||||
return Message{}, false, nil
|
||||
}
|
||||
if err := b.flushAssistantMessage(ctx, threadID); err != nil {
|
||||
return Message{}, false, err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
state := b.outputs[threadID]
|
||||
if state == nil {
|
||||
b.mu.Unlock()
|
||||
return Message{}, false, nil
|
||||
}
|
||||
tool, ok := state.tools[itemID]
|
||||
if !ok || tool.messageID == 0 {
|
||||
b.mu.Unlock()
|
||||
return Message{}, false, nil
|
||||
}
|
||||
tool.approvalHTML = approvalHTML
|
||||
tool.approvalMarkup = markup
|
||||
tool.editedAt = editedAtTimestamp()
|
||||
state.tools[itemID] = tool
|
||||
combined := tool.html()
|
||||
msg := Message{MessageID: tool.messageID, Chat: Chat{ID: tool.chatID}}
|
||||
b.mu.Unlock()
|
||||
|
||||
_, err := b.tg.EditMessageText(ctx, msg.Chat.ID, msg.MessageID, combined, EditMessageTextOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||
if err := ignoreTelegramMessageNotModified(err); err != nil {
|
||||
b.clearToolApproval(threadID, itemID)
|
||||
b.logger.Printf("edit tool approval message %s/%s: %v", threadID, itemID, err)
|
||||
return Message{}, false, nil
|
||||
}
|
||||
b.markOutputSent(threadID)
|
||||
return msg, true, nil
|
||||
}
|
||||
|
||||
func (b *Bot) clearToolApproval(threadID, itemID string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if state := b.outputs[threadID]; state != nil {
|
||||
if tool, ok := state.tools[itemID]; ok {
|
||||
tool.approvalHTML = ""
|
||||
tool.approvalMarkup = nil
|
||||
state.tools[itemID] = tool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) resolveApprovalMessageHTML(approval store.PendingApproval, decision string) string {
|
||||
approvalHTML := renderApprovalHTML(approval.Kind, json.RawMessage(approval.PayloadJSON), approvalStatusLine(decision))
|
||||
if approval.ItemID == "" {
|
||||
return approvalHTML
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
state := b.outputs[approval.CodexThreadID]
|
||||
if state == nil {
|
||||
return approvalHTML
|
||||
}
|
||||
tool, ok := state.tools[approval.ItemID]
|
||||
if !ok || tool.messageID == 0 || tool.messageID != approval.MessageID {
|
||||
return approvalHTML
|
||||
}
|
||||
tool.approvalHTML = approvalHTML
|
||||
tool.approvalMarkup = nil
|
||||
tool.editedAt = editedAtTimestamp()
|
||||
state.tools[approval.ItemID] = tool
|
||||
return tool.html()
|
||||
}
|
||||
|
||||
func ignoreTelegramMessageNotModified(err error) error {
|
||||
if err != nil && strings.Contains(err.Error(), "message is not modified") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (b *Bot) appendAssistantDelta(ctx context.Context, threadID, delta string) error {
|
||||
if delta == "" {
|
||||
return nil
|
||||
@@ -1560,8 +1811,12 @@ func (b *Bot) completeTurnOutput(ctx context.Context, threadID string) error {
|
||||
}
|
||||
chatID := state.chatID
|
||||
sentAny := state.sentAny
|
||||
workingIndicatorOff := state.workingIndicatorOff
|
||||
delete(b.outputs, threadID)
|
||||
b.mu.Unlock()
|
||||
if workingIndicatorOff != nil {
|
||||
workingIndicatorOff()
|
||||
}
|
||||
|
||||
if !sentAny {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, "Done.", SendMessageOptions{})
|
||||
@@ -1606,10 +1861,14 @@ func (b *Bot) sendLong(ctx context.Context, chatID int64, text string) error {
|
||||
}
|
||||
|
||||
func (b *Bot) sendHTML(ctx context.Context, chatID int64, htmlText string) error {
|
||||
_, err := b.tg.SendMessage(ctx, chatID, htmlText, SendMessageOptions{ParseMode: "HTML"})
|
||||
_, err := b.sendHTMLMessage(ctx, chatID, htmlText, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Bot) sendHTMLMessage(ctx context.Context, chatID int64, htmlText string, markup *InlineKeyboardMarkup) (Message, error) {
|
||||
return b.tg.SendMessage(ctx, chatID, htmlText, SendMessageOptions{ParseMode: "HTML", ReplyMarkup: markup})
|
||||
}
|
||||
|
||||
func (b *Bot) sendError(ctx context.Context, chatID int64, prefix string, err error) error {
|
||||
_, sendErr := b.tg.SendMessage(ctx, chatID, EscapeHTML(prefix+": "+err.Error()), SendMessageOptions{ParseMode: "HTML"})
|
||||
return sendErr
|
||||
|
||||
Reference in New Issue
Block a user