Skip to content

fix(core): throttle message recall fallback probes - #1321

Merged
chenhg5 merged 2 commits into
chenhg5:mainfrom
qvictl:fix/feishu-recall-probe-throttle
Jun 28, 2026
Merged

fix(core): throttle message recall fallback probes#1321
chenhg5 merged 2 commits into
chenhg5:mainfrom
qvictl:fix/feishu-recall-probe-throttle

Conversation

@qvictl

@qvictl qvictl commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Problem

In Feishu/Lark deployments, cc-connect can continuously call the Feishu OpenAPI endpoint below while an interactive agent turn is still active:

GET /open-apis/im/v1/messages/{message_id}?user_id_type=open_id

This happens through the optional MessageRecallDetector fallback path. While a message is being processed, core starts a message-recall monitor and calls IsMessageRecalled on the platform. The Feishu implementation checks recall/deletion state by fetching the original message with client.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:

platform = "feishu"
enable_feishu_card = true
progress_style = "card"

Then:

  1. A user sends a message to a Feishu-backed cc-connect session.
  2. The agent turn remains active for a long time, hangs, or otherwise does not reach normal completion.
  3. cc-connect starts the recall fallback monitor for that active message.
  4. Feishu IsMessageRecalled repeatedly calls GET /im/v1/messages/{message_id} for the same original message.
  5. The Feishu app shows high OpenAPI usage even though there is little or no new chat activity.

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

  • Increase the fallback recall monitor interval from 2 seconds to 1 minute.
  • Add per-active-message fallback probe state to interactiveState:
    • remember the last probed message ID and timestamp;
    • skip repeated probes for the same message during a cooldown window;
    • prevent concurrent monitor/busy-session triggers from probing the same message at the same time.
  • Reset probe state when a new active message starts, so recall detection still works for new turns.

This preserves recall detection while preventing one active/stuck Feishu turn from continuously burning OpenAPI quota.

Test plan

  • go test ./core
  • go test ./agent/... ./config ./core ./daemon ./platform/... ./tests/...
  • go test ./... (blocked locally: web/embed.go requires generated web/dist)

🤖 Generated with Claude Code

@chenhg5 chenhg5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论: 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 触发:
    1. recallProbeInFlight 防并发 (同一 message 在前次 probe 完成前被再次触发)
    2. lastRecallProbeMessageID == messageID + < cooldown 防短时间内重复 (poll loop 2s/次, 没有 throttle 就会 1 分钟内打 30 次)
    3. processInteractiveMessageWithcurrentMessageID 变化时重置 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 -l flag (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 化为 Engine field 或 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) 现有 MessageRecallDetector interface 实现散落在 platform/* 各处 (slack / feishu / 等)。Throttling 应该在 engine 层 (本 PR 正确放置), 但需要确保所有 platform 实现的 IsMessageRecalled 都能承受被 throttle (即多次调用不产生 side effect 如 send log / 计数埋点)。建议 author 顺手过一下各 platform impl 是否有不可重入的副作用。

🔵 可选优化:

  • P3 messageRecallProbeCooldownmessageRecallPollInterval 都是 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_cooldownmessage_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 -v 1/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 关注。

@qvictl

qvictl commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

感谢 review,已按建议补了一个 follow-up commit:93c657f5

已采纳的点:

  • 补了 CHANGELOG.md 的 unreleased Fixed 记录。
  • 加了 slog.Debug:当 recall fallback probe 因 in-flight 或 cooldown 被跳过时,会输出 debug 级别日志,便于排查但不影响默认生产日志。
  • 采纳 Q1/P3 的担心:我把 messageRecallPollInterval 从 1 分钟改回了原来的 2 秒。这个 PR 不再改变 monitor 的本地检查节奏,只通过 per-message cooldown/in-flight guard 限制真正调用平台 IsMessageRecalled 的频率,避免把“降低外部 API 调用”和“改变 recall UX 延迟”混在一起。

对确认问题的回答:

Q1:messageRecallPollInterval 改到 1 分钟不是必须的 combined fix。核心修复是 per-active-message cooldown 和 in-flight guard。为了避免 recall 检测延迟从约 2 秒变成约 60 秒,我已回退 poll interval 到原来的 2 秒。现在最坏情况下 monitor 仍每 2 秒本地醒来一次,但同一 message 在 1 分钟 cooldown 内不会再次调用 Feishu GET /im/v1/messages/{message_id}

Q2:暂时不在 config.example.toml 增加 message_recall_probe_cooldownmessage_recall_poll_interval。这两个值目前仍是 core 内部保护参数,不是用户可配置项;暴露配置会扩大本 PR 范围,也需要 i18n/docs/config parsing/test 的配套。当前 PR 先固定默认值解决 quota burn,后续如果不同平台确实需要差异化,可以再单独引入配置。

额外 cross-check:当前仓库里只有 Feishu 平台实现了 IsMessageRecalled(测试 stub 除外),没有发现其它平台实现会因为 engine 层 throttle 产生副作用。

验证:

  • go test ./core
  • go test ./agent/... ./config ./core ./daemon ./platform/... ./tests/...

@chenhg5 chenhg5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论: 实质 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:
    1. P3 「poll 间隔是否过慢」: messageRecallPollInterval 从 commit 96963a3 引入的 time.Minute 改回 2 * time.Second,与原 main 一致。这等于告诉后续读者"短 spike 靠 throttle 吸收,长间隔自然没问题",符合前 review 推荐的"throttle 在 engine 层,poll 频率不必为它让步"的架构判断。
    2. 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 行为异常时可 grep recallProberecall.*throttled 直接命中。debug log 默认不输出,prod 不会污染。
    3. 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),这是文档小瑕疵见下。
  • 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 化为 Engine field 或 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.Debugnext_probe_intime.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 标识,作者习惯。但 96963a3Co-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 TestStopCurrentMessageIfRecalledThrottlesRepeatedFallbackChecks PASS。
  • 本地 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 一次即可合入。

claude added 2 commits June 23, 2026 09:10
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>
@qvictl
qvictl force-pushed the fix/feishu-recall-probe-throttle branch from 93c657f to 7122a3b Compare June 23, 2026 01:16

@chenhg5 chenhg5 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论: 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.go 0 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

@chenhg5
chenhg5 merged commit e739e2e into chenhg5:main Jun 28, 2026
5 checks passed
chenhg5 pushed a commit that referenced this pull request Jun 28, 2026
* 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>
chenhg5 pushed a commit that referenced this pull request Jun 28, 2026
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>
JayGarland pushed a commit to JayGarland/cc-connect that referenced this pull request Jun 30, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants