#2362·MemOS

[Bug] attachSkillSubscriber wired with bgLlm, not bgReflectLlm — skillEvolver dedicated client never invoked

Author: chiefmojoCreated Sep 14, 2026Updated Sep 17, 2026
Labelstypes:bugarea:coreai:pr-readystatus:in-progress

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

Bug Description

Summary

In core/pipeline/deps.ts, attachSkillSubscriber is wired with llm: bgLlm (the main model), never bgReflectLlm (the dedicated client built from the skillEvolver config block). As a result the skill subscriber's LLM-backed work (crystallization evaluation, evolution) always runs on the main model regardless of skillEvolver.* config, and skillEvolver.lastOkAt never updates because the dedicated client is never invoked.

Root cause

typescript
// deps.ts, attachSkillSubscriber call
const skillHandle = attachSkillSubscriber({
    repos: deps.repos,
    embedder: bgEmbedder,
    llm: bgLlm,          // ← BUG: should be bgReflectLlm
    bus: buses.skill,
    l2Bus: buses.l2,
    rewardBus: buses.reward,
    log: log.child({ channel: "core.skill" }),
    config: algorithm.skill,
    ...
});

bgReflectLlm is constructed earlier in the same file specifically from the skillEvolver config block, but is only read for metadata (health/overview endpoint), never passed into the skill subscriber itself.

Verified via git log -L on this call site: it has never been wired to bgReflectLlm since the v2.0 Reflect2Evolve rewrite (60b97444) — this is not a regression, it's never worked.

Relation to #2148

This is a distinct instance of the same wiring-bug class fixed in #2148 (PR #2151) — that issue was about captureRunner's reflectLlm slot getting the wrong client (skill-evolver client where main llm was needed). This issue is the opposite direction: the skill subscriber never gets the dedicated skillEvolver client it's supposed to have. Different call site, not fixed by #2151.

Impact

  • Operators configuring a distinct skillEvolver.* model (e.g. to isolate skill-evolution cost/quality from the main model) get silently ignored — the main llm handles it instead.
  • skillEvolver.lastOkAt in the overview/health endpoint reads null indefinitely on any install that relies on it as a liveness signal for skill evolution, even when skill evolution is running fine (just on the wrong model).

Suggested Fix

typescript
const skillHandle = attachSkillSubscriber({
    ...
    llm: bgReflectLlm ?? bgLlm,
    ...
});