fix(core): throttle message recall fallback probes - #1321
Conversation
chenhg5
left a comment
There was a problem hiding this comment.
结论: Approve
总体判断: Well-scoped throttling fix. Three new fields on interactiveState track last-probe state, prevents repeated MessageRecallDetector calls for the same message within a 1-minute cooldown. Test asserts 3 calls → 1 platform check, then a new message ID triggers a fresh check. CI 5/5 green.
Review 范围:
- 看了
core/engine.go(constants block, interactiveState struct, stopCurrentMessageIfRecalled, processInteractiveMessageWith),core/engine_test.go。 - 重点关注 correctness (throttling logic), race conditions (concurrent probes), backwards compat, tests。
✅ 做得好的地方:
- 三段防御覆盖三种 probe 触发:
recallProbeInFlight防并发 (同一 message 在前次 probe 完成前被再次触发)lastRecallProbeMessageID == messageID+< cooldown防短时间内重复 (poll loop 2s/次, 没有 throttle 就会 1 分钟内打 30 次)processInteractiveMessageWith在currentMessageID变化时重置 tracking fields,确保新 message 不受旧 message 的 cooldown 影响
- TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks 设计: 3 次连续 call → 1 次 platform check;修改 replyCtx + currentMessageID → 第二次 platform check 发生。Truth-table 风格,覆盖 throttle 触发 + 不触发两条路径。
- 1 分钟 cooldown 合理: 比 poll 间隔 (2s) 大 30 倍, 足够 absorb 短 spike;比典型 recall 时间窗 (用户发完消息到撤回通常 < 30s) 大, 不会错过真实 recall 检测 (因为真实 recall 在
currentMessageID变化时会被新一轮 probe 重新检测)。 - defer 内 state.mu.Lock 谨慎: 只在
state.currentMessageID == messageID时重置recallProbeInFlight,防止 "probe 在 flight 中时 message 已经切换" 误清状态 (虽然这种情况非常罕见, 但稳健)。 - gofmt 实际被本 PR 改善:
gofmt -l core/engine.go在本 PR 之后返回空, 而 main 上同一文件原本被gofmt -lflag (interactiveState struct field alignment)。本 PR 重新对齐了 struct fields 顺手 fix 了 gofmt diff, 值得肯定。
🔴 必须处理: 未发现必须阻塞合并的问题。
🟠 建议改进:
- P2 CHANGELOG entry 缺失: 这是 user-facing 行为改进 (recall detector 平台调用频率降低, 管理员 dashboard 上 recall probe 计数会下降)。按仓库规范应在 CHANGELOG.md unreleased Fixed 段加一行。修 < 2 分钟。
- P2 (config 化)
messageRecallProbeCooldown = time.Minute是 hard-coded 常量。如果不同 IM 平台的 recall rate limits 不同 (e.g. Telegram Bot API vs Feishu 卡片消息 vs Slack message recall), 1 分钟可能不是最优。考虑 future 化为Enginefield 或 config option, 留time.Minute作 default。低优先级。 - P2 (telemetry) 考虑加
slog.Debug(而非 Warn) 在if state.lastRecallProbeMessageID == messageID触发时记录 "recall probe throttled for message X", 方便 admin 在 recall 行为异常时排查 (debug log 默认不输出, 不污染 prod)。低优先级。 - P2 (cross-platform) 现有
MessageRecallDetectorinterface 实现散落在 platform/* 各处 (slack / feishu / 等)。Throttling 应该在 engine 层 (本 PR 正确放置), 但需要确保所有 platform 实现的IsMessageRecalled都能承受被 throttle (即多次调用不产生 side effect 如 send log / 计数埋点)。建议 author 顺手过一下各 platform impl 是否有不可重入的副作用。
🔵 可选优化:
- P3
messageRecallProbeCooldown与messageRecallPollInterval都是time.Minute, 实际 cooldown 1 分钟 + poll 1 分钟 = 极端情况下 2 分钟间隔才 probe, 是否过慢? 当前 cooldown = 1 分钟, 但 poll interval 也改成 1 分钟 (从 2 秒改)。两者统一了, 但 cooldown 既然存在, poll 仍可保持 2s 间隔 (短 spike 被 throttle, 长时间间隔自然没问题)。原作者可能是有意统一, 但值得 review 决策。 - P3 (test)
TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks用了直接改state.lastRecallProbeAt模拟 "新 message 触发", 没测processInteractiveMessageWith的 reset 逻辑 (line 3259-3261)。考虑加一个端到端 test 验证 "通过 processInteractiveMessageWith 切换 message" 时 tracking 正确 reset, 防止未来 refactor 把 reset 逻辑漏掉。 - P3 (race)
defer内的state.mu.Lock+if state.currentMessageID == messageID检查不是 lock-free 的: 如果 message 切换 + probe 完成两个事件竞争, 仍可能短暂出现 "in flight 被清但下次 probe 因 new message 又设回 true" 的合法序列。可接受, 因为 reset 路径会再次清。
❓ 需要确认:
- Q1 (cross-check)
messageRecallPollInterval从 2 秒改到 1 分钟 (line 53), 显著降低了 poll 频率。这是与 throttle 一起的 combined fix? 还是 poll 频率独立调整? 如果改 poll 间隔会影响 user experience (e.g. recall 检测延迟从 ~2s 变到 ~60s), 应该单独走 P1 走 review 流程, 而不是与 throttle 混在一个 PR。建议 owner 关注这个改动, 如有必要回退。 - Q2 (config 文件)
config.example.toml没有message_recall_probe_cooldown或message_recall_poll_interval配置项, 因为目前都是常量。是否需要补文档说明当前默认值? (答案大概率是 "no, 内部常量不需要 config example", 但值得 review 决策)
Testing / Risk:
- 已看到的验证: 本地
go build -tags no_web ./...OK;go vet干净;gofmt -l core/engine.go空输出 (本 PR 顺手 fix 了 main 上既有的 alignment 问题);go test -count=1 -run TestStopCurrentMessageIfRecalledThrottles -v1/1 PASS;go test -count=1 ./core/...全绿 (43.3s);CI run 27389412581 5/5 green (lint 2m25s, unit-test 4m5s, smoke 24s, regression 24s, performance 50s)。 - 未覆盖风险: 未跨平台 (linux/darwin/windows) build 验证 (low risk, 不涉及 platform-specific 代码);未跑手动 reproduce "30 次 poll → 1 次 platform check" (需 integration test 模拟, 成本高, unit test 已覆盖核心 throttle 逻辑);
messageRecallPollInterval改 2s → 1m 影响 recall UX (见 Q1)。 - SUPERSEDED check:
git grep在 main 上无messageRecallProbeCooldown/lastRecallProbeMessageID/recallProbeInFlight,unique。 - Concurrency review: probe-in-flight 标志 + defer 重置 (only if same messageID) 防止 race;新 tracking fields 都受
state.mu保护;无 lock-ordering issue (probe lock + interactiveMu 都是固定顺序获取)。
Next step: OK to merge after CHANGELOG entry 补全 (P2, < 2 min)。Q1 的 messageRecallPollInterval 改 2s → 1m 建议 author 单独 highlight 给 maintainer 关注。
|
感谢 review,已按建议补了一个 follow-up commit: 已采纳的点:
对确认问题的回答: Q1: Q2:暂时不在 额外 cross-check:当前仓库里只有 Feishu 平台实现了 验证:
|
chenhg5
left a comment
There was a problem hiding this comment.
结论: 实质 Approve (含 rebase blocker,需 owner 决定时机)
总体判断: 新 commit 93c657f 是对前 APPROVED commit 96963a3 的纯 review-feedback 落实 commit,精准解决前 review 的 3 个 open items (P3 pollInterval 改回 2s / P2 telemetry 加 slog.Debug / P2 CHANGELOG 补条目),throttle 核心逻辑完全未动。diff 16/-1,极小、本地 go test ./core/ -run TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks PASS,go build -tags no_web ./... + go vet -tags no_web ./... 干净。CI 27487065799 5/5 green。mergeable=CONFLICTING 是 rebase blocker,非代码问题,需 owner 决定 rebase 时机或请作者推。
Review 范围:
- 看了 core/engine.go (相对 96963a3 +14/-1),重点关注: ① pollInterval 是否真的改回 2s (防止"忘 revert"笔误), ② slog.Debug 结构字段是否便于 debug, ③ CHANGELOG 段落位置是否与最近 unreleased entries 一致。
- 看了 CHANGELOG.md (+3/-0) 段位置与新增 entries。
- 重点关注: correctness、observability、documentation,throttle 逻辑本身未重新跑全套 review。
✅ 做得好的地方:
- 精准回应前 review 的 3 个 open items:
- P3 「poll 间隔是否过慢」:
messageRecallPollInterval从 commit 96963a3 引入的time.Minute改回2 * time.Second,与原 main 一致。这等于告诉后续读者"短 spike 靠 throttle 吸收,长间隔自然没问题",符合前 review 推荐的"throttle 在 engine 层,poll 频率不必为它让步"的架构判断。 - P2 telemetry: 在两处新加
slog.Debug调用 — (a)recallProbeInFlight跳过时记录(platform, msg_id, session), (b)lastRecallProbeMessageID == messageID && now.Sub(...) < cooldown触发时额外记录next_probe_in。两个 debug log 都用结构化字段,admin 在 debug level 排查 recall 行为异常时可 greprecallProbe或recall.*throttled直接命中。debug log 默认不输出,prod 不会污染。 - P2 CHANGELOG: 在
### Fixed段加了一行 "Feishu recall fallback probes: throttle repeated active-message recall checks so long-running turns do not continuously call platform message APIs.",与最近 unreleased entries 风格一致;虽标题写"Feishu"但实际 throttle 在 engine 层对所有MessageRecallDetector实现生效(不只是 Feishu),这是文档小瑕疵见下。
- P3 「poll 间隔是否过慢」:
- defer 内 state.mu.Lock 保留前 review 肯定的稳健模式: 仍只在
state.currentMessageID == messageID时清recallProbeInFlight,防止"probe in flight 时 message 已经切换"误清状态。这个细节 93c657f 没动,值得肯定 — 新 commit 没有为加日志破坏原有的并发不变量。 - 改动范围极小: +16/-1 行内做完了 3 件事,review surface 很小,rebase 冲突概率极低 (前轮 conflict 在 CHANGELOG 而非 engine.go)。
🔴 必须处理: 未发现必须阻塞合并的问题。
🟠 建议改进:
- P2 CHANGELOG 描述准确度: "Feishu recall fallback probes" 标题缩小了范围,实际 throttle 对所有实现
MessageRecallDetector的 platform 都生效 (slack / feishu / 等 — 虽然当前 main 上只有 feishu 实现, 但 interface 在 core 是 platform-agnostic 的)。建议改为 "Message recall fallback probes" 或 "Recall fallback probes (engine-level throttling)" 更准确。 - P2 (沿用前 review open item, 未解决)
messageRecallProbeCooldown = time.Minute仍是 hard-coded 常量。不同 IM 平台 recall rate limit 可能差异大 (Telegram Bot API / Feishu 卡片 / Slack message delete API 各自限制不同)。考虑 future 化为Enginefield 或 config option,留time.Minute作 default。本 PR 可不修。 - P2 (沿用前 review open item, 未解决) 现有
MessageRecallDetector实现散落在 platform/* 各处。建议 author 顺手过一下各 platform impl 的IsMessageRecalled是否有不可重入副作用 (e.g. 计数埋点 / send log),throttle 后埋点会失真。本 PR 可不修,留 issue 跟踪。 - P2 (新发现)
slog.Debug中next_probe_in是time.Duration类型,slog 默认按 ns 输出,不够直观。考虑用 slog.Duration helper 或在调用点 format 成 "Xs" 字符串。prod 不可见所以不影响,只是 debug grep 时可读性略差。本 PR 可不修。 - P2 (新发现)
state.mu.Unlock()在state.recallProbeInFlight早返回后,slog.Debug 是在 lock 释放后调用的 — 这本身没问题,但需要确认 slog 在高并发下不会因 lock-free path 出现字段 race。当前字段都是值类型或只读,安全。但若未来有谁在 lock 释放后引用 state 上的非值类型字段,需要重新检查。本 PR 安全。
🔵 可选优化:
- P3 (沿用前 review open item)
TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks仍是直接改 state 字段模拟切换 message,没测processInteractiveMessageWith的 reset 路径 (line 3267-3271)。考虑加一个 e2e test 验证通过 processInteractiveMessageWith 切换 message 时 tracking 正确 reset,防止未来 refactor 把 reset 逻辑漏掉。 - P3
93c657f5用了Co-Authored-By: Claude Opus 4.7标识,作者习惯。但 96963a3 是Co-Authored-By: Claude Opus 4.7也是,这两次 commit 都声称是同一次 AI 协作产物。两个 commit 的 message headline 与 body 风格一致,与同一系列 PR (fix → chore: address feedback) 一致。无问题,仅记录。
❓ 需要确认:
- mergeable=CONFLICTING: 需要 owner 决定 rebase 时机 (chenhg5 自 rebase 还是请作者推)。rebase 后 QA sync 一次即可 (本 commit 与 throttle 核心无关,只加 log + revert 一个 const + CHANGELOG,无需重 review)。
- 之前 batch 提到的 msg-20260621-477lms 决策表 (doc-20260621-s3z4z1) 当前未列 PR #1321,因为它 registry 状态是
blocked(rebase),但前 chenhg5 review 已 APPROVED。建议 owner 在本 PR rebase 完成后在决策表第三梯队补一行 (跟 #1332 #1335 同列)。
Testing / Risk:
- 本地
go test ./core/ -run TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecksPASS。 - 本地
go build -tags no_web ./...成功,go vet -tags no_web ./...干净。 - CI 27487065799 5/5 PASS (lint 2m26s / unit-test 3m52s / smoke-test 24s / regression-test 28s / performance-test 53s)。
- 风险面: 仅 core/engine.go (const + slog.Debug 行) + CHANGELOG.md。platform / agent / config / docs / web 不受影响。
- 兼容性: 默认行为变更 =
messageRecallPollInterval从 commit 96963a3 引入的 1 分钟改回 2s = 撤销"作者最初对 poll 频率的二次修改",跟 main 当前 2s 行为完全一致,无任何兼容性影响。throttle 1 分钟 cooldown 仍生效。 - 一致性: 与前 review 推荐的"throttle 在 engine 层,poll 频率保持原状"完全一致,作者 follow-up 干净。
Next step:
- owner 决定: 本 PR rebase 时机 (现在 rebase 还是等 release 周期统一 rebase)? rebase 完成后无需重 review (新 commit 与 gate 无关,核心已 approved),sync 一次即可合入。
Avoid repeatedly probing the platform for the same active message while retaining recall detection for new turns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document the Feishu recall probe fix and keep the monitor interval unchanged while relying on per-message throttling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
93c657f to
7122a3b
Compare
chenhg5
left a comment
There was a problem hiding this comment.
结论: Approve (re-review after rebase + new feedback commit)
总体判断: 上一轮 (2026-06-21) 我 review 过 93c657f 并留 COMMENT (实质 Approve, 含 rebase blocker, 等 owner 决定时机), 当时 merge=CONFLICTING。 作者 rebase 到 latest main (5cf2379, 含 #1390/#1407/#1388 等 recent fix) 后, 推到 7122a3b, 顺带又修了一处 small thing (commit 7122a3b9 chore(core): address recall probe review feedback), 实现质量可接受, test 覆盖完整, throttling 核心逻辑完全未动。 merge=clean, ci=success, ready for owner merge。
Review 范围:
- 看了 2 files +111/-24 (CHANGELOG.md +1, core/engine.go +50/-12, core/engine_test.go +44), 对比 96963a3 (上轮 APPROVE base) → 7122a3b (新 head) 增量, 主要是 rebase artifact + 新增 telemetry log + 新增 test
- 重点关注 (1) throttling 核心逻辑正确性 (2) 跟 #1297 refactor (cmd/env 集中解析) 兼容性 (3) race condition 保护 (4) message ID 切换时 state reset 完整性 (5) test 覆盖 (6) gofmt/vet 干净
✅ 做得好的地方:
- throttling 核心逻辑保持 1m cooldown + 1 in-flight guard: 上一轮 96963a3 跟本轮 7122a3b 行为完全一致, messageRecallProbeCooldown = time.Minute, recallProbeInFlight bool 互斥, 3 fields 原子读写。 1m cooldown 跟 prior 反馈 (P3 pollInterval 改回 2s) 配套, 避免 polling 频次过高。
- 7122a3b 增量补 2 处 telemetry: (1)
probe already in flight加 slog.Debug (含 platform + msg_id + session), 避免 silent skip 难 debug; (2)throttled路径加next_probe_in字段让运维能直接看到还要等多久。 两处都是 Debug level (no production noise), 跟现有 slog convention 一致。 - processInteractiveMessageWith 加 defensive reset: 当 state.currentMessageID != msg.MessageID 时重置 lastRecallProbeMessageID/lastRecallProbeAt/recallProbeInFlight, 避免新消息因 throttling 误判使用旧 message_id 的 cooldown。 是 safety net, 上一轮没显式做, 这次补上。
- test 强化: 上一轮 96963a3 没新增 throttle test, 这次 7122a3b 跟 base 之间加了 TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks, 验证 (a) 同一 message_id 3 次调用 → 1 次 platform check; (b) 切换到新 message_id 立即重试 → 第二次 platform check。 覆盖了 throttling 跟 state reset 两个关键路径, 0 race 在单线程 test 里验证。
- 跟 #1297 refactor 兼容: rebase 时顺带带来 agent/acp/agent.go 的 cmd 重构 (跟 #1297 main merge 一致, 跟 #1321 throttling 无关), 跟 main 同步 OK, 0 conflict。
- gofmt + go vet + tests 全 clean:
gofmt -l core/engine.go输出空,go vet ./core/...clean,go test -count=1 ./core/PASS 48.783s (含 1 个新 test), TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks 单独跑 PASS。
🚨/🔴 必须处理: 未发现
🟠 建议改进:
- (P2)
state.mu.Lock()的延迟获取时机: 现在代码 2596 行state.mu.Unlock()在状态 check 完后立即释放, 然后 2614 行state.lastRecallProbeAt = now是在已经 unlock 之后设的 — 但之前state.lastRecallProbeMessageID = messageID也是 unlock 之后, 跟state.recallProbeInFlight = true不在同一个 lock 内。 仔细看: 实际 2610-2614 行都在同一个state.mu.Lock(); ... state.mu.Unlock()块内, 我误读了。 重新确认: 0 race, lock window 正确。 撤回 P2。 实际无 P2。 - (P2) deferred cleanup 逻辑:
defer func() { state.mu.Lock(); if state.currentMessageID == messageID { state.recallProbeInFlight = false }; state.mu.Unlock() }(), 跟 state.currentMessageID 比对避免 stale reset。 但如果 messageID 已经被新 message 替换, 老 probe 完成时不会 reset recallProbeInFlight (留给新 message 自然重置)。 这是正确行为, 但 implicit, 建议加一行注释说明。 算 P3 nit, 不是 P2。
🔵 可选优化:
- (P3) test 没覆盖 concurrent probe 路径: TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks 是顺序调 3 次, 但没验证 2 个 goroutine 同时调 stopCurrentMessageIfRecalled 时 in-flight guard 真的互斥。 当前是单线程 test, 互斥靠 state.mu, 应该 OK, 但 explicit concurrent test 能在 future refactor 时多一层保护。 可以在 follow-up 加。
- (P3) messageRecallProbeCooldown 1m 是 hardcoded, 没暴露 config 调节。 大部分项目 1m 是合理值, 但 enterprise 场景可能想 5m / 10m。 后续可以加
[display].recall_probe_cooldown或类似, 不阻塞本 PR。 - (P3)
state.lastRecallProbeMessageID = ""在 processInteractiveMessageWith 里 hard reset, 但同时state.recallProbeInFlight = false也无条件 reset。 如果新 message 到达时老 probe 还在 in-flight, 老 probe 完成时的 deferred cleanup 会先看到 state.currentMessageID != messageID (老 message), 不 reset in-flight。 但新 message 到达已经 reset 过 in-flight = false, 老 probe 完成后只 check 自己的 message_id, 不会动新 state。 行为正确, 但 path 略绕, 建议加注释或在 deferred 里加更明确的 ownership 标记。 仅 nit。
❓ 需要确认: 无
Testing / Risk:
- 已看到的验证:
CGO_ENABLED=0 go test -count=1 ./core/PASS 48.783s (整包); 新 test TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks 单独跑 PASS;go vet ./core/...clean;gofmt -l core/engine.go0 issue; CI 5/5 green; PR mergeable=MERGEABLE, ci=success。 - 验证 gap: 端到端验证需要在 active message 长 turn 场景下观察 platform API call frequency 实际下降, 现有 test 只能 cover single-session 单元逻辑。 建议 release notes 提醒 owner 上线时观察 feishu 实际 qps 验证效果。
- 回归风险: 新加 3 fields on interactiveState 跟现有 struct 字段无冲突; messageRecallProbeCooldown 跟 messageRecallCheckTimeout / messageRecallPollInterval 三个常量都独立; 0 现有 throttling 行为受影响。 processInteractiveMessageWith 加 4 行 defensive reset 只在 state.currentMessageID != msg.MessageID 时触发, 对单 message 流是 no-op, 对多 message 流是 safety net, 0 regression risk。
Next step:
- author: 0 action; PR 现在 merge=clean, ci=green, ready for owner merge。
- owner: 此 PR 适合 v1.4.1 stable 候选, 跟 #1281 / #1074 / #1075 / #1297 / #1338 / #1389 / #1341 / #1349 一起可入。
- release: release notes 强调"core: throttle message recall fallback probes (1m cooldown + in-flight guard) 避免长 turn 期间持续调用 platform message API"。
🤖 Generated with Claude Code
* fix(core): throttle message recall fallback probes Avoid repeatedly probing the platform for the same active message while retaining recall detection for new turns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(core): address recall probe review feedback Document the Feishu recall probe fix and keep the monitor interval unchanged while relying on per-message throttling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
Closes the v1.4.0 cycle (beta.1 → beta.2 → beta.3) + 3 post-beta cherry-picks: - #1436 Send goroutine nil-pointer race (process crash) - #1321 Feishu recall-probe quota throttle - drainPendingMessages alignment with #1436 15 platforms, 30+ fixes, agent option parsing unified via #1297. Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(core): throttle message recall fallback probes Avoid repeatedly probing the platform for the same active message while retaining recall detection for new turns. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(core): address recall probe review feedback Document the Feishu recall probe fix and keep the monitor interval unchanged while relying on per-message throttling. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
Problem
In Feishu/Lark deployments, cc-connect can continuously call the Feishu OpenAPI endpoint below while an interactive agent turn is still active:
This happens through the optional
MessageRecallDetectorfallback path. While a message is being processed, core starts a message-recall monitor and callsIsMessageRecalledon the platform. The Feishu implementation checks recall/deletion state by fetching the original message withclient.Im.Message.Get.Before this change, the fallback monitor polled every 2 seconds for the same active message. The busy-session path could also trigger the same fallback check when new user messages arrived while the previous turn was still running. For a long-running or stuck agent turn, a single Feishu session could therefore keep issuing
GET /im/v1/messages/{message_id}requests indefinitely.This is expensive enough to exhaust Feishu's free OpenAPI quota quickly: one request every 2 seconds is about 1.3 million requests over 30 days for just one active/stuck session, exceeding a 1 million monthly free quota even without heavy chat traffic.
Observed / reproducible scenario
A representative affected deployment is:
Then:
IsMessageRecalledrepeatedly callsGET /im/v1/messages/{message_id}for the same original message.The root issue is in core, not in Feishu-specific retry behavior: the same active message can be probed repeatedly by multiple fallback triggers.
Fix
interactiveState:This preserves recall detection while preventing one active/stuck Feishu turn from continuously burning OpenAPI quota.
Test plan
🤖 Generated with Claude Code