Skip to content

fix(channels): stop dropping multi-task messages when a channel memory is saved - #7608

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
zjunothing:fix/7601-memory-intent-early-return
Jul 24, 2026
Merged

fix(channels): stop dropping multi-task messages when a channel memory is saved#7608
wenshao merged 1 commit into
QwenLM:mainfrom
zjunothing:fix/7601-memory-intent-early-return

Conversation

@zjunothing

Copy link
Copy Markdown
Collaborator

What this PR does

Makes a classifier-detected remember intent a side-effect instead of a message-consuming command: ChannelBase.processInbound() still saves the memory, but now suppresses the bot's "Channel memory … saved." confirmation and lets the full message continue through normal routing to the agent, so the other tasks in the message actually run. The agent's reply becomes the single response, and because the save invalidates unattended memory before the prompt is built, the just-saved entry reaches that same prompt via channel-memory recall — the model knows the save happened without any injected turn. Save failures still surface unconditionally. Everything else keeps its early return by design: explicit remember phrases matched by parseChannelMemoryIntent (there the whole message is the command) and every management intent — list/inspect/update/remove/clear and their confirmation flows.

Why it's needed

#7601: a multi-task message like "Review PR #123. Remember that code reviews should use inline comments. Also check PR #456" saved the memory, replied with a save confirmation, and silently dropped tasks 1 and 3 — the early return after handleChannelMemoryIntent() never routed the message to the agent. The triage confirmed this root cause in processInbound() and endorsed exactly this fix direction (option 2: make the save silent and continue with the full message), scoped to processInbound() and handleChannelMemoryIntent().

Reviewer Test Plan

How to verify

  1. In a channel session (e.g. qwen serve --channel dingtalk-qa), send one message that combines a real task with a natural-language remember, e.g. "Review PR [BUG]内建工具网络访问问题 #123. Remember that code reviews should use inline comments. Also check PR feat: add project-specific memory storage support #456." Before this PR: the bot only replies "Channel memory … saved." and the PR-review tasks never run. After: the memory is saved, no bot confirmation is injected, and the agent handles the whole message (its prompt even contains the just-saved entry via recall).
  2. Explicit commands are unchanged: "帮我记一下,发布前跑 npm run build" still gets the immediate confirmation without waking the agent, and list/inspect/update/remove/clear flows behave as before.
  3. npx vitest run src/ChannelBase.test.ts in packages/channels/base (502/502; whole package 890/890).

Evidence (Before & After)

E2E against the compiled ChannelBase (real Map-backed memory store, bridge records exactly what reaches the agent, deterministic stand-in for the LLM classifier), before/after via git stash + rebuild:

before/after E2E + tests

check unpatched (origin/main) patched
memory saved from the multi-task message
"Review PR #123" / "check PR #456" reach the agent ❌ (nothing reaches the agent) ✅ (full message + recalled memory)
chat reply "Channel memory m-…1 saved." only agent's single reply
new test: multi-task remember saves and still runs the other tasks
new test: save failure reports the error and still forwards the message
explicit remember / management intents keep early return ✅ (unchanged)

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS (Darwin 24.6), Node v22; vitest. npm run typecheck, eslint --max-warnings 0, prettier --check all clean.

Risk & Scope

  • Main risk or tradeoff: for a message that is purely a natural remember (no other tasks), the confirmation with the memory id is replaced by the agent's natural acknowledgement — the classifier has no way to tell "pure remember" from "remember + tasks", and a silent-save-plus-continue gives a single coherent reply in both cases (the id remains discoverable via the list/inspect commands). Existing classifier-remember tests were updated accordingly.
  • Not validated / out of scope: the multi-id confirmation strings in handleChannelMemoryIntent are now only reachable from the explicit single-text parse path; left in place to keep the diff minimal. No classifier/prompt changes.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #7601

中文说明

本 PR 做了什么

把分类器识别出的 remember 意图从「吞掉整条消息的命令」改为「副作用」:ChannelBase.processInbound() 仍然保存记忆,但抑制机器人的 "Channel memory … saved." 确认消息,并让完整消息继续走常规路由交给 agent,消息里的其它任务得以真正执行。agent 的回复成为唯一响应;且由于保存后会失效未读记忆缓存,刚保存的条目会经 channel-memory recall 注入同一条 prompt——模型无需任何注入回合就知道保存已发生。保存失败仍然无条件提示。其余路径按设计保持早退:parseChannelMemoryIntent 命中的显式记忆短语(整条消息就是命令本身),以及全部管理类意图——list/inspect/update/remove/clear 及其确认流程。

为什么需要

#7601:多任务消息如 "Review PR #123. Remember that code reviews should use inline comments. Also check PR #456" 会保存记忆、回复保存确认,然后静默丢弃任务 1 和 3——handleChannelMemoryIntent() 之后的早退 return 使消息永远到不了 agent。triage 已确认 processInbound() 中的这一根因,并背书了正是本 PR 的修复方向(方案 2:静默保存并继续处理完整消息),范围限定在 processInbound()handleChannelMemoryIntent()

审阅测试计划

如何验证

  1. 在 channel 会话(如 qwen serve --channel dingtalk-qa)发送一条「真实任务 + 自然语言记忆」组合消息:本 PR 之前机器人只回 "Channel memory … saved." 且任务不执行;之后记忆保存、无确认注入、agent 处理整条消息(其 prompt 中还能通过 recall 看到刚保存的条目)。
  2. 显式命令不变:"帮我记一下,发布前跑 npm run build" 仍立即确认且不唤醒 agent;list/inspect/update/remove/clear 流程行为如旧。
  3. packages/channels/basenpx vitest run src/ChannelBase.test.ts(502/502;整包 890/890)。

证据(Before & After)

对编译产物 ChannelBase 的 E2E(真实 Map 存储、bridge 记录到达 agent 的内容、确定性分类器替身),git stash + 重建做前后对照:未修复时 agent 收不到任何内容、只回保存确认;修复后完整消息 + recall 记忆到达 agent、单一回复。两条新测试在未修复源码上失败;显式/管理类路径行为不变。

测试平台

macOS 已本地验证(✅);Windows / Linux 依赖 CI(⚠️)。

环境

macOS(Darwin 24.6)、Node v22;vitest;typecheck / eslint / prettier 全绿。

风险与范围

  • 主要风险/权衡:纯自然语言记忆消息(无其它任务)的带 id 确认被 agent 的自然确认取代——分类器无法区分「纯记忆」与「记忆+任务」,静默保存并继续在两种情况下都给出单一连贯回复(id 仍可通过 list/inspect 查询)。相应更新了既有分类器 remember 测试。
  • 未验证/超出范围:handleChannelMemoryIntent 中的多 id 确认文案现仅显式单条解析路径可达,为保持最小 diff 原样保留;未改动分类器/提示词。
  • 破坏性变更/迁移说明:无。

关联 Issue

Fixes #7601

🤖 Generated with Claude Code

…y is saved

A classifier-detected `remember` intent made processInbound() return early,
so a message like "Review PR QwenLM#123. Remember X. Also check PR QwenLM#456" saved
the memory, replied with a confirmation, and never routed the message to
the agent — tasks 1 and 3 were silently dropped (QwenLM#7601).

Classifier-detected remembers now save silently as a side-effect and let
the message continue to the agent: the confirmation is suppressed (the
agent's reply is the single response, and the just-saved entry reaches the
same prompt via channel-memory recall), while save failures still surface.
Explicit remember phrases and every management intent (list/inspect/
update/remove/clear and their confirmations) keep the early return — there
the whole message is the command.

Fixes QwenLM#7601

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed bug with solid evidence. Issue #7601 (filed by a maintainer, labeled type/bug + welcome-pr) describes multi-task messages being silently dropped when a classifier-detected remember intent triggers an early return in processInbound(). The PR includes before/after E2E evidence showing the agent receives nothing on the unpatched build vs. the full message on the patched build.

Direction: aligned — this is a straightforward bug fix in channel memory routing. The early return after handleChannelMemoryIntent() unconditionally consumes the message, which is correct for explicit memory commands (the whole message is the command) but wrong for classifier-detected remembers embedded in a multi-task message. The fix direction (make the save a silent side-effect, let the message continue to the agent) matches the option endorsed in the issue triage.

Size: not applicable — changes are in packages/channels/base/src/, not a core module path.

Approach: the scope feels right. Two files, ~20 lines of production logic, the rest is tests. The suppressSaveConfirmation option is a clean way to reuse the existing save path without duplicating it, and the memoryIntentFromClassifier flag correctly distinguishes classifier-detected remembers (side-effect) from explicit parse matches (whole-message command). Save failures still surface unconditionally, which is the right call. No unrelated changes in the diff.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,证据充分。Issue #7601(由维护者提交,标记 type/bug + welcome-pr)描述了多任务消息在分类器识别出 remember 意图后,因 processInbound() 中的早退 return 而被静默丢弃。PR 提供了 before/after E2E 证据:未修复时 agent 收不到任何内容,修复后完整消息到达 agent。

方向:对齐——这是 channel 记忆路由的直接 bug 修复。handleChannelMemoryIntent() 之后的无条件早退对显式记忆命令是正确的(整条消息就是命令),但对嵌入在多任务消息中的分类器识别记忆则是错误的。修复方向(静默保存,让消息继续到达 agent)与 issue triage 中背书的方案一致。

规模:不适用——改动在 packages/channels/base/src/,非核心模块路径。

方案:范围合理。两个文件,约 20 行生产逻辑,其余为测试。suppressSaveConfirmation 选项复用了现有保存路径,memoryIntentFromClassifier 标志正确区分了分类器识别的记忆(副作用)和显式解析匹配(整条消息命令)。保存失败仍无条件提示。diff 中无无关改动。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 1cfa5326e11f3b45d86bb5df74caa001980cc19b · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given the root cause (unconditional early return after handleChannelMemoryIntent() drops the rest of a multi-task message), I would distinguish classifier-detected remembers from explicit parse matches, save the memory silently for the former, and let the message continue to the agent. This is exactly what the PR does — the approach matches.

Findings: no critical blockers or convention violations.

  • handleChannelMemoryIntent is private with a single call site in processInbound() — no downstream consumers to worry about. The new options parameter defaults to {}, so the signature change is safe.
  • The suppressSaveConfirmation check is placed after invalidateUnattendedMemory() (cache invalidation still happens) and after the error catch block (save failures still surface unconditionally). Correct ordering.
  • memoryIntentFromClassifier is only set when the classifier returns non-null, and memorySaveIsSideEffect requires both memoryIntentFromClassifier && memoryIntent.kind === 'remember' — explicit parse matches and all management intents (list/inspect/update/remove/clear) keep the early return. The boundary is precise.
  • DingtalkAdapter and WeComAdapter both override processInbound but just wrap super.processInbound() in a try/finally — fully compatible with the changed return behavior.
  • Test updates are consistent: existing classifier-remember tests now expect bridge.prompt to be called and the agent's response to be sent; two new tests cover the multi-task scenario and the save-failure-still-forwards case. No test weakening.
  • No unrelated changes, no drive-by refactors, no scope creep.

Testing

Unit tests (PR branch, packages/channels/base):

 ✓ src/ChannelBase.test.ts (502 tests) 8188ms

 Test Files  1 passed (1)
      Tests  502 passed (502)
   Duration  8.87s

Full package suite:

 Test Files  18 passed (18)
      Tests  890 passed (890)
   Duration  12.69s

Typecheck (tsc --noEmit): clean.

Build + CLI smoke test (tmux, PR branch):

$ npm run dev -- --version
0.20.1

$ npm run dev -- -p 'Say hello in one word'
Hello

$ npm run dev -- channel --help
qwen channel
Manage messaging channels (Telegram, Discord, etc.)
Commands:
  qwen channel start [name]    Start channels
  qwen channel stop            Stop the running channel service
  qwen channel status          Show channel service status
  ...

Full E2E channel testing (sending a multi-task message through a live DingTalk/WeCom channel) requires a configured channel server with API credentials, which isn't available in this CI environment. The unit tests exercise the exact processInbound()handleChannelMemoryIntent() path with mock channels and a deterministic classifier stand-in, covering the before/after behavior difference directly.

中文说明

代码审查

独立方案: 根据根因(handleChannelMemoryIntent() 后的无条件早退丢弃了多任务消息的其余部分),我会区分分类器识别的记忆和显式解析匹配,对前者静默保存并让消息继续到达 agent。PR 的做法与此完全一致。

发现: 无关键阻塞项或规范违反。

  • handleChannelMemoryIntentprivate 方法,仅在 processInbound() 中调用一次——无下游消费者。新的 options 参数默认为 {},签名变更安全。
  • suppressSaveConfirmation 检查位于 invalidateUnattendedMemory() 之后(缓存失效仍会执行)和错误 catch 块之后(保存失败仍无条件提示)。顺序正确。
  • memoryIntentFromClassifier 仅在分类器返回非 null 时设置,memorySaveIsSideEffect 要求两者同时满足——显式解析匹配和所有管理意图保持早退。边界精确。
  • DingtalkAdapter 和 WeComAdapter 的 processInbound 覆写仅包装 super.processInbound()——完全兼容。
  • 测试更新一致:现有分类器记忆测试现在期望 bridge.prompt 被调用;两个新测试覆盖多任务场景和保存失败仍转发场景。无测试弱化。
  • 无无关改动、无顺手重构、无范围蔓延。

测试

单元测试(PR 分支):ChannelBase.test.ts 502/502 通过;整包 890/890 通过。类型检查干净。构建 + CLI 冒烟测试正常。完整 E2E channel 测试需要配置好的 channel 服务器(DingTalk/WeCom),CI 环境不可用。单元测试以 mock channel 和确定性分类器替身直接覆盖了 processInbound()handleChannelMemoryIntent() 路径的前后行为差异。

Qwen Code · qwen3.8-max-preview

Reviewed at 1cfa5326e11f3b45d86bb5df74caa001980cc19b · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean, minimal fix for a real bug; would merge without hesitation.

This is a textbook bug fix. The problem is real (multi-task messages silently dropped when a classifier-detected remember triggers an early return), the root cause is correctly identified (unconditional return after handleChannelMemoryIntent()), and the fix is the minimal change that solves it: distinguish classifier-detected remembers from explicit commands, suppress the confirmation for the former, and let the message continue to the agent.

The implementation is ~20 lines of production logic. The suppressSaveConfirmation option reuses the existing save path without duplication. The memoryIntentFromClassifier flag draws the boundary precisely — explicit parse matches and management intents keep their early return. Save failures still surface. Cache invalidation still runs. The DingtalkAdapter and WeComAdapter overrides are unaffected.

Tests are thorough: two new tests cover the multi-task scenario and the save-failure-still-forwards case, and existing classifier-remember tests are updated to expect the new behavior (agent gets the message). 502/502 in ChannelBase.test.ts, 890/890 across the package, typecheck clean.

The only thing I couldn't verify is a live E2E channel test (requires a configured DingTalk/WeCom server), but the unit tests exercise the exact processInbound()handleChannelMemoryIntent() path with a deterministic classifier stand-in, which is the right level of coverage for this change.

中文说明

置信度:5/5 —— 干净、最小化的真实 bug 修复;毫不犹豫地合并。

这是一个教科书式的 bug 修复。问题真实存在(多任务消息在分类器识别出 remember 意图后因早退而被静默丢弃),根因定位正确(handleChannelMemoryIntent() 后的无条件 return),修复是解决问题所需的最小变更:区分分类器识别的记忆和显式命令,对前者抑制确认消息,让消息继续到达 agent。

生产逻辑约 20 行。suppressSaveConfirmation 选项复用现有保存路径,无重复。memoryIntentFromClassifier 标志精确划定边界——显式解析匹配和管理意图保持早退。保存失败仍提示,缓存失效仍执行。DingtalkAdapter 和 WeComAdapter 覆写不受影响。

测试充分:两个新测试覆盖多任务场景和保存失败仍转发场景,现有分类器记忆测试更新为期望新行为。ChannelBase.test.ts 502/502,整包 890/890,类型检查干净。

唯一无法验证的是实际 E2E channel 测试(需要配置好的 DingTalk/WeCom 服务器),但单元测试以确定性分类器替身直接覆盖了 processInbound()handleChannelMemoryIntent() 路径,对此变更而言是正确的覆盖层级。

Qwen Code · qwen3.8-max-preview

Reviewed at 1cfa5326e11f3b45d86bb5df74caa001980cc19b · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: CI still running. Reviewed.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Maintainer local verification — real build & tests ✅

I built this PR in an isolated worktree at head 1cfa5326e (base 4f7429a0b, current main tip 8bc1cf0a5) and ran real suites, gates, an independent E2E, and a discriminating A/B. The bug reproduces on origin/main, the fix resolves it, and the change is surgical. Merge-ready from a verification standpoint.

PR #7608 local verification

1 · Suites & gates on the PR head — all green

check result
focused src/ChannelBase.test.ts 502 passed (502)
whole @qwen-code/channel-base package 890 passed (890) · 18 files
prettier --check (changed files) clean
tsc --noEmit clean (exit 0)
eslint --max-warnings 0 (changed files) clean (exit 0)

2 · Independent E2E — my own spec, not the PR's tests (3/3)

I wrote a fresh spec that drives the real ChannelBase.processInbound() through a concrete subclass, a Map-backed memory store, and a bridge.prompt() spy used as the single "what reached the agent" oracle, with a deterministic classifier stand-in:

  • A — classifier remember inside "Review PR #123. Remember … Also check PR #456." → memory saved silently, and the full message reaches the agent (both PR-review tasks survive); the agent reply is the only chat output, no saved confirmation.
  • B — explicit "remember: …" phrase → confirmation delivered and consumed, agent never woken (unchanged).
  • C — management "what do you remember?" → handled locally, agent never woken (unchanged).

3 · Discriminating A/B — revert the fix, keep the tests

Overlaying origin/main's ChannelBase.ts back under the tests (the source is the only thing that changes):

  • PR's own suite: 9 failed | 493 passed — the 9 failures are exactly the classifier-remember tests. Flagship assertion on the multi-task test: bridge.promptNumber of calls: 0 — i.e. on origin/main the agent receives nothing, which is precisely the bug(channel): saving channel memory interrupts multi-task execution flow #7601 bug.
  • My spec on the same base source: scenario A fails (agent reached 0×) while B and C still pass ⇒ the change touches only the classifier-remember path; explicit and management intents are untouched.

Notes

  • Save failures still surface unconditionally (the failure path reports the error and still forwards the message — covered by the suite).
  • Diff is 2 files inside packages/channels/base/src, no package.json/dependency changes, no scope creep.
  • CI: 14 checks green, 0 failing; MERGEABLE.

Verdict: LGTM — merge-ready. The 3-stage triage bot review and this independent local reproduction agree.

Verified on macOS (Darwin 24.6) · Node v22 · vitest v3.2.4. The independent spec (verify7608.e2e.test.ts) was a local scratch file and is not part of the PR.

中文说明(点击展开)

维护者本地验证 —— 真实构建与测试 ✅

我在隔离 worktree(head 1cfa5326e,base 4f7429a0b,当前 main8bc1cf0a5 中真实构建并运行了完整测试套件、gate 检查、一个独立 E2E,以及一次判别式 A/B。该 bug 在 origin/main 上可复现,本 PR 修复了它,且改动是外科手术式的(仅影响分类器 remember 路径)。 从验证角度看可以合并。

(截图见上方英文部分)

1 · PR head 上的套件与 gate —— 全绿

  • 聚焦 src/ChannelBase.test.ts502/502 通过
  • 整包 @qwen-code/channel-base890/890 通过(18 个文件)
  • prettier --check / tsc --noEmit / eslint --max-warnings 0(改动文件):全部干净

2 · 独立 E2E —— 我自己写的 spec(非 PR 自带测试),3/3

该 spec 通过一个具体子类、Map 存储、以及作为「是否到达 agent」唯一探针的 bridge.prompt() spy,配合确定性分类器替身,驱动真实的 ChannelBase.processInbound()

  • A —— 多任务消息 "Review PR #123. Remember … Also check PR #456." 中的分类器 remember → 记忆静默保存完整消息到达 agent(两个 PR 审查任务都保留),聊天只输出 agent 回复,无 saved 确认。
  • B —— 显式 "remember: …" 短语 → 仍发确认消费整条消息,不唤醒 agent(行为不变)。
  • C —— 管理类 "what do you remember?" → 本地处理,不唤醒 agent(行为不变)。

3 · 判别式 A/B —— 还原修复、保留测试

origin/mainChannelBase.ts 覆盖回测试之下(唯一变化的就是源码):

  • PR 自带套件: 9 失败 | 493 通过 —— 这 9 个失败正是分类器 remember 相关测试。多任务测试的关键断言:bridge.promptNumber of calls: 0,即在 origin/main 上 agent 收不到任何内容,正是 bug(channel): saving channel memory interrupts multi-task execution flow #7601 的 bug。
  • 我的 spec 跑同一 base 源码: 场景 A 失败(agent 被触达 0 次),而 B、C 仍通过 ⇒ 改动影响分类器 remember 路径,显式与管理类意图不受影响。

补充

  • 保存失败仍无条件提示(失败路径既报错转发消息,套件已覆盖)。
  • diff 仅 packages/channels/base/src 下 2 个文件,无 package.json/依赖变更,无范围蔓延。
  • CI:14 项检查全绿、0 失败;MERGEABLE

结论:LGTM,可合并。 三阶段 triage 机器人审查与本次独立本地复现结论一致。

验证环境:macOS(Darwin 24.6)· Node v22 · vitest v3.2.4。独立 spec(verify7608.e2e.test.ts)是本地临时文件,不属于 PR。

@wenshao
wenshao added this pull request to the merge queue Jul 24, 2026
Merged via the queue into QwenLM:main with commit 9878ad5 Jul 24, 2026
64 checks passed
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.

bug(channel): saving channel memory interrupts multi-task execution flow

3 participants