Reduce Telegram tool message noise

This commit is contained in:
Codex
2026-05-21 11:20:44 +00:00
parent 139471df31
commit e9425c6d9b
4 changed files with 422 additions and 11 deletions

View File

@@ -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