Skip to content

fix(external-context): Align deletion responses with Mem0 SDK - #11397

Merged
doudouOUC merged 3 commits into
mainfrom
codex/mem0-delete-sdk-response
Sep 9, 2026
Merged

fix(external-context): Align deletion responses with Mem0 SDK#11397
doudouOUC merged 3 commits into
mainfrom
codex/mem0-delete-sdk-response

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

Aligns single-memory DELETE response handling with the Mem0 SDK: accept successful HTTP status and parse bounded UTF-8 JSON without matching message text or imposing additional response-field rules. An exact follow-up GET must still confirm absence before the tool reports deleted.

Why it's needed

A real Holo deletion returned HTTP 200 with Memory <id> deleted successfully. and removed the target, but the existing fixed-message check reported unknown. Treating example English messages as protocol constants rejects otherwise usable responses.

Reviewer Test Plan

How to verify

Approve deletion against a service returning the captured Holo response. Confirm exactly GET → DELETE → GET and deleted only after the final read confirms absence. Arbitrary valid JSON and successful HTTP statuses must follow the same rule. A surviving target must yield unknown; malformed or empty JSON, non-success HTTP responses and connection loss must not cause retries. Rejecting approval or supplying mismatched scope or full text must stop before DELETE.

Evidence (Before & After)

Before: live Holo returned success and a subsequent independent GET returned 404, while the tool reported unknown. After: a local provider reproducing that response through the rebuilt daemon/MCP returns deleted after GET → DELETE → GET. All 12 daemon scenarios passed. Package tests, root build/typecheck/bundle and package lint passed. No visual UI changes.

Tested on

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

Environment (optional)

macOS, Node.js v22.22.3, npm 10.9.8. Built from baseline 1a73f5bff6201473f237c5106367e944ba2d092b with this patch. E2E uses actual daemon/MCP processes, a controlled model and a local HTTP provider reproducing the previously captured Holo acknowledgement.

Risk & Scope

  • Main risk or tradeoff: DELETE JSON fields no longer independently determine success, including provider-specific error/status/event/cascade fields. A successful HTTP status, valid bounded JSON and exact post-delete absence are all required; no linked-delete option is added. Empty 204 responses remain unknown because JSON parsing fails.
  • Not validated / out of scope: No fresh live Holo or PolarDB acceptance was performed after this patch. PolarDB's GET response wrapper and literal-write semantics remain unchanged, as do GET validation, approval, scope/full-text checks, timeouts and no-retry behavior.
  • Breaking changes / migration notes: No configuration or schema migration. Successful responses previously rejected solely by message or extra fields can now report deleted when exact absence is verified.

Linked Issues

Follow-up to #11337. No issue is auto-closed.

中文说明

What this PR does

将单条记忆 DELETE 响应处理与 Mem0 SDK 对齐:接受成功 HTTP 状态并解析有界 UTF-8 JSON,不匹配消息文案,也不增加额外回执字段规则。工具仍必须通过紧接的一次精确 GET 确认不存在,才能返回 deleted

Why it's needed

真实 Holo 删除返回 HTTP 200 和 Memory <id> deleted successfully.,目标也已删除,但现有固定文案判断返回了 unknown。将英文示例消息视为协议常量,会拒绝原本可用的响应。

Reviewer Test Plan

How to verify

对返回已捕获 Holo 回执的服务批准删除。确认请求顺序严格为 GET → DELETE → GET,且只有最后读取确认不存在才返回 deleted。任意有效 JSON 和成功 HTTP 状态均遵循同一规则。目标仍存在时必须返回 unknown;无效或空 JSON、非成功 HTTP 响应和连接中断不得触发重试。拒绝审批,或 scope、完整正文不匹配,必须在 DELETE 前停止。

Evidence (Before & After)

修改前:真实 Holo 返回成功,随后独立 GET 返回 404,但工具返回 unknown。修改后:本地服务复现相同响应,通过重新构建的 daemon/MCP 执行 GET → DELETE → GET 后返回 deleted。12 个 daemon 场景全部通过。包内测试、全仓 build/typecheck/bundle 和包内 lint 均通过。没有视觉 UI 改动。

Tested on

OS Status
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

Environment (optional)

macOS,Node.js v22.22.3,npm 10.9.8。基于 1a73f5bff6201473f237c5106367e944ba2d092b 加本补丁构建。E2E 使用真实 daemon/MCP 进程、受控模型,以及复现此前捕获 Holo 回执的本地 HTTP 服务。

Risk & Scope

  • Main risk or tradeoff: DELETE JSON 字段不再单独决定成功,包括服务自定义的 error/status/event/cascade 字段。仍必须同时满足成功 HTTP 状态、有效有界 JSON 和删除后精确读取确认不存在;没有新增关联删除选项。204 空响应因无法解析 JSON,仍返回 unknown。
  • Not validated / out of scope: 修改后没有重新执行真实 Holo 或 PolarDB 验收。PolarDB 的 GET 包装结构和原文写入语义保持不变;GET 校验、审批、scope/全文校验、超时和不重试行为也保持不变。
  • Breaking changes / migration notes: 无配置或 schema 迁移。此前仅因消息或额外字段被拒绝的成功响应,在精确确认不存在后可以返回 deleted

Linked Issues

后续修复,关联 #11337。不自动关闭任何 issue。

Accept successful HTTP responses and parse JSON without fixed acknowledgement rules, retaining exact post-delete absence verification.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E test report / E2E 测试报告

12/12 scenarios passed using real daemon/MCP processes with default permission handling, a controlled model and a local HTTP provider. This reproduces the previously captured real Holo acknowledgement; no fresh cloud-service requests or credentials were used.

  • Holo message containing the memory ID, null/arbitrary JSON and HTTP 202 with absent target: deleted after exactly GET → DELETE → GET.
  • HTTP 202 with surviving target: unknown after GET → DELETE → GET.
  • Invalid JSON, empty 204, non-success HTTP with valid JSON, and connection loss: unknown, exactly GET → DELETE, no retry.
  • Approval rejection: zero HTTP requests. Wrong scope or changed full content: one GET, zero DELETE.

Nine DELETE requests across the suite, at most one per submitted call. Test daemons and temporary directories were removed. Independent focused review found no issues. A first harness run parsed the wrong daemon error-output field; the harness was corrected and the full suite rerun without product changes.

12 项均通过:使用真实 daemon/MCP 默认审批、受控模型和本地 HTTP 服务复现已捕获的 Holo 回执;没有重新访问云实例或使用真实凭证。带 ID 回执、任意 JSON 和 202 均须精确读取确认不存在才返回 deleted;目标仍在或响应/连接失败返回 unknown,不重试。拒绝审批为零请求,scope/正文不匹配仅一次 GET。合计 9 次 DELETE,每次调用最多一次,临时进程及目录已清理。独立审查无发现。首轮测试脚本读取了错误的 daemon 错误输出字段,修正脚本后全套重跑通过,未因此修改产品。

Validation: root build, typecheck and bundle; package lint; final package suite 326 tests across 17 files. macOS / Node.js v22.22.3 / npm 10.9.8. Windows and Linux were not locally tested. PolarDB GET-wrapper and literal-write compatibility are outside this patch.

验证:全仓 build、typecheck、bundle,包内 lint,以及最终 17 个文件共 326 项测试。环境为 macOS / Node.js v22.22.3 / npm 10.9.8,未在本地验证 Windows/Linux。PolarDB 读取包装及原文写入兼容不在本补丁范围。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Third pass, same head — nothing has landed since 5791f56, so the gate findings below are unchanged from my last review, and I re-verified them against the head sources rather than against my own earlier comment. What has moved is the evidence around them: CI on this commit has finished and the sandboxed /verify run has reported. One check is red and it is not this PR's — the commits that establish that are named in the code-review comment.

Template looks good ✓ — all required sections present, including the Risk & Scope tradeoffs and the bilingual block.

Problem: observed, not theoretical. A real Holo deletion returned HTTP 200 with Memory <id> deleted successfully. — ID interpolated, trailing period — the target really was gone, and the tool still reported unknown. That is a concrete captured response, and the root cause is confirmable straight from the diff: the old isDeleteAcknowledgement compared message against two exact English strings lifted from example docs. Treating sample prose as a protocol constant was always going to break on the first provider that interpolates. Worth noting the design doc itself already conceded the point — it records that Holo's single-record DELETE acknowledgement was not verified at design time, so the two strings were never evidence-backed in the first place.

Direction: aligned. This is a follow-up to the just-merged #11337, correcting a contract that landed too narrow rather than adding new surface. The important part is what it does not weaken: deleted still requires an exact follow-up GET confirming absence under the configured notFound rule, and readTarget — the actual oracle — is untouched, still strict on error/errors, id field, scope and full content. The relaxation is confined to a response body that was never proof of anything. CHANGELOG: no direct reference, but the mem0 delete contract is documented in docs/design/external-context-mem0-explicit-delete.md, and this PR updates it in lockstep.

Size: the core-module gate does not apply — nothing under packages/; all six files sit in integrations/external-context-mem0/ and docs/. For reference: 20 production lines (+3/−17), 104 test lines (+66/−38), 45 doc lines (+23/−22). The net production delta is negative 14 — this deletes more code than it adds.

Approach: the scope feels right, and I could not find a meaningfully simpler version of it. Dropping the fixed-message check is the minimal correct fix; the alternative — widening the string list to also accept the interpolated form — would have been worse: more branches, still guessing at provider prose. No drive-by refactors either. The design-doc table churn is just Prettier re-padding a column after one cell grew, and the README edits are the user-facing half of the same contract. 5791f56 adds one more thing I'd have asked for: it rewords the deleted message so the tool no longer claims "the provider confirmed deletion", which after this change it genuinely does not verify.

One honest question, not a blocker, and unchanged from my last pass: an empty-body 2xx — 204 No Content, about the most common successful-DELETE shape there is — still lands on unknown even when the follow-up GET would have confirmed absence. You've documented that explicitly as a tradeoff and it fails in the safe direction, so I'm not asking you to change it here. But it is the same class of false unknown that motivated this PR, just reached through a different provider shape. I did get the mechanism wrong last time and want to correct it in the thread: the rejection happens in readBoundedBody, which throws on a null body before JSON.parse ever runs — not in the discarded parse. Details in the code review. (One update from the sandboxed run, which measured this rather than reasoning about it: the official SDK's own response.json() throws on a 204 too, so this behaviour matches the client the PR is aligning to. Still a candidate for a follow-up, no longer a divergence.)

Risk: no elevated risk signals — no revert-correlated paths touched. The redirect-safety property also survives: redirect: 'manual' plus rejecting a non-ok response means a 301 still cannot turn a single-record DELETE into a collection DELETE, and the test pins 301 rejection even with a valid JSON body.

Moving on to code review. 🔍

中文说明

感谢贡献!第三轮审查,head 未变 —— 自 5791f56 起没有新提交,因此下面的门禁结论与上一轮一致,而我是拿 head 源码重新核对的,不是拿我自己上一条评论核对的。变化的是围绕它们的证据:该提交上的 CI 已跑完,沙箱 /verify 也已出报告。有一项检查是红灯,但不是本 PR 造成的 —— 能证明这一点的具体提交在代码审查评论里点名了。

模板完整 ✓ —— 所有必需章节齐全,包含 Risk & Scope 的取舍说明和中英文对照。

问题: 是已观测到的问题,不是理论性加固。真实 Holo 删除返回 HTTP 200 和 Memory <id> deleted successfully.(插入了 ID、带句尾句点),目标确实已删除,但工具仍返回 unknown。这是一个具体捕获到的响应,根因从 diff 就能确认:旧的 isDeleteAcknowledgementmessage 与两个从示例文档里抄来的英文字符串做精确比对。把示例文案当成协议常量,遇到第一个会做插值的服务端就必然失效。值得一提的是,设计文档本身就已经承认了这一点 —— 它记录 Holo 单条 DELETE 回执在设计时并未核实,所以那两个字符串从来就不是有证据支撑的。

方向: 对齐。这是对刚合并的 #11337 的后续修正,纠正的是落地时定得过窄的契约,而不是新增能力。关键在于它没有削弱什么:deleted 仍要求紧接一次精确 GET、按配置的 notFound 规则确认不存在;而真正的判据 readTarget 完全未改动,对 error/errors、ID 字段、scope 和完整正文依旧严格。放宽的只是一个本来就不能证明任何事的响应体。CHANGELOG:没有直接对应条目,但 mem0 删除契约记录在 docs/design/external-context-mem0-explicit-delete.md,本 PR 同步更新了该文档。

规模: 核心模块门禁不适用 —— 没有触及 packages/,六个文件全部位于 integrations/external-context-mem0/docs/。供参考:生产代码 20 行(+3/−17),测试 104 行(+66/−38),文档 45 行(+23/−22)。生产代码净变化为 负 14 行,即删除多于新增。

方案: 范围合理,我找不到明显更简的做法。去掉固定文案判断就是最小的正确修复;另一种选择——把插值形式也加进字符串白名单——反而更糟:分支更多,且仍在猜测服务端文案。也没有夹带顺手重构:设计文档表格的变化只是某个单元格变长后 Prettier 重新对齐列宽,README 的改动是同一契约面向用户的那一半。5791f56 还补了一件我本来会提的事:改写了 deleted 的提示文案,使工具不再声称"服务端已确认删除"——因为改动之后它确实不再校验这一点。

一个诚实的疑问,不是阻塞项,与上次相同:空响应体的 2xx —— 也就是 204 No Content,DELETE 成功时最常见的形态 —— 即使后续 GET 能确认不存在,仍会落在 unknown。你已经把它明确写成权衡,且失败方向是安全的,所以我不要求在此修改。但它与本 PR 想解决的是同一类误报 unknown,只是换了另一种服务端形态才触发。上次我把触发机制说错了,在此更正:拒绝发生在 readBoundedBody 里——它在 JSON.parse 之前就因响应体为 null 而抛出——而不是那个被丢弃的 parse。详见代码审查。(沙箱运行带来一处更新,它是实测而非推理:官方 SDK 自己的 response.json() 在 204 上同样抛错,所以该行为与本 PR 要对齐的客户端是一致的。仍然可以作为后续项,但不再是分歧。)

风险: 无升级风险信号 —— 未触及与回滚相关的历史高危路径。重定向安全性也得以保留:redirect: 'manual' 加上非 ok 即拒绝,意味着 301 仍不可能把单条 DELETE 变成集合 DELETE,测试已固定"301 即使携带合法 JSON 也拒绝"这一行为。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 5791f56dd34731b047d16073f78cf5e99bd7c6be · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal first. Reading only the title and the "Why it's needed" section, the bug is that a provider's free-text message is being matched against two hard-coded English strings copied out of example documentation. Three ways out: (a) stop interpreting the DELETE body as an oracle at all — require a successful HTTP status, keep the bounded read for resource and encoding safety, and let the already-mandatory post-delete GET decide; (b) loosen the match to a pattern that tolerates an interpolated ID; (c) make the accepted message set configurable per dialect. I'd pick (a). The follow-up GET is the only step that actually proves removal, so any body heuristic is a weaker check stacked on top of a stronger one, and (b)/(c) both keep guessing at provider prose while adding a regex or config surface to maintain.

The PR matches (a), and on this head it exceeds it. 5791f56 also rewords the deleted message from "The provider confirmed deletion and a subsequent exact read confirmed absence" to "The delete request returned a successful HTTP response and a subsequent exact read confirmed absence". That is the half my proposal missed: leaving the old wording in place would have had the tool assert something the code no longer checks, on a destructive operation, in the one string the user actually reads. It also pins the new text in delete-mcp.test.ts, so it cannot drift silently.

What I verified rather than taking the description's word for:

  • No new false-deleted path. deleted still requires after.status === 'absent', and readTarget returns absent only via HTTP 404 under http-404 or HTTP 200 + JSON null under null-200. That function is untouched. The DELETE body went from being a second, weaker oracle to not being an oracle at all — the real oracle did not move.
  • The strictness asymmetry is deliberate, not an oversight. The DELETE response no longer checks error/errors, but readTarget still throws Invalid target. when the GET carries them. Strictness was dropped where it proved nothing and kept where it decides the verdict. Worth saying out loud because it reads inconsistently at a glance.
  • 202 is handled in the safe direction. An async 202 Accepted now reaches the confirming GET; if the record is still there the result is unknown, which is exactly the right answer for "may have been deleted, do not retry, read to check". Both halves are pinned — verifies absence after HTTP 201/202 with JSON for the surviving target, and the new reports deleted after HTTP 201/202 when the follow-up GET confirms absence.
  • Redirect safety intact. With redirect: 'manual' a 3xx is never ok, so the design doc's concern about URL normalization turning a single-record DELETE into a collection DELETE still holds. rejects non-success DELETE HTTP 301 even with valid JSON is the stronger form of that assertion.
  • Consistency with the sibling engine, which I'd call the strongest argument for the change. createRequestEngine in the same package already gates on !response.ok. The DELETE engine moving off status !== 200 makes the two agree on the primary predicate instead of each having its own idea of success. (Search carries an extra explicit 3xx guard; ok is 200–299 by definition, so it is redundant there. No action either way.)
  • No regression outside the changed test file. A relaxation is exactly the kind of change that silently inverts an assertion elsewhere, so I checked the two mock-server suites that also exercise this path. Both delete.integration.test.ts and integration-tests/cli/external-context-mem0-daemon-delete.test.ts answer DELETE with 200 + {"message":"Memory deleted successfully!"}, which is still accepted, and both follow it with a 404. Their unknown expectations come from drop, slow-preflight-body, slow-delete-body, slow-verification-body, cumulative-deadline and a 500 — abort and deadline paths, not the body predicate. Nothing flips.
  • No dead code. hasError and isRecord survive via readTarget, so removing isDeleteAcknowledgement orphans nothing and does not trip noUnusedLocals. isDeleteAcknowledgement is fully gone with no dangling references, and the ack()/calls()/absent() helpers all still resolve in the head test file. The old message string has exactly one occurrence repo-wide — the definition being changed.
  • Failures stay terminal. A throw from the bounded read or the parse lands in the outer catch with submitted === trueunknown, no retry.

Re-verified on this pass, not carried over on trust. The head is unchanged since my last review, so the findings above stand — but I re-checked them against the head sources rather than against my own earlier comment. readTarget is byte-identical between base and head (both fetched at their own refs and compared), so the oracle really did not move. hasError/isRecord are still referenced inside it at head, so removing isDeleteAcknowledgement orphans nothing and cannot trip noUnusedLocals. The head forgetMessages.deleted string matches the new delete-mcp.test.ts pin character for character. And readBoundedBody's if (!response.body) throw sits at request-engine.ts:181, ahead of any parse — which is what makes the corrected account of the discarded JSON.parse below the accurate one, and my earlier version the overstatement.

A correction to my own previous pass, because it is still sitting in this thread. I wrote that the discarded JSON.parse was "load-bearing twice over: it enforces the bounded read, and its throw is what turns invalid or empty JSON into unknown". The first half is wrong. readBoundedBody enforces all of it on its own — the declared content-length cap, the streamed 1 MiB cap, TextDecoder('utf-8', { fatal: true }), and an explicit if (!response.body) throw. An empty-body 204 therefore becomes unknown inside readBoundedBody, before JSON.parse runs. What the discarded parse uniquely adds is narrower than I claimed: the body must be syntactically valid JSON. Deleting that line would let a 200 carrying not json proceed to the confirming GET — which, on this PR's own thesis that the body is not an oracle, is arguably the more consistent behaviour rather than a regression. The suggestion to comment the line still stands, but it protects one property, not two, and I don't want the overstatement to be the version a future reader trusts.

One non-blocking coverage note, carried over. The most alarming newly-accepted shape — HTTP 200 with {"error": "provider-specific response", "status": "FAILED", "cascade_count": 1} — is still tested only paired with a target that turns out to be absent (→ deleted). The surviving-target case is pinned for other payloads (ack() in cannot claim deletion without confirming absence, and now the 201/202 rows), and the code no longer reads the body at all, so the property holds by construction rather than by test. Coverage, not a defect. 5791f56 added adjacent rows rather than this one, which is fine — I'm recording it, not asking for it.

I skipped both optional enrichments again: no sequence diagram, because the GET → DELETE → GET flow already existed and only one predicate on it changed; no changed-files table, at six files of which two are docs and two are tests.

Test evidence

Unattended CI run — I did not build, run, checkout or apply anything from this PR. The evidence below is this PR's own CI, read through the API for commit 5791f56dd34731b047d16073f78cf5e99bd7c6be, fetched once. No polling. The head sources quoted in the code review above were fetched read-only at that same OID via the contents API.

CI on this commit has now finished: one red check, and it is not this PR's.

Final CI results for 5791f56 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ❌ failure
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The red check, classified with commit-level evidence rather than a shrug. Test (ubuntu-latest, Node 22.x) failed on exactly two tests, both in packages/web-shell/client/App.test.tsx, and the failing job's log names the cause:

FAIL  App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: false)
FAIL  App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: true)
   ReferenceError: mockUseDaemonActivePromptBridge is not defined
 Test Files  1 failed | 287 passed (288)
      Tests  2 failed | 6710 passed (6712)

Every link in the chain below was checked, not assumed:

  • No mechanism. This PR's six files contain no path under packages/ at all — the files API returns only docs/design/… and integrations/external-context-mem0/…. Nothing in this diff can reach a web-shell test module.
  • The identifier does not exist. mockUseDaemonActivePromptBridge appears nowhere in the repository. It was introduced by main commit 70cf363feat(web-shell): Improve split-view session navigation (#11250), 2026-09-08T17:33Z — which is exactly this PR's base.sha.
  • Main fixed it before this PR's CI even started. 3a75f37fix(web-shell): Replace undefined mock in split rerender tests (#11404) (#11406), 2026-09-09T00:10Z — swaps the undefined mock for mockUseDaemonSessionActivityBridge in precisely these two tests. That is 2h27m before this run began at 02:37Z.
  • This head does not contain the fix. compare/3a75f37...5791f56diverged, behind_by: 5. The branch's last merge of main was b282e82 at 2026-09-08T17:36Z — three minutes after the breakage landed, and hours before the repair.
  • Main is green again. The Qwen Code CI run on dcbb0b5f9 (06:07Z) reports Test (ubuntu-latest, Node 22.x) ✅ success.

So this is a broken-main window that this PR's merge commit happened to sit inside. Merging current main into the branch clears it; no change to this PR's own code is required. I am calling it pre-existing and not attributable with the commits named — not "known flake". A ReferenceError on an undefined identifier is deterministic, and here it is deterministic for a reason I can point at.

The suite that actually covers this diff passed inside that same failing job:

> @qwen-code/external-context-mem0@0.23.1 test:ci
 ✓ src/delete-request-engine.test.ts (82 tests) 49ms
 ✓ src/delete-mcp.test.ts (22 tests) 108ms
 ✓ src/delete.integration.test.ts (8 tests) 2203ms
 Test Files  17 passed (17)
      Tests  328 passed (328)

delete.integration.test.ts is a real end-to-end test — it spawns the packaged dist/delete-main.js over MCP stdio against a real node:http provider with real record state — so the packaged deletion path is green on this head, not only the mocked unit layer. Also green: Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, Desktop Shell (ubuntu + windows), Classify PR, review-pr. A further 8 checks are skipped by workflow conditions (Test (macos-latest), Test (windows-latest), Integration Tests (CLI, No Sandbox), precheck-pr, ack-review-request, resolve-pr, review-config, publish-resolution) — normal gating for a PR of this shape, not a red signal.

The web-shell E2E red my previous pass left open in this thread is now settled. web-shell E2E Smoke was ❌ on d0a7fc1 — the sub-2px scroll-anchor tolerance at web-shell.history-viewport.spec.ts:275 — and is ✅ on 5791f56. Last pass I explicitly declined to call that a known flake without a base-commit comparison, and said the run then in flight would settle whether it recurred once this head picked up main's web-shell split-view work. It did not recur. Question closed by measurement, not by assumption.

The behavioural claim is now substantiated — and not by me. Last pass I named @qwen-code /verify as the lane that could settle whether this change is load-bearing rather than merely green, and deliberately refused to predict its outcome. It has since reported on this exact head (run 34304026537): merge-ready — 311 scripted assertions, 311 passed / 0 failed, in an isolated token-free container. The parts that bear on the claim static review could not settle:

  • Four-arm A/B (base / only the !response.ok half / only the acknowledgement-removal half / head) against a real loopback HTTP server holding a real record store, 32 scenarios, each cell cross-checked with an independent GET so server truth is observed rather than inferred. On the captured Holo shape (200 + Memory memory:1 deleted successfully.): base = unknown, head = deleted. The acknowledgement removal carries the fix; the !response.ok half contributes nothing to that bug, and 201/202 + arbitrary JSON needs both halves — each necessary, together sufficient.
  • The SDK-alignment claim was tested against the real SDK, not against the description: the shipped mem0ai@3.1.8 _fetchWithErrorHandling driven across 143 status × body combinations. HEAD differs from the SDK in 6 cells, all of them this repo's deliberate extra strictness (1 MiB cap ×3, strict UTF-8 ×3); base differed in 23. HEAD's accept set is a subset of the SDK's and base's is a subset of HEAD's — this PR only widens, never tightens.
  • Mutation matrix 9/9 killed, 0 survivors, control 328/328 green, with dist/ rebuilt per mutant. Worth noting M5 — the further relaxation of dropping JSON.parse altogether — is killed by exactly one existing test, so that axis is guarded and adopting it would be a deliberate design decision, not a coverage gap.
  • Flakiness gate: both changed test files × 5 identical rounds, no divergence.
  • Its two nits are the same two I reached independently: the discarded JSON.parse wants a comment, and the new new Response(null, { status: 200 | 202 }) fixtures exercise readBoundedBody's null-body guard rather than a wire-reachable empty body (measured equivalent — a real empty 200 fails one level later at JSON.parse(''), same unknown outcome).

A second /verify was triggered at 06:31Z and is still in flight; it owns the second qwen-triage:verify comment and will report there. I am not predicting or summarising it — the report above is the one that has actually landed.

Real-scenario testing: N/A for this run. This is an unattended CI execution, so nothing was driven in tmux and no terminal capture is pasted — that lane is local-invocation only. Stated plainly rather than papered over: the author's E2E report covers macOS only, with Windows and Linux untested, and says outright that no fresh live Holo or PolarDB acceptance was run after the patch. That is the author's claim, not evidence I re-ran. The verify run reproduces the wire shape the author describes against a local provider — a faithful replay of the reported bytes, not a live Holo call. PolarDB's GET-wrapper and literal-write behaviour are explicitly out of scope.

中文说明

代码审查

先说我自己的方案。 只看标题和"Why it's needed",这个 bug 的本质是把服务端自由文本 message 与两个从示例文档里抄来的硬编码英文字符串做比对。三条出路:(a) 完全不再把 DELETE 响应体当判据 —— 只要求 HTTP 状态成功,保留有界读取以维持资源与编码安全,由本来就必须执行的一次删除后 GET 来判定;(b) 把匹配放宽成能容忍插入 ID 的模式;(c) 把可接受的 message 集合做成按 dialect 可配置。我会选 (a)。只有那次复核 GET 才真正证明记录已移除,任何响应体启发式都是叠在更强判据之上的更弱判据,而 (b)/(c) 都仍在猜测服务端文案,还要额外维护一个正则或一套配置面。

本 PR 采用的正是 (a),而且在当前 head 上超出了我的方案。 5791f56 还把 deleted 的提示文案从"The provider confirmed deletion and a subsequent exact read confirmed absence"改为"The delete request returned a successful HTTP response and a subsequent exact read confirmed absence"。这正是我的方案漏掉的那一半:保留旧文案,会让工具在一个破坏性操作上、在用户真正会读到的那一行字符串里,声称一件代码已经不再校验的事。它同时在 delete-mcp.test.ts 里固定了新文案,因此不会静默漂移。

以下是我逐一核对、而非照抄描述的部分:

  • 没有新增误报 deleted 的路径。 deleted 仍要求 after.status === 'absent',而 readTarget 只在 http-404 下收到 HTTP 404、或 null-200 下收到 HTTP 200 + JSON null 时才返回 absent。该函数未被改动。DELETE 响应体从"第二个更弱的判据"变成"完全不是判据",真正的判据没有移动。
  • 严格度的不对称是刻意设计,不是疏漏。 DELETE 响应不再检查 error/errors,但 readTargetGET 携带这些字段时仍会抛 Invalid target.。严格度被从"证明不了任何事"的地方移除,保留在"决定最终结论"的地方。一眼看去像前后不一致,所以值得点明。
  • 202 的处理方向是安全的。 异步的 202 Accepted 现在会走到复核 GET;若记录仍在,结果是 unknown —— 对于"可能已删除、不要重试、请读取核查"这正是正确答案。两个分支都已被固定:目标仍存在时由 verifies absence after HTTP 201/202 with JSON 覆盖,新增的 reports deleted after HTTP 201/202 when the follow-up GET confirms absence 覆盖确认不存在的情形。
  • 重定向安全性完好。 配合 redirect: 'manual',3xx 永远不属于 ok,因此设计文档所担心的"URL 规范化把单条 DELETE 变成集合 DELETE"依然被挡住。rejects non-success DELETE HTTP 301 even with valid JSON 是该断言更强的形式。
  • 与同包姊妹引擎的一致性,我认为这是支持本次改动最有力的论据。 同一个包里的 createRequestEngine 本来就以 !response.ok 为门禁。DELETE 引擎从 status !== 200 改过来,使两者在主判据上达成一致,而不是各自定义什么叫成功。(search 那边还额外带了一个显式 3xx 判断;ok 按定义就是 200–299,所以那一句在它那里是冗余的。两边都不需要动。)
  • 改动文件之外没有回归。 放宽正是那种会在别处静默反转断言的改动,所以我检查了另外两个同样走这条路径的 mock server 测试套件。delete.integration.test.tsintegration-tests/cli/external-context-mem0-daemon-delete.test.ts 都以 200 + {"message":"Memory deleted successfully!"} 应答 DELETE —— 新逻辑下依旧接受,且随后都返回 404。它们期望 unknown 的场景来自 dropslow-preflight-bodyslow-delete-bodyslow-verification-bodycumulative-deadline 以及一个 500,走的是中止与超时路径,而非响应体判据。没有断言被翻转。
  • 没有死代码。 hasErrorisRecord 仍被 readTarget 使用,所以删除 isDeleteAcknowledgement 不会留下孤儿、也不会触发 noUnusedLocalsisDeleteAcknowledgement 已完全移除且无残留引用,head 版本测试文件里的 ack()/calls()/absent() 辅助函数均仍可解析。旧的提示文案在全仓只有一处出现,即被修改的那个定义。
  • 失败仍是终止态。 有界读取或解析抛出后进入外层 catch,此时 submitted === trueunknown,不重试。

本轮重新核对过,不是出于信任而沿用。 自上次审查以来 head 没有变化,所以上述结论依然成立 —— 但我是拿 head 源码重新核对的,而不是拿我自己上一条评论核对的。readTarget 在 base 与 head 之间逐字节相同(两边各自按其 ref 取回后比对),所以判据确实没有移动。hasError/isRecord 在 head 上仍被它引用,因此移除 isDeleteAcknowledgement 不会留下孤儿,也不可能触发 noUnusedLocals。head 上的 forgetMessages.deleted 字符串与 delete-mcp.test.ts 里新增的固定断言逐字符一致。而 readBoundedBodyif (!response.body) throw 位于 request-engine.ts:181,在任何 parse 之前 —— 这正是下面那段"对被丢弃的 JSON.parse 的更正"之所以准确、而我上一轮的说法之所以夸大的原因。

对我上一轮审查的一处更正,因为它仍留在本讨论串里。 我曾写那个被丢弃的 JSON.parse"承担两件事:强制执行有界读取,并且它的抛出正是无效或空 JSON → unknown 的来源"。前半句是错的。有界读取完全由 readBoundedBody 自己保证 —— 声明的 content-length 上限、流式 1 MiB 上限、TextDecoder('utf-8', { fatal: true }),以及显式的 if (!response.body) throw。因此空响应体的 204 是在 readBoundedBody 内部就变成 unknown 的,发生在 JSON.parse 之前。那个被丢弃的 parse 唯一独占的作用比我说的要窄:响应体必须是语法合法的 JSON。删掉这一行,会让携带 not json 的 200 继续走到复核 GET —— 而按本 PR 自己"响应体不是判据"的主张,这反而更像是一致行为,而不是回归。加注释的建议依然成立,但它保护的是一个属性而非两个;我不希望那句夸大的说法成为后来者信任的版本。

一条非阻塞的覆盖度备注,延续上轮。 最令人警惕的新接受形态 —— HTTP 200 且响应体为 {"error": "provider-specific response", "status": "FAILED", "cascade_count": 1} —— 仍然只与"目标最终不存在"(→ deleted)组合测试过。目标仍存在的情况在其他 payload 上已有固定(cannot claim deletion without confirming absence 里的 ack(),以及现在的 201/202 两行),而且代码已完全不读响应体,所以该性质是由构造保证、而非由测试保证。这是覆盖度问题,不是缺陷。5791f56 补的是相邻的用例而不是这一个,这没问题 —— 我只是记录在案,并非要求修改。

我再次跳过了两项可选增强:没有时序图,因为 GET → DELETE → GET 流程本来就有,只改了其中一个判定条件;也没有变更文件表,六个文件里两个是文档、两个是测试。

测试证据

无人值守 CI 运行 —— 我没有构建、运行、checkout 或 apply 本 PR 的任何内容。 下面的证据是本 PR 自己的 CI,通过 API 针对提交 5791f56dd34731b047d16073f78cf5e99bd7c6be 一次性读取,没有轮询。上文代码审查中引用的 head 源码,也是通过 contents API 以只读方式取自同一个 OID。

该提交上的 CI 现已跑完:一项红灯,而且不是本 PR 造成的。(检查结论表见上方英文区,由 finalize 作业在 CI 结束后原地更新。)

这项红灯的归类,附提交级证据,而不是含糊带过。 Test (ubuntu-latest, Node 22.x) 只失败在两个用例上,都在 packages/web-shell/client/App.test.tsx,失败作业日志直接给出了原因:

FAIL  App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: false)
FAIL  App.test.tsx > App session callbacks > does not rerender App for other split sessions (outer pending: true)
   ReferenceError: mockUseDaemonActivePromptBridge is not defined
 Test Files  1 failed | 287 passed (288)
      Tests  2 failed | 6710 passed (6712)

下面每一环都是核对过的,不是假设:

  • 不存在作用机制。 本 PR 的六个文件里没有任何 packages/ 下的路径 —— files API 只返回 docs/design/…integrations/external-context-mem0/…。本 diff 无法触及 web-shell 的测试模块。
  • 这个标识符根本不存在。 mockUseDaemonActivePromptBridge 在整个仓库里都找不到。它由 main 提交 70cf363 引入 —— feat(web-shell): Improve split-view session navigation (#11250),2026-09-08T17:33Z —— 而这正是本 PR 的 base.sha
  • main 在本 PR 的 CI 开始之前就已修好。 3a75f37 —— fix(web-shell): Replace undefined mock in split rerender tests (#11404) (#11406),2026-09-09T00:10Z —— 恰好把这两个用例里未定义的 mock 换成 mockUseDaemonSessionActivityBridge。这比本轮 CI 于 02:37Z 启动早了 2 小时 27 分。
  • 当前 head 不含该修复。 compare/3a75f37...5791f56divergedbehind_by: 5。该分支最后一次合并 main 是 b282e82(2026-09-08T17:36Z)—— 在破坏落地三分钟后,在修复落地前数小时。
  • main 现已恢复绿灯。 dcbb0b5f9(06:07Z)上的 Qwen Code CI 运行中,Test (ubuntu-latest, Node 22.x) 为 ✅ success。

因此这是本 PR 的合并提交恰好落在其中的一段"main 已坏"窗口。把当前 main 合并进本分支即可消除,本 PR 自身代码无需任何改动。 我把它归为既存问题、不可归因于本 PR,并且点名了具体提交 —— 而不是称之为"已知 flake"。对未定义标识符的 ReferenceError 是确定性的,而这里的确定性有可指认的原因。

真正覆盖本 diff 的那套测试,是在同一个失败作业内部跑绿的:

> @qwen-code/external-context-mem0@0.23.1 test:ci
 ✓ src/delete-request-engine.test.ts (82 tests) 49ms
 ✓ src/delete-mcp.test.ts (22 tests) 108ms
 ✓ src/delete.integration.test.ts (8 tests) 2203ms
 Test Files  17 passed (17)
      Tests  328 passed (328)

delete.integration.test.ts 是真正的端到端测试 —— 它 spawn 打包后的 dist/delete-main.js,经 MCP stdio 对接一个带真实记录状态的真实 node:http 服务 —— 所以打包后的删除路径在该 head 上是绿的,不只是 mock 掉的单元层。其余绿灯:Lint & StaticIntegration Tests (no-AK, No Sandbox)web-shell E2E SmokeDesktop Shell(ubuntu + windows)、Classify PRreview-pr。另有 8 项检查因工作流条件为 skippedTest (macos-latest)Test (windows-latest)Integration Tests (CLI, No Sandbox)precheck-prack-review-requestresolve-prreview-configpublish-resolution)—— 这是此类 PR 的正常门禁,不是负面信号。

上一轮留在本讨论串里的那项 web-shell E2E 红灯现已定论。 web-shell E2E Smoked0a7fc1 上是 ❌(web-shell.history-viewport.spec.ts:275 处小于 2px 的滚动锚点容差),在 5791f56 上是 ✅。上一轮我明确拒绝在没有 base 提交对照的情况下称之为已知 flake,并说当时在跑的这一轮才能定论它是否会在本 head 合入 main 的 split-view 改动后复现。它没有复现。这个问题由测量关闭,而非由假设关闭。

行为性主张现已有实证 —— 而且不是我给的。 上一轮我点名 @qwen-code /verify 是唯一能定论"该改动是否承重、而不只是绿灯"的通道,并刻意拒绝预测其结论。它此后已在同一个 head 上出报告(run 34304026537):merge-ready —— 311 条脚本化断言,311 通过 / 0 失败,运行于隔离且无凭证的容器中。与静态审查无法定论的那部分主张相关的要点:

  • 四臂 A/B(base / 只含 !response.ok 那一半 / 只含移除回执校验那一半 / head),对接持有真实记录存储的真实回环 HTTP 服务,32 个场景,每格都用一次独立 GET 交叉核对,使服务端真实状态是被观测的而非被推断的。在捕获到的 Holo 形态(200 + Memory memory:1 deleted successfully.)上:base = unknown,head = deleted。承载修复的是移除回执校验那一半;!response.ok 那一半对该 bug 无贡献,而 201/202 + 任意 JSON 需要两半同时到位 —— 各自必要,合起来充分。
  • "与 SDK 对齐"这一主张是拿真实 SDK 验的,不是拿描述验的:出厂的 mem0ai@3.1.8 _fetchWithErrorHandling 在 143 种状态码 × 响应体组合上被驱动。HEAD 只在 6 格与 SDK 不同,且全部是本仓有意的加严(1 MiB 上限 ×3、严格 UTF-8 ×3);base 有 23 格不同。HEAD 的接受集是 SDK 接受集的子集,base 的接受集是 HEAD 的子集 —— 本次只放宽、从不收紧。
  • 变异矩阵 9/9 全部击杀,0 存活,对照组 328/328 绿,每个变异体前都重建 dist/。值得注意的是 M5 —— 即"彻底去掉 JSON.parse"这个更进一步的放宽 —— 恰好被 1 个现有用例击杀,说明该轴有人看守,采纳它属于刻意的设计决定,而非覆盖缺口。
  • 抖动门:两个被改动的测试文件 × 5 轮完全相同的运行,无差异。
  • 它的两条 nit 与我独立得出的两条一致:被丢弃的 JSON.parse 该加注释;新增的 new Response(null, { status: 200 | 202 }) fixture 走的是 readBoundedBody 的 null-body 分支,而非线上可达的空响应体(实测等价 —— 真实的空 200 会在下一层 JSON.parse('') 失败,结果同为 unknown)。

第二次 /verify 于 06:31Z 被触发,仍在运行中;它持有第二条 qwen-triage:verify 评论,报告会发布在那里。我不预测也不转述它 —— 上面那份是真正已经落地的报告。

真实场景测试:本次运行为 N/A。 这是无人值守 CI 执行,因此没有在 tmux 中驱动产品、也没有粘贴终端截取 —— 那条通道仅限本地调用。此处如实说明未验证的部分,而不是掩盖:作者的 E2E 报告只覆盖 macOS,Windows 与 Linux 未测试,并明确说明打补丁后没有重新做真实 Holo 或 PolarDB 验收。那是作者的主张,不是我复跑的证据。verify 运行复现的是作者所描述的线上字节形态(对接本地服务),是对所报字节的忠实回放,不是对真实 Holo 的调用。PolarDB 的 GET 包装结构与原文写入语义明确不在本次范围内。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 5791f56dd34731b047d16073f78cf5e99bd7c6be · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the code is right, the behavioural claim is now independently substantiated rather than argued from the diff, and the single red check on this commit is provably main's and not this PR's; what keeps it off 5 is that the branch has to merge current main before that check can go green, plus two notes I am explicitly not blocking on.

Stepping back, the picture is unchanged from my last pass and better-evidenced. This is a maintainer correcting a contract they landed hours earlier in #11337, and the correction removes code: +3/−17 production lines, and the function being deleted was the entire source of the bug. I went looking again for the way this could be wrong, and the honest answer is still that the safety model never depended on the thing being changed. deleted has always required a follow-up GET confirming absence under the configured notFound rule, and that GET path — readTarget — is byte-identical between base and head, still strict about error/errors, id field, scope and full content. The DELETE response body was a second, weaker check stacked on top of a stronger one, calibrated against two English strings the design doc itself admits were never verified against Holo. It was never load-bearing, and this PR correctly stops pretending it was.

The user-facing bug is concrete and worth fixing on its own terms: someone deleted a memory from Holo, the record really was gone, and the tool told them unknown on a destructive operation. That is the worst direction for a delete tool to fail — it leaves the user unsure whether their data was removed, which is exactly the question the feature exists to answer.

What moved this pass is the evidence, not my reading of the diff. Two things landed since I last wrote here:

  • CI finished on 5791f56. One red: Test (ubuntu-latest, Node 22.x), two App.test.tsx tests in web-shell, ReferenceError: mockUseDaemonActivePromptBridge is not defined. I traced it to the commits rather than classifying it by vibe: the identifier arrived on main at 70cf363 (feat(web-shell): Improve split-view session navigation #11250) — which is exactly this PR's base.sha — and main repaired it at 3a75f37 (fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406, literally titled "Replace undefined mock in split rerender tests") 2h27m before this PR's CI started. This head is behind_by: 5 from that fix and touches no file under packages/ at all. Main has been green since. So: a broken-main window this branch happened to sit inside, not a defect here. Meanwhile the suite that does cover the diff — external-context-mem0, 17 files / 328 tests, including the packaged end-to-end delete.integration.test.ts that spawns dist/delete-main.js over real MCP stdio — passed green inside that same failing job.
  • The sandboxed /verify run reported, and it settles the claim I said last pass was not mine to make. Four-arm A/B against a real loopback provider with a real record store: on the captured Holo shape, base = unknown, head = deleted, with the acknowledgement removal carrying the fix and the !response.ok half contributing nothing to that specific bug. The SDK-alignment claim was checked against the real shipped mem0ai@3.1.8 across 143 response combinations — head's accept set is a strict subset of the SDK's, and base's is a subset of head's, so this only widens and never tightens. Mutation matrix 9/9 killed with dist/ rebuilt per mutant, flakiness gate clean over 5 rounds, 311/311 assertions. That is stronger evidence than anything I could produce statically, and it is why I'm acting on the verdict this time instead of deferring again.

Am I approving because it's genuinely good, or because I ran out of reasons to say no? The former. My proposal before reading the diff was to drop the body match entirely and let the HTTP status plus the confirmation GET decide — which is what this does. The alternatives I considered, tolerating an interpolated ID by pattern or making the accepted message set configurable per dialect, both keep guessing at provider prose and add surface to maintain. I could not find a simpler version of this fix, and I looked harder this pass precisely because the answer was already sitting in the thread.

Two things on the record, both non-blocking, both already stated in the review above:

  • The discarded JSON.parse deserves a one-line comment. It is the only statement in the function whose entire purpose is invisible at the call site, and the sandboxed run's mutant M4 shows 8 tests go red if someone tidies it away as dead code — so the intent is currently carried only by the test suite. Cheap to fix in the code as well.
  • A 204 No Content — the most standard successful-DELETE shape there is — still resolves to unknown without ever consulting the confirming GET that would have settled it. Pre-existing, documented as a tradeoff in both the README and the design doc, and it fails in the safe direction. The sandboxed run also measured it as matching the SDK, whose own response.json() throws on a 204 too — so it is a shared limitation of the client contract, not a divergence this PR introduced. Worth a follow-up if a 204-style provider ever shows up; not this PR's problem to solve.

Three things I want stated rather than implied:

  • I did not run this code. Unattended CI re-run, so the review is static plus this PR's own CI and the completed /verify report, all read through the API. Nothing was built, executed, checked out or applied; the head sources I quote were fetched read-only at the reviewed OID.
  • My approval does not bypass the red check, and the branch still needs a merge of main. mergeStateStatus is BLOCKED, and Test (ubuntu-latest, Node 22.x) has to go green on its own merits. Merging current main clears it — main has contained the fix since 00:10Z and has been green since — and requires no change to this PR's own code. Worth knowing: pushing that merge dismisses this approval (dismiss_stale_reviews) and re-runs CI. That is the mechanism working as designed, not a setback; a /triage re-run on the new head re-approves against green CI.
  • @chiga0's approval at 06:31Z is a separate vote, not a substitute for mine. main wants two, and mine is now recorded against 5791f56 specifically via commit_id, so it deliberately does not survive a force-push or a new merge commit.

If I maintain this in six months I'd thank the author. The code got shorter, the two engines in the package now agree on what a successful HTTP response is instead of each having its own idea, the user-facing message tells the truth about what is actually verified, and the docs and README moved in the same commit as the behaviour rather than after it.

✅ Approving, pinned to 5791f56dd34731b047d16073f78cf5e99bd7c6be.

中文说明

Confidence: 4/5 —— 代码是对的,行为性主张现在有独立实证、而不再是从 diff 推论出来的,而本提交上唯一的红灯可以被证明属于 main、不属于本 PR;没给到 5 分的原因是:该分支必须先合并当前 main,那项检查才能转绿,另有两条我明确不作为阻塞项的备注。

退一步看整体:结论与上一轮相同,但证据更充分。这是一位 maintainer 在修正自己几小时前随 #11337 落地的契约,而这次修正是删代码:生产代码 +3/−17,被删掉的那个函数正是 bug 的全部来源。我又一次去找"这可能错在哪",诚实的答案仍然是:安全模型从来就不依赖于被改动的这一环。deleted 一直要求紧接一次精确 GET、按配置的 notFound 规则确认不存在;而那条 GET 路径 —— readTarget —— 在 base 与 head 之间逐字节相同,对 error/errors、ID 字段、scope 和完整正文依旧严格。DELETE 响应体是叠在更强判据之上的第二个更弱判据,其标尺是设计文档自己承认从未对 Holo 核实过的两个英文字符串。它从来就不承重,本 PR 正确地不再假装它承重。

面向用户的 bug 具体、且本身就值得修:有人从 Holo 删除了一条记忆,记录确实没了,而工具在一个破坏性操作上告诉他 unknown。这是删除类工具最糟的失败方向 —— 它让用户无法确定数据是否已被移除,而这恰恰是该功能存在的唯一理由。

本轮变化的是证据,不是我对 diff 的解读。自我上次发言后落地了两件事:

  • CI 在 5791f56 上跑完了。 一项红灯:Test (ubuntu-latest, Node 22.x),web-shell 的两个 App.test.tsx 用例,ReferenceError: mockUseDaemonActivePromptBridge is not defined。我是追到具体提交、而不是凭感觉归类的:该标识符由 main 的 70cf363feat(web-shell): Improve split-view session navigation #11250)引入 —— 而这正是本 PR 的 base.sha —— main 已在 3a75f37fix(web-shell): Replace undefined mock in split rerender tests (#11404) #11406,标题就叫"Replace undefined mock in split rerender tests")修好,比本 PR 的 CI 启动 2 小时 27 分。当前 head 距该修复 behind_by: 5,且完全没有触及 packages/ 下的任何文件。main 此后一直是绿的。所以:这是该分支恰好身在其中的一段"main 已坏"窗口,不是本 PR 的缺陷。与此同时,真正覆盖本 diff 的套件 —— external-context-mem0,17 个文件 / 328 个用例,含 spawn dist/delete-main.js 走真实 MCP stdio 的打包端到端测试 delete.integration.test.ts —— 在那个失败作业内部是绿的。
  • 沙箱 /verify 出报告了,它定论了上一轮我说"还不属于我"的那个主张。四臂 A/B,对接持有真实记录存储的真实回环服务:在捕获到的 Holo 形态上,base = unknown,head = deleted;承载修复的是移除回执校验那一半,!response.ok 那一半对该 bug 无贡献。"与 SDK 对齐"这一主张是拿真实出厂的 mem0ai@3.1.8 在 143 种响应组合上验的 —— head 的接受集是 SDK 接受集的严格子集,base 的接受集是 head 的子集,因此本次只放宽、从不收紧。变异矩阵 9/9 全部击杀(每个变异体前重建 dist/),抖动门 5 轮无差异,311/311 条断言通过。这比我在静态层面能给出的任何证据都更强,也是我这次据以行动、而不是再次 defer 的原因。

我批准,是因为它真的好,还是因为我找不到反对理由了?是前者。我在读 diff 之前的方案就是:彻底去掉响应体匹配,交给 HTTP 状态加那次复核 GET 判定 —— 本 PR 做的正是这件事。我考虑过的另外两条路(用模式容忍插入的 ID、或把可接受 message 集合做成按 dialect 可配置)都仍在猜测服务端文案,并且增加了要维护的表面积。我找不到更简的版本;这一轮我查得更用力,恰恰因为答案已经躺在讨论串里了。

两条记录在案的备注,均非阻塞,且都已在上面的审查中说明:

  • 那个被丢弃的 JSON.parse 值得加一行注释。它是函数里唯一一处"全部目的在调用点不可见"的语句,而沙箱运行的变异体 M4 显示:若有人把它当死代码清理掉,会有 8 个用例转红 —— 也就是说该意图目前只由测试套件承载。在代码里也补上,成本很低。
  • 204 No Content —— DELETE 成功时最标准的形态 —— 仍会落在 unknown,而从未去问那次本可定论的复核 GET。这是既存行为,README 与设计文档都写成了权衡,且失败方向是安全的。沙箱运行还实测到它与 SDK 一致:SDK 自己的 response.json() 在 204 上同样抛错 —— 所以这是客户端契约的共有局限,不是本 PR 引入的分歧。若将来出现 204 形态的服务端,值得作为后续项;不该由本 PR 解决。

三件我希望明说、而不是暗示的事:

  • 我没有运行这份代码。 无人值守 CI 重跑,因此审查是静态的,加上本 PR 自己的 CI 与已完成的 /verify 报告,全部通过 API 读取。没有构建、执行、checkout 或 apply 任何内容;我引用的 head 源码是在受审 OID 上以只读方式取回的。
  • 我的批准不会绕过那项红灯,分支仍需合并 main。 mergeStateStatusBLOCKEDTest (ubuntu-latest, Node 22.x) 必须凭自身转绿。合并当前 main 即可清除 —— main 自 00:10Z 起就含该修复,且此后一直绿 —— 且本 PR 自身代码无需任何改动。需要知道的是:推上这个合并会作废本次批准(dismiss_stale_reviews)并重跑 CI。这是机制按设计工作,不是挫折;在新 head 上重跑 /triage 会在 CI 转绿后重新批准。
  • @chiga0 于 06:31Z 的批准是另一张票,不能替代我这张。 main 需要两票,而我这张现在是通过 commit_id 记录在 5791f56 上的,因此它刻意不会在 force-push 或新的合并提交之后存活。

如果六个月后由我来维护这份代码,我会感谢作者。代码变短了,包里两个引擎对"什么算成功的 HTTP 响应"终于达成一致、而不是各自定义,面向用户的文案如实说明了实际校验了什么,文档与 README 也是与行为同一个提交里改的、而不是事后补的。

✅ 已批准,绑定到 5791f56dd34731b047d16073f78cf5e99bd7c6be

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 5791f56dd34731b047d16073f78cf5e99bd7c6be · re-run with @qwen-code /triage

@doudouOUC doudouOUC self-assigned this Sep 8, 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.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread docs/design/external-context-mem0-explicit-delete.md Outdated
…s statuses

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 311 passed · 0 failed · 311 total

Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:311 通过 · 0 失败 · 311 总计

抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 11397 — deep verification

Verdict: merge-ready — 311 scripted assertions executed, 311 passed / 0 failed.
Verified head OID: 5791f56dd34731b047d16073f78cf5e99bd7c6be (= snapshot headRefOid, = HEAD^2).
Base tip: a5bc6c54970bab1d0472605c5a1468754ccfd76d (HEAD^1).

中文摘要

结论:merge-ready(311 条脚本化断言全部通过,0 失败)。

A/B 结论。 本 PR 实际包含两个独立的改动:(A) response.status !== 200!response.ok;(B) 删除 isDeleteAcknowledgement() 文案/字段校验。我编译了 四个 arm(base / 只改 A / 只改 B / head),用真实回环 HTTP 服务跑了 32 个场景,每格都用一次独立 GET 记录服务端真实状态。issue 中的 Holo 回执(HTTP 200 + Memory memory:1 deleted successfully.):base = unknown(2 次请求),head = deleted(GET→DELETE→GET,独立 GET 确认 404)。承载修复的是 B,A 对该 bug 无贡献;而 201/202 + 任意 JSON 只有 head 能返回 deleted,两个半边单独都不够 —— 二者各自必要、合起来才充分。

与 SDK 对齐的说法已对着 SDK 自己的代码验证。 从 npm 取回 mem0ai@3.1.8,用 Object.create(MemoryClient.prototype) 直接执行其出厂的 _fetchWithErrorHandling(不触发构造函数里的 ping 与埋点),对 11 种状态码 × 13 种响应体 = 143 格逐一比对「是否接受该 DELETE 响应」。SDK 的实现正是 if (!response.ok) throw + 无条件 await response.json(),不检查 message/status/event/cascade_count/errorHEAD 只在 6 格与 SDK 不同,且全部是本仓有意的加严(1 MiB 上限 ×3、严格 UTF-8 ×3);base 与 SDK 有 23 格不同。集合不变式也已断言:HEAD 的接受集是 SDK 接受集的子集(HEAD 从不接受 SDK 会拒绝的响应),base 的接受集是 HEAD 的子集(本次只放宽、从不收紧)。另外重定向策略上 SDK 用默认 follow、本仓钉死 manual:301→200 的场景 SDK 接受且真的打到了 /final(1 次),HEAD 拒绝且 /final 命中 0 次。

204/空响应/非 JSON 的取舍经测量属于「与 SDK 一致」,不是遗漏。 我原本怀疑这是与被修复 bug 同一类的漏网形状,实测否证了更坏的读法:SDK 的 response.json() 在 204、空体、text/plain、HTML 上同样抛错,143 格中所有 204 格 SDK 与 HEAD 完全一致。差别只在 SDK 抛异常、本仓映射为 unknown(更安全),且 unknown 的固定文案已经引导模型显式再读一次。方向是 fail-safe,无数据风险。

变异矩阵:9/9 全部被杀,0 存活,对照组 328/328 绿。 每个变异都重建了 dist/(包内集成测试会 spawn dist/delete-main.js,不重建会掩盖击杀)。归因精确:M1(回退 A)恰好被新增的 4 个 201/202 用例杀掉;M8(回退 deleted 文案)恰好被本 PR 新增到 delete-mcp.test.ts 的那条断言杀掉 —— 新断言非空转。反向变异 M5(把 JSON.parse(...) 换成只 await readBoundedBody(...),即放宽空体/非 JSON 的候选进一步修复)被恰好 1 个现有用例杀掉(bounds responses before and after DELETE'{bad' 行)。也就是说:那条轴并非无人看守,采纳该修复必须同时改这条测试 —— 这是维护者的设计决定,不是覆盖缺口。

门禁(均带存活性证明)。 包内 vitest 在受验 HEAD 树上 17 文件 / 328 用例全绿;tsc --noEmit 退出 0;eslint src 退出 0。为证明绿灯不是「没匹配到文件」,我在被改动的那个文件里植入未使用变量与显式 any,eslint 退出 1 并报出 no-explicit-any / no-unused-vars,tsc 退出 2 并报出 TS6133,随后完整还原(两棵树的 git status --porcelain 均为空)。

发现(均为 nit,不阻塞),见 Findings 表:一是 JSON.parse(...) 的返回值被丢弃、成为纯表达式语句,没有注释说明这是有意的有效性闸门;二是新增测试用 new Response(null, {status: 200/202}) 构造「空响应」,而真实 HTTP 的 200/202 不会给出 null body,线上走的是 JSON.parse('') 抛错这条不同路径 —— 我用真实服务量过两种形状,结局同为 unknown,无行为差异。

未覆盖:仓库级 daemon E2E(PR 声称的「12 个 daemon 场景」)、真实 Holo/PolarDB 验收、Windows、以及逐 commit 归因(depth-2 浅克隆下 git rev-list --count HEAD^1..HEAD^2 返回 1,而快照 commits 有 3 条;快照的 baseRefOid 本地不可达)。详见 Not covered。

1. Scope selection

Diff is 6 files / +92 −77, entirely inside integrations/external-context-mem0 plus its design doc:

file change
src/delete-request-engine.ts −18/+2: the change under test
src/delete-request-engine.test.ts rewritten acknowledgement cases
src/delete-profile.ts one line: the deleted message text
src/delete-mcp.test.ts +6: pins that message
README.md, docs/design/external-context-mem0-explicit-delete.md docs

Central claim. A DELETE that returns a successful HTTP status with arbitrary JSON is no longer rejected by message text or response-field rules; the follow-up exact GET alone decides deleted vs unknown. Concretely: the captured Holo response (HTTP 200, Memory <id> deleted successfully.) must now yield deleted.

Secondary claims. (1) The handling matches the official Mem0 client. (2) Malformed/empty bodies, non-success statuses and connection loss still yield unknown with no retries.

The change bundles two independent hunks, so the A/B has four arms rather than two (§2). Out of scope by choice: the repo-level daemon E2E, live providers, Windows (§6).

2. Central claim — four-arm A/B

The PR's engine change is two separable edits:

  • hunk Aif (response.status !== 200)if (!response.ok)
  • hunk B — delete isDeleteAcknowledgement(value) and its message/status/event/cascade_count/error rules

Arms: A=base (neither), B=hunk A only, C=hunk B only, D=head (both). Each arm is the real createDeleteRequestEngine compiled by esbuild from that arm's own TypeScript source, driving a real node:http server over loopback TCP with real record state. Nothing on the request path is stubbed. After each cell the harness issues its own independent GET, so "what the provider actually says now" is an observation rather than a hypothesis.

Witness: 01-ab-four-arm-matrix.png (the 32×4 table as it printed). Raw: logs/01-ab-full.log, logs/ab-results.json.

32 cells × 4 arms × 2 oracles (status, request count) + 5 harness-validity + 6 wire-shape = 267 assertions, 267 passed.

Selected cells (A/B/C/D = the four arms; reqs = requests the peer actually saw; ind.GET = the harness's independent read afterwards):

cell A base B hunk-A-only C hunk-B-only D head reqs (D) ind.GET
G1 Holo captured: 200 + Memory memory:1 deleted successfully. unknown unknown deleted deleted 3 (GET,DELETE,GET) 404
G2 control: 200 + either base-recognised literal (…successfully! and …successfully) deleted deleted deleted deleted 3 404
G3 200 + arbitrary JSON — {}, null, [], "ok", {"status":"PENDING","event":"DELETE","cascade_count":0}, {"status":"SUCCEEDED","error":null} (6 of the 7 cells) unknown unknown deleted deleted 3 404
G3 200 + {"error":"boom","status":"FAILED","cascade_count":1} (the 7th) unknown unknown deleted deleted 3 404
G4 201/202 + the base-recognised literal unknown deleted unknown deleted 3 404
G4b 201/202 + arbitrary JSON {"message":"accepted"} unknown unknown unknown deleted 3 404
G5 301/400/401/403/404/429/500 + valid JSON ack unknown ×4 arms 2 200 (record survived)
G7 record survives after a literal ack unknown ×4 arms 3 200
G8 connection destroyed on DELETE unknown ×4 arms 2 200

Attribution — what each half buys. G1 is the reported bug: hunk B alone fixes it; hunk A contributes nothing (B=unknown), because Holo answered 200. G4 is the mirror: for 201/202 with a recognised message, hunk A alone fixes it (B=deleted, C=unknown). G4b shows the combination case: for 201/202 with arbitrary JSON, neither half alone suffices — only D reports deleted. A two-cell A/B could not have produced this: the two hunks are individually necessary for different inputs and jointly sufficient for the third.

No retry, on any path. Every failure cell sits at exactly 2 requests (GET, DELETE) or 3 (GET, DELETE, GET) — never 4. G5 covers 7 non-success statuses, G8 covers transport loss mid-DELETE, and the body-veto group (§3) covers 8 more; none re-issued a DELETE.

Wire oracle, both sides (6 assertions, head arm, G1). The peer saw exactly GET /api/memories/memory%3A1, DELETE same path, GET same path — every one with accept: application/json, authorization: Token synthetic-token, zero-length body. The ID stayed a single percent-encoded path segment, so no URL normalisation could turn a single-record DELETE into a collection DELETE. Caller side: deleted, and stderr clean.

3. The SDK-alignment claim, tested against the SDK

The description and README both claim alignment with the official client. That is falsifiable, and the registry was reachable, so I ran the real thing rather than reasoning about it.

npm pack mem0ai3.1.8. Its shipped dist/index.mjs _fetchWithErrorHandling is:

if (!response.ok) { const errorData = await response.text(); throw createExceptionFromResponse(...); }
const jsonResponse = await response.json();
return snakeToCamelKeys(jsonResponse);

That is hunk A verbatim (!response.ok), hunk B verbatim (no message/status/event/cascade_count/error inspection), and an unconditional response.json(). The client was instantiated with Object.create(MemoryClient.prototype) so the real method executes with no constructor side effects (the constructor pings the host and posts telemetry). Nothing was transcribed or re-implemented.

Witness: 02-sdk-differential-head-vs-base-vs-sdk.png. Raw: logs/03-sdk-differential.log (full 143-row matrix), logs/sdk-differential.json.

11 statuses × 13 bodies = 143 cells, each reduced to one boolean — did this gate accept the DELETE response? — with all three gates reading the same scripted response from the same server:

gate disagreements with the SDK direction
HEAD 6 / 143 all 6 are HEAD being stricter: json-1mib-valid ×3 statuses (1 MiB bound) and invalid-utf8 ×3 (fatal UTF-8 decode)
BASE 23 / 143 all 23 are BASE being stricter, and they include the reported Holo shape

The 6 HEAD divergences are exactly the two hardenings the README already discloses ("bounded to 1 MiB", "bounded UTF-8 JSON"). Asserted as a set, not eyeballed: the distinct body names where the SDK accepts and HEAD refuses are precisely ['invalid-utf8', 'json-1mib-valid']. Note the invalid-utf8 case is subtle — the bytes 22 FF 22 decode non-fatally to the valid JSON string "\uFFFD", so the SDK genuinely accepts a corrupted-UTF-8 acknowledgement and HEAD refuses it.

Set invariants (all asserted, all passed) — these are the safety-critical direction:

  • HEAD's accept set ⊆ SDK's accept set. There is no cell where HEAD treats a response as success that the official client would reject.
  • BASE's accept set ⊆ HEAD's accept set. The change only ever widens acceptance; it never narrows it, so no previously-deleted shape regressed.
  • Every HEAD-vs-SDK disagreement is HEAD refusing, never HEAD accepting.

Redirect policy diverges deliberately, and in the repo's favour. The SDK calls fetch with the default redirect: 'follow'; the repo pins redirect: 'manual'. Probe: a DELETE answering 301 → Location: /final where /final returns a 200 JSON ack. SDK: ACCEPT, /final hit 1 time. HEAD: unknown, /final hit 0 times. Not folded into the alignment count — it is a documented, pre-existing repo hardening, unchanged by this PR.

SDK differential: 19 assertions, 19 passed.

The declared 204 / empty-body / non-JSON tradeoff — measured, and the scarier reading does not hold

The description and README both declare that empty responses (including 204) and invalid JSON remain unknown. Since that is the same shape of complaint as the bug being fixed ("a real successful deletion reported as unknown"), I swept the siblings rather than accepting the declaration. 8 body shapes, each with the server actually deleting the record:

DELETE response all four arms reqs independent GET afterwards
204 No Content unknown 2 404 — the record is gone
200, zero-length body unknown 2 404
202, no body unknown 2 404
200 text/plain OK unknown 2 404
200 text/html proxy page unknown 2 404
200 {bad unknown 2 404
200 invalid UTF-8 unknown 2 404
200 valid JSON, 1 MiB + 1 unknown 2 404

So yes: on these shapes the tool reports unknown while the record is verifiably gone, and it never issues the confirming GET that would have told it so. But the scarier readings do not hold, and I checked each:

  • It is not a divergence from the claimed reference implementation. The SDK's unconditional response.json() throws on every one of these too. All 13 204/* cells agree between SDK and HEAD; text-plain-ok, text-html, malformed-json, empty-body all refuse on both. HEAD is SDK-faithful here, not stricter-than-necessary-by-accident.
  • It is not a regression. All four arms agree, base included — this PR neither introduced nor widened it.
  • It is not unsafe. unknown is the fail-safe direction: forgetMessages.unknown is verbatim "The record may have been deleted. Do not retry automatically; explicitly read the target to check its current state.", which resolves the question in one extra read. Nothing is reported as deleted that was not confirmed absent, and nothing is reported as not_deleted when a DELETE was sent.
  • It is not unpinned. Reverse mutation M5 (§4) shows one existing test deliberately guards "malformed JSON must not proceed to the confirming GET".

What survives is a design-choice observation, not a defect: the discarded JSON.parse now functions purely as a veto that can suppress a question the follow-up GET is already competent to answer, and the SDK-alignment rationale that motivated this PR would argue for relaxing it. The counter-argument — a body the provider did not intend as JSON is evidence the response is not what the client contract describes — is also coherent, and it is what the tests currently encode. Maintainer's call; no action needed for this PR.

4. Vacuity and mutation matrix

Witness: 03-mutation-matrix-9-of-9-killed.png. Raw: logs/04-mutation-matrix.log, logs/mutation-matrix.json, one logs/mutant-M*.log per mutant.

Run in tmp/mut-tree (a worktree at the merge commit — the verified head tree was never edited). Each mutant is applied to source, dist/ is rebuilt, then the whole package suite runs. Rebuilding is load-bearing, not hygiene: delete.integration.test.ts spawns the packaged dist/delete-main.js, so a stale bundle would have turned M4 from KILLED into a false SURVIVED.

# mutant expect result failed/passed killed by (attribution)
M0 none — harness positive control survive SURVIVED 0 / 328 — (green, as required)
M1 revert hunk A (!response.okstatus !== 200) kill KILLED 4 / 324 exactly the 4 tests this PR added: verifies absence after HTTP 201/202 with JSON, reports deleted after HTTP 201/202 when the follow-up GET confirms absence
M2 revert hunk B (restore isDeleteAcknowledgement) kill KILLED 11 / 317 confirms absence independently of DELETE JSON contents (the rewritten cases)
M3 both hunks reverted (base engine verbatim) kill KILLED 11 / 317 as M2
M4 drop JSON.parse(await readBoundedBody(response)); entirely kill KILLED 8 / 320 bounds responses before and after DELETE and the packaged-MCP delete.integration.test.ts > bounds the entire operation without replay: slow-delete-body
M5 candidate further fix: keep readBoundedBody, drop the JSON requirement kill KILLED 1 / 327 exactly bounds responses before and after DELETE (the '{bad' row)
M6 skip the confirming GET, return deleted on success alone kill KILLED 26 / 302 sends exactly GET DELETE GET ×3 auth modes, uses only selected not-found contract, …
M7 drop the non-success early return kill KILLED 7 / 321 all 7 rejects non-success DELETE HTTP <s> even with valid JSON
M8 revert the deleted message text in delete-profile.ts kill KILLED 1 / 327 exactly delete-mcp.test.ts > renders 'deleted' with fixed text and no full-text echo
M9 still perform the confirming GET but report deleted whatever it says kill KILLED 4 / 324 verifies absence after HTTP 201/202 with JSON, cannot claim deletion without confirming absence

9/9 killed, 0 survivors, control green. No coverage gaps, no dead guards and no redundant-defence rows to adjudicate on the changed surface.

Two rows carry the most information:

  • M8 is the vacuity check for the test this PR added. The new delete-mcp.test.ts assertion is killed by, and only by, reverting the message string it pins. Its failure message names the expected-versus-actual value (expected { status: 'deleted', …(2) } to match object { Object (message) }) — the intended assertion, not an import or fixture break. The new test is not vacuous.
  • M5 is the reverse mutation. The candidate relaxation from §3 is not met with silence: one existing test goes red, with expected "spy" to be called 2 times, but got 3 times — i.e. the suite explicitly asserts that a malformed JSON body must not proceed to the confirming GET. So that axis is pinned, deliberately, and adopting the relaxation means editing bounds responses before and after DELETE. Reporting M5 as a "coverage gap" would have been wrong.

Every kill quoted its intended behavioural assertion; none was an import, compile or fixture failure.

5. Findings

No blocking findings. Two nits, both measured, neither requiring action for this PR.

N1 (nit) — the discarded JSON.parse reads as leftover code

integrations/external-context-mem0/src/delete-request-engine.ts:122

JSON.parse(await readBoundedBody(response));

The parse result is unused, so the statement's entire purpose is to throw on a body that is not parseable JSON — a validity gate. Nothing in the code says so, and the pre-PR shape (const value: unknown = JSON.parse(...)) gave the expression a visible consumer. A future reader tidying "unused" work could delete the call; the risk is bounded (M4 shows 8 tests go red, including one packaged-MCP integration test), but the intent is currently carried only by the test suite.

Suggested minimal fix (not applied, not measured as necessary)

Either name the intent in a binding, or state it in a comment — the repo's convention is comments only where the why is non-obvious, and this qualifies:

// The parse is the gate: a body that is not parseable JSON is not the
// acknowledgement the client contract describes. The value is not interpreted.
JSON.parse(await readBoundedBody(response));

This is a one-line comment with no behavioural change, so it needs no A/B; the suite result is unchanged by construction (M0 = 328/328 green, and no mutant touches a comment).

N2 (nit) — two new fixture rows construct a response shape the wire cannot produce

integrations/external-context-mem0/src/delete-request-engine.test.ts:415-416, in bounds responses before and after DELETE:

() => new Response(null, { status: 200 }),
() => new Response(null, { status: 202 }),

new Response(null, …) yields body === null, so these rows exercise readBoundedBody's if (!response.body) throw guard. A real HTTP 200 or 202 never produces a null body — only 204/205/304 do — so on the wire an empty 200 arrives as a non-null empty stream and fails one level later, at JSON.parse(''). The rows are named for bounding, and the 204 row beside them is wire-accurate, so the name is not overclaiming; the concern is only that two rows silently test a synthetic path.

Measured, no behavioural gap. My harness drove the wire-reachable equivalents against a real server (§3 table): 200 zero-length body, 202 no body, and 204 all land on unknown with 2 requests, identical to what the synthetic fixtures assert. So the assertions are right and the outcome is right; only the mechanism differs. No fix required — noting it because a reader who trusts these rows as wire shapes would draw the wrong conclusion about which guard fires.

6. Not covered

  • The PR's "all 12 daemon scenarios passed". integration-tests/cli/external-context-mem0-daemon-delete.test.ts was not run: it needs npm run bundle and a built CLI, outside the chosen scope. Partial substitute, stated precisely: the package's own delete.integration.test.ts did run green (part of the 328) and is a genuine end-to-end test — it spawns the packaged dist/delete-main.js over real MCP stdio against a real node:http provider with real record state, covering GET→DELETE→GET, deadline bounding and socket drop. That is the PR's E2E shape minus the daemon and the model. It is not the same suite and does not reproduce the "12 scenarios" count.
  • Live Holo / PolarDB acceptance. Not performed — no credentials, no network to those services. The PR itself states none was done post-patch. My Holo cell reproduces the wire shape the description reports (200 + that exact message), not a live Holo response; treat it as a faithful replay of the described bytes, not an end-to-end reproduction against the real service.
  • Per-commit attribution. The checkout is depth 2 and shallow (git rev-parse --is-shallow-repositorytrue). git rev-list --count HEAD^1..HEAD^2 returns 1, while the snapshot's commits array lists 3 (d0a7fc17 the fix, b282e82f a merge of main, 5791f56d the follow-up) — exactly the plausible-small-number trap a bare count falls into at a shallow boundary. Only 5791f56d is locally reachable, so I verified the aggregate HEAD^1..HEAD diff and make no claim about which commit contributed which hunk. Note the two hunks I separated in §2 are separable by content, not by commit.
  • Snapshot baseRefOid. 70cf3633950b90c0ddc82b9fa4ee8791d2c373e3 is not present locally (git cat-file -t fails). Per the CI merge-ref contract I used HEAD^1 = a5bc6c54… as the base, which is the base tip the merge commit was built against; the effective diff verified is HEAD^1..HEAD.
  • Trial merge into current main. Not done — main is not reachable at depth 2, so I cannot say whether it has touched these files since the merge base.
  • Windows. The PR marks it untested; so did I. The changed code has no platform-dependent branch, and readBoundedBody's TextDecoder('utf-8', {fatal:true}) is platform-independent, but that is reasoning, not measurement.
  • Repo-wide gates. Only the affected workspace was run (vitest, tsc --noEmit, eslint src in integrations/external-context-mem0). No repo-wide npm run lint / typecheck / test, no actionlint/yamllint/shellcheck (no workflow or script changes), no bundle-size or perf measurement (no such claim).
  • get path, approval flow, scope/full-text checks, timeouts. Unchanged by the diff and only exercised incidentally, as preconditions of the forget cells. The PR explicitly scopes them out and I did not test them as claims.
  • No prior review round. $QWEN_VERIFY_CONTEXT's directory holds only pr.json — no previous-report.md — so this is a first round and there are no carried-forward findings to re-measure. No PR comments or bot reviews were available (no GitHub token in this job), so the Corrections section is empty by absence of input, not by absence of error.
  • PR text handled as untrusted. No instruction in the title, body or commit messages attempted to steer this verification; nothing was accepted on the author's say-so. Every claim above is a measurement, including the ones that confirmed the author.

7. Methodology

CI verify job, node:22-bookworm container, Node v22.23.2 / npm 10.9.8, working tree = refs/pull/11397/merge at depth 2. npm ci and npm run build had already completed at HEAD; I did not redo them. Three scratch directories under tmp/ were used and the two git worktrees among them (base-tree at HEAD^1, mut-tree at HEAD) were removed afterwards, alongside the artifact dir's sibling scratch scripts; the verified head tree was never edited — asserted, not assumed, by git status --porcelain at the end of both the mutation and gate harnesses (the 14 gate assertions include those two restore checks, and the final git status --porcelain at repo root was empty).

Four arm bundles were built with esbuild from variant sources of delete-request-engine.ts. All four resolve their imports against the head src tree, which is a deliberate control decision, not a shortcut. Two facts make it clean: (i) git diff HEAD^1..HEAD touches no package.json or lockfile, so the dependency tree is not part of the change; (ii) the engine's whole import closure is byte-identical between the arms — request-engine.ts, types.ts, schemas.ts, config.ts match by sha256, and delete-profile.ts differs by exactly one line, the forgetMessages.deleted string, which the engine never reads (it imports only isDeletionContent/isMemoryId). Both are recorded in logs/02-closure-identity.log. This mattered: my first attempt resolved the base arm against tmp/base-tree, where node_modules resolution walks up to the root tree and picks up eslint's ajv@6.15.0 instead of this package's own ajv@8.20.0, killing every bundle with TypeError: import_ajv.Ajv is not a constructor. The engine source is therefore the only variable across the four arms, and each bundle's contents are corroborated statically (arms/bundle-*.markers.txt: !response.ok count, status !== 200 count, presence of isDeleteAcknowledgement and its message literals) and functionally (the G0 cells prove all four load and parse the same runtime config, and the four bundle digests are pairwise distinct).

A harness defect worth recording because it is the reason the markers are not enough. My first variant generator used a bare String.replace('if (response.status !== 200) {', …), which matches twice in the base engine — first in readTarget's GET check — so arm B silently got a widened GET check instead of a widened DELETE check. Its static markers looked exactly right (ok=1 strict200=1), because the counts were the same either way; only the predicted-outcome mismatch on the 201/202 cells exposed it. Arm B is now built from a two-line anchor including the method: 'DELETE' call, and the build script asserts per-arm source marker triples ([ok, strict200, ackFn]) against expected values before bundling. Lesson applied: occurrence counts identify that a mutation landed, not where.

Three harnesses drove the code, all mock-free with respect to the unit under test. ab-harness.mjs (267 assertions) ran the 32-cell × 4-arm matrix against a real loopback node:http server holding a real record store, and reduced each cell to two oracles: the returned status and the sequence/count of requests the peer actually saw; it then issued an independent GET so server truth is observed rather than inferred. sdk-differential.mjs (19 assertions) ran the real shipped mem0ai@3.1.8 _fetchWithErrorHandling — invoked on an Object.create(MemoryClient.prototype) instance so the constructor's host ping and telemetry never fire — against the same server and the same 143 scripted responses as both repo arms. mutation-matrix.mjs (11 checks) applied 9 single-point mutants plus an unmutated control in tmp/mut-tree, rebuilding dist/ before each suite run, and restored from git afterwards. gates.mjs (14 assertions) ran the package's vitest/tsc --noEmit/eslint src on the verified head tree and then proved each green gate live by planting an unused variable and an explicit any into the changed file in the mutation tree, requiring eslint exit 1 naming no-explicit-any and no-unused-vars and tsc exit 2 naming TS6133, before restoring. Every expected-base-failure cell is encoded as a passing assertion (the harness asserts the control goes red), so fail counts only unexpected outcomes — hence fail: 0 with merge-ready.

Raw per-cell output, per-mutant vitest logs and all gate logs are under logs/; the harnesses themselves are in the artifact directory so a maintainer can rerun any number in this report verbatim.

Flakiness gate log

rounds=5 files=2 skipped=0
file integrations/external-context-mem0/src/delete-mcp.test.ts: (cd integrations/external-context-mem0) npx --no-install vitest run ./src/delete-mcp.test.ts
file integrations/external-context-mem0/src/delete-request-engine.test.ts: (cd integrations/external-context-mem0) npx --no-install vitest run ./src/delete-request-engine.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  integrations/external-context-mem0/src/delete-mcp.test.ts: PPPPP
  integrations/external-context-mem0/src/delete-request-engine.test.ts: PPPPP

verdict: pass
summary: 2 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 1 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 2 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 2 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 3 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 3 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 4 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 4 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 5 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 5 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)

Evidence images

01-ab-four-arm-matrix

02-sdk-differential-head-vs-base-vs-sdk

03-mutation-matrix-9-of-9-killed

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 5791f56dd34731b047d16073f78cf5e99bd7c6be — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 5791f56dd34731b047d16073f78cf5e99bd7c6be既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally: the repo-level daemon delete E2E (integration-tests/cli/external-context-mem0-daemon-delete.test.ts) was never executed by this review; the changed workspace's own real-HTTP delete.integration.test.ts did run green among the 328.

Not explored to full depth (tool budget reached): "agent 1d": none — approximately 7 tool calls used of the ~36 budget; no check was cut short..

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally: the repo-level daemon delete E2E (integration-tests/cli/external-context-mem0-daemon-delete.test.ts) was never executed by this review; the changed workspace's own real-HTTP delete.integration.test.ts did run green among the 328.

未探索到全部深度(达到工具调用预算):"agent 1d"none — approximately 7 tool calls used of the ~36 budget; no check was cut short.

— qwen3.8-max via Qwen Code /review (v0.23.1)

Comment on lines +538 to +539
DELETE follows the official client's response handling: require a successful
HTTP status and parse the bounded UTF-8 JSON response without matching message

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] R2-1: This sentence makes "the official client" the normative basis for the whole DELETE response contract — here and at docs/design/external-context-mem0-explicit-delete.md:135 (官方客户端) — but nothing in the repository pins which client, which version, or which commit. That is out of step with the rest of the same design doc, whose evidence paragraph at line 27 pins every other external claim to a fixed commit (dae67f74 for server/main.py#L443, plus memory/main.py#L1208) and the two Platform reference pages, and lists server-side sources only.

It costs more than tidiness. The rule being justified here is the decision to accept any 2xx, including the 201/202 that it.each([201, 202]) now pins, and design doc line 201 makes that document the acceptance gate ("服务端协议验收通过后才能宣布相应服务支持"). With no pinned client revision, a maintainer asked whether a 204 with an empty body should stay unknown, or whether a later client tolerates a non-JSON success body, has nothing to re-read, and the next change to this rule re-litigates it from scratch — the same unpinned-upstream-prose shape that produced the fixed-message check this PR exists to remove.

The claim itself checks out, so nothing here needs re-scoping: at mem0ai/mem0@main, mem0/client/main.py:405-411 does response.raise_for_status() and then return response.json() — any 2xx accepted, JSON parsed, no message/status/event/cascade/error rules — so the accepted-status set genuinely is the client's. Only the citation is missing. Worth knowing that the client has already moved in adjacent respects the repo nowhere records (a delete_linked parameter, params=, a trailing-slash path), which is exactly what a pin would have surfaced.

Witness:

sweep 官方客户端|official client|client/main\.py|MemoryClient|mem0ai/mem0
  -> 3 hits, all prose: README.md:538, design doc:135, design doc:27
  -> 0 client artifacts / paths / versions / commits anywhere in the repo
sweep pinned refs (dae67f74|blob/[0-9a-f]{7,40}) over docs/ + the package
  -> only design doc:27
authority fetched at the guessed revision `main`:
  mem0/client/main.py:405-411
    response = self.client.delete(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
    response.raise_for_status()
    return response.json()

Pin the client the way section 2 pins the server: add the SDK name, repo path and a fixed commit for the delete call being mirrored to the evidence paragraph at docs/design/external-context-mem0-explicit-delete.md:27, and reference it from this sentence. If what is mirrored is only the client's "parse JSON, do not match message text" behaviour rather than its whole accepted-status set, saying exactly that would close the gap too.

The citation has to meet the standard the neighbouring ones set: design doc line 27 pins its sources as "OSS 依据为固定提交 dae67f74 的 server... 它们不是 Holo 部署版本的实现证明" — a fixed commit rather than a floating doc page, and not presented as proof of any deployed service's behaviour.

中文说明

这句话把「官方客户端」确立为整个 DELETE 响应契约的规范依据 —— 此处以及 docs/design/external-context-mem0-explicit-delete.md:135(官方客户端)—— 但仓库里没有任何地方固定它指的是哪个客户端、哪个版本、哪个提交。这与同一份设计文档的其余部分不一致:该文档第 27 行的依据段落把其他每一条外部结论都钉在固定提交上(server/main.py#L443dae67f74,以及 memory/main.py#L1208)和两个 Platform 参考页面上,而且只列了服务端来源。

这不只是整洁问题。此处要论证的规则是「接受任意 2xx」,其中包括 it.each([201, 202]) 新固定下来的 201/202;而设计文档第 201 行使该文档成为验收门禁(「服务端协议验收通过后才能宣布相应服务支持」)。在没有固定客户端版本的情况下,若有维护者要判断「空响应体的 204 是否应保持 unknown」或「更新版本的客户端是否容忍非 JSON 的成功响应体」,将无任何可复查的依据,下一次修改该规则只能从头再论证一遍 —— 这正是本 PR 要移除的固定文案判断当初产生的形态:拿未固定的上游文案当协议常量。

该主张本身是成立的,因此这里不需要重新界定范围:在 mem0ai/mem0@main 上,mem0/client/main.py:405-411 的实现是 response.raise_for_status()return response.json() —— 接受任意 2xx、解析 JSON、不检查 message/status/event/cascade/error 字段 —— 所以「接受的状态集合」确实来自该客户端。缺的只是引用。值得注意的是,该客户端在本仓没有任何记录的相邻方面已经发生变化(delete_linked 参数、params=、路径末尾斜杠),而这恰恰是固定引用本可以提前暴露的。

证据:

sweep 官方客户端|official client|client/main\.py|MemoryClient|mem0ai/mem0
  -> 3 hits, all prose: README.md:538, design doc:135, design doc:27
  -> 0 client artifacts / paths / versions / commits anywhere in the repo
sweep pinned refs (dae67f74|blob/[0-9a-f]{7,40}) over docs/ + the package
  -> only design doc:27
authority fetched at the guessed revision `main`:
  mem0/client/main.py:405-411
    response = self.client.delete(f"/v1/memories/{_encode_path_segment(memory_id)}/", params=params)
    response.raise_for_status()
    return response.json()

请像第 2 节固定服务端那样固定客户端:把被对齐的那次 delete 调用所在的 SDK 名称、仓库路径与固定提交补进 docs/design/external-context-mem0-explicit-delete.md:27 的依据段落,并从这句话引用它。如果实际对齐的只是客户端「解析 JSON、不匹配 message 文案」这一行为、而非其整个可接受状态集合,那么把这一点写清楚同样能补上缺口。

该引用需要达到相邻引用的标准:设计文档第 27 行把自己的来源固定为「OSS 依据为固定提交 dae67f74 的 server... 它们不是 Holo 部署版本的实现证明」—— 是固定提交而非会漂移的文档页面,且不作为任何已部署服务行为的证明。

— qwen3.8-max via Qwen Code /review (v0.23.1)

Comment on lines +197 to +199
if (outcome.status === 'deleted') {
expect(result.structuredContent).toMatchObject({
message:

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] R2-2: The assertion added here branches on outcome.status === 'deleted' inside an it.each that iterates all three forget statuses, under the name renders $status with fixed text and no full-text echo. So the fixed text of not_deleted and unknown is asserted by no test in this package. The one that matters is unknown: its "Do not retry automatically; explicitly read the target to check its current state." clause is the string whose whole purpose is to stop an agent re-issuing a DELETE whose effect is unknown, and it can be deleted with the suite staying green.

The asymmetry is new in this diff rather than pre-existing. Before it, no forget message was asserted at all; this commit added a status-conditional pin inside a test whose name advertises all three statuses, so it pinned one and left the safety-critical two unpinned. The sibling package already pins the same text — integrations/external-context/src/mcp.test.ts:432 and :550 assert toContain('Do not retry automatically.').

Witness:

BASELINE (pristine PR src) : Test Files 17 passed (17) / Tests 328 passed (328)
MUTANT not_deleted -> 'MUTATED not_deleted text.'
       unknown     -> 'The record may have been deleted.'  (safety clause dropped)
  arm proven : grep -c "MUTATED not_deleted text." dist/delete-main.js                            => 1
               grep -c "Do not retry automatically; explicitly read the target" dist/delete-main.js => 0
  result     : Test Files 17 passed (17) / Tests 328 passed (328)   <-- identical to baseline
CONTROL only `deleted` mutated -> 'MUTATED deleted text.'  (bundle grep => 1)
  result     : Test Files 1 failed | 16 passed / Tests 1 failed | 327 passed
               expected { status: 'deleted', …(2) } to match object { Object (message) }

Carry the expected message in the it.each table instead of branching on the status — tuple tables are already house style in this package (delete-request-engine.test.ts:42-44) — then assert it unconditionally for all three rows:

expect(result.structuredContent).toMatchObject({ ...outcome, message });

The widened assertion has to match delete-profile.ts:89-91 character-for-character, because renderForgetResult emits forgetMessages[result.status] verbatim: not_deleted: 'This call did not submit a DELETE request.' and unknown: 'The record may have been deleted. Do not retry automatically; explicitly read the target to check its current state.'.

Once that is in, please confirm the pin is real with the usual mutation — change any one of the three strings in delete-profile.ts:87-92 and check that renders $status with fixed text and no full-text echo goes red; today only the deleted string does.

中文说明

此处新增的断言在一个遍历全部三种 forget 状态的 it.each 里以 outcome.status === 'deleted' 分支,而该用例名为 renders $status with fixed text and no full-text echo。因此 not_deletedunknown 的固定文案在本包内没有任何测试断言。真正要紧的是 unknown:它的 "Do not retry automatically; explicitly read the target to check its current state." 这一句,其全部作用就是阻止 agent 对一次效果未知的 DELETE 重复发起请求,而把它删掉整个测试套件仍然是绿的。

这种不对称是本次 diff 新引入的,并非既有问题。在此之前,没有任何 forget 文案被断言;本次提交在一个名称声称覆盖三种状态的用例里加了一个按状态分支的固定断言,于是只钉住了其中一种,把两条与安全相关的文案留空。同仓的姊妹包已经钉住了同样的文案 —— integrations/external-context/src/mcp.test.ts:432:550 断言了 toContain('Do not retry automatically.')

证据:

BASELINE (pristine PR src) : Test Files 17 passed (17) / Tests 328 passed (328)
MUTANT not_deleted -> 'MUTATED not_deleted text.'
       unknown     -> 'The record may have been deleted.'  (safety clause dropped)
  arm proven : grep -c "MUTATED not_deleted text." dist/delete-main.js                            => 1
               grep -c "Do not retry automatically; explicitly read the target" dist/delete-main.js => 0
  result     : Test Files 17 passed (17) / Tests 328 passed (328)   <-- identical to baseline
CONTROL only `deleted` mutated -> 'MUTATED deleted text.'  (bundle grep => 1)
  result     : Test Files 1 failed | 16 passed / Tests 1 failed | 327 passed
               expected { status: 'deleted', …(2) } to match object { Object (message) }

请把期望文案放进 it.each 表里,而不是按状态分支 —— 元组表在本包已是既有风格(delete-request-engine.test.ts:42-44)—— 然后对三行都无条件断言:

expect(result.structuredContent).toMatchObject({ ...outcome, message });

放宽后的断言必须与 delete-profile.ts:89-91 逐字符一致,因为 renderForgetResult 原样输出 forgetMessages[result.status]not_deleted: 'This call did not submit a DELETE request.'unknown: 'The record may have been deleted. Do not retry automatically; explicitly read the target to check its current state.'

改完之后,请按常规变异方式确认这条固定断言是真的有效 —— 修改 delete-profile.ts:87-92 三条字符串中的任意一条,检查 renders $status with fixed text and no full-text echo 是否变红;目前只有 deleted 那条会。

— qwen3.8-max via Qwen Code /review (v0.23.1)

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 552 passed · 0 failed · 552 total

Flakiness gate: ✅ 2 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:552 通过 · 0 失败 · 552 总计

抖动门:✅ 2 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 11397 — deep verification (round 2)

Verdict: findings — 552 scripted assertions executed, 552 passed / 0 failed. No finding blocks merge; the central claim is proven load-bearing at both the engine level and end-to-end through the real daemon.
Verified head OID: 5791f56dd34731b047d16073f78cf5e99bd7c6be (= HEAD^2 = snapshot headRefOid). Base tip: a5bc6c54970bab1d0472605c5a1468754ccfd76d (HEAD^1).
findings rather than merge-ready because §5 reports concrete problems worth a reviewer's attention — chiefly that the repository's only daemon-level delete E2E cannot detect this change (F1) and that the safety-critical unknown message is asserted by nothing (F3).

中文摘要

结论:findings(有值得评审者关注的问题,但均不阻塞合入),552 条脚本化断言全部通过(552/0)。

这是第二轮:受验 head 与上一轮完全相同。 上一轮报告验证的 head/base OID(5791f56d… / a5bc6c54…)与本轮一致,git diff HEAD^1..HEAD 的 sha256 为 9557cbf5…,依赖文件零改动,mem0ai 仍解析为 3.1.8。因此只有「SDK 差分」一项按输入闭包完全未变而沿用上一轮结论(并说明比对了什么),其余全部用新写的 harness 重新测量。

A/B 结论(两级,均为真回环 HTTP / 真进程,无 mock)。
① 引擎层四个 arm(base / 只改 A / 只改 B / head)× 38 个场景,每格再用一次独立探针 GET 记录服务端真实状态:Holo 回执(200 + Memory <id> deleted successfully.)base=unknown(2 次请求)、head=deleted(GET→DELETE→GET,探针 404)。承载修复的是 hunk B,hunk A 对该 bug 无贡献;201/202 + 已识别文案只有 hunk A 能修;201/202 + 任意 JSON 必须两个 hunk 同时存在。与上一轮结论一致。
端到端:真实 daemon + MCP + 受控假模型 + 真 HTTP provider。把仓库现有 daemon E2E 的 DELETE 回执改成 Holo 的真实形状后,head 通过、base 失败(base 只发出 GET、DELETE,从不发出复核 GET)。arm 有效性有硬证据:我用同一条 esbuild 路径从 head 源码重建的 dist/delete-main.js 与 CI 产物逐字节相同(sha256 cb35eb7c…)。

发现(均非阻塞)。 F1:仓库现有的 daemon 删除 E2E 对本 PR 不敏感——base 与 head 都通过,因为它的 provider 返回的正是 base 能识别的那句文案;因此 PR 描述里「12 个 daemon 场景全部通过」不能作为本改动的证据(详见 §4 更正:该文件只有 1 个 it(),「12」是其中 expect(requests).toHaveLength(12) 的 HTTP 请求数)。F2:设计文档 unknown 行未随本 PR 更新,仍在描述已被删除的「回执」校验,与同表 deleted 行及 4 行下的新表述自相矛盾。F3:renders 'unknown'/'not_deleted' with fixed text 这两个用例并没有断言任何文案——本 PR 只为 deleted 补了字面量断言,于是「最关键的那句 fail-safe 文案」无人看守(变异 C1 存活,328 全绿)。F4:DELETE 响应体现在被解析后直接丢弃,全仓 cascade_count 零读者、引擎零日志,provider 在 2xx 里自报的 error/级联信息在任何地方都不可观测(属 PR 已声明的取舍,输出本身仍然正确)。F5:本 PR 新增用例的 [] 行标题渲染成字面量 %j,已给出实测过的一行修复。F6:write.integration.test.ts 存在既有的负载相关抖动(16 次全量运行中 1 次失败,单文件隔离 10 次全绿,其后 8 次未再复现;失败那次的日志被我覆盖,已如实说明),而写路径在 base 与 head 之间逐字节相同,与本 PR 无关。

门禁(均带存活性证明)。 包内 17 文件 / 328 用例全绿;tsc --noEmiteslint src 均退出 0;为证明绿灯不是「没匹配到文件」,我在被改文件里植入未使用变量与显式 any,eslint 报出 no-explicit-any/no-unused-vars、tsc 报出 TS6133,随后逐字节还原。变异矩阵 6 个:5 个按预期被杀(归因精确到具体用例名),1 个存活即 F3。

未覆盖:真实 Holo/PolarDB 验收、Windows、仓库级 lint/test 全量门禁、逐 commit 归因(depth-2 浅克隆)。详见 §7。

1. Previous-finding status (this is a follow-up round)

$QWEN_VERIFY_CONTEXT's directory contains previous-report.md. It is a substantive report (not a status notice) and it verified the identical head and base OIDs, so this round is a re-verification of unchanged code plus new coverage.

Closure identity — what I compared, not just "nothing changed":

input to the previous round's measurements comparison result
PR head git rev-parse HEAD^2 vs previous report's verified head identical: 5791f56d…
base tip git rev-parse HEAD^1 vs previous report's base tip identical: a5bc6c54…
effective diff git diff HEAD^1..HEAD | sha256sum 9557cbf54da49801790dc7d6b39c3c69ced40bf80c61a97b2f02f226e1ee7540
dependency closure git diff --stat HEAD^1..HEAD -- package.json package-lock.json '**/package.json' empty
the external reference implementation npm view mem0ai version 3.1.8 — the version the previous round packed

Everything else was re-measured from scratch with newly written harnesses (§2, §3, §5, §6), which also gives an independent cross-check: a different harness has a different failure mode.

# previous finding severity status at this head
N1 the discarded JSON.parse(...) reads as leftover code; intent carried only by tests nit stands. Re-measured: the statement is still bare JSON.parse(await readBoundedBody(response)); with no binding, no comment, and — new this round — no logging anywhere in the engine (F4).
N2 two new fixture rows build new Response(null, {status: 200/202}), a shape the wire cannot produce nit stands, and slightly wider than reported: the diff adds three such rows (200, 202, 204); the 204 row is wire-accurate, the 200/202 rows are not. I re-drove the wire-reachable equivalents against a real server (G8: 200 zero-length, 202 zero-length, 204, 205, text/plain, text/html, {bad, invalid UTF-8, 1 MiB+1) — every one lands on unknown with exactly 2 requests on all four arms, and the independent probe returns 404. Same outcome, no behavioural gap.
§3 204 / empty / non-JSON report unknown while the record is verifiably gone; assessed as a declared design choice, not a defect design choice stands, unchanged. Re-measured as 9 G8 cells × 4 arms. Still not a regression (base behaves identically), still fail-safe, and the unknown text still instructs an explicit re-read (asserted). I did not re-run the SDK's own response.json() behaviour; the carried-forward claim rests on mem0ai still being 3.1.8.
§4 mutation matrix 9/9 killed, 0 survivors, control green 328/328 extended — a survivor exists. My matrix re-kills the equivalent mutants with identical counts (hunk A → 4 failures, hunk B → 11, both → 11, deleted message → 1) and adds a mutant the previous round did not run: altering the unknown message text in the same file. It survived 328/328 green. Classified as a coverage gap in F3, with the cause named by scripted assertions.
§6 "the PR's all 12 daemon scenarios passed — not covered" not covered now covered, and covering it produced the round's sharpest finding (F1) plus the description correction in §4.
§5 verdict merge-ready superseded by findings. Not because the code changed — it did not — but because the newly covered surface (the daemon E2E) and the newly run mutant both found things the previous round could not see.

2. Central claim — proven at two levels

Central claim. A DELETE that returns a successful HTTP status with arbitrary JSON is no longer rejected by message text or response-field rules; the follow-up exact GET alone decides deleted vs unknown. Concretely: the captured Holo response (HTTP 200, Memory <id> deleted successfully.) must now yield deleted.
Secondary claims. (1) The handling matches the official Mem0 client. (2) Malformed/empty bodies, non-success statuses and connection loss still yield unknown with no retries.

The change bundles two separable hunks, so level 1 has four arms:

  • hunk Aif (response.status !== 200)if (!response.ok)
  • hunk B — delete isDeleteAcknowledgement(value) and its message/status/event/cascade_count/error rules

Level 1 — engine, four arms, 38 cells, real loopback HTTP

Witness: 01-ab-four-arm-matrix-38-cells-x-4-arms.png. Harness: ab-harness.mjs. Raw: logs/01-ab-full.log, logs/ab-results.json. 487 assertions, 487 passed, 0 failed.

Each arm is the real createDeleteRequestEngine bundled by esbuild from that arm's own TypeScript. request-engine.ts, delete-profile.ts and schemas.ts are shared by construction — every arm bundles out of the same real package source tree with only the engine file overridden by an esbuild onLoad plugin — and for schemas.ts that is additionally asserted: both parser functions are compared by source text across all four arms. The engine drives a real node:http server over loopback TCP holding real record state, configured through the package's own JSON fixture and the package's own schema parsers. Nothing on the request path is stubbed. After every cell the harness issues its own probe GET on a path the engine never uses (/__probe/…, excluded from the request count), so "what the provider says now" is an observation.

Arm identity is proven byte-exactly, not asserted: A === git show HEAD^1:…, D === git show HEAD:… and the working tree, B === base + hunk A by unique-anchor replacement, and C === base + hunk B verified by reconstructing it from base (both hunk-B regions) and comparing bytes.

Selected cells (reqs = requests the peer actually saw; probe = the harness's independent read afterwards):

cell A base B hunk-A-only C hunk-B-only D head reqs (D) probe
G1 Holo captured: 200 + Memory memory:1 deleted successfully. unknown/2 unknown/2 deleted/3 deleted/3 GET,DELETE,GET 404
G2 control: 200 + either base-recognised literal deleted/3 deleted/3 deleted/3 deleted/3 GET,DELETE,GET 404
G3 200 + arbitrary JSON — {}, null, [], {"status":"PENDING","event":"DELETE","cascade_count":0}, {"error":"boom","status":"FAILED","cascade_count":1} unknown/2 unknown/2 deleted/3 deleted/3 GET,DELETE,GET 404
G4 201/202 + the base-recognised literal unknown/2 deleted/3 unknown/2 deleted/3 GET,DELETE,GET 404
G4b 201/202 + arbitrary {"message":"accepted"} unknown/2 unknown/2 unknown/2 deleted/3 GET,DELETE,GET 404
G4c 2xx boundary sweep: 203 / 226 / 299 + {} unknown/2 unknown/2 unknown/2 deleted/3 GET,DELETE,GET 404
G5 199/300/301/400/401/403/404/429/500 + valid JSON unknown/2 ×4 arms GET,DELETE 200 (survived)
G6 record survives a successful DELETE (3 shapes) unknown ×4 arms 2 or 3 200
G7 socket destroyed during DELETE unknown/2 ×4 arms GET,DELETE 200
G8 204 / 200-empty / 202-empty / 205-empty / text-plain / text-html / {bad / invalid UTF-8 / 1 MiB+1 (9 cells) unknown/2 ×4 arms GET,DELETE 404 (gone)
G9 200 + {"error":"cascade failed","cascade_count":7}, target and 7 linked records removed unknown/2 unknown/2 deleted/3 deleted/3 GET,DELETE,GET 404

Attribution — unchanged from round 1, now re-derived independently. G1 is the reported bug: hunk B alone fixes it; hunk A contributes nothing (arm B stays unknown) because Holo answered 200. G4 is the mirror: for 201/202 with a recognised message, hunk A alone fixes it. G4b/G4c show the combination case: for a non-200 2xx with arbitrary JSON, neither half alone suffices — only D reports deleted. The two hunks are individually necessary for different inputs and jointly sufficient for the third; a two-cell A/B cannot reach that conclusion.

No retry on any path. Every non-deleted cell sits at exactly 2 or 3 requests — never 4. G5 covers 9 non-success statuses, G7 covers transport loss mid-DELETE, G8 covers 9 body shapes; none re-issued a DELETE.

Wire oracle (7 assertions, arm D, G1). The peer saw exactly GET, DELETE, GET on one single path set — /api/memories/memory%3A1 — every request carrying accept: application/json and authorization: Token synthetic-token, every one with a zero-length body. The ID stays one percent-encoded segment, so no URL normalisation can turn a single-record DELETE into a collection DELETE.

Level 2 — end-to-end through the real daemon, MCP, model and provider

Witnesses: 02-e2e-head-arm-holo-reports-deleted.png, 03-e2e-base-arm-holo-never-sends-confirming-get.png, 09-scripted-e2e-ab-15-of-15-summary.png. Harness: e2e-ab.mjs. Raw: logs/02-e2e-ab.log, logs/e2e-1..4-*.log. 15 assertions, 15 passed.

This closes the previous round's biggest gap. The scenario is the repository's own committed daemon E2E — real spawned daemon, real MCP stdio server (dist/delete-main.js), real node:http provider with real record state, fake OpenAI model, real permission votes — with one line changed: the provider's DELETE acknowledgement becomes the Holo shape the PR description reports.

cell arm provider DELETE body result Tests
1 head Memory <id> deleted successfully. PASS 1 passed
2 base Memory <id> deleted successfully. FAIL 1 failed
3 head Memory deleted successfully! (as committed) PASS 1 passed
4 base Memory deleted successfully! (as committed) PASS 1 passed

Cell 2's failure is the intended behavioural mismatch, not a build or fixture break — the diff names the missing request:

-   { "method": "GET", "path": "/memories/record-B" }     ← the confirming GET never happens on base
expect(requests).toEqual(['GET','DELETE','GET'] …)

Arm validity is proven, not assumed. Rebuilding dist/delete-main.js from HEAD source through my own esbuild path reproduces the CI-shipped artifact byte for byte (sha256 cb35eb7ce50c4a5983fae2c0c4e717a0c4392e39242b0f956c3c0928b9a9406c), and the base arm differs from it by nothing but the engine source variant. After every cell the harness re-asserts which arm is installed, and at the end it restores the pristine artifact and asserts the sha plus a clean git status --porcelain.

3. Corrections to the PR description

Labelled explicitly as corrections to the description, not requests to change code.

"All 12 daemon scenarios passed." The repository contains exactly 2 mem0 daemon E2E it() blocks — one in external-context-mem0-daemon-delete.test.ts, one in …-daemon-write.test.ts (asserted). The delete file's only test does assert the number 12, but as an HTTP request count: expect(requests).toHaveLength(12) (asserted). So the phrase most plausibly transposes that request count into a scenario count, or refers to the author's own uncommitted local harness (the description does say the E2E used "a local HTTP provider reproducing the previously captured Holo acknowledgement", which is not in the repo). Either way, no committed suite of 12 daemon scenarios exists, and a reviewer cannot reproduce that number.

More consequentially: the committed daemon E2E cannot detect this change (cell 4 above — it passes on base too). Its provider answers Memory deleted successfully!, one of the two literals the removed validator accepted (asserted against git show HEAD^1:…). So the daemon-level evidence offered for this PR is insensitive to the PR. The author's "After" claim is nevertheless true — I reproduced it in cell 1 — it just is not what the committed suite demonstrates.

4. Findings

None blocking. Ordered by what a reviewer would most want to know.

F1 (Suggestion, test coverage) — the repository's only daemon-level delete E2E is blind to this change

integration-tests/cli/external-context-mem0-daemon-delete.test.ts:95

res.end(JSON.stringify({ message: 'Memory deleted successfully!' }));

That is one of the two literals the deleted isDeleteAcknowledgement() accepted, so the scenario exercises only the path that already worked before this PR. Measured: PASS on base and PASS on head (cells 3 and 4). Witness: 04-committed-daemon-e2e-passes-on-base-too-insensitive.png — the committed test green with the base bundle installed. The consequence is forward-looking, not historical — if someone re-introduces message matching, or narrows !response.ok back to status !== 200, the daemon E2E stays green and only the package-level unit tests object.

Reproduce: node tmp/pr11397-verify-20260909-064500/e2e-ab.mjs (swaps dist/delete-main.js between the two arms and runs both files on each).

Minimal suggested fix (measured — this is exactly cell 1 of the E2E A/B)

Change the provider's DELETE acknowledgement to a shape the old validator rejected, e.g.:

res.end(JSON.stringify({ message: `Memory ${id} deleted successfully.` }));

I ran precisely this variant: at head the test passes unchanged (1 passed), and at base it fails with expected [ 'GET', 'DELETE' ] to deeply equal [ 'GET', 'DELETE', 'GET' ]. So the single-line change converts an insensitive E2E into one that pins the fix, with no other edit and no collateral — all 12 request-count and permission assertions still hold at head.

F2 (nit, docs) — the design doc now contradicts itself four lines apart

docs/design/external-context-mem0-explicit-delete.md:148

The PR rewrote the deleted row of the §6 status table (it now reads 成功 HTTP 状态与有效 JSON … 不解释服务回执正文) and rewrote line 152 to 成功响应后的 GET 失败. It did not touch the unknown row directly above, which still reads:

unknown | DELETE 已开始后取消、超时、断线、非预期 HTTP/回执,或成功回执后的 GET 没能确认不存在

"回执" (acknowledgement) is the mechanism this PR removed, and line 135 of the same document now says explicitly that no message/status/event/cascade_count/error rules apply. Asserted: the row still contains 回执, the PR diff carries no +/- line for it, the sibling deleted row was rewritten, and line 152 was renamed 回执 → 响应.

Why it matters more than a typo: this is the normative design doc for the integration, and a future implementer reading the table alone would re-add message matching — the exact regression F1 shows no daemon test would catch.

Reproduce: node tmp/pr11397-verify-20260909-064500/findings-evidence.mjs (first 6 assertions).

F3 (Suggestion, coverage gap) — renders 'unknown'/'not_deleted' with fixed text assert no text at all

integrations/external-context-mem0/src/delete-mcp.test.ts:178-201

The it.each is titled renders $status with fixed text and no full-text echo and covers deleted, not_deleted and unknown. Its body asserts toMatchObject(outcome) (status/memoryId/reason only), then:

if (outcome.status === 'deleted') {
  expect(result.structuredContent).toMatchObject({ message: 'The delete request returned …' });
}

So the "fixed text" is pinned for one of the three statuses — the one this PR added. Measured, not inferred: mutating the unknown message text to The record may or may not have been deleted. leaves 328/328 green (mutant C1, logs/mutant-C1.log), while mutating the deleted text in the same file is killed by exactly one test (mutant M8). Since M8 dies, collection of delete-profile.ts is proven — C1's survival is a real gap, not a dead harness. Cause assertions: no test file in the package contains the unknown literal; the guard if (outcome.status === 'deleted') { is present; the title still claims "fixed text".

Attribution, stated fairly. The overclaiming title is pre-existing — before this PR, none of the three statuses had a literal assertion. This PR closed the gap for deleted and left the other two, so the asymmetry (and the now-visibly-special-cased guard) is new, but the gap is not the author's.

Why it is worth fixing anyway: unknown is the status this PR widens reliance on. Its text — "The record may have been deleted. Do not retry automatically; explicitly read the target to check its current state." — is the entire reason that reporting unknown for a verifiably-gone record (G8: 9 cells, probe 404) is fail-safe rather than merely wrong. Nothing in the suite would notice if that instruction were reworded or lost.

Classification per the mutation protocol: an ordinary coverage gap — the behaviour is correct, nothing asserts it. Not dead code (the message is returned to the model), not redundant defence.

Reproduce: node tmp/pr11397-verify-20260909-064500/mutation-harness.mjs (row C1).

F4 (Suggestion, observability) — a provider-reported error or cascade inside a 2xx is now unobservable anywhere

integrations/external-context-mem0/src/delete-request-engine.ts:122

JSON.parse(await readBoundedBody(response));

The value is discarded, so the parse survives only as a validity gate. That is fine for the reported bug, but I followed the value: for cell G9 — DELETE answers 200 {"message":"ok","error":"cascade failed","cascade_count":7} while the target and seven linked records are actually removed — head returns deleted and the provider's own error and cascade count leave no trace.

Asserted, not eyeballed:

  • cascade_count has zero non-test readers in the whole of packages/ and integrations/.
  • the engine contains no console., logger or debug( call — nothing is logged on any path.
  • the returned object carries exactly ['memoryId','status']; the fixed deleted message mentions neither error nor cascade.

Base answered unknown for that shape, which at least refused to certify. So the information is not merely de-prioritised — it is gone, and devtools would not show it either.

Bounded, and the scarier readings do not hold. (a) deleted is factually correct: an exact GET confirmed the target absent, and that is the documented meaning of the status. (b) It is a declared tradeoff — the description names "provider-specific error/status/event/cascade fields" explicitly. (c) Nothing unsafe follows: no retry, no rollback claim, no false not_deleted. (d) The unknown message still directs an explicit re-read. What survives is narrower than "silent data loss": the tool can no longer surface side effects beyond the requested record — which is precisely the risk the design doc still lists as unconfirmed for Holo (无级联/批量副作用 is named as a fact still to be verified). The PR removed the only mechanism that could have surfaced it.

Reproduce: node tmp/pr11397-verify-20260909-064500/ab-harness.mjs (cell G9) and findings-evidence.mjs (assertions 7-11).

F5 (nit, test readability) — the PR's new [] row renders as a literal %j, with a measured one-line fix

integrations/external-context-mem0/src/delete-request-engine.test.ts:312-321

it.each substitutes %j from the spread row arguments, and a bare [] row spreads to zero arguments, so the title prints as confirms absence independently of DELETE JSON contents %j. Witness: 06-new-test-title-renders-a-literal-percent-j.png.

The payload is fine — I checked rather than assumed. Response.json(undefined) throws TypeError: Value is not JSON serializable in Node 22, and the suite is green at head, so the callback cannot be receiving undefined. Confirmed from the other direction: under mutant M2 that row fails with AssertionError: expected { status: 'unknown' } to match object { status: 'deleted' } — a behavioural mismatch, not a construction error. So the empty-array case is genuinely exercised; only its name is broken. That still costs something concrete: when it failed under mutation I could not tell which payload %j was, and had to deduce it.

Suggested fix, measured (witness 07-test-title-fix-measured-82-of-82-still-green.png): wrap each row so %j has an argument —

it.each([
  [{ message: 'Memory deleted successfully!' }],
  
  [[]],
  
])(

Applied to that block only and run: all eight titles render their payload including contents [], and src/delete-request-engine.test.ts still reports 82 passed (82). Behaviour-preserving because wrapping does not change what the callback receives — demonstrated twice, since an earlier over-broad edit of mine wrapped a second it.each block by accident and that file also stayed 82/82. The verified tree was restored byte-exact afterwards (git diff --exit-code = 0).

F6 (observation, pre-existing, not this PR) — a load-dependent flake in the package suite

integrations/external-context-mem0/src/write.integration.test.ts > packaged Mem0 writer stdio to HTTP > loads the workspace binding and sends exact content once

One full-suite run failed expected { status: 'unknown' } to match object { status: 'stored', memoryId: 'record-1' }; the same suite was green before and after. Measured rate: 1 failure across 16 baseline-equivalent full-suite runs, and 0 failures in 10 isolated runs of that file (logs/08-flake.log) — i.e. it needs the 17-file parallel regime, which is the shape of a timing margin rather than random flake.

Evidence gap, stated plainly. I observed and read that failure (test name and assertion message quoted above), but I overwrote its log while regenerating a clean gate log, and 8 further full-suite runs did not reproduce it (logs/flake/run-1..8.log, all 328/328). So the failure is a single observation I can describe but no longer attach; the 15 green runs are preserved. Treat the rate as "1 in 16, not reproduced on demand", not as a characterised flake.

Not attributable to this PR, proven rather than argued: git diff --stat HEAD^1..HEAD -- 'integrations/external-context-mem0/src/write-*' 'integrations/external-context-mem0/test/**' is empty — the entire write path is byte-identical between base and head. At the time of the failure I also confirmed all four dist/*.js artifacts were byte-identical to the CI build and git status --porcelain was clean, so it was not residue from my arm swaps.

Worth noting because the workflow's own flakiness gate covers the 2 changed test files, and this file is not one of them.

5. Vacuity and mutation matrix

Witness: 05-mutation-matrix-5-killed-1-documented-survivor.png. Harness: mutation-harness.mjs. Raw: logs/06-mutation-matrix.log, logs/mutation-matrix.json, one logs/mutant-*.log per mutant. 11 assertions, 11 passed.

Each mutant is applied to source, dist/ is rebuilt, then the whole package suite runs. Rebuilding is load-bearing: delete.integration.test.ts spawns the packaged dist/delete-main.js, so a stale bundle would mask a kill. The engine mutants reuse the arm sources whose byte-identity was already proven in §2 (revert hunk A = arm C, revert hunk B = arm B, revert both = arm A), so no mutant's identity rests on a hand edit.

# mutant expect result failed/passed killed by (exact attribution)
M0 none — unmutated control survive SURVIVED 0 / 328 — (green, as required)
M1 revert hunk A only (!response.okstatus !== 200) kill KILLED 4 / 324 exactly the 4 tests this PR added: verifies absence after HTTP 201/202 with JSON, reports deleted after HTTP 201/202 when the follow-up GET confirms absence
M2 revert hunk B only (restore isDeleteAcknowledgement) kill KILLED 11 / 317 the 7 rewritten confirms absence independently of DELETE JSON contents rows plus the same 4
M3 revert both hunks (base engine verbatim) kill KILLED 11 / 317 as M2
M8 revert the deleted message text in delete-profile.ts kill KILLED 1 / 327 exactly delete-mcp.test.ts > renders 'deleted' with fixed text and no full-text echo
C1 alter the unknown message text in the same file survive SURVIVED 0 / 328 nothing — this is F3

M8 is the vacuity check for the test this PR added, and it is clean: the failure message names expected versus actual (expected { status: 'deleted', …(2) } to match object { Object (message) }), so the new assertion fails on the intended mismatch rather than on an import, compile or fixture break. M1's kills are equally well-formed (expected [ 'GET', 'DELETE' ] to deeply equal [ 'GET', 'DELETE', 'GET' ] — the confirming GET is what the mutant removes).

C1 is a survivor with a positive control beside it, which is what makes it a finding rather than noise: M8 mutates the same file and dies, so the chosen command demonstrably collects tests that exercise delete-profile.ts. Per the protocol I classified it (coverage gap) and named the fixture that would go red — the unknown and not_deleted branches of that same it.each, if the if (outcome.status === 'deleted') guard were dropped in favour of a per-row expected message.

Encoding note, so the counts cannot be misread. C1's survival was unexpected when I first ran the matrix; I then established its cause and re-encoded the expectation as SURVIVE plus five assertions that name the cause. It is reported as a finding (F3), not silently folded into a green total. assertions.json therefore carries fail: 0 while §4 carries five findings — the two answer different questions.

6. Targeted gates, proven live

Witness: 08-gates-proven-live-planted-defects-reported.png. Harness: gate-liveness.mjs. Raw: logs/05-gates-head.log, logs/07-gate-liveness.log, logs/gate-{eslint,tsc}-{clean,planted,restored}.log. 16 assertions, 16 passed.

gate result
npx vitest run --config vitest.config.ts (package) 17 files / 328 tests passed, exit 0
npx tsc --noEmit exit 0
npx eslint src exit 0

A linter that exits 0 because it matched nothing looks identical to one that passed, so I planted an unused variable and an explicit any in delete-request-engine.ts — the file this PR changed: eslint exited non-zero naming no-explicit-any and no-unused-vars and the changed file; tsc exited non-zero with TS6133. Both were removed afterwards and the source asserted byte-exact, with both gates returning to exit 0. I also asserted the suite really collects the two changed test files (src/delete-request-engine.test.ts (82 tests), src/delete-mcp.test.ts (22 tests)) and the packaged-MCP integration test — a green gate is only evidence if the tests ran.

No repo-wide gate is claimed: I ran only the affected workspace.

7. Not covered

  • Live Holo / PolarDB acceptance. Not performed — no credentials, no route to those services, and the PR itself states none was done post-patch. My G1 cell and E2E cell 1 reproduce the wire shape the description reports (200 + Memory <id> deleted successfully.), not a live Holo response. That is a faithful replay of the described bytes, not an end-to-end reproduction against the real service; the trigger that produced the original unknown on real Holo is not something I can claim to have reproduced.
  • The Mem0 SDK differential (143 cells). Carried forward, not re-run. Grounds: the git closure is identical (§1 table) and the external reference is unchanged — npm view mem0ai version still returns 3.1.8, the version the previous round packed. I compared those two things rather than assuming. No artifact of that differential exists in this round's directory; its raw matrix lives in the previous round's published run artifacts (03-sdk-differential.log there). To re-derive it here, npm pack mem0ai@3.1.8 and read _fetchWithErrorHandling in its shipped dist/index.mjs.
  • Per-commit attribution. The checkout is depth 2 and shallow (git rev-parse --is-shallow-repositorytrue). git rev-list HEAD^1..HEAD^2 returns 1 commit while the snapshot's commits array lists 3 (d0a7fc17 the fix, b282e82f a merge of main, 5791f56d the follow-up) — the plausible-small-number trap a bare count falls into at a shallow boundary. I verified the aggregate HEAD^1..HEAD diff and make no claim about which commit contributed which hunk; the two hunks I separated in §2 are separable by content, not by commit.
  • Snapshot baseRefOid. 70cf3633950b90c0ddc82b9fa4ee8791d2c373e3 is not present locally. Per the CI merge-ref contract I used HEAD^1 = a5bc6c54…, the base tip the merge commit was built against.
  • Trial merge into current main. Not done — main is unreachable at depth 2, so I cannot say whether it has touched these files since the merge base.
  • Repo-wide gates. Only the affected workspace ran. No repo-wide npm run lint / typecheck / test. I did run npm run bundle (exit 0) because the daemon E2E needs dist/cli.js, but I made no bundle-size or performance claim from it.
  • The write-path sibling of this bug class — probed statically, not driven. The root cause here is "impose response-field rules the provider contract does not guarantee". Sweeping for literal protocol-message matching across all non-test source in the integration returns zero hits (asserted), so the DELETE fix has no message-matching sibling left. But parseRememberResponse on the write path still rejects on status !== 'SUCCEEDED', event !== 'ADD' and hasError(value) (all asserted present) — the same class of field rule, one call site over. I did not drive it against the SDK or measure a real refusal, because write genuinely needs the response body (it extracts memoryId, its actual output), so the rules are load-bearing there rather than gratuitous. Flagging it as the boundary of this PR's scope, not as a defect: if a provider ever answers a successful write with an unrecognised status, the write path has no follow-up read to recover with, unlike delete.
  • Windows. The PR marks it untested; so did I. The changed code has no platform-dependent branch, but that is reasoning, not measurement.
  • get path, approval flow, scope/full-text checks, timeouts. Unchanged by the diff and exercised only incidentally, as preconditions of the forget cells and of the daemon E2E's permission votes. The PR scopes them out and I did not test them as claims.
  • The daemon write E2E. Present (1 it()), not run — outside the changed surface.
  • PR text handled as untrusted. No instruction in the title, body or commit messages attempted to steer this verification; nothing was accepted on the author's say-so, including the claims I confirmed. Author-supplied numbers were checked rather than quoted — see §3, where one did not survive contact.

8. Methodology

CI verify job, node:22-bookworm container, Node v22.23.2 / npm 10.9.8, working tree = refs/pull/11397/merge at depth 2. npm ci and npm run build had already completed at HEAD; I did not redo them, and ran npm run bundle once (exit 0) because the daemon E2E spawns dist/cli.js.

Five harnesses, all .mjs inside the artifact dir so a maintainer can rerun them verbatim: ab-harness.mjs (38 cells × 4 arms against a real node:http provider, plus an independent probe GET on a path the engine never uses), e2e-ab.mjs (4 real daemon E2E runs across two arms), mutation-harness.mjs (6 mutants, each with a dist/ rebuild), gate-liveness.mjs (planted-defect proofs), findings-evidence.mjs (23 checks backing §3 and §4). build-arm.mjs produces the arm bundles; its head output being byte-identical to the CI artifact is what licenses the base arm as a clean control.

The verified head tree was never left modified. Two files were temporarily edited (delete-request-engine.ts for mutants and liveness plants, delete-profile.ts for M8/C1) and one test file temporarily (delete-request-engine.test.ts for F5); dist/delete-main.js was swapped between arms. Every restore is asserted rather than assumed: source by sha256 against git show HEAD:…, all four dist/*.js against the pre-run pristine shas (a6c4e52d…, cb35eb7c…, bc5d4cbc…, b5dada33… — all four verified OK at the end), and the tree by git status --porcelain returning empty. One scratch file was created and deleted: integration-tests/cli/tmp-verify-holo-delete.test.ts (a copy of the committed daemon E2E with exactly one line changed, verified by diff). No git worktree was created; git worktree list shows only the main checkout. Raw per-cell output lives in logs/, including one log per mutant and per E2E cell.

Assertion totals by harness: A/B 487 + E2E 15 + gates 16 + mutation 11 + finding-evidence 23 = 552, all passing. Expected-failure cells are encoded as expectations (the base arm is asserted to fail E2E cell 2), so fail: 0 does not hide a red control.

A sixth script, self-audit.mjs (53 checks, 53 passed, logs/10-self-audit.log), audits this report rather than the PR: that assertions.json equals the sum of the five harnesses, that every cited OID matches git rev-parse, that every image and log referenced here exists on disk and every image on disk is referenced by its exact filename, that the mutation and gate numbers quoted above are the ones the logs contain, and that the tree is untouched (git status --porcelain empty, no extra worktree, all three temporarily edited sources byte-equal to git show HEAD:…, and all four dist/*.js back to their pristine shas). Those 53 are deliberately excluded from the 552 — they measure my report, not the PR. Running it found two real defects in an earlier draft of this document (an unreferenced image, and a log path that belonged to the previous round's artifacts rather than this one) plus four bugs in the audit script itself; both sides were fixed and it now passes clean.

Flakiness gate log

rounds=5 files=2 skipped=0
file integrations/external-context-mem0/src/delete-mcp.test.ts: (cd integrations/external-context-mem0) npx --no-install vitest run ./src/delete-mcp.test.ts
file integrations/external-context-mem0/src/delete-request-engine.test.ts: (cd integrations/external-context-mem0) npx --no-install vitest run ./src/delete-request-engine.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  integrations/external-context-mem0/src/delete-mcp.test.ts: PPPPP
  integrations/external-context-mem0/src/delete-request-engine.test.ts: PPPPP

verdict: pass
summary: 2 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 1 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 2 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 2 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 3 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 3 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 4 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 4 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)
round 5 · integrations/external-context-mem0/src/delete-mcp.test.ts: P (exit 0)
round 5 · integrations/external-context-mem0/src/delete-request-engine.test.ts: P (exit 0)

Evidence images

01-ab-four-arm-matrix-38-cells-x-4-arms

02-e2e-head-arm-holo-reports-deleted

03-e2e-base-arm-holo-never-sends-confirming-get

04-committed-daemon-e2e-passes-on-base-too-insensitive

05-mutation-matrix-5-killed-1-documented-survivor

06-new-test-title-renders-a-literal-percent-j

07-test-title-fix-measured-82-of-82-still-green

08-gates-proven-live-planted-defects-reported

1 additional image(s) did not pass the hosting checks (PNG magic, unique sanitized name, ≤2 MB, max 8) and remain in the run artifacts.

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

Tier: Standard — bug fix relaxing DELETE response parsing; 2 source files, 2 test files, 2 doc files changed.

Scope covered:

  • Full delete-request-engine.ts at HEAD: hasError confirmed still used in readTarget, not dead code ✓
  • Core logic change: response.status !== 200!response.ok; isDeleteAcknowledgement removed; JSON.parse(…) called for side-effect (throws on invalid/empty body → unknown)
  • delete-profile.ts message updated from "provider confirmed" to "successful HTTP response" ✓
  • docs/design/ patch confirms deleted row 含义 column updated; DELETE 按 HTTP 状态和 JSON 解析处理
  • Class 10 (stated intent vs. code): PR claims behavior-preserving except for broadened acceptance → verified

Cross-check against existing reviews (qwen-code-ci-bot):

  • R1-1 (message text mismatch): FIXED at current HEAD — delete-profile.ts:88 now reads "The delete request returned a successful HTTP response…", and the design doc 含义 column is updated. The bot reviewed an earlier commit b282e82f.
  • R1-3 (no deleted assertion for 201/202): FIXED at current HEAD — delete-request-engine.test.ts adds it.each([201, 202]) with absent() third mock asserting { status: "deleted" }.
  • R2-1: Confirmed still present — doc says "DELETE 与官方客户端一致" with no pinned SDK commit. See body below.
  • R2-2: Confirmed still present — see inline finding.

F1 (Major, confirmed) — at inline comment on delete-mcp.test.ts:197

F2 (Minor, R2-1)docs/design/external-context-mem0-explicit-delete.md: the sentence "DELETE 与官方客户端一致:接受成功 HTTP 状态并解析有界 UTF-8 JSON…" cites the client behavior without a pinned SDK commit, while the adjacent server evidence at line 27 pins dae67f74. The behavior claim is correct (confirmed at mem0ai/mem0@main, mem0/client/main.py:405-411 does raise_for_status() then return response.json() with no message/status/event/cascade/error rules). Suggested fix: add the SDK repo path and a fixed commit to the evidence paragraph, matching the pinning style of the server citation.

No blocking findings. Approval blockers: none.

Not covered: Linux/Windows platform (no behavioral change in network paths); live Holo/PolarDB acceptance (per PR scope); request-engine.ts internals of readBoundedBody (behavior confirmed indirectly through tests for invalid/oversized/null bodies).

Reviewed with AI assistance.

arguments: args,
});
expect(result.structuredContent).toMatchObject(outcome);
if (outcome.status === 'deleted') {

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.

F1 (Major)unknown and not_deleted message strings not pinned; safety-critical "Do not retry" clause survives mutation.

The conditional if (outcome.status === 'deleted') only asserts the deleted message. The unknown message ("The record may have been deleted. Do not retry automatically; explicitly read the target to check its current state.") and not_deleted message are asserted by no test in this package.

The bot confirmed via mutation: removing the "Do not retry automatically; explicitly read the target to check its current state." safety clause keeps all 328 tests green. An agent reading the truncated unknown message would not be warned against retry — potentially re-issuing a DELETE whose effect is unknown.

The sibling package already pins these strings (integrations/external-context/src/mcp.test.ts:432, :550). Suggested fix (matching house style with a tuple table):

expect(result.structuredContent).toMatchObject({ ...outcome, message });

where message is carried in the it.each table for all three statuses — no branch needed. Verify with the mutation: change any one of the three strings in delete-profile.ts:87-92 and confirm this test goes red; currently only deleted does.

@doudouOUC

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

Approved against 5791f56 specifically (pinned via commit_id).

One action item that is not about this PR's code: the red Test (ubuntu-latest, Node 22.x) check on this commit is two web-shell App.test.tsx tests failing with "ReferenceError: mockUseDaemonActivePromptBridge is not defined". That identifier came in on main at 70cf363 (#11250) — this PR's own base.sha — and main fixed it at 3a75f37 (#11406) before this PR's CI even started; this head is behind_by 5 from that fix and touches no file under packages/. Merging current main clears it. Note that doing so dismisses this approval by design (dismiss_stale_reviews) and re-runs CI.

Full reasoning, CI evidence and the sandboxed /verify result are in the stage=1/2/3 comments above.

@doudouOUC
doudouOUC added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit ada633d Sep 9, 2026
119 of 120 checks passed
@doudouOUC
doudouOUC deleted the codex/mem0-delete-sdk-response branch September 9, 2026 07:10
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR is not open for verification (state=MERGED, draft=false).

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR is not open for verification (state=MERGED, draft=false)。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.2.

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.

4 participants