feat(external-context): Add daemon memory deletion - #11337
Conversation
Add an opt-in workspace-bound deletion profile with exact target reads, full-text and scope verification, single-shot deletion, and absence checks. Preserve full record identifiers and cover approval and failure boundaries. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
E2E verification reportPR head: The deletion implementation was verified with disposable records and loopback services. No real Holo record was created or deleted, and no user daemon or workspace configuration was changed.
The independent protocol run covered strict configuration/version isolation, an archive unpacked outside the repository and node_modules, complete writer IDs, scope and whole-text checks, forbidden collection paths, finite acknowledgement/absence contracts, bounded UTF-8/JSON bodies, one cumulative deadline, and cancellation without replay. The collection DELETE trap was never hit. A 600 ms total budget with each of three stages delayed by 230 ms returned unknown in approximately 604 ms. The daemon matrix covered default ask/reject/cancel, invalid and stale votes, all REST SSE subscriptions disconnecting, Hook/YOLO semantics, interleaved workspaces, same-path config reload, restart without local confirmation state, runtime generation changes, permission timeout and MCP process loss after DELETE without unsafe replay. The actual browser votes were separate from the SDK test driver's client IDs. A timed-out browser-control attempt was not counted as a rejection; a fresh reject-only browser run supplied the successful zero-request rejection evidence. Test processes, browser tabs and temporary directories were cleaned up. Reproduction commands# Repository root
npm run build
npm run typecheck
npm run bundle
# integrations/external-context-mem0
npx vitest run
npm run lint
# packages/web-shell
npx vitest run client/adapters/transcriptAdapter.test.ts client/components/messages/ToolApproval.test.ts client/components/messages/ToolApproval.test.tsx
# integration-tests
QWEN_SANDBOX=false npx vitest run cli/external-context-mem0-daemon-delete.test.ts --retry=0The additional independent scripts and local raw reports are working artifacts rather than committed test entry points. The durable stdio and daemon tests above are included in the PR. Tested locally on macOS / Node v22.22.3; Windows and Linux were not run locally. LimitsReal Holo conformance remains unverified because the previously supplied temporary credential was unavailable; the preflight sent zero service requests. The browser DOM length, leading/trailing text, escaped controls and scroll-to-end were observed, but the additional full character-by-character DOM comparison did not return before the control tool timed out. No atomic version-delete guarantee is claimed: the independent synthetic test demonstrated that an update after the final GET can still be deleted. Service-side ID reuse, scope ownership/authorization, cascading behavior and real search-index propagation require deployment conformance checks. Two self-audit passes and an independent read-only review found no remaining production defects. An obsolete README statement denying any deletion tool was corrected. Review did not substitute passing tests for inspecting the actual call paths. 中文验收说明删除实现仅使用可丢弃的合成记录与 loopback 服务验证,没有创建或删除真实 Holo 记录,也未改动用户 daemon 或 workspace 配置。最新 main 基线上 build/typecheck/bundle、330 项完整扩展测试、67 项 Web Shell 回归及持久 daemon 集成测试通过。此前在相同 Extension 实现上完成了 170 项独立 stdio/HTTP/归档矩阵及 21 项 daemon 权限与生命周期矩阵;变更基线后复跑的持久 daemon 测试继续覆盖 workspace 归属和审批。 实际 Chrome 批准 4000 code point 目标产生一次 DELETE,独立空正文 Reject 产生零 GET/DELETE;两次 voter 均来自浏览器而非 SDK 驱动。浏览器控制超时导致的清理取消未计为拒绝成功,已用独立拒绝分支补验。集合 DELETE 陷阱零命中;600ms 总预算、每阶段230ms延迟的用例约604ms返回 unknown。测试进程、浏览器标签及临时目录均已清理。可重复执行的 stdio 和 daemon 回归测试包含在 PR,额外独立脚本与原始报告作为本地工作产物保留。 真实 Holo 因临时凭证不可用尚未验证,预检服务请求为零。浏览器 DOM 长度、首尾、控制字符转义与滚动末尾可见已观察;额外全文逐字比较因控制工具超时未返回,不计为通过。合成测试明确演示 GET→DELETE 非原子窗口;真实服务 ID 复用、scope 权威性/授权、级联行为及索引传播仍须部署验收。仅本地 macOS / Node v22.22.3 已测试,Windows/Linux 未运行。两轮自审与独立只读审查无遗留生产缺陷;README 旧说法已修正。 |
|
Thanks for the PR! Template looks good ✓ — every required heading is present, and the Risk & Scope section is unusually candid about what was not verified, which is exactly what a reviewer needs. Problem: real, and I checked it rather than taking the description's word. Direction: aligned. It completes the writer rather than opening a new front, it's off by default behind its own V5 instance config, its own credential env var and its own MCP entry, and it stays inside Size: Stage 0 doesn't apply (no core paths). For orientation: ~590 production lines (~560 of it TypeScript), ~1727 test lines, 140 lines of schema JSON, ~360 lines of design doc + README, 29 lines of fixture. Under the 1000-line advisory, and the test-to-production ratio is about 3:1. Approach: the shape matches the merged write path file-for-file —
Risk: no elevated risk signals — none of the changed files match the high-risk paths. The genuinely unproven part is the confirmed-deletion branch: by the PR's own account it has never run against a real Mem0 service, so the exact-acknowledgement matching and the scope-field echo check are informed by the API's documented shape rather than by a live response. I'll come back to this in the review. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必需小标题都在,Risk & Scope 部分对"哪些没有验证"写得异常坦诚,这正是 reviewer 需要的。 问题: 真实存在,我核实过而不是照抄描述。 方向: 对齐。它补全了 writer,而不是另开一条战线;默认关闭,需要独立的 V5 实例配置、独立凭证环境变量和独立 MCP 入口;改动全部留在 规模: Stage 0 不适用(未触及核心路径)。供参考:约 590 行生产代码(其中约 560 行 TypeScript)、约 1727 行测试、140 行 schema JSON、约 360 行设计文档 + README、29 行 fixture。低于 1000 行大 PR 建议阈值,测试与生产代码比约 3:1。 方案: 结构与已合并的 write 路径逐文件对应 ——
风险: 无升级风险信号 —— 变更文件均未命中高风险路径。真正未被证明的是"确认删除"这条分支:按 PR 自己的说明,它从未对真实 Mem0 服务运行过,所以精确回执匹配和 scope 字段回显校验是依据 API 文档形态而非真实响应设计的。这一点我会在代码审查里再回来谈。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote my own proposal before opening the diff: one The PR is roughly four times that, and after reading it I think most of the extra is load-bearing rather than decorative. The part my version gets wrong: without I traced every exit path looking for one that deletes without a verified exact match, or reports No critical blockers. Three non-blocking things I'd want on the record: 1. The absence check is gated behind the weakest signal in the chain. 2. The exact-text precondition is the part I'd expect to fail most often in real use. 3. On the search change itself: the new test is well-built — it pins the actual collision (a 129-character ID truncating onto a real 128-character neighbour) and the code-point boundary with emoji, so 128 surrogate-pair characters survive while 129 are dropped. That's the right way to test a Unicode-length rule. My Stage 1 question about losing the record's content along with its ID still stands as a question, not an objection. Reuse is good: The flow, since the ordering is the design: sequenceDiagram
participant P1 as Model
participant P2 as context_forget
participant P3 as delete engine
participant P4 as Mem0 provider
P1->>P2: memoryId plus complete expectedContent
P2->>P2: validate ID charset, text bounds, not aborted
P2->>P3: forget
Note over P3,P4: one AbortSignal.timeout covers all three calls
P3->>P4: GET record
P4-->>P3: id, scope fields, content
P3->>P3: id matches, every scope field matches, content matches exactly
P3->>P4: DELETE record
P4-->>P3: HTTP 200 and exact success message
P3->>P4: GET record again
P4-->>P3: 404
P3-->>P2: deleted
P2-->>P1: deleted
Testing evidenceThis is an unattended CI run, so I did not build or execute any PR code — everything below is the PR's own CI, read through the API for the reviewed commit. CI has now settled: every lane that covers this PR is green, and one lane that does not is red. I've classified that failure below with the evidence rather than waving it off.
About that red check.
So I'm naming it as pre-existing infra noise, not as a finding against this PR. It will still hold the merge gate until it's re-run — that's a workflow condition rather than a code problem, and it deserves its own flaky-test look instead of blocking this diff. Which of those lanes actually cover this PR matters more than the green ticks, so I checked rather than assuming:
Sandboxed verification would settle the remaining gaps, and the author has write access so both lanes are open: 中文说明代码审查 我在打开 diff 前先写了自己的方案:在新的 delete server 上加一个 这个 PR 大约是我的四倍,读完之后我认为多出来的部分大多是承重的,而不是装饰。我的版本错在哪里:没有 我把每一条退出路径都走了一遍,找有没有哪条会在未核对精确匹配的情况下删除、或者在没做不存在复核的情况下报 没有关键阻塞项。 三点非阻塞、但我希望记录在案: 1. 不存在复核被挡在整条链最弱的信号后面。 2. 精确原文前置条件是实际使用中最容易失败的一环。 3. 关于搜索侧改动本身:新测试写得好——它既钉住了真实的碰撞(129 字符 ID 截断后撞上真实的 128 字符邻居),也用 emoji 钉住了码点边界,所以 128 个代理对字符保留、129 个被丢弃。这是测试 Unicode 长度规则的正确方式。我在 Stage 1 提的"记录内容随 ID 一起丢失"仍然是个问题,不是反对意见。 复用做得好: 测试证据 这是无人值守的 CI 运行,所以我没有构建或执行任何 PR 代码——以下全部是通过 API 读取的该 PR 自己在被审 commit 上的 CI 结果。CI 已经跑完:覆盖本 PR 的通道全绿,一个不覆盖本 PR 的通道是红的。下面我给出证据来归类那次失败,而不是一笔带过。 关于那个红 check。
所以我把它归为既有的基础设施噪声,而不是本 PR 的问题。它在被重跑之前仍会卡住合并门禁——那是 workflow 状态而非代码问题,应该单独开一个 flaky test 的排查,而不是用它阻塞这个 diff。 哪些通道真正覆盖了这个 PR,比绿色勾更重要,所以我核实过而不是假设:
沙箱化验证可以补上剩余缺口,且作者有写权限,两条通道都开放: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — the safety design holds up under tracing and the test coverage is genuinely load-bearing; the reservations are about whether it will work against a real provider, not about whether it's safe. Stepping back. My independent proposal was a quarter of this size, and I went in expecting to argue that the extra machinery was over-engineering. I can't make that argument honestly after reading it. The piece that changed my mind is the approval prompt: without What I actually verified rather than accepted: I walked every exit path in Six months from now I'd thank whoever maintains this rather than curse them. The design doc and the README spell out the contract, including the parts that don't work yet; The scope is honest. Every change maps to the stated goal, there's no formatting churn or drive-by refactor, and the one edit outside the delete path — dropping overlong IDs from search instead of truncating them — is two lines, is arguably a prerequisite for treating IDs as deletion keys, and comes with a test that pins the actual collision rather than a synthetic one. I'd have preferred it as its own commit so it's revertable separately, but that's a preference, not a finding. On the pattern question: this is the third PR in a line from the same author (#11246 auto-recall, #11311 writes, this one), all merged, and the author is reviewing their own feature line with self-reported evidence. I checked whether that was wearing down my judgment rather than being evaluated on merit, and I don't think it is — the series is coherent, each PR is separately justified, and this one's verification story is weaker than the last one's only in the sense that the last one had the same limitation. But it's exactly why I'd run Two things a maintainer should know before merging, neither of which I think blocks it:
Approving, pinned to the commit I reviewed. The three Stage 2 findings are non-blocking and I'd be happy to see them as follow-ups rather than as churn on this diff. 中文说明Confidence: 4/5 —— 安全设计经得起逐路径推演,测试覆盖是真正承重的;我的保留意见在于它对真实 provider 是否能用,而不在于它是否安全。 退一步看。我自己的方案只有这个的四分之一,我本来是打算论证多出来的机制是过度设计的。读完之后我没法诚实地这么讲。让我改变想法的是审批提示:没有 我真正核实过、而不是照单接受的:我把 六个月后维护它,我会感谢作者而不是骂作者。设计文档和 README 把契约写清楚了,包括现在还不能用的部分; 范围是诚实的。每处改动都对应既定目标,没有格式化噪声或顺手重构;delete 路径之外唯一的那处改动——搜索丢弃超长 ID 而不是截断——只有两行,可以说正是"把 ID 当作删除键"的前提,而且配的测试钉住的是真实碰撞而不是人造场景。我更希望它是一个独立 commit,以便单独回滚,但那是偏好,不是发现。 关于"是否形成了模式":这是同一作者的系列第三篇(#11246 auto-recall、#11311 writes、本篇),全部已合并,而且作者在用自述证据审自己的特性线。我检查过这是否在磨损我的判断而不是按实质评估,我认为没有——这个系列是自洽的,每个 PR 都各自站得住,本篇的验证故事比上一篇弱,也只是在"上一篇有同样局限"这个意义上。但这恰恰是我会在合并前而不是合并后跑 维护者合并前应当知道两件事,我都不认为构成阻塞:
批准,并钉在我审过的那个 commit 上。Stage 2 的三条发现都是非阻塞的,我更乐意看到它们作为后续工作,而不是在这个 diff 上再翻一轮。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. The three Stage 2 findings are non-blocking follow-ups, and the red web-shell E2E Smoke check is pre-existing infra flake unrelated to this diff (evidence in the Stage 2 comment) — it still needs a re-run to clear the merge gate. ✅
chiga0
left a comment
There was a problem hiding this comment.
PR #11337 Review — feat(external-context): Add daemon memory deletion
Tier: Standard(新 MCP delete server + 破坏性工具,仅限 integrations/ 包,packages/ 零改动)
审查结果:无阻塞问题,批准 ✅
已验证
Class 1 — Contract 对称性:CLEAN
isMemoryId用value.length <= 256检查长度,允许字符集全为 ASCII([A-Za-z0-9._:-]),每个字符恰好是一个 UTF-16 code unit,所以.length与 code point 计数等价。✓isDeletionContent用Array.from(value).length <= 4000正确按 Unicode code point 计数。✓readTarget用value[idField] !== memoryId做逐字节精确比对,不宽松。✓DeleteInstanceConfigV5的schemaVersion: 5与 V2/V3/V4 严格分离,独立配置路径,不自动继承 writer/reader 权限。✓
Class 3 — 错误处理:CLEAN
逐条追踪 forget() 的所有出口:
| 触发条件 | 结果 | 是否安全 |
|---|---|---|
isMemoryId/isDeletionContent 失败 |
not_deleted / invalid_input |
✓ |
提交前 signal.aborted |
not_deleted / cancelled |
✓ |
| 预读 ID 不匹配 | not_deleted / verification_failed |
✓ |
| 预读 scope 不符 | not_deleted / target_unavailable |
✓ |
| 预读正文不符 | not_deleted / target_changed |
✓ |
| DELETE 返回非 200 | unknown |
✓(fail-closed) |
| DELETE 回执无法识别 | unknown |
✓(fail-closed) |
| 验证 GET 未返回 absent | unknown |
✓(不伪造 deleted) |
| 提交后任何 throw | unknown(submitted=true 分支) |
✓ |
没有任何路径在未经预读精确核对的情况下执行 DELETE,也没有任何路径在没有 absence 验证的情况下返回 deleted。
Class 4 — 授权与路径安全:CLEAN
- scope 核对循环:只检查
expected !== undefined的 scope 字段;若响应中缺少或值不符则返回unavailable,不泄漏内容。✓ isWithinRepository防止 delete server 在 workspace 外运行。✓validateStaticPath阻止配置 prefix 中的//、%、?、#、\和控制字符。✓encodeURIComponent(memoryId)+ charset 严格限制,防止路径注入。✓stderr不泄漏路径和 token(集成测试明确断言)。✓
Class 10 — 代码与设计文档一致性:CLEAN
- 设计文档:"最多执行一次精确 GET" →
get()内readTarget调用一次。✓ - 设计文档:"最多提交一次 DELETE 并复核不存在" →
forget()在通过全部前置检查后执行一次 DELETE,再做一次验证 GET。✓ - 设计文档:"执行端重新核对 ID、全部配置 scope 和完整正文" →
readTarget()执行三层核对(ID → scope → content),与描述一致。✓
profile.ts ID 截断修复:CLEAN
从 id: truncate(source.id, 128) 改为跳过 Array.from(source.id).length > 128 的记录。正确:返回截断 ID 会形成指向另一条记录的假 key,现在改为完整放行或整条省略。新测试钉住了 emoji 码点边界(128 代理对保留、129 丢弃)和真实碰撞场景。✓
累积超时覆盖所有三次 HTTP 调用:SAFE
AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) 在 request() 中构造一次,被 GET、DELETE、验证 GET 共享。超时在第一次 GET 之前开始,剩余预算覆盖后续调用。若超时在 DELETE 之后触发,外层 catch 因 submitted=true 返回 unknown——fail-closed,不声称 deleted。集成测试中 600 ms 总预算 × 3×230 ms 延迟已验证此行为。✓
Cross-check(与 qwen-code-ci-bot 的 APPROVED Review 对比)
CI bot 已作三阶段深度分析(Stage 1-3)。我独立得出的三项 non-blocking 发现与其一致:
isDeleteAcknowledgement的触发门槛偏窄(仅识别两个精确 message 字符串),导致 204、措辞略有不同的 200 等均跳过验证 GET,返回unknown——但absent分支无法伪造deleted,fail-closed,安全。- 精确正文前置条件可能成为高频失败点(长记忆的逐码点完全比对),
not_deleted / target_changed的 reason 不给差异位置提示——安全,但可用性折中。 context_search可能返回context_forget拒绝的 ID(charset 限制不同),fail-closed(invalid_input),不是缺陷。
CI bot 的额外识别(452 行 CLI 集成测试因 merge_group 门控未在 CI 中执行、Holo 真实服务未验证)与我的覆盖评估一致。我未发现额外阻塞项。
未审查维度
- 无本地工作树,未执行测试套件;
delete.integration.test.ts已通过真实 stdio + HTTP server 覆盖所有 fail-closed 分支。 - 对真实 Holo 服务的协议一致性(临时凭证不可用,作者已在 Risk & Scope 中明确说明)。
Reviewed with AI assistance.
|
Released in v0.23.1. |
What this PR does
Adds an independently enabled, workspace-bound deletion profile for daemon memory. The read helper returns one complete target; an explicitly requested deletion carries the exact ID and original text through ordinary MCP approval, rechecks ID, every configured scope and full text, submits at most one DELETE, and verifies absence. Search now omits overlong IDs instead of returning truncated identifiers.
Why it's needed
The merged daemon writer allows users to save memories, but removing an obsolete or incorrect record still requires a separate administration interface. Search summaries and truncated IDs cannot safely identify the complete deletion target. This change provides a narrow explicit deletion workflow while keeping the default extension search-only.
Reviewer Test Plan
How to verify
Enable the separate deletion server in a trusted workspace with a disposable memory service, default approval mode and an explicit ask rule. Read a target, confirm that approval contains its complete ID and text, reject it and observe zero deletion-side requests. Approve a new call and confirm exactly one target disappears while control records remain. Change the text during approval or use another workspace’s scope and confirm no DELETE occurs. Check that unknown responses, cancellation after submission and transport loss never trigger automatic replay. A repeated call for an absent record must report that no DELETE was submitted.
Evidence (Before & After)
Before: the default reader/writer had no deletion tool, and a 129-character search ID could be returned as another record’s 128-character ID. After: the overlong candidate is omitted; a separately enabled daemon tool reads and checks the complete target before one deletion. Actual Chrome approval of a 4000-code-point target produced one DELETE, and a separate empty-text rejection produced zero GET/DELETE requests, with browser voter IDs distinct from the test driver. The existing approval UI is reused; its extra full-DOM character-by-character assertion did not complete because the control tool timed out.
Tested on
Environment (optional)
macOS with Node v22.22.3, local built daemon/ACP/stdio MCP and loopback synthetic providers. The initial global CLI baseline used qwen 0.23.0 on Node v24.12.0. Build, typecheck and bundle passed; 330 package tests, 67 Web Shell tests and the durable daemon integration test passed. Additional independent verification covered 170 protocol/package cases and 21 daemon scenarios. The extension implementation is unchanged from those independent runs; core build and focused tests were refreshed on the current main base.
Risk & Scope
Linked Issues
Follow-up to #11311. No issue is automatically closed.
中文说明
What this PR does
为 daemon 记忆增加独立启用、绑定 workspace 的删除能力。读取辅助工具返回一条完整目标;用户明确请求删除后,精确 ID 和原文通过现有 MCP 审批,执行端重新核对 ID、全部配置 scope 和完整正文,最多提交一次 DELETE 并复核不存在。搜索遇到超长 ID 时改为省略该记录,不再返回截断标识符。
Why it's needed
已合并的 daemon writer 支持保存记忆,但删除过期或错误记录仍需另外使用管理接口。搜索摘要和截断 ID 无法安全标识完整删除目标。本次提供范围明确的显式删除流程,同时保持默认 Extension 仅搜索。
Reviewer Test Plan
How to verify
在可信 workspace 为可丢弃的记忆服务启用独立删除 server,使用默认审批模式及显式 ask 规则。读取目标,确认审批包含完整 ID 和正文,拒绝后观察删除侧零请求。批准新的调用后,确认仅一条目标消失且对照记录保留。在审批期间修改正文或使用其他 workspace 的 scope,确认没有 DELETE。检查未知回执、提交后取消及传输断开均不触发自动重放。对已不存在记录再次调用,应报告未提交 DELETE。
Evidence (Before & After)
变更前:默认 reader/writer 没有删除工具,129 字符的搜索 ID 可能作为另一条记录的 128 字符 ID 返回。变更后:超长候选被省略,独立启用的 daemon 工具先读取并核对完整目标,再执行一次删除。实际 Chrome 批准 4000 code point 目标产生一次 DELETE;另一次空正文拒绝产生零 GET/DELETE,两次浏览器 voter ID 均与测试驱动不同。复用现有审批 UI;额外的 DOM 全文逐字符断言因控制工具超时未完成。
Tested on
Environment (optional)
macOS、Node v22.22.3,本地构建的 daemon/ACP/stdio MCP 与 loopback 合成 provider。初始全局 CLI 基线使用 Node v24.12.0 上的 qwen 0.23.0。build、typecheck、bundle 通过;330 项包测试、67 项 Web Shell 测试和持久 daemon 集成测试通过。额外独立验收覆盖 170 个协议/打包用例和 21 个 daemon 场景。Extension 实现与独立验收时一致;已在当前 main 基线上重新执行核心构建和相关测试。
Risk & Scope
Linked Issues
接续 #11311,不自动关闭任何 issue。