fix(telegram): clean up subagent tool feedback

This commit is contained in:
Anton Bogdanovich 2026-05-06 15:41:49 -07:00
parent 5292ddf325
commit fdbca6d56d
6 changed files with 131 additions and 0 deletions

View file

@ -49,3 +49,12 @@ func (a *channelManagerAdapter) DismissToolFeedback(
) {
a.inner.DismissToolFeedback(ctx, channel, chatID, outboundCtx)
}
func (a *channelManagerAdapter) DismissToolFeedbackForSession(
ctx context.Context,
channel, chatID string,
outboundCtx *bus.InboundContext,
sessionKey string,
) {
a.inner.DismissToolFeedbackForSession(ctx, channel, chatID, outboundCtx, sessionKey)
}

View file

@ -51,4 +51,16 @@ type ChannelManager interface {
// outboundCtx carries topic/thread info needed for channels that use
// scoped tracker keys (e.g., Telegram forum topics); may be nil.
DismissToolFeedback(ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext)
// DismissToolFeedbackForSession clears a session-scoped tool feedback
// message. This is used for background sub-turns whose progress messages
// are visible in the originating chat, but whose final result is delivered
// asynchronously through the parent turn instead of as a direct channel
// response.
DismissToolFeedbackForSession(
ctx context.Context,
channel, chatID string,
outboundCtx *bus.InboundContext,
sessionKey string,
)
}

View file

@ -447,6 +447,17 @@ func spawnSubTurn(
// Result Delivery Strategy (Async vs Sync)
if cfg.Async {
if al != nil && al.channelManager != nil && childTS.channel != "" {
dismissCtx, dismissCancel := context.WithTimeout(context.Background(), 5*time.Second)
al.channelManager.DismissToolFeedbackForSession(
dismissCtx,
childTS.channel,
childTS.chatID,
childTS.opts.InboundContext,
childID,
)
dismissCancel()
}
deliverSubTurnResult(al, parentTS, childID, result)
}

View file

@ -206,6 +206,32 @@ func dismissTrackedToolFeedbackMessage(
}
}
func dismissTrackedToolFeedbackMessageForSession(
ctx context.Context,
ch Channel,
chatID string,
outboundCtx *bus.InboundContext,
sessionKey string,
) {
sessionKey = strings.TrimSpace(sessionKey)
if sessionKey == "" {
dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx)
return
}
trackedChatID := resolveOutboundChatID(ch, chatID, outboundCtx)
if trackedChatID == "" {
return
}
trackedChatID += "#session:" + sessionKey
if cleaner, ok := ch.(toolFeedbackMessageCleaner); ok {
cleaner.DismissToolFeedbackMessage(ctx, trackedChatID)
return
}
if tracker, ok := ch.(toolFeedbackMessageTracker); ok {
tracker.ClearToolFeedbackMessage(trackedChatID)
}
}
func clearTrackedToolFeedbackMessage(
ch Channel,
chatID string,
@ -235,6 +261,16 @@ func (m *Manager) DismissToolFeedback(
dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx)
}
func (m *Manager) DismissToolFeedbackForSession(
ctx context.Context, channelName, chatID string, outboundCtx *bus.InboundContext, sessionKey string,
) {
ch, ok := m.GetChannel(channelName)
if !ok {
return
}
dismissTrackedToolFeedbackMessageForSession(ctx, ch, chatID, outboundCtx, sessionKey)
}
func prepareToolFeedbackMessageContent(ch Channel, content string) string {
prepared := strings.TrimSpace(content)
if prepared == "" {

View file

@ -468,6 +468,7 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
// DeleteMessage implements channels.MessageDeleter.
func (c *TelegramChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error {
chatID = telegramToolFeedbackDeliveryChatID(chatID)
cid, _, err := parseTelegramChatID(chatID)
if err != nil {
return err

View file

@ -383,6 +383,7 @@ func TestSend_FinalReplyDoesNotFinalizeDifferentSessionToolFeedback(t *testing.T
},
}
ch := newTestChannel(t, caller)
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
baseCtx := bus.InboundContext{
Channel: "telegram",
@ -421,6 +422,67 @@ func TestSend_FinalReplyDoesNotFinalizeDifferentSessionToolFeedback(t *testing.T
assert.Equal(t, "1", msgID)
}
func TestSend_SessionScopedToolFeedbackUpdatesExistingTelegramMessage(t *testing.T) {
nextMessageID := 0
caller := &stubCaller{
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
nextMessageID++
return successResponseWithMessageID(t, nextMessageID), nil
},
}
ch := newTestChannel(t, caller)
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
baseCtx := bus.InboundContext{
Channel: "telegram",
ChatID: "-1001234567890",
TopicID: "42",
Raw: map[string]string{
"message_kind": "tool_feedback",
},
}
_, err := ch.Send(context.Background(), bus.OutboundMessage{
ChatID: "-1001234567890",
SessionKey: "subturn-1",
Content: "Working...\n• tool: `read_file`",
Context: baseCtx,
})
require.NoError(t, err)
ids, err := ch.Send(context.Background(), bus.OutboundMessage{
ChatID: "-1001234567890",
SessionKey: "subturn-1",
Content: "Working...\n• tool: `read_file`\n• tool: `mcp_gpt_researcher_deep_research`",
Context: baseCtx,
})
require.NoError(t, err)
require.Equal(t, []string{"1"}, ids)
require.Len(t, caller.calls, 2)
assert.Contains(t, caller.calls[1].URL, "editMessageText")
assert.NotContains(t, caller.calls[1].URL, "%23session")
assert.NotContains(t, caller.calls[1].URL, "#session")
}
func TestDismissToolFeedbackMessage_SessionScopedKeyDeletesTelegramMessage(t *testing.T) {
caller := &stubCaller{
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
return successResponse(t), nil
},
}
ch := newTestChannel(t, caller)
ch.RecordToolFeedbackMessage("-1001234567890/42#session:subturn-1", "7", "Working...\n• tool: `read_file`")
ch.DismissToolFeedbackMessage(context.Background(), "-1001234567890/42#session:subturn-1")
require.Len(t, caller.calls, 1)
assert.Contains(t, caller.calls[0].URL, "deleteMessage")
assert.NotContains(t, caller.calls[0].URL, "%23session")
assert.NotContains(t, caller.calls[0].URL, "#session")
_, ok := ch.currentToolFeedbackMessage("-1001234567890/42#session:subturn-1")
assert.False(t, ok)
}
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
ch := newTestChannel(t, &stubCaller{
callFn: func(context.Context, string, *ta.RequestData) (*ta.Response, error) {