Skip to content

feat(channels): support natural memory references - #6952

Merged
wenshao merged 8 commits into
QwenLM:mainfrom
qqqys:feat/channel-memory-phase-3b1
Jul 15, 2026
Merged

feat(channels): support natural memory references#6952
wenshao merged 8 commits into
QwenLM:mainfrom
qqqys:feat/channel-memory-phase-3b1

Conversation

@qqqys

@qqqys qqqys commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds natural-language channel-memory lookup, update, and removal planning while preserving exact-ID commands as the deterministic fast path. Natural requests can return a filtered list, resolve one current entry, or return stateless candidate IDs when ambiguous. Classifier output is bounded, treated as untrusted, and revalidated before any memory operation.

Why it's needed

People can refer to a recent preference by its content or context, but requiring a remembered opaque ID for every follow-up is cumbersome. This change adds natural references without allowing ambiguous selections or stale planner output to mutate another entry.

Reviewer Test Plan

How to verify

  1. Start a disposable channel with several entries. Confirm a natural filtered list preserves stored order and returns stable IDs.
  2. Confirm natural inspect, update, and removal operate only when exactly one current entry matches. For an ambiguous request, confirm the response lists candidates and does not mutate memory; retrying with an exact ID mutates only that entry.
  3. Confirm exact-ID update and removal do not invoke natural planning.
  4. Force the classifier request to fail. Confirm the request falls through to normal agent handling without a memory mutation.
  5. Restart a disposable daemon-managed channel and confirm memory remains available for the same channel target.

Evidence (Before & After)

Focused suites passed: core 64 tests, channel base 439 tests, and CLI 107 tests. Repository build, bundle, typecheck, lint, changed-file Prettier, and whitespace checks passed on macOS. An independent whole-branch review found one filtered-list routing gap; the TDD fix was re-reviewed with zero remaining Critical, Important, or Minor findings and a Ready to merge verdict.

Live Telegram/model E2E was not observed because no disposable PHASE3B1_* credentials were already available to the verifier. No normal user credentials or unrelated daemon were inspected or used.

Tested on

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

Environment (optional)

macOS, Node.js v25.9.0, package-local Vitest suites, repository build and static checks.

Risk & Scope

  • Main risk or tradeoff: model-mediated natural references are only acted on after current-entry validation; stale natural mutations are guarded by compare-and-swap text checks.
  • Not validated / out of scope: disposable live Telegram/model delivery, classifier fallback over a real provider, and daemon-restart persistence require dedicated disposable credentials.
  • Breaking changes / migration notes: none.

Linked Issues

N/A

中文说明

此 PR 的内容

增加自然语言的频道记忆查询、更新和删除规划,同时保留精确 ID 命令作为确定性快速路径。自然请求可以返回过滤后的列表、解析出一个当前条目,或者在存在歧义时返回无状态的候选 ID。分类器输出有长度限制,被视为不可信数据,并且在任何记忆操作前都会再次校验。

为什么需要

用户可能通过内容或上下文引用最近的偏好,但每次后续操作都要求记住不透明 ID 会很麻烦。此改动增加自然引用,同时不允许歧义选择或过期的规划输出修改其他条目。

审阅者测试计划

如何验证

  1. 启动一个包含多个条目的可丢弃频道。确认自然语言过滤列表保持存储顺序并返回稳定 ID。
  2. 确认自然语言查看、更新和删除仅在恰好匹配一个当前条目时执行。对于有歧义的请求,确认回复列出候选项且不修改记忆;使用精确 ID 重试时,只修改该条目。
  3. 确认精确 ID 更新和删除不会调用自然语言规划。
  4. 强制分类器请求失败。确认请求会回退到正常 agent 处理,且不会修改记忆。
  5. 重启可丢弃的 daemon 托管频道,确认同一频道目标的记忆仍可使用。

证据(前后对比)

聚焦测试套件已通过:core 64 个测试、channel base 439 个测试、CLI 107 个测试。仓库构建、bundle、typecheck、lint、改动文件 Prettier 和空白检查已在 macOS 上通过。独立整分支审查发现了一个过滤列表路由缺口;TDD 修复经复审后 Critical、Important、Minor 均为 0,结论为 Ready to merge。

由于验证者没有已安全提供的可丢弃 PHASE3B1_* 凭证,实时 Telegram/model E2E 未被观察。未检查或使用普通用户凭证或无关 daemon。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js v25.9.0、包级 Vitest 测试套件、仓库构建和静态检查。

风险与范围

  • 主要风险或权衡:模型介导的自然引用只会在当前条目校验后执行;过期的自然修改由基于文本的比较并交换检查保护。
  • 未验证 / 范围外:可丢弃的实时 Telegram/model 投递、真实 provider 上的分类器回退,以及 daemon 重启后的持久化需要专用的可丢弃凭证。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

不适用

@qqqys
qqqys marked this pull request as ready for review July 15, 2026 13:38
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is a feature extension, not a bug fix. The PR adds natural-language channel memory lookup, update, and removal planning — a logical next phase after the existing exact-ID channel memory (PRs #6051, #6376, #6617 in CHANGELOG). The user problem is real: requiring opaque IDs for every follow-up is cumbersome.

Direction: Aligned. Channel memory is an established feature with a clear phase progression, and natural references are the obvious next step. CHANGELOG confirms the trajectory (add natural channel memory intents in #6376).

Size: Not applicable — no core infrastructure paths matched. Production logic: 458 lines (ChannelBase.ts +235/-32, channel-memory-intent.ts +10/-1, types.ts +17/-7, memory-intent-classifier.ts +139/-17). Test: 754 lines. Docs: 20 lines. All well within thresholds.

Approach: Scope feels right for the stated goal. The design preserves exact-ID as the deterministic fast path and only invokes the classifier for non-exact requests. CAS protection via expectedText prevents stale classifier output from mutating a different entry — a genuine safety concern handled well. The ambiguity gate (return candidates, don't mutate) is the conservative choice. Regex expansion for delete/remove/change/更新 patterns is minimal. One thing to think about: the classifier now receives all current entries and is called on every non-exact-ID memory request, which adds a list-read per invocation — acceptable for correctness but worth noting for high-traffic channels.

Re-run note: No new commits since the prior pass. Acknowledging @wenshao's human review — all six suggestions are non-blocking follow-up material.

中文说明

感谢贡献!

模板完整 ✓

问题:这是功能扩展,不是 bug 修复。PR 增加了自然语言的频道记忆查询、更新和删除规划——在现有精确 ID 频道记忆(CHANGELOG 中的 PR #6051#6376#6617)之后的合理下一步。用户痛点真实存在:每次后续操作都需要记住不透明 ID 很不方便。

方向:对齐。频道记忆是已有功能,有清晰的阶段演进,自然引用是显而易见的下一步。CHANGELOG 确认了发展轨迹(#6376 中的 add natural channel memory intents)。

规模:不适用——未触及核心基础设施路径。生产逻辑 458 行,测试 754 行,文档 20 行,均在阈值内。

方案:范围与目标匹配。设计保留了精确 ID 作为确定性快速路径,仅在非精确请求时调用分类器。通过 expectedText 的 CAS 保护防止过期分类器输出修改其他条目——处理得当的真实安全隐患。歧义门控(返回候选项,不修改)是保守选择。delete/remove/change/更新 等正则扩展是最小化的。值得注意的一点:分类器现在接收所有当前条目,且在每个非精确 ID 记忆请求时调用,每次增加一次列表读取——对于高流量频道值得留意。

重跑说明: 自上次审查以来没有新提交。确认 @wenshao 的人工审查——六条建议均为非阻塞的后续改进项。

Qwen Code · qwen3.7-max

Reviewed at 2f281b1da0a5762e986263187010589ba2662d00 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Code Review

No critical issues found. The implementation is clean and security-conscious.

Independent proposal comparison: My approach would have been nearly identical — extend the classifier to accept current entries, return structured plans with targetIds, add CAS protection via expectedText, and gate ambiguity by returning candidates. The PR matches this and goes further with thorough validation: unknown target IDs are normalized to none, duplicate IDs are rejected, classifier output keys are checked against a strict per-intent allowlist, and confidence is validated for NaN/Infinity/out-of-range. The memory manifest carefully sanitizes entry text (unpaired surrogate normalization, quote/backslash stripping) and stays within a 64K code-point budget. All of this is well-executed.

Notable strengths:

  • The exact-ID fast path is preserved — parseChannelMemoryIntent runs first, and the classifier is only invoked when no exact match is found. Exact-ID operations don't carry expectedText, keeping the fast path unchanged.
  • CAS protection is correctly scoped to natural operations only — the expectedText/expectedTextById fields are optional and only populated for natural_update and natural_remove.
  • The ResolvedChannelMemoryIntent discriminated union keeps the handler readable despite the added complexity.
  • Test coverage is thorough — CAS conflict, ambiguity, no-match, invalid classifier output, untrusted data sanitization, 500-entry budget test, and exact-ID fast-path bypass are all covered.

Minor observations (non-blocking):

  • handleChannelMemoryIntent re-fetches entries for ambiguous and list_matches cases, even though classifyChannelMemoryIntent already listed them. The re-fetch ensures fresh data but adds a small overhead. Acceptable tradeoff.
  • The classifier trigger regex expansion (删掉, 改成, 更新, 刚才那条, 只/仅看/列出…偏好/习惯) is well-bounded. The CJK pattern [\p{Script=Han}\s]{0,12} caps the lookahead appropriately.

@wenshao's review alignment: Their six suggestions are all follow-up material — trigger broadening cost monitoring, manifest budget floor for extreme entry counts, unpaginated candidate lists, carrying previews in the resolved intent, CAS-conflict user messaging, and prompt wording for multi-target plans. None block the merge.

Real-Scenario Testing

Channel memory operations require a running messaging bot (Telegram, DingTalk, etc.) — this feature can't be exercised via qwen -p in tmux. The PR author confirms: "Live Telegram/model E2E was not observed because no disposable PHASE3B1_* credentials were available."

Unit tests re-verified from the PR branch in the worktree (all passing):

 ✓ packages/channels/base/src/channel-memory-intent.test.ts  (9 tests)    7ms
 ✓ packages/channels/base/src/ChannelBase.test.ts            (430 tests)  7794ms
 ✓ packages/cli/src/commands/channel/memory-intent-classifier.test.ts (26 tests) 1284ms

 Total: 465 tests passed, 0 failed

Key test scenarios covered:

  • Natural update with CAS protection and CAS conflict handling
  • Natural removal with CAS protection
  • Natural inspect of unique match
  • Filtered list preserving document order
  • Ambiguous multi-match returning candidates without mutation
  • Empty target IDs returning no-match without mutation
  • Invalid classifier output (unknown IDs, bad confidence, extra fields) normalized to none
  • Untrusted data sanitization (injection attempts in user text and entry metadata)
  • 500-entry manifest staying within code-point budget
  • Exact-ID fast path bypassing classifier entirely
中文说明

代码审查

未发现关键问题。实现干净且安全意识强。

独立方案对比: 我的方案几乎一致——扩展分类器接收当前条目,返回带 targetIds 的结构化规划,通过 expectedText 添加 CAS 保护,通过返回候选项处理歧义。PR 匹配了这些并更进一步:未知目标 ID 归一化为 none,拒绝重复 ID,按意图类型严格检查分类器输出键名,校验 NaN/Infinity/越界置信度。记忆清单仔细清理条目文本(不成对代理对规范化、引号/反斜杠剥离)并控制在 64K 码点预算内。执行良好。

显著优点:

  • 精确 ID 快速路径保持不变——先运行 parseChannelMemoryIntent,仅在无精确匹配时调用分类器。精确 ID 操作不携带 expectedText
  • CAS 保护正确限定于自然操作——expectedText/expectedTextById 是可选的,仅在 natural_updatenatural_remove 时填充。
  • ResolvedChannelMemoryIntent 可辨识联合类型使处理器在增加复杂度后仍保持可读。
  • 测试覆盖全面——CAS 冲突、歧义、无匹配、无效分类器输出、不可信数据清理、500 条目预算测试、精确 ID 快速路径绕过均有覆盖。

次要观察(非阻塞):

  • handleChannelMemoryIntentambiguouslist_matches 情况重新获取条目,尽管 classifyChannelMemoryIntent 已经列出过。重新获取确保数据新鲜但有少量开销。可接受的权衡。
  • 分类器触发正则扩展(删掉改成更新刚才那条只/仅看/列出…偏好/习惯)有良好边界。CJK 模式 [\p{Script=Han}\s]{0,12} 适当限制了前瞻范围。

@wenshao 审查一致性: 其六条建议均为后续改进项——触发正则开销监控、极端条目数下的清单预算下限、未分页候选列表、在解析意图中携带预览、CAS 冲突用户提示、以及多目标规划的提示措辞。均不阻塞合并。

真实场景测试

频道记忆操作需要运行中的消息机器人(Telegram、钉钉等)——此功能无法通过 qwen -p 在 tmux 中测试。PR 作者确认:"由于没有可用的可丢弃 PHASE3B1_* 凭证,未观察到实时 Telegram/model E2E。"

已从 PR 分支在工作树中重新验证单元测试(全部通过):

 ✓ packages/channels/base/src/channel-memory-intent.test.ts  (9 个测试)    7ms
 ✓ packages/channels/base/src/ChannelBase.test.ts            (430 个测试)  7794ms
 ✓ packages/cli/src/commands/channel/memory-intent-classifier.test.ts (26 个测试) 1284ms

 总计:465 个测试通过,0 个失败

Qwen Code · qwen3.7-max

Reviewed at 2f281b1da0a5762e986263187010589ba2662d00 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid implementation of a well-scoped feature in an established area; the only gap is live bot E2E which neither the author nor I could run.

This PR extends channel memory with natural-language references — a clear next phase after the existing exact-ID operations. The implementation is careful in all the right places: CAS protection prevents stale classifier output from mutating the wrong entry, ambiguity is handled conservatively (return candidates, never mutate), and classifier output is treated as untrusted data with thorough revalidation. The exact-ID fast path is preserved and verified by tests to bypass the classifier entirely.

The code is straightforward — the ResolvedChannelMemoryIntent discriminated union keeps the handler readable despite the added complexity, and the classifier normalization is thorough without being over-engineered. 465 unit tests pass with comprehensive coverage of happy paths, edge cases, and adversarial inputs.

@wenshao's human review (LGTM with six non-blocking suggestions) corroborates the automated assessment. Their points about trigger broadening cost, manifest budget floor, and CAS-conflict user messaging are all good follow-up material.

The one thing I'd flag: real E2E with a live messaging bot (Telegram, DingTalk) would give extra confidence, but this requires disposable bot credentials that neither the author nor the test environment has. The unit test coverage is thorough enough to ship.

Approval already pinned to 2f281b1da0a5762e986263187010589ba2662d00 — no new commits, no re-approval needed. ✅

中文说明

信心度:4/5 — 在成熟领域中实现良好的功能扩展;唯一的差距是作者和我都无法运行实时机器人 E2E 测试。

此 PR 为频道记忆增加了自然语言引用——在现有精确 ID 操作之后的清晰下一阶段。实现在所有关键位置都很谨慎:CAS 保护防止过期分类器输出修改错误条目,歧义保守处理(返回候选项,永不修改),分类器输出被视为不可信数据并进行彻底重新验证。精确 ID 快速路径保持不变,测试验证其完全绕过分类器。

代码简洁——ResolvedChannelMemoryIntent 可辨识联合类型使处理器在增加复杂度后仍保持可读,分类器规范化彻底但不过度工程化。465 个单元测试通过,全面覆盖正常路径、边缘情况和对抗性输入。

@wenshao 的人工审查(LGTM 加六条非阻塞建议)印证了自动评估。其关于触发正则开销、清单预算下限和 CAS 冲突用户提示的观点都是好的后续改进项。

唯一要指出的:使用实时消息机器人(Telegram、钉钉)的真实 E2E 测试会增加信心,但这需要作者和测试环境都没有的可丢弃机器人凭证。单元测试覆盖足够全面可以发布。

批准已锚定在 2f281b1da0a5762e986263187010589ba2662d00——无新提交,无需重新批准。✅

Qwen Code · qwen3.7-max

Reviewed at 2f281b1da0a5762e986263187010589ba2662d00 · 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.

Reviewed. Suggestions are inline. Not reviewed: coverage — could not read the agents' transcripts (no subagent transcripts at /home/github-runner/actions-runner-5/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-5--work-qwen-code-qwen-code/subagents/820bc888-48c8-444e-97bd-7eebb9e9fde1 (ENOENT: no such file or directory, scandir '/home/github-runner/actions-runner-5/_work/_temp/qwen-home/projects/-home-github-runner-actions-runner-5--work-qwen-code-qwen-code/subagents/820bc888-48c8-444e-97bd-7eebb9e9fde1'). The harness writes one per agent; if there are none, either no agents ran or the harness could not write them.), so this run cannot show that any of the diff was read.

— qwen3.7-max via Qwen Code /review

Comment on lines +2765 to +2770
if (intent.kind === 'ambiguous') {
const channelMemory = await this.getChannelMemory(envelope);
if (!channelMemory) return;
let entries: ChannelMemoryEntry[];
try {
entries = await channelMemory.listChannelMemoryEntries(

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.

[Suggestion] listChannelMemoryEntries is fetched again here, but classifyChannelMemoryIntent already fetched entries and resolved intent.ids against that snapshot. Re-fetching creates both redundant I/O and a TOCTOU gap: if entries change between the two fetches (e.g., concurrent memory mutation), intent.ids resolved against snapshot A may not match snapshot B — an entry could silently vanish from the rendered candidate list. — Concrete cost: every ambiguous/list_matches/inspect resolution triggers two reads of the memory store, doubling latency for high-latency backends.

Consider threading the already-fetched entries through the resolved intent (e.g., add an optional entries field to ambiguous, list_matches, and inspect variants) so the handler renders from the classifier's snapshot.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Review

Overview

This extends channel memory with natural-language references: the classifier now receives the current entry manifest and can plan list/inspect/update/remove against known IDs, while exact-ID commands stay on the deterministic fast path (which also gains 删除/删掉/更新…为/delete/remove/change…to forms). Ambiguous or no-match plans never mutate; single-entry mutations are guarded by compare-and-swap on the entry text.

What I verified

  • CAS is real end-to-end, not just types. expectedText/expectedTextById land in packages/core/src/memory/channel-memory.ts (already on main), which throws Channel memory entry changed on mismatch — and start.ts/daemon-worker.ts pass the core functions as direct references, so nothing drops the fields in between. The no-retry/no-session-invalidation behavior on conflict is pinned by a test.
  • Defense in depth holds. The CLI classifier strictly validates shape (allowed keys per intent, targetIds ⊆ known IDs, no duplicates, finite 0–1 confidence), and ChannelBase independently re-validates the result as unknown — so a third-party classifier implementation can't inject unchecked IDs either.
  • Fast path is untouched. New exact-ID patterns still require ^m-[a-f0-9]{12}$, and tests assert the classifier is never invoked for them.
  • Injection hardening looks careful: previews strip quotes/backslashes/control chars/lone surrogates and are code-point-budgeted, the user message is JSON-escaped, both sections are labeled untrusted, and the classifier runs in an isolated throwaway session.
  • Tests: I ran the three changed suites from this branch locally — channels/base 439 passed, cli classifier 26 passed. Ubuntu CI (which gates on eslint --max-warnings 0 before vitest) is green.

Edge-case coverage is genuinely good (NaN/∞ confidence, duplicate/unknown IDs, unknown JSON fields, 500-entry manifest budget stress, lone-surrogate metadata, group-mention gating, planner-read failures).

Suggestions

Nothing blocking; in rough priority order:

  1. Trigger broadening costCHANNEL_MEMORY_CLASSIFIER_TRIGGER_RE now fires on bare \b(delete|remove|update|change)\b (plus 删除/删掉/改成/更新). These are everyday dev-chat words ("update the deps", "remove that flag"), and each hit now costs a store read plus a full classifier round-trip (new bridge session) before the agent turn starts. The old regex only had remember|memory|forget on the English side. Probably an acceptable tradeoff for the feature, but worth watching latency in real channels; if it stings, bare verbs could require a memory-ish co-occurrence. Relatedly, the user message is embedded in the classifier prompt unbudgeted — the entry manifest is capped at 64k code points but JSON.stringify(text) is not; a length cap in shouldClassifyChannelMemoryIntent would be cheap insurance.
  2. Manifest budget floorbuildMemoryManifest's 64k budget only squeezes previews. Metadata lines alone are ~110 code points/entry, so past ~550 entries the manifest exceeds the budget with previews already at zero (the store caps bytes at 1MB, not entry count, so thousands of tiny entries are possible). Consider bailing out of natural planning (return none/null) past an entry-count threshold instead of emitting an oversized prompt.
  3. Unpaginated candidate listslist_matches/ambiguous render every matched entry under a hardcoded page 1/1, unlike the 20-per-page unfiltered list. Adapters chunk oversized sends (e.g. Telegram), so it's cosmetic, but a broad filter over a large store produces one giant multi-chunk reply.
  4. +1 to the existing inline comment about re-fetching entries in the ambiguous/list_matches handlers: carrying the previews in the resolved intent would drop a read and close the small window where all matched entries vanish and the reply is a bare header with zero candidates.
  5. CAS-conflict copy — a conflict surfaces as the generic "Failed to update channel memory: …". A distinct message ("that entry changed since I looked — 查看记忆 and retry with its ID") would make the guard understandable to the user rather than looking like an I/O error. Fine as a follow-up.
  6. Prompt wording nit — the classifier prompt says inspect/remove take "one or more" targetIds, but multi-target plans never execute (always demoted to a candidates list). Nudging the model to return the single best match when the user clearly means one entry would reduce needless ambiguous replies. On the flip side, English coverage of the new natural phrasings is thin — filtered lists are only reachable via the Chinese 只看/仅列出…偏好/习惯 arm or words already in the trigger.

One user-visible behavior change worth being aware of: 删除 m-xxx / delete m-xxx previously fell through to the agent (the old classifier prompt explicitly mapped single-entry deletion to none); they now mutate deterministically and immediately. That's the point of the PR and the docs say so — just flagging it since it changes what an existing message does.

Verdict

Well-scoped, security-conscious implementation with strong tests; the suggestions above are all minor or follow-up material. LGTM.


🤖 Generated with Claude Code — Claude Fable 5

@wenshao

wenshao commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao added this pull request to the merge queue Jul 15, 2026
Merged via the queue into QwenLM:main with commit 859095b Jul 15, 2026
74 of 75 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.

3 participants