Skip to content

轻量模式强制打开检索用的 LLM 过滤,它自己的 2 秒上限让本地模型每轮都被中止,然后静默改用宿主模型 #2499

Open

Description

@MechDesign618

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}

三个原因叠在一起。

  1. 轻量分支把操作者的配置覆盖掉。deps.js 里原来是:

    llmFilterEnabled: alg.lightweightMemory.enabled ? true : alg.retrieval.llmFilterEnabled,
    

    所以默认(轻量)安装下过滤是强制开的,配置里写

    algorithm:
      retrieval:
        llmFilterEnabled: false
    

    是空操作:文件能读、schema 能过、不打任何警告,然后在组装检索依赖时被覆盖。

  2. 过滤自己的预算是硬编码的 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 秒。

  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 | 如何重现

  1. llm.provider = openai_compatible 指向自建端点;algorithm.lightweightMemory 保持默认;
  2. 在 config 里写 algorithm.retrieval.llmFilterEnabled: false(期望关掉过滤);
  3. 跑任意一次 turn_start,然后看插件的 api_logs:
    • retrieval.retrieval.filter.v5 仍然被调用(配置被轻量分支覆盖);
    • 若该端点一次请求要 2 秒以上,就会出现 status=fallback、fallbackProvider=host,
      以及配对的 llm_unavailable / "This operation was aborted"。
  4. 注意测法:同一个提示词连续打会命中 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

  1. added
    status:needs-triageNeeds initial triage | 需要初步判断 & 问题复现
    types:bugSomething isn't working | 功能异常
    on Oct 15, 2026
  2. akushonkamen commented on Oct 16, 2026

    @akushonkamen

    Claiming this one.

    Root cause confirmed on main @ a7367d0 (matches 2.0.20): three stacked defects on the retrieval-filter LLM path:

    1. Timeout is misclassified as provider-unavailable. mergeSignals in core/llm/fetcher.ts:375-382 calls ctrl.abort() with no reason, so AbortSignal.timeout()'s TimeoutError reason is discarded and fetch rejects with a plain AbortError (DOMException code 20). isTimeout (core/llm/fetcher.ts:339-345) only matches name === "TimeoutError", the string "ABORT_ERR" (never true — DOMException.code is numeric), or a /timeout/i message — so an internal budget timeout lands in LLM_UNAVAILABLE instead of LLM_TIMEOUT.
    2. The filter's wall-clock budget is hard-coded at 2000 ms (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 + teach isTimeout about AbortError/DOMException code 20 (caller cancellation is already handled before that check), and make the filter budget configurable via algorithm.retrieval.llmFilterTimeoutMs (default 2000 preserves current behavior). The lightweightMemory override of llmFilterEnabled (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.)

  3. akushonkamen commented on Oct 16, 2026

    @akushonkamen

    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 — mergeSignals now forwards the abort reason, so AbortSignal.timeout()'s TimeoutError survives when the caller passes its own signal; isTimeout additionally recognizes AbortError / DOMException code 20 (the old string check code === "ABORT_ERR" never matched, since code is 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 via algorithm.retrieval.llmFilterTimeoutMs (100–30 000, default 2000 = legacy behavior), still clamped by the remaining retrieval deadline.
    • The lightweightMemory override of llmFilterEnabled (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):

    • Regression tests red→green: budget-timeout classification fails on unpatched source with exactly the reported llm_unavailable / "This operation was aborted" and passes with llm_timeout after the fix; the llmFilterTimeoutMs: 4321 budget 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.)

  4. akushonkamen commented on Oct 16, 2026

    @akushonkamen

    PR opened: #2506 (base dev-v2.0.36, branch fix/plugin-llmfilter-timeout-budget @ fd7fa62) — forwards the abort reason so budget timeouts report llm_timeout instead of llm_unavailable, and adds algorithm.retrieval.llmFilterTimeoutMs (default 2000 preserved, still clamped by the retrieval deadline).

  5. MechDesign618 commented on Oct 16, 2026

    @MechDesign618
    Author

    配置项那条我接受你们的判断:轻量模式"保留 retrieval filter"确实是 #2063 定义的契约,我不再把它当缺陷。

    补两点实测,供你们参考:

    1. 时间预算那条在我们这边可复现,数字如下。本地 OpenAI 兼容端点 + gpt-oss-20b,提示词按过滤器真实构造的形状(query + 候选列表):

      2.46s / 2.55s / 2.55s / 2.56s / 2.57s(5 次全超 2s,冷启动 8.3s)

      提醒一句:同一个提示词反复打会命中 KV 前缀缓存,看着只要 1.25s,拿它做基准会得出"够快"的错误结论。
      所以 llmFilterTimeoutMs 这个旋钮对我们是有用的。

    2. 还有一个会让这个旋钮"看起来没生效"的前置问题:严格实现 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 层愿意做同样的降级(或者做成配置),自建端点这条路会顺很多。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:pluginOpenClaw & Hermesstatus:needs-triageNeeds initial triage | 需要初步判断 & 问题复现types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions