Skip to content

[codex] Add explicit channel memory for messaging channels - #6051

Merged
qqqys merged 28 commits into
QwenLM:mainfrom
qqqys:feat/channel-memory-mvp
Jul 1, 2026
Merged

[codex] Add explicit channel memory for messaging channels#6051
qqqys merged 28 commits into
QwenLM:mainfrom
qqqys:feat/channel-memory-mvp

Conversation

@qqqys

@qqqys qqqys commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds explicit per-chat/thread channel memory for Qwen Code messaging channels. Authorized channel members can save, view, and clear stable context with channel commands, and Qwen Code injects that memory when a fresh channel session starts.

Why it's needed

Messaging channels are often used by teams that need chat-specific context such as environment defaults, release norms, ownership notes, or workflow constraints. Repeating that context manually is noisy, while putting it in channel-wide instructions is too broad. Channel memory gives each chat or thread a small explicit memory surface with strict allowedUsers control.

Reviewer Test Plan

How to verify

Configure a channel with allowedUsers containing the sender. Run /remember-channel Prefer concise release triage., then /channel-memory and confirm the saved memory is shown. Run /clear, send a normal prompt, and confirm the new session receives the saved channel memory. Run /forget-channel confirm, then /channel-memory, and confirm it reports no saved memory. Also confirm a sender not listed in allowedUsers, or a channel with empty allowedUsers, cannot read, write, or clear channel memory.

Evidence (Before & After)

N/A for local automated verification. This change is covered by focused unit tests for the core memory store, channel command behavior, prompt injection, concurrency edge cases, and CLI channel startup wiring.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node.js workspace tests and build on local macOS.

Risk & Scope

  • Main risk or tradeoff: Channel memory is injected into the first prompt of a session, so concurrency around session invalidation and queued prompts must remain correct. The implementation includes regression tests for slow memory reads, collect mode buffering, /clear races, and read failure cleanup.
  • Not validated / out of scope: Proactive routines, automatic memory writes, Slack support, and scheduler work are not included.
  • Breaking changes / migration notes: None. Channel memory is inert unless channel startup wires callbacks and authorized users invoke the new commands.

Linked Issues

Closes #6050

中文说明

What this PR does

为 Qwen Code 的消息频道增加显式的按 chat/thread 隔离的频道记忆。授权频道成员可以通过频道命令保存、查看、清除稳定上下文,Qwen Code 会在新的频道会话开始时注入这段记忆。

Why it's needed

团队在消息频道中使用 agent 时,经常需要按聊天保存环境默认值、发布规范、负责人信息或工作流约束。每次手动重复这些上下文很吵,而写入整个频道的 instructions 又过于宽泛。频道记忆提供了一个小而显式的记忆面,并严格由 allowedUsers 控制。

Reviewer Test Plan

How to verify

配置一个频道,并让 allowedUsers 包含发送者。执行 /remember-channel Prefer concise release triage.,再执行 /channel-memory,确认能看到保存的记忆。执行 /clear 后发送普通 prompt,确认新会话会收到保存的频道记忆。执行 /forget-channel confirm,再执行 /channel-memory,确认返回没有保存的记忆。还要确认未列入 allowedUsers 的发送者,或 allowedUsers 为空的频道,不能读取、写入或清除频道记忆。

Evidence (Before & After)

本地自动化验证无需截图。该改动由 focused unit tests 覆盖,包括 core 记忆存储、频道命令行为、prompt 注入、并发边界,以及 CLI channel startup wiring。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

本地 macOS Node.js workspace 测试和构建。

Risk & Scope

  • Main risk or tradeoff: 频道记忆会注入到 session 的首个 prompt,因此 session invalidation 和 queued prompt 的并发行为必须保持正确。实现中已经加入慢 memory read、collect mode buffering、/clear race 和 read failure cleanup 的回归测试。
  • Not validated / out of scope: 不包含主动 routines、自动写记忆、Slack 支持和 scheduler 工作。
  • Breaking changes / migration notes: 无。除非 channel startup 注入 callbacks 且授权用户调用新命令,否则频道记忆不会生效。

Linked Issues

Closes #6050

@qqqys
qqqys marked this pull request as ready for review June 30, 2026 07:37
…e-pr-6051-conflicts

# Conflicts:
#	packages/channels/base/src/ChannelBase.ts
#	packages/cli/src/commands/channel/start.test.ts
#	packages/cli/src/commands/channel/start.ts
@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: Per-chat channel memory for messaging channels fills a real gap — teams using channels need persistent context (env defaults, release norms, ownership notes) per-chat without repeating it every session. This sits squarely within qwen-code's channel system and complements the existing session/instructions infrastructure. No direct Claude Code analogue, but the area is clearly relevant to qwen-code's channel feature set.

On approach: The scope is well-scoped — three commands (/remember-channel, /channel-memory, /forget-channel), file-based storage with proper-lockfile, callback-based wiring to keep core free of channel-base dependencies. The concurrency model (serialized appends per-file + lockfile retries) is well thought out. The .gitignore addition for .worktrees/ is a minor drive-by but harmless.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:按聊天保存频道记忆是消息频道功能的实际需求——团队需要在每个聊天中持久化环境默认值、发布规范或负责人信息,而不必每次重复。这在 qwen-code 的频道系统范围内,与现有的 session/instructions 基础设施互补。

方案:范围合理——三个命令(/remember-channel/channel-memory/forget-channel),基于文件的存储加 proper-lockfile,回调式注入保持 core 包不依赖 channel-base。并发模型(按文件串行化 append + lockfile 重试)考虑周全。.gitignore 添加 .worktrees/ 是小的顺手改动,无影响。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Code Review

No blocking issues found.

Independently, before reading the diff: I'd have built a file-backed per-channel+chat memory store in packages/core/memory, wired it through callbacks into ChannelBase, added three slash commands with allowedUsers authorization and group-chat guards, and injected the memory on the first prompt of a session alongside instructions. The PR's approach matches this almost exactly.

What the implementation gets right:

  • Clean separation — core storage (channel-memory.ts) has no channel-base dependency; ChannelBase receives callbacks via options
  • Authorization is enforced at both the command layer (all three handlers) and the injection path (shared session + open policy blocks injection to prevent cross-user leakage)
  • sanitizePromptText applied on both display and injection paths — prevents stored text from carrying bidi overrides or control chars into prompts
  • serializeAppend + proper-lockfile + in-lock size re-check handles concurrent writes and the 1 MB cap correctly
  • invalidateSessionContext after writes so the next prompt re-injects updated memory

One actionable note (pre-existing, not blocking this PR): packages/channels/base/package.json has no test:ci script, so the 235 ChannelBase.test.ts tests don't run in CI. Adding "test:ci": "vitest run" (matching packages/channels/telegram) would wire them into the merge gate. Worth doing alongside or right after this merge.

Test Results

Suite Tests Status
packages/core/src/memory/channel-memory.test.ts 17 / 17 ✅ pass
packages/channels/base/src/ChannelBase.test.ts 235 / 235 ✅ pass
packages/cli/src/commands/channel/start.test.ts 20 / 20 ✅ pass
tsc --build (core + channel-base) 0 errors ✅ pass

tmux real-scenario testing: N/A. Channel memory is backend logic exercised through messaging adapters (Telegram, Feishu, QQ). There is no CLI TUI surface to drive — the feature's behavior is fully covered by the unit test suites above (command handlers, authorization, group guards, prompt injection, sanitization, concurrency).

Prior maintainer verification reports on this PR include real-binary E2E harness tests (50/50 assertions), mutation testing (all guards proven load-bearing), and 30-way concurrent append probes (zero cap violations). These are independent confirmations that go beyond what unit tests alone can show.

中文说明

代码审查

未发现阻塞性问题。

独立方案(读 diff 前): 我会在 packages/core/memory 里做一个基于文件的按频道+聊天的记忆存储,通过回调接入 ChannelBase,加三个 slash 命令并带上 allowedUsers 鉴权和群聊守卫,在 session 首个 prompt 中注入记忆。PR 的方案与此几乎完全一致。

实现优点:

  • 清晰分层——core 存储不依赖 channel-base;ChannelBase 通过 options 接收回调
  • 鉴权在命令层(三个 handler)和注入路径(共享会话 + open 策略阻止注入以防跨用户泄露)都有
  • sanitizePromptText 在展示和注入两条路径上都应用——防止存储的文本携带 bidi 或控制字符进入 prompt
  • serializeAppend + proper-lockfile + 锁内体积复检正确处理了并发写入和 1 MB 上限
  • 写入后 invalidateSessionContext 确保下一个 prompt 重新注入更新后的记忆

一个可操作的建议(既有问题,不阻塞本 PR): packages/channels/base/package.json 缺少 test:ci 脚本,235 个 ChannelBase.test.ts 测试不在 CI 中运行。加上 "test:ci": "vitest run"(参考 packages/channels/telegram)即可。

测试结果

套件 测试数 状态
packages/core/src/memory/channel-memory.test.ts 17 / 17 ✅ 通过
packages/channels/base/src/ChannelBase.test.ts 235 / 235 ✅ 通过
packages/cli/src/commands/channel/start.test.ts 20 / 20 ✅ 通过
tsc --build(core + channel-base) 0 错误 ✅ 通过

tmux 真实场景测试:不适用。 频道记忆是通过消息 adapter(Telegram、Feishu、QQ)使用的后端逻辑,没有 CLI TUI 界面可驱动——功能行为已被上述单测套件完整覆盖。

此前维护者验证报告包括真实编译产物的端到端测试(50/50 断言通过)、变异测试(所有守卫均承重)、30 路并发 append 探针(零超限)。这些是超越单测的独立确认。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

This PR is well-designed and well-tested. The implementation does exactly what the description says: per-chat channel memory with strict authorization, group-chat guards, prompt-injection sanitization, and correct concurrency handling. The approach matches what I'd have done independently, and I don't see a simpler path that covers the same ground.

Every security guard is load-bearing (proven by mutation testing in prior verification rounds). The test suites pass clean (272 tests across three packages). Typecheck is clean. The only gap — channels/base not running tests in CI — is pre-existing and easily fixed.

The design choices I'd want to flag for awareness (not blocking):

  • Multi-line memory is flattened to single-line on display/injection by sanitizePromptText — deliberate defense against prompt injection, but worth a docs note if multi-line memories are expected
  • The .gitignore addition for .worktrees/ is a minor drive-by — harmless but unrelated to the feature

Approving. ✅

中文说明

这个 PR 设计合理、测试充分。实现完全符合描述:按聊天的频道记忆,带严格鉴权、群聊守卫、防 prompt 注入的净化和正确的并发处理。方案与我独立想到的吻合,没有看到更简的路径能覆盖同样场景。

每个安全护栏都承重(此前变异测试证明)。三个包的测试套件全部通过(272 个测试),类型检查无错误。唯一的缺口——channels/base 不在 CI 跑测试——是既有问题,容易修复。

提醒注意(非阻塞):

  • 多行记忆在展示/注入时被压平为单行——这是刻意的防注入设计,但如果预期支持多行记忆建议在文档里说明
  • .gitignore.worktrees/ 是小的顺手改动,无影响但与本功能无关

批准。✅

Qwen Code · qwen3.7-max

@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 Request changes to Comment: CI still running.

3 Critical findings and 4 Suggestions below. The Critical items are (1) a persistent prompt-injection vector via stored channel memory, (2) a TOCTOU race that lets concurrent appends silently bypass the 1 MB cap, and (3) unhandled errors from the new slash-command callbacks that can surface as generic failures to users and operators.

— qwen3.7-max via Qwen Code /review

this.channelMemoryTarget(envelope),
)
)?.trim();
if (channelMemory) {

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.

[Critical] Persistent prompt injection via stored channel memory

Channel memory content is concatenated directly into the agent prompt here without going through sanitizePromptText. The codebase already applies sanitizePromptText to user messages (e.g. ChannelBase.ts:1105) to strip PROMPT_UNSAFE_INVISIBLES — bidi overrides (U+202A–U+202E), C1 controls including NEL U+0085, Unicode line/paragraph separators U+2028/U+2029, zero-width chars, and C0/DEL controls. Stored channel memory bypasses all of those defenses.

An authorized user who runs /remember-channel with crafted content (e.g. [SYSTEM]: ignore prior instructions… or text containing bidi overrides for a trojan-source attack) stores it verbatim. On every subsequent new session in that chat, the unsanitized text is prepended ahead of the real user message and is seen by the model as authoritative context. It also survives /clear and persists across senders — any user in the chat receives the injected instructions, even if they are not in allowedUsers (since the prompt-prepend path does not gate on isAuthorizedForChannelMemory, see related Suggestion).

Apply the same sanitizer used for user messages:

Suggested change
if (channelMemory) {
if (channelMemory) {
context.push(
`Channel memory for this chat:\n${sanitizePromptText(channelMemory)}`,
);
}

— qwen3.7-max via Qwen Code /review


await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.appendFile(filePath, `${entry}\n`, 'utf8');
return { changed: true, filePath };

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.

[Critical] TOCTOU race lets concurrent appends bypass the 1 MB cap

The size check at line 106 (getExistingSize via fs.stat) and the actual write at line 111 (fs.appendFile) are separated by fs.mkdir and an await, so two concurrent /remember-channel calls targeting the same file can both read a small existingSize, both pass the cap, and both write. The result is a file that silently grows past MAX_CHANNEL_MEMORY_BYTES.

Worse, readChannelMemory silently returns '' for oversized files (no log, no warning), so after the race the memory becomes invisible to both the /channel-memory command and the prompt-injection path — a silent data-loss mode that is only discoverable by ls-ing the hashed directory on disk.

Make the check and write atomic on the same open file descriptor:

Suggested change
return { changed: true, filePath };
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
const fh = await fs.open(filePath, 'a+');
try {
const stat = await fh.stat();
if (stat.size + appendBytes > MAX_CHANNEL_MEMORY_BYTES) {
throw new Error('Channel memory exceeds maximum size');
}
await fh.appendFile(`${entry}\n`, 'utf8');
} finally {
await fh.close();
}
return { changed: true, filePath };

— qwen3.7-max via Qwen Code /review

return true;
}
await channelMemory.appendChannelMemory(
this.channelMemoryTarget(envelope),

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.

[Critical] Unhandled errors from memory callbacks propagate out of command handlers

/remember-channel, /channel-memory, and /forget-channel call appendChannelMemory, readChannelMemory, and clearChannelMemory with no try/catch. When a callback throws (e.g. Error('Channel memory exceeds maximum size') from appendChannelMemory, a disk-full EACCES, or a transient I/O error), the exception propagates out of handleInbound. Adapters then surface a generic "something went wrong" reply to the user, and the operator-side log lacks the chat ID / thread ID / sender ID needed to triage. On the /remember-channel path, the throw also skips invalidateSessionContext, leaving the in-process session context stale relative to whatever partial state landed on disk.

Other command handlers in this file guard their logic and always send a user-facing message before returning true. These three should follow the same pattern:

Suggested change
this.channelMemoryTarget(envelope),
const channelMemory = await this.getChannelMemory(envelope);
if (!channelMemory) {
return true;
}
try {
await channelMemory.appendChannelMemory(
this.channelMemoryTarget(envelope),
args.trim(),
);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
await this.sendMessage(
envelope.chatId,
`Failed to save channel memory: ${msg}`,
);
return true;
}
this.invalidateSessionContext(envelope);
await this.sendMessage(envelope.chatId, 'Channel memory updated.');
return true;

Apply the same try/catch + user-visible message pattern to the /channel-memory handler (around readChannelMemory) and the /forget-channel confirm handler (around clearChannelMemory).

— qwen3.7-max via Qwen Code /review

if (context.length > 0) {
promptText = `${context.join('\n\n')}\n\n${promptText}`;
}
} catch (error) {

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] Prompt-injection path reads memory regardless of isAuthorizedForChannelMemory

The three slash commands are gated by ensureChannelMemoryAuthorized, which requires allowedUsers.length > 0 && allowedUsers.includes(senderId). This prompt-prepend path at lines 1300–1316 is unconditional: any sender who reaches shouldPrependSessionContext causes a memory read that is concatenated into the agent's context.

Two consequences:

  1. In an open channel with a non-empty allowedUsers list, an unauthorized user can send a message and then ask the agent to repeat or summarize the injected memory content — bypassing the command-level gate.
  2. If allowedUsers is later cleared (or a user is removed), previously written memory still influences every new session. The documentation says "If allowedUsers is empty, channel memory commands are disabled for everyone", which operators will read as "fully inactive" — but the read path stays active.

Either gate the read the same way the commands are gated (call isAuthorizedForChannelMemory(envelope) before reading), or document explicitly that channel memory is visible to all chat participants through the agent and remove the authorization gate from /channel-memory for consistency.

— qwen3.7-max via Qwen Code /review

return fs.readFile(filePath, 'utf8');
}

export async function appendChannelMemory(

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] Oversized-file branch is silent and indistinguishable from "no memory"

When size > MAX_CHANNEL_MEMORY_BYTES, readChannelMemory returns '' with no log, no warning, and no error. The /channel-memory command then reports "No channel memory saved." even though a file with data exists on disk. Combined with the TOCTOU race in appendChannelMemory, this is the likely end state after a concurrent overshoot — and it is undiagnosable from any observable surface (command response, prompt injection, log output).

At minimum, emit a stderr warning so an operator can tell the file exists and is oversized:

Suggested change
export async function appendChannelMemory(
if (size > MAX_CHANNEL_MEMORY_BYTES) {
process.stderr.write(
`[channel-memory] WARNING: ${filePath} is ${size} bytes (max ${MAX_CHANNEL_MEMORY_BYTES}); treating as empty\n`,
);
return '';
}

Ideally surface this to the /channel-memory command response as well (e.g. return a sentinel or throw, so the handler can say "Channel memory file is too large to read" instead of "No channel memory saved.").

— qwen3.7-max via Qwen Code /review

}

export interface ChannelMemoryWriteResult {
changed: boolean;

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] Duplicated callback contract with divergent filePath optionality

ChannelMemoryTarget and ChannelMemoryWriteResult are defined in two places: packages/core/src/memory/channel-memory.ts:18-27 and packages/channels/base/src/types.ts:105-120. TypeScript structural typing masks the duplication today, but the filePath field has different optionality: required (filePath: string) in the core implementation, optional (filePath?: string) in the channel-base callback contract. A consumer of ChannelMemoryCallbacks therefore sees filePath as potentially undefined, while the implementation always provides it; any future branch on result.filePath === undefined is dead code.

Worse, a maintainer who adds a field to one definition (e.g. createdAt) gets no compile error — the callback boundary is satisfied structurally, and the divergence is only discovered at runtime.

Either import the shared types from @qwen-code/qwen-code-core (or a shared types package), or at minimum make the optionality consistent and add a // SYNC: keep in lockstep with packages/core/src/memory/channel-memory.ts comment on both definitions.

— qwen3.7-max via Qwen Code /review

expect(ch.sent[0]!.text).not.toContain('/global-only');
});

it('/remember-channel appends memory for an allowed user', async () => {

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] Test coverage gaps around the new channel-memory surface

The new command handlers and memory callbacks have several untested branches that could regress silently:

  1. /remember-channel with empty or whitespace-only args (the Usage: /remember-channel <text> branch at ChannelBase.ts:516-520).
  2. /forget-channel confirm when clearChannelMemory returns { changed: false } (the No channel memory saved. branch at ChannelBase.ts:576).
  3. Authorization denial with a non-empty allowedUsers that does not include the sender (only allowedUsers: [] is tested for denial; the includes half of isAuthorizedForChannelMemory is unverified).
  4. Non-ENOENT errors from fs.stat / fs.unlink in readChannelMemory and clearChannelMemory (the throw error branches at channel-memory.ts:86 and channel-memory.ts:126).
  5. Callback-throws behavior from command handlers (the scenario that makes the Critical finding above user-visible).

Add focused tests for each so the authorization, error-propagation, and branch-coverage contracts are pinned.

— qwen3.7-max via Qwen Code /review

@DragonnZhang DragonnZhang 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.

Automated Review — PR #6051

Reviewed the channel memory implementation for persistence correctness, data isolation, memory size limits, and cleanup. Two high-confidence findings below.

Note: This PR contains substantial changes beyond the channel memory feature (see inline comment on scope).

this.instructedSessions.add(sessionId);
}
if (shouldPrependSessionContext) {
try {

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.

[HIGH] Prompt injection reads channel memory without allowedUsers authorization

The prompt injection path calls this.channelMemory?.readChannelMemory(this.channelMemoryTarget(envelope)) directly, without the ensureChannelMemoryAuthorized(envelope) check that gates the three channel memory commands (/remember-channel, /channel-memory, /forget-channel).

This means saved channel memory is injected into the prompt of every sender who starts a new session in this chat — including senders NOT listed in allowedUsers. The PR description states: "Only users listed in allowedUsers can read, write, or clear channel memory."

The implementation diverges from this contract on the read side: unauthorized users cannot use the /channel-memory command, but their prompts still receive the saved memory.

This matters for sessionScope: 'user' (per-sender sessions) and for channels with restricted allowedUsers where some members should not see team-shared context.

Suggested fix: Either (a) gate the injection with the same isAuthorizedForChannelMemory(envelope) check so only authorized senders receive the memory in their prompt, or (b) update the PR description and docs to clarify that channel memory is write-restricted but read-shared with all session participants.

}

const appendBytes = Buffer.byteLength(`${entry}\n`, 'utf8');
const existingSize = await getExistingSize(filePath);

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.

[HIGH] TOCTOU race in appendChannelMemory size check

The size enforcement has a time-of-check-time-of-use gap: getExistingSize(filePath) reads the current file size, then fs.appendFile writes the entry (line 105). Between these two async operations, another concurrent call can read the same pre-write size, also pass the check, and both writes land — pushing the file past MAX_CHANNEL_MEMORY_BYTES.

Once the file exceeds the limit, readChannelMemory (line 82) silently returns '' for all future reads, making the memory invisible to both prompt injection and the /channel-memory command — while the file continues to exist on disk. The user has no recovery path short of manually deleting the file.

The /remember-channel command handler serializes through sessionQueues for a given session, which mitigates this for the common case. But different sessions targeting the same file (e.g., sessionScope: 'user' with multiple authorized senders in the same chat) can still race.

Suggested fix: Use fs.open with 'a+' mode, fstat the fd for current size, check the limit, then write — all on the same file descriptor. This makes the check-and-append atomic from the process's perspective.

@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.

Code Review Summary

Found 4 findings (1 Critical, 2 Suggestions, 1 Needs Human Review). The overall design is solid — file-based per-channel memory with serialization is a good approach. The critical item is a race condition in clearChannelMemory that could silently defeat /forget-channel when used concurrently with /remember-channel.

): Promise<ChannelMemoryWriteResult> {
const filePath = getChannelMemoryFilePath(target);
try {
await fs.unlink(filePath);

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.

🔴 Critical: clearChannelMemory bypasses serializeAppend queue

clearChannelMemory calls fs.unlink directly without going through the serializeAppend queue. This creates a race condition:

  1. User runs /remember-channel somethingappendChannelMemory enters serializeAppend, acquires lock, opens file with 'a+'
  2. User runs /forget-channel confirm concurrently → clearChannelMemory calls fs.unlink, deleting the file
  3. The pending append's fs.open('a+') (already holding the old handle or opening after unlink) recreates the file
  4. Result: the clear is silently defeated, memory reappears

Fix: Wrap the unlink in serializeAppend to ensure mutual exclusion with appends:

export async function clearChannelMemory(
  target: ChannelMemoryTarget,
): Promise<ChannelMemoryWriteResult> {
  const filePath = getChannelMemoryFilePath(target);
  return serializeAppend(filePath, async () => {
    try {
      await fs.unlink(filePath);
      return { changed: true, filePath };
    } catch (error) {
      if (isMissingFile(error)) {
        return { changed: false, filePath };
      }
      throw error;
    }
  });
}

promptText = `${context.join('\n\n')}\n\n${promptText}`;
}
} catch (error) {
this.instructedSessions.delete(sessionId);

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: Missing structured logging on prompt injection error path

All three slash command handlers (/remember-channel, /channel-memory, /forget-channel) use this.logChannelMemoryError(...) for structured error logging. However, this catch block in the session context injection path only deletes the session and re-throws — without calling logChannelMemoryError.

This means channel memory read failures during session setup are silently swallowed from the structured log, making production debugging harder.

Fix: Add structured logging before the throw:

} catch (error) {
  this.logChannelMemoryError('read', envelope, this.channelMemoryErrorMessage(error));
  this.instructedSessions.delete(sessionId);
  throw error;
}

}

private channelMemoryErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);

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: Error messages may disclose absolute filesystem paths

channelMemoryErrorMessage returns error.message verbatim from fs operations. Node.js filesystem errors include the full absolute path (e.g., ENOENT: no such file or directory, open '/home/user/.qwen/channels/memory/...'). These messages are sent back to chat users via the slash command handlers.

Consider returning a generic message to avoid leaking server filesystem layout:

private channelMemoryErrorMessage(error: unknown): string {
  return 'An error occurred while accessing channel memory.';
}

Keep the detailed error for logChannelMemoryError (which already sanitizes via sanitizeLogText), but don't expose it to end users.

if (size > MAX_CHANNEL_MEMORY_BYTES) {
return '';
}
return fs.readFile(filePath, 'utf8');

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.

🔵 Needs Human Review: stat-then-read TOCTOU with concurrent clearChannelMemory

readChannelMemory calls fs.stat then fs.readFile. If a concurrent clearChannelMemory (which bypasses serializeAppend — see finding on line 133) deletes the file between these two calls, fs.readFile will throw ENOENT, which is not caught here — only the fs.stat ENOENT is handled.

This is a narrow race on local filesystem, but becomes relevant given the clearChannelMemory serialization bug. If clearChannelMemory is fixed to use serializeAppend, the risk here is also reduced since reads would be the only non-serialized operation remaining.

Consider adding an ENOENT catch around fs.readFile as well, or wrapping the read in serializeAppend too.

@DragonnZhang DragonnZhang 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.

Incremental Review — Commits 26f050f..6de49e0

Both HIGH findings from the previous review are resolved:

  1. Prompt injection auth bypass (FIXED): The prompt injection path now gates readChannelMemory behind isAuthorizedForChannelMemory(envelope) and applies sanitizePromptText() to the memory content before injection. Tests confirm unauthorized senders (e.g. bob) do not trigger a memory read, and Unicode bidi overrides are stripped.

  2. TOCTOU race in appendChannelMemory (FIXED): The size check and write are now atomic within a serializeAppend in-process queue plus a proper-lockfile cross-process lock. The check reads size from the open file handle (handle.stat()) rather than a separate fs.stat, eliminating the race window. A new concurrent-append test verifies the cap holds under contention.

The error handling additions (try/catch on all three command handlers) are correct and well-tested. The serializeAppend queue implementation is sound — tasks chain on predecessor completion without propagating failures to successors, and the queue map is cleaned up properly.

No new high-confidence findings in this increment.

}

let shouldPrependSessionContext = !this.instructedSessions.has(sessionId);
if (shouldPrependSessionContext) {

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.

[Critical] Unauthorized-first-sender starves channel memory for shared sessions

instructedSessions.add(sessionId) fires unconditionally before the serialization queue. Inside the queue, isAuthorizedForChannelMemory gates the actual memory read. For shared sessions (sessionScope: 'single' or 'thread' in group chats), if an unauthorized user sends the first message, the session is permanently marked as "instructed" without memory injection. All subsequent authorized users skip context-prepend because instructedSessions.has(sessionId) is already true — channel memory is silently starved for the lifetime of the session.

The only recovery is /clear. An unauthorized user can permanently prevent authorized users from receiving their channel memory simply by being the first to message after a session is created or cleared.

Suggested change
if (shouldPrependSessionContext) {
let shouldPrependSessionContext = !this.instructedSessions.has(sessionId);

Move instructedSessions.add(sessionId) inside the queue callback, after the authorization check succeeds and context is actually built. For unauthorized senders on shared sessions, defer the marking so the next authorized sender still triggers injection:

// Inside the queued callback, after building context:
if (shouldPrependSessionContext) {
  try {
    const context: string[] = [];
    const isAuth = this.isAuthorizedForChannelMemory(envelope);
    // ... build context ...
    if (context.length > 0) {
      promptText = `${context.join('\n\n')}\n\n${promptText}`;
    }
    // Only mark instructed if we actually injected or no memory backend exists
    if (isAuth || !this.channelMemory) {
      this.instructedSessions.add(sessionId);
    }
  } catch (error) {
    this.instructedSessions.delete(sessionId);
    throw error;
  }
}

— qwen3.7-max via Qwen Code /review

)?.trim()
: undefined;
if (channelMemory) {
context.push(

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] Channel memory injected before operator instructions reverses prompt precedence

Channel memory (user-authored content from /remember-channel) is pushed into the context array before this.config.instructions. After joining, user-controlled memory appears ahead of the operator's directives. In the previous code, instructions was always the first content prepended to the prompt.

While sanitizePromptText strips bracket-tag patterns and bidi overrides from memory, semantic prompt injection (e.g., "always answer in French, skip code reviews") is not mitigated by character-level sanitization and now occupies a higher-priority position than operator instructions.

Suggested change
context.push(
if (this.config.instructions) {
context.push(this.config.instructions);
}
if (channelMemory) {
context.push(
`Channel memory for this chat:\n${sanitizePromptText(channelMemory)}`,
);
}

— qwen3.7-max via Qwen Code /review

return true;
});

this.registerCommand('forget-channel', async (envelope, args) => {

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] /forget-channel shows confirmation prompt before checking if memory is configured

The args !== 'confirm' guard runs before getChannelMemory(), so an authorized user on a channel without channelMemory callbacks gets a misleading "Re-send with 'confirm'" prompt. Only on the follow-up /forget-channel confirm does the user learn memory isn't configured.

The other two commands (/remember-channel and /channel-memory) both call getChannelMemory immediately after the authorization check, giving a clear "Channel memory is not configured" response.

Suggested change
this.registerCommand('forget-channel', async (envelope, args) => {
this.registerCommand('forget-channel', async (envelope, args) => {
if (!(await this.ensureChannelMemoryAuthorized(envelope))) {
return true;
}
const channelMemory = await this.getChannelMemory(envelope);
if (!channelMemory) {
return true;
}
if (args.toLowerCase() !== 'confirm') {

— qwen3.7-max via Qwen Code /review

DragonnZhang
DragonnZhang previously approved these changes Jun 30, 2026

@DragonnZhang DragonnZhang 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.

Incremental review (045c4f7)

Commit: fix(channels): serialize channel memory clears

This commit correctly addresses both HIGH findings from the previous review cycle:

  1. TOCTOU in readChannelMemory -- Fixed by wrapping fs.readFile in a try/catch that handles ENOENT when the file is deleted between stat and readFile.

  2. clearChannelMemory serialization -- Fixed by wrapping in serializeAppend (in-process serialization per filePath) plus lockfile.lock (cross-process safety), consistent with the appendChannelMemory pattern.

Additional improvements:

  • User-facing error messages now use a generic string instead of leaking raw error details (EACCES, disk full, etc.) to chat users. Raw errors are still logged to stderr for debugging.
  • Added missing logChannelMemoryError call in the prompt-building catch block for observability.
  • Tests updated with stderr spy assertions and a new serialization concurrency test.

No new high-confidence findings. LGTM.

this.channelMemoryErrorMessage(error),
);
this.instructedSessions.delete(sessionId);
throw error;

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.

[Critical] Memory read error in prompt injection path re-throws, silently dropping user message

When readChannelMemory throws during first-session context injection, this throw error propagates through the session queue and causes handleInbound to reject — the user's message is never sent to the agent. No response reaches the chat.

The three slash command handlers (/remember-channel, /channel-memory, /forget-channel) handle the same error class gracefully: they log the detail, send a user-facing error message, and return true. But the prompt injection path does not degrade — a non-critical feature (channel memory read) kills the entire message processing path.

Any transient filesystem issue (NFS timeout, permission change, disk pressure) on the first message of a session silently drops the user's turn.

Suggested change
throw error;
// Degrade gracefully — don't drop the user's message over a memory read failure

— qwen3.7-max via Qwen Code /review

}
}

export async function readChannelMemory(

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] readChannelMemory not serialized with writes

Both appendChannelMemory and clearChannelMemory go through serializeAppend (in-process serialization) + proper-lockfile (cross-process locking). readChannelMemory uses neither — it does a bare fs.stat then fs.readFile with no lock and no queue.

A concurrent read during a write could yield partially-written content. The practical impact is low (writes are fast, the race window is tiny), but the asymmetry creates a structural gap.

Suggested change
export async function readChannelMemory(
export async function readChannelMemory(
target: ChannelMemoryTarget,
): Promise<string> {
const filePath = getChannelMemoryFilePath(target);
return serializeAppend(filePath, async () => {
// ... existing stat + readFile logic ...
});
}

— qwen3.7-max via Qwen Code /review

: undefined;
if (channelMemory) {
context.push(
`Channel memory for this chat:\n${sanitizePromptText(channelMemory)}`,

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] sanitizePromptText strips \n from channel memory, collapsing multi-entry memory into a single line

sanitizePromptText applies .replace(/[\u0000-\u001f\u007f]/g, ' ') which strips \n (U+000A). When multiple /remember-channel entries are stored (each on its own line), they merge into one space-separated line in the prompt, making it harder for the model to distinguish discrete instructions. No test covers this multi-line scenario.

Consider sanitizing each line individually and rejoining with \n, or using a channel-memory-specific sanitizer that preserves structural newlines while still stripping bracket patterns and invisible characters.

— qwen3.7-max via Qwen Code /review

return '_';
}
return safeName || '_';
}

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] safeChannelName is a many-to-one mapping — channel names like "ops/alerts", "ops-alerts", and "ops alerts" all sanitize to "ops_alerts". Since the channel name is the first path component isolating one channel's memory from another, a collision means two independently configured channels share the same CHANNEL.md file. A user authorized on one channel can read or poison the memory of the colliding channel, which may have a different allowedUsers list.

Consider using a hash-based directory name (optionally prefixed with a truncated slug for debuggability) to make the mapping injective:

Suggested change
}
function safeChannelName(channelName: string): string {
const slug = channelName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 20);
const hash = createHash('sha256').update(channelName).digest('hex').slice(0, 16);
return `${slug}-${hash}`;
}

— qwen3.7-max via Qwen Code /review

};
}

private invalidateSessionContext(envelope: Envelope): void {

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] invalidateSessionContext only removes the calling sender's session from instructedSessions (resolved via router.getSession with the sender's ID). In user-scoped group chats, each user has a distinct session, so when user A runs /remember-channel or /forget-channel, only A's session is marked for re-injection. Other authorized users' ongoing sessions keep the previously-injected (now stale) channel memory until they individually run /clear.

Consider either:

  1. Adding a getSessionsForChat(channelName, chatId, threadId) method to SessionRouter to enumerate all sessions for the chat and invalidate them all.
  2. Documenting this as a known limitation in the channel memory docs, noting that other participants need /clear to pick up memory changes.

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jul 1, 2026

@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.

Review Summary (Incremental)

This review adds 2 new findings after deduplicating against 51 existing comments. Most previously raised concerns (file permissions, duplicate types, test gaps, senderPolicy design) are already well-covered by prior review rounds.

Additional concern not on a diff line

Loop job path skips channel memory injection (ChannelBase.ts:292): The loop job path calls this.instructedSessions.add(sessionId) and prepends only this.config.instructions, but does not inject channel memory. Since the main message path (lines 1938-1972) now reads and injects channel memory inside the queue callback, loop-triggered messages in the same session will have the session marked as "instructed" without ever receiving channel memory context. This is a behavioral regression — loop jobs get instructions but not the channel memory that a normal first message would receive.

CI Status

6 of 30 checks are failing (Test on all 3 platforms, Integration Tests, Post Coverage Comment, review-pr). The CI failures themselves may warrant investigation before merge.

context.push(this.config.instructions);
}
if (context.length > 0) {
promptText = `${context.join('\n\n')}\n\n${promptText}`;

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] No prompt-size budget for channel memory injection

Up to 1 MB of channel memory (the MAX_CHANNEL_MEMORY_BYTES cap) can be prepended to promptText here with no truncation or token budget. On models with smaller context windows, this could crowd out the user's actual message or cause truncation elsewhere.

Consider adding a token/character budget that accounts for the model's context window and the user's prompt length, truncating channel memory with a [...truncated] marker when it exceeds the budget.

}
if (shouldPrependSessionContext) {
const context: string[] = [];
let channelMemory: string | undefined;

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] Local channelMemory (string) shadows this.channelMemory (callbacks object)

This local variable has the same name as the instance field this.channelMemory: ChannelMemoryCallbacks declared at line 146. Inside this block, this.channelMemory?.readChannelMemory(...) refers to the callbacks while channelMemory refers to the string result — easy to confuse during maintenance.

Consider renaming to memoryText or channelMemoryContent.

await this.channelMemory?.readChannelMemory(
this.channelMemoryTarget(envelope),
)
)?.trim();

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.

[HIGH] Prompt injection path crashes with TypeError when channelMemory is not wired but allowedUsers is non-empty

this.channelMemory is typed ChannelMemoryCallbacks | undefined (optional). The authorization guard isAuthorizedForChannelMemory only checks allowedUsers — it does not verify that this.channelMemory is defined.

When this.channelMemory is undefined and the sender passes the allowedUsers check, the optional chain this.channelMemory?.readChannelMemory(...) resolves to undefined. The subsequent .trim() is then called on undefined, throwing a TypeError.

The catch block handles the error, but it also calls this.instructedSessions.delete(sessionId), which causes the next message in the same session to retry the same failing injection — creating a loop of crash-log-retry on every message.

The CLI (start.ts) always provides channelMemory callbacks, so this is not hit in the standard path today. But ChannelBase accepts channelMemory as an optional option while allowedUsers is a standard config field, so external adapters, integration tests, or future refactors can trigger it.

// Fix: guard on channelMemory existence before the read
if (
  this.channelMemory &&
  this.isAuthorizedForChannelMemory(envelope) &&
  (!this.isSharedSession(envelope) ||
    this.config.senderPolicy === 'allowlist')
) {

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 1, 2026
@qqqys
qqqys dismissed stale reviews from qwen-code-ci-bot and wenshao via 5c2151c July 1, 2026 08:12
@wenshao

wenshao commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary — PR #6051

Base branch

main

Conflicted file

packages/channels/base/src/ChannelBase.ts

Conflicts

Conflict 1 — Field declaration (line ~155)

  • HEAD (PR): Added private readonly channelMemory?: ChannelMemoryCallbacks;
  • origin/main: Added private groupHistory: GroupHistoryStore;
  • Resolution: Kept both fields. They are independent features — channel memory (PR) and group history store (main).

Conflict 2 — Constructor initialization (line ~202)

  • HEAD (PR): Added this.channelMemory = options?.channelMemory;
  • origin/main: Added this.groupHistory = new GroupHistoryStore(...) initialization block.
  • Resolution: Kept both initializations in sequence. No semantic overlap between the two.

Commit

fix(channels): resolve merge conflicts with main (group history store)

Notes

  • All other files merged cleanly (auto-merged by git).
  • Only ChannelBase.ts required manual conflict resolution.
  • Imports for both features (ChannelMemoryCallbacks, GroupHistoryStore, getGlobalQwenDir, join) were already present from auto-merge.

@qqqys
qqqys enabled auto-merge July 1, 2026 08:58
@qqqys

qqqys commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

@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. ✅

);
}
}
if (this.config.instructions) {

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.

[Critical] Loop path does not clean up instructedSessions on memory read failure

When readChannelMemory throws here, the catch block logs the error but does not call this.instructedSessions.delete(sessionId). Line 344 then unconditionally adds to instructedSessions. Compare the main dispatch path (~line 1999) which correctly does this.instructedSessions.delete(sessionId) in its catch block.

A transient I/O error (disk hiccup, lock timeout, NFS stall) during a loop iteration permanently marks the session as "instructed," suppressing all future memory injection for that loop session — even after the transient condition resolves. Since loops run repeatedly, a single momentary failure silently degrades channel memory for the lifetime of the process.

Suggested change
if (this.config.instructions) {
} catch (error) {
process.stderr.write(
`[${this.name}] channel memory read failed for loop ${job.id} chat ${sanitizeLogText(job.target.chatId, 64)}: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}\n`,
);
this.instructedSessions.delete(sessionId);
}

— qwen3.7-max via Qwen Code /review

@@ -321,6 +305,41 @@ export abstract class ChannelBase {
'loop dropped because it is no longer enabled',
);
}

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] Duplicated context-injection logic between loop and main dispatch paths

~30 lines of near-identical logic (authorization check, memory read, instructions prepend, error recovery) appear in both this loop path (lines 307–344) and the main dispatch path (lines ~1978–2015). The two copies have already diverged in error-handling behavior — the loop path is missing instructedSessions.delete on failure, unsanitized error logging, and the second staleness check — which is exactly the risk duplication creates.

Consider extracting a shared private method (e.g., buildSessionContext(promptText, senderId, target, sessionId)) that both dispatch paths call.

— qwen3.7-max via Qwen Code /review

@@ -5562,6 +5802,55 @@ describe('ChannelBase', () => {
]);

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] Missing tests for loop-path memory read failure and staleness check

The main dispatch path has explicit tests for memory read failure ('continues the user prompt when channel memory read fails', 'cleans up first-session context claim when memory read fails') and for /clear races during slow memory reads ('drops a queued turn cleared during a slow memory read'). None of these scenarios are tested for the loop dispatch path (runLoopPrompt).

This gap allowed the missing instructedSessions.delete and the missing second staleness check to go undetected. Suggested tests:

  1. Loop path retries memory injection after a transient read failure
  2. Loop path drops the prompt when /clear arrives during the async memory read

— qwen3.7-max via Qwen Code /review

yiliang114
yiliang114 previously approved these changes Jul 1, 2026

@yiliang114 yiliang114 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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Qwen review aborted with an API error before posting comments. See workflow logs.

@qqqys

qqqys commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qqqys
qqqys dismissed qwen-code-ci-bot’s stale review July 1, 2026 09:54

Addressed by b97d9e6: loop channel-memory read failures no longer mark the session as instructed, and loop prompts now re-check session generation after async memory reads. Added focused regression coverage.

@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. ✅

@qqqys
qqqys added this pull request to the merge queue Jul 1, 2026
Merged via the queue into QwenLM:main with commit 6a5ad45 Jul 1, 2026
24 checks passed

@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.

Review Summary: After thorough analysis across correctness, security, code quality, performance, test coverage, and three undirected audit personas, only 1 genuinely new finding was identified beyond the 51+ existing inline comments. Build passes and all 272 tests pass. ESLint and tsc are clean on all changed files.

— qwen3.7-max via Qwen Code /review

const context: string[] = [];
let sessionContextReady = true;
if (
this.channelMemory &&

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] Missing test for loop path unauthorized sender memory skip

The loop path checks isSenderAuthorizedForChannelMemory(job.target.senderId) here to gate memory injection, but no test verifies that an unauthorized sender's loop prompt correctly skips the memory read. The main dispatch path has this test at line ~3327 ("does not read channel memory for unauthorized senders"), but the loop path equivalent is missing.

A sender could pass isStoredLoopTargetAuthorized (which checks groupGate and shared-session authorization) while failing isSenderAuthorizedForChannelMemory (which checks allowedUsers.includes(senderId)). If this guard is accidentally removed, unauthorized loop prompts would silently inject channel memory.

— qwen3.7-max via Qwen Code /review

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.

Add explicit channel memory for messaging channels

6 participants