Repository navigation
Description
Pre-submission checklist | 提交前检查
- I have searched existing issues and this hasn't been mentioned before | 我已搜索现有问题,确认此问题尚未被提及
- I have read the project documentation and confirmed this issue doesn't already exist | 我已阅读项目文档并确认此问题尚未存在
- This issue is specific to MemOS and not a general software issue | 该问题是针对 MemOS 的,而不是一般软件问题
Bug Description | 问题描述
我配了个本地的 OpenAI 兼容端点(就是为了不花 API 钱),摘要没问题,但每个 turn_start 都会留下这么一对记录:
system_error {"role":"llm","provider":"openai_compatible","model":"gpt-oss-20b",
"message":"llm_unavailable: openai_compatible request failed: This operation was aborted",
"code":"llm_unavailable"}
system_model_status {"role":"llm","op":"retrieval.retrieval.filter.v5","status":"fallback",
"provider":"openai_compatible","model":"gpt-oss-20b","fallbackProvider":"host"}
也就是本地模型被白调一次,活是宿主模型干的,检索统计里看到的是:
stats.llmFilter = {"outcome":"llm_failed_safe_cutoff","kept":1,"dropped":4}
三个原因叠在一起。
-
轻量分支把操作者的配置覆盖掉。deps.js 里原来是:
llmFilterEnabled: alg.lightweightMemory.enabled ? true : alg.retrieval.llmFilterEnabled,所以默认(轻量)安装下过滤是强制开的,配置里写
algorithm: retrieval: llmFilterEnabled: false是空操作:文件能读、schema 能过、不打任何警告,然后在组装检索依赖时被覆盖。
-
过滤自己的预算是硬编码的 2000ms(retrieve.js 里 Math.max(1, Math.min(2_000, deadlineAt - now)))。
我这边本地 gpt-oss-20b 按过滤真实构造的提示词(query + 候选列表)要 2.4–2.6 秒,5 次全超。
提醒一句:别拿同一个提示词反复打来做基准,那会命中 KV 前缀缓存,看着只要 1.25 秒;冷启动实测是 8.3 秒。 -
超时被记成"provider 不可用"。fetcher.js 合并调用方信号和自己的超时控制器,超时触发时调的是
ctrl.abort(),不带 reason ⇒ 抛出来的是 AbortError 而不是 TimeoutError。判超时那几条件找的是字符串
"ABORT_ERR",而 DOMException 的 code 是数字 20 ⇒ 一条都不中 ⇒ 落成 llm_unavailable。
这个类是可回退的,于是门面又拿宿主模型重放了一次。
影响:配置本地端点本来就是为了省钱,结果每轮都往本地发一个注定超时的请求,真正干活的还是宿主模型(花钱的那个)。
期望做到其中一条就能解决:
① 轻量分支别覆盖 retrieval.llmFilterEnabled(轻量模式管的是演化链,不该顺手改检索配置);
② 把过滤的时间预算做成可配的(或者提高 2000ms),否则自建模型按设计就没法用;
③ 超时判定认 AbortError / DOMException code 20;超时应该降级(passthrough 或 safe cutoff),而不是换一家 provider 重放。
环境:@memtensor/memos-local-plugin 2.0.20,Windows 11,Node 24(Electron 宿主);lightweightMemory 用默认值;
llm.provider = openai_compatible 指向私网自建端点(不需要 key)。以上数字来自插件自己落盘的 api_logs,
外加一次离线的过滤耗时复现,没有改插件代码去插桩。
How to Reproduce | 如何重现
- llm.provider = openai_compatible 指向自建端点;algorithm.lightweightMemory 保持默认;
- 在 config 里写 algorithm.retrieval.llmFilterEnabled: false(期望关掉过滤);
- 跑任意一次 turn_start,然后看插件的 api_logs:
- retrieval.retrieval.filter.v5 仍然被调用(配置被轻量分支覆盖);
- 若该端点一次请求要 2 秒以上,就会出现 status=fallback、fallbackProvider=host,
以及配对的 llm_unavailable / "This operation was aborted"。
- 注意测法:同一个提示词连续打会命中 KV 前缀缓存(看着只要 ~1.25s),
要用新提示词冷跑才准。
Environment | 环境信息
@memtensor/memos-local-plugin 2.0.20;Windows 11;Node 24(Electron 宿主);algorithm.lightweightMemory 用默认值。
Additional Context | 其他信息
No response
Willingness to Implement | 实现意愿
- I'm willing to implement this myself | 我愿意自己解决
- I would like someone else to implement this | 我希望其他人来解决
Activity
akushonkamen commentedon Oct 16, 2026
Claiming this one.
Root cause confirmed on
main@ a7367d0 (matches 2.0.20): three stacked defects on the retrieval-filter LLM path:mergeSignalsincore/llm/fetcher.ts:375-382callsctrl.abort()with no reason, soAbortSignal.timeout()'sTimeoutErrorreason is discarded andfetchrejects with a plainAbortError(DOMExceptioncode20).isTimeout(core/llm/fetcher.ts:339-345) only matchesname === "TimeoutError", the string"ABORT_ERR"(never true —DOMException.codeis numeric), or a/timeout/imessage — so an internal budget timeout lands inLLM_UNAVAILABLEinstead ofLLM_TIMEOUT.filterTimeoutMs,core/retrieval/retrieve.ts:691-694), so local OpenAI-compatible endpoints that legitimately need >2 s per filter call are aborted every turn.Plan: forward the abort reason in
mergeSignals+ teachisTimeoutaboutAbortError/DOMExceptioncode 20 (caller cancellation is already handled before that check), and make the filter budget configurable viaalgorithm.retrieval.llmFilterTimeoutMs(default 2000 preserves current behavior). ThelightweightMemoryoverride ofllmFilterEnabled(core/pipeline/deps.ts:166) stays as-is — it matches the documented lightweight contract (#2063: lightweight "keeps only summarize + embedding + retrieval filter").Regression tests will cover both fixes. (Implementation assisted by AI; report limited to behavior verified locally.)
akushonkamen commentedon Oct 16, 2026
Fix is committed and locally verified.
Branch:
fix/plugin-llmfilter-timeout-budget@fd7fa62(PR to follow from the contributor pipeline).Change (expectations ② + ③; ① left alone — see below):
core/llm/fetcher.ts—mergeSignalsnow forwards the abortreason, soAbortSignal.timeout()'sTimeoutErrorsurvives when the caller passes its own signal;isTimeoutadditionally recognizesAbortError/DOMExceptioncode 20 (the old string checkcode === "ABORT_ERR"never matched, sincecodeis numeric). Caller cancellation keeps its distinct "was cancelled" path — regression-pinned.core/retrieval/retrieve.ts+core/config/schema.ts+core/config/defaults.ts+core/pipeline/deps.ts— the filter wall-clock budget is now configurable viaalgorithm.retrieval.llmFilterTimeoutMs(100–30 000, default 2000 = legacy behavior), still clamped by the remaining retrieval deadline.lightweightMemoryoverride ofllmFilterEnabled(core/pipeline/deps.ts:166) is intentionally unchanged: algorithm.lightweightMemory.enabled: true does not actually skip evolution pipeline (reward/L2/L3/skill still run) #2063 defined lightweight mode as "keeps only summarize + embedding + retrieval filter", so the forced filter is documented behavior, not a bug. With the budget knob, local endpoints complete inside the window and the fallback replay disappears.Verification (all run locally, macOS arm64, Node 24.21):
llm_unavailable/ "This operation was aborted" and passes withllm_timeoutafter the fix; thellmFilterTimeoutMs: 4321budget test fails (2000) unpatched, passes patched.npm run lint(tsc --noEmit): clean.npm test: 186 files — 1602 passed, 3 skipped, 0 failed.(Implementation assisted by AI; report limited to behavior verified locally.)
akushonkamen commentedon Oct 16, 2026
PR opened: #2506 (base
dev-v2.0.36, branchfix/plugin-llmfilter-timeout-budget@fd7fa62) — forwards the abort reason so budget timeouts reportllm_timeoutinstead ofllm_unavailable, and addsalgorithm.retrieval.llmFilterTimeoutMs(default 2000 preserved, still clamped by the retrieval deadline).MechDesign618 commentedon Oct 16, 2026
配置项那条我接受你们的判断:轻量模式"保留 retrieval filter"确实是 #2063 定义的契约,我不再把它当缺陷。
补两点实测,供你们参考:
时间预算那条在我们这边可复现,数字如下。本地 OpenAI 兼容端点 + gpt-oss-20b,提示词按过滤器真实构造的形状(query + 候选列表):
2.46s / 2.55s / 2.55s / 2.56s / 2.57s(5 次全超 2s,冷启动 8.3s)
提醒一句:同一个提示词反复打会命中 KV 前缀缓存,看着只要 1.25s,拿它做基准会得出"够快"的错误结论。
所以 llmFilterTimeoutMs 这个旋钮对我们是有用的。
还有一个会让这个旋钮"看起来没生效"的前置问题:严格实现 OpenAI 规范的本地服务端(我这边是 LM Studio)不接受
response_format={"type":"json_object"},只接受 json_schema / text,直接回 HTTP 400:
'response_format.type' must be 'json_schema' or 'text'
我们在本地把 provider 的 complete() 改成"json_object → json_schema(宽松) → text"的降级链(仅在上一次 HTTP 400 时降级)才通。
也就是说:预算放大之后,第一次真正打到本地端点的调用会先撞上这个 400,然后才轮到超时那条。
如果 provider 层愿意做同样的降级(或者做成配置),自建端点这条路会顺很多。