跳到主要内容

MemScheduler:异步摄取与激活记忆刷新

30 秒导读: 前面几章讲了记忆"怎么抽、怎么组织、怎么检索"——但这些活很重,不能卡在用户等回答的主线程里。MemScheduler 就是那层后台调度:把重排、去重、固化这类任务丢进一个队列,由后台线程池异步消费、按"任务标签"分发给对应处理器,并周期性把明文记忆固化成 KV-Cache 激活记忆。它不发明新算法,它决定这些算法在什么时候、由谁、以什么顺序被调用


1. 这是什么(零基础也能懂)

一句话定义: MemScheduler 是 MemOS 的异步任务调度层——一个"你把活交给我,我在后台按轻重缓急慢慢干"的后台工人系统。

它要解决什么问题。 想象你在和一个带记忆的 AI 聊天。每说一句话,系统理论上都该做一堆事:把这句话读成结构化记忆、更新工作记忆的排序、去重、必要时把常用记忆"预热"成缓存。如果这些全在你等回复的那几秒里同步做完,你会等到崩溃。

思路:把重活挪到后台。 于是 MemOS 把这些任务拆成消息,丢进一个队列,主线程"提交完就返回",真正的计算交给后台线程池。这正是经典的生产者-消费者模式(一边往队列塞任务,一边有工人从队列取任务干)。

给谁用。 它不直接面向终端用户,而是被 MOSCore(第 2 章的内核)在内部挂接和驱动。开发者通过 enable_mem_scheduler 开关决定要不要启用它。

用起来什么样。 从内核视角,启用后只是多了一个后台服务在转:

# 示意,非源码:MOSCore 内部如何用调度器
scheduler = SchedulerFactory.from_config(scheduler_config) # 按 backend 造实例
scheduler.initialize_modules(chat_llm, process_llm, db_engine) # 装配子模块
scheduler.start() # 启动后台消费线程 + 监控线程
# ……此后主线程只管把消息 submit 进去,后台自己消费
scheduler.stop() # 优雅停机

一句话直觉: 把它想成餐厅后厨的叫号+传菜系统。前台(主线程)接单就走,订单进"票夹"(队列),后厨(线程池)按菜品类型(任务标签)分给不同灶台(处理器),还有个领班(监控)盯着谁卡住了。


2. 顶层全景(它大概怎么转)

先看一张"一条任务从提交到执行"的总图。怎么读: 从左到右是数据流;上半是"入队",下半是"出队消费"。

┌─────────────────────────────────────────────┐
MOSCore 调用 │ BaseScheduler │
submit_messages │ (骨架:装配子模块 + 管理 mem_cube + 生命周期) │
─────────────► │ │
└───────┬─────────────────────────────┬───────┘
│ 高优先级(LEVEL_1) │ 普通任务
│ 立即同步执行 ▼
│ ┌──────────────────┐
│ │ ScheduleTaskQueue │
│ │ 队列(二选一): │
│ │ · 本地内存队列 │
│ │ · Redis Streams │
│ └────────┬─────────┘
│ │
┌─────────────▼──────────────┐ │ 后台消费线程
│ SchedulerDispatcher │ ◄────────────┘ _message_consumer
│ 按 (user, cube, label) 分组 │ 循环:取一批 → dispatch
│ → 查 handler → 线程池执行 │
└─────────────┬──────────────┘
│ 按标签路由
┌─────────────────┼─────────────────┬──────────────┐
▼ ▼ ▼ ▼
query_handler add_handler mem_update_handler ……
(重排工作记忆) (写入记忆) (刷新激活记忆)

各部件一句话职责:

部件干什么在哪个文件
BaseScheduler调度器骨架:装配子模块、管理 mem_cube、控制启停生命周期base_scheduler.py:69
GeneralScheduler具体实现:注册各任务标签的处理器general_scheduler.py:16
OptimizedScheduler增强实现:加 API 混合检索 + 更好的工作记忆替换optimized_scheduler.py:37
SchedulerFactory工厂:按配置 backend 造出对应调度器scheduler_factory.py:9
ScheduleTaskQueue队列封装:本地内存队列 / Redis Streams 二选一task_schedule_modules/task_queue.py:23
SchedulerDispatcher分发器:分组、查处理器、丢线程池执行task_schedule_modules/dispatcher.py:38
ActivationMemoryManager把明文记忆固化成 KV-Cache 激活记忆memory_manage_modules/activation_memory_manager.py:18
各监控模块盯线程池健康、指标、任务状态monitors/

主线走一遍(高层): MOSCore 把一句话包成 ScheduleMessageItemsubmit_messages 入队 → 后台 _message_consumer 循环取一批 → Dispatcher 按标签路由到 handler → handler 里可能触发"周期性固化激活记忆"。


3. 调度器骨架与生命周期

本节讲 BaseScheduler 这个"底座":它怎么把一堆子模块装配起来,怎么管理记忆立方体(mem_cube),以及启停时都做了什么。

3.1 装配子模块:initialize_modules

调度器是个"空壳"直到它被喂进两个 LLM、一个数据库引擎。initialize_modules 一次性把监控、检索器、后处理器、激活记忆管理器全建起来:

# base_scheduler.py:225-242(节选)
self.monitor = SchedulerGeneralMonitor(process_llm=..., config=..., db_engine=...)
self.dispatcher_monitor = SchedulerDispatcherMonitor(config=self.config)
self.retriever = SchedulerRetriever(process_llm=self.process_llm, config=self.config)
self.post_processor = MemoryPostProcessor(process_llm=self.process_llm, config=self.config)
self.activation_memory_manager = ActivationMemoryManager(...)

这段真实实现见 base_scheduler.py:202initialize_modules关键设计:enable_parallel_dispatch 为真,这里会顺手把 dispatcher_monitor 启动起来盯线程池(base_scheduler.py:247-249)。

3.2 失败即回滚:_cleanup_on_init_failure

装配是"要么全成、要么别留半拉子"。整段 initialize_modules 包在 try/except 里,一旦任何子模块建到一半抛错,就调 _cleanup_on_init_failure 把已启动的监控线程停掉,再把异常重新抛出:

# base_scheduler.py:270-274
except Exception as e:
logger.error(f"Failed to initialize scheduler modules: {e}", exc_info=True)
self._cleanup_on_init_failure() # 关掉已启动的 dispatcher_monitor
raise

_cleanup_on_init_failure 本体在 base_scheduler.py:276——目前只负责停 dispatcher_monitor,防止"初始化失败却留了个后台线程在转"这种资源泄漏。

3.3 记忆立方体管理:单个 vs. 多个

调度器要知道"我在调度谁的记忆"。这里有两层:

  • init_mem_cube(base_scheduler.py:175):绑定单个 mem_cube,顺带取出它的 text_memreranker,并按需建一个 searcher(检索器,回指第 5 章的检索管线)。
  • mem_cubes 属性 setter(base_scheduler.py:356):注册一批立方体;若 current_mem_cube 还没设,就优先选 current_mem_cube_id 指定的那个,否则确定性地取第一个,再走 init_mem_cube 完成绑定。
# base_scheduler.py:375-386(节选:选哪个 cube 的逻辑)
if self.current_mem_cube_id and self.current_mem_cube_id in self._mem_cubes:
selected_cube = self._mem_cubes[self.current_mem_cube_id]
else:
first_id, first_cube = next(iter(self._mem_cubes.items())) # 确定性回退
self.current_mem_cube_id = first_id
selected_cube = first_cube

mem_cube 属性本身还带懒加载兜底(base_scheduler.py:284):真被访问到却是 None 时,尝试用 init_components() 造一个 naive 兜底立方体,避免直接崩。

3.4 启停生命周期

生命周期方法不在 BaseScheduler 本体,而在它继承的 BaseSchedulerQueueMixin(base_mixins/queue_ops.py)里:

方法干什么位置
start启动消费者 + 后台指标监控线程queue_ops.py:266
start_consumerscheduler_startup_mode 起线程或起进程跑 _message_consumerqueue_ops.py:285
_message_consumer核心消费循环:取一批消息 → dispatcher.dispatchqueue_ops.py:162
stop / stop_consumer优雅停机:停消费、关线程池、停监控queue_ops.py:340 / :309

启动模式scheduler_startup_mode 决定,默认是线程模式 STARTUP_BY_THREAD(general_schemas.py:35);也可切成 STARTUP_BY_PROCESS 用独立进程消费。消费线程都用 daemon=True,主程序退出时不被卡住。


4. 具体调度实现:三个文件的分工

本节讲 general_scheduler.pyoptimized_scheduler.pyscheduler_factory.py 各管什么——它们是继承关系,层层加料。

继承链是 BaseSchedulerGeneralSchedulerOptimizedScheduler:

BaseScheduler 骨架/队列/生命周期(第 3 节)

GeneralScheduler 注册各标签的处理器(query/add/mem_update…)

OptimizedScheduler 额外加:API 混合检索 + 更强的工作记忆替换

4.1 GeneralScheduler:把处理器挂上去

GeneralScheduler 的活很聚焦——构造时把一堆能力(校验、提交、激活记忆刷新、工作记忆替换等)打包成 SchedulerHandlerServices,再交给 SchedulerHandlerRegistry 构建"标签 → 处理器"的分发表并注册:

# general_scheduler.py:47-48
self._handler_registry = SchedulerHandlerRegistry(scheduler_context)
self.register_handlers(self._handler_registry.build_dispatch_map())

处理器本体在 task_schedule_modules/handlers/(如 query_handler.pyadd_handler.pymemory_update_handler.py)。

4.2 OptimizedScheduler:加 API 混合检索

OptimizedScheduler(optimized_scheduler.py:37)在通用调度器之上,多注册了一个 API_MIX_SEARCH_TASK_LABEL 处理器,并提供了一套"快搜同步返回 + 细搜异步补算"的混合检索:

  • mix_search_memories(optimized_scheduler.py:117):先跑快搜/细搜给用户即时结果,再调 submit_memory_history_async_task 把"把这次结果写回 Redis 供下次复用"作为异步任务丢回队列。
  • 若没开 Redis 队列,直接降级成"只快搜、不复用历史、不异步更新"(optimized_scheduler.py:129-143)——诚实地告诉你退化了。
  • replace_working_memory(optimized_scheduler.py:285):重排 + 过滤(不相关/冗余)后替换工作记忆,回指第 4/5 章的组织与检索算法。

4.3 SchedulerFactory:按配置造实例

工厂极薄——一张 backend 到类的映射表,加一个 from_config:

# scheduler_factory.py:12-23
backend_to_class = {
"general_scheduler": GeneralScheduler,
"optimized_scheduler": OptimizedScheduler,
}
@classmethod
def from_config(cls, config_factory):
backend = config_factory.backend
if backend not in cls.backend_to_class:
raise ValueError(f"Invalid backend: {backend}")
return cls.backend_to_class[backend](config_factory.config)

5. 消息/任务模型与队列隔离

本节讲"一条任务长什么样、任务分几种、队列怎么按用户隔离、Redis 怎么接线、以及优先级/自动恢复/配额"。

5.1 消息模型:ScheduleMessageItem

所有后台任务都是一个 ScheduleMessageItem(message_schemas.py:38)。关键字段:

字段含义
user_id / mem_cube_id归属:谁的、哪个立方体的
label任务标签,决定路由到哪个 handler
content任务负载(通常是 JSON 串)
task_id可选的业务级任务 ID,多条消息可共享一个,用于聚合完成状态
redis_message_idRedis Stream 分配的消息 ID,用于 ack
trace_id / api_path贯穿日志的追踪信息

to_dict(message_schemas.py:85)负责把它序列化成 Redis Stream 能存的扁平字典。

5.2 任务标签与优先级

任务种类就是一组标签常量(task_schemas.py:31-41):queryansweraddmem_readmem_organizemem_updateapi_mix_searchpref_addmem_feedback 等。

优先级只有三档(task_schemas.py:23TaskPriorityLevel):LEVEL_1(最高)→ LEVEL_3(默认最低)。优先级的实际效果在提交时体现——LEVEL_1 的消息绕开队列直接同步执行,其余入队等后台消费:

# base_mixins/queue_ops.py:81-85
task_priority = self.orchestrator.get_task_priority(task_label=msg.label)
if task_priority == TaskPriorityLevel.LEVEL_1:
immediate_msgs.append(msg) # 立即执行,不进队列
else:
queued_msgs.append(msg) # 入队,后台慢慢消费

优先级/最小空闲时间的登记由 SchedulerOrchestrator(orchestrator.py:30)持有,get_task_priority 默认返回 LEVEL_3(orchestrator.py:81)。

5.3 队列隔离:本地 vs. Redis Streams

ScheduleTaskQueue(task_queue.py:23)是个门面,按 use_redis_queue 二选一底层实现:

# task_queue.py:37-48(节选)
if self.use_redis_queue:
self.memos_message_queue = SchedulerRedisQueue(...) # 生产:Redis Streams
else:
self.memos_message_queue = SchedulerLocalQueue(...) # 单机:内存队列

按用户/立方体隔离是靠 Redis Stream 的 key 结构做到的——每个 (user_id, mem_cube_id, task_label) 组合是一条独立的 stream,key 形如 {prefix}:{user_id}:{mem_cube_id}:{task_label}(前缀默认 scheduler:messages:stream:v2.0,task_schemas.py:79)。这样一个用户的任务洪峰不会淹没另一个用户。

5.4 Redis Streams 接线:消费组 / 自动恢复 / 配额

SchedulerRedisQueue(task_queue → redis_queue.py:34)基于 Redis Stream 的**消费者组(consumer group)**语义。三个"生产稳定性"要点:

  • 消费组 + ack: 每条 stream 建消费组(redis_queue.py:493_ensure_consumer_group),消息被取走后进入 pending 列表,处理完必须 ack_message(redis_queue.py:574)才算真正消费——处理器崩了也不丢消息
  • 自动恢复(auto-claim): task_broker(redis_queue.py:314)会计算每条 stream 需要"补捞"多少条卡在 pending 太久的消息并重新认领,配合 _check_xautoclaim_support(redis_queue.py:167)探测 Redis 版本是否支持 XAUTOCLAIM。只有空闲时间超过阈值(默认 1 小时,DEFAULT_PENDING_CLAIM_MIN_IDLE_MS,task_schemas.py:63)的 pending 才会被抢回,避免抢走正在处理的消息。
  • 配额: get_stream_quotas(orchestrator.py:88)给每条 stream 分配本轮可取条数,当前默认按 stream 平均分(每 stream 等于 consume_batch_size)。

ack 放在 finally——即使处理器抛异常也 ack。 分发器的任务包装器在 finally 里对 Redis 消息统一 ack(dispatcher.py:272-289),确保失败消息不会永远卡在 pending 里反复重投。

5.5 消费循环怎么转

后台 _message_consumer(queue_ops.py:162)是个 while self._running 循环:

每轮:
1. 若并行且在跑任务数 ≥ max_workers → sleep 一下,跳过(背压)
2. 从队列取一批(consume_batch,默认 3)
3. 给每条打 dequeue 事件、算排队等待时长
4. dispatcher.dispatch(messages) 分发
5. sleep(consume_interval,默认 0.01s)

第 1 步是背压:线程池满了就不再取新消息,防止过载(queue_ops.py:165-169)。分发器 dispatch(dispatcher.py:651)再按 (user, cube, label) 分组、查处理器、execute_task(dispatcher.py:603)丢进 ContextThreadPoolExecutor(默认 50 workers,DEFAULT_THREAD_POOL_MAX_WORKERS)执行。


6. 激活记忆的周期刷新(把明文记忆固化成 KV-Cache)

本节是本章与第 1 章的接口:调度器怎么把"明文记忆"预热成"激活记忆(KV-Cache)",以及为什么要周期性做而不是每次做。

6.1 为什么要固化

第 1 章讲过三类记忆,其中**激活记忆(Activation Memory)**本质是 KV-Cache——把常用的明文记忆预先喂给模型算出 KV 缓存,下次推理直接复用,省掉重复的前向计算。问题是:算 KV-Cache 很贵,不能每来一条记忆就重算。于是调度器用"周期触发"来控制频率。

6.2 两个入口:立即 vs. 周期

BaseScheduler 暴露两个薄封装,都转发给 ActivationMemoryManager:

  • update_activation_memory(base_scheduler.py:394):立即固化一批明文记忆。
  • update_activation_memory_periodically(base_scheduler.py:417):够钟才固化。

周期版的闸门逻辑在 activation_memory_manager.py:112——用 monitor.timed_trigger 判断距上次是否已过 interval_seconds,没到就直接跳过:

# activation_memory_manager.py:121-127(节选)
if (self.monitor.last_activation_mem_update_time == datetime.min
or self.monitor.timed_trigger(
last_time=self.monitor.last_activation_mem_update_time,
interval_seconds=interval_seconds)):
... # 够钟,执行固化

timed_trigger(general_monitor.py:262)就是"elapsed >= interval_seconds 才返回 True"的简单时间闸。

6.3 固化的真实动作

真正把明文变 KV-Cache 在 ActivationMemoryManager.update_activation_memory(activation_memory_manager.py:31):

1. 把 list[记忆] 用 MEMORY_ASSEMBLY_TEMPLATE 拼成一段编号文本(:63)
2. 若新拼的文本 == 现有缓存文本 → 跳过(避免无谓重算,:80)
3. 否则 act_mem.delete_all() 清旧(:88)
4. cache_item = act_mem.extract(new_text_memory) # 算 KV-Cache(:90)
5. act_mem.add([cache_item]); act_mem.dump(路径) # 存内存 + 落盘(:94-95)

这里的 act_mem 就是第 1 章的 KVCacheMemory(memories/activation/kv.py:16),extract/add/dump 分别在 kv.py:32/:54/:183;也支持 vLLM 版 VLLMKVCacheMemory去重优化很关键(activation_memory_manager.py:80):新旧组合文本一致就直接跳过,不浪费一次昂贵的 extract

6.4 谁来触发周期刷新

触发点在 memory_update_handler.py:156-163——当一个 mem_update 任务处理完工作记忆替换后,若开了 enable_activation_memory,就顺势调 update_activation_memory_periodically,把刚更新的工作记忆按周期固化进激活记忆:

# memory_update_handler.py:156-163(节选)
if self.scheduler_context.get_enable_activation_memory():
self.scheduler_context.services.update_activation_memory_periodically(
interval_seconds=monitor.act_mem_update_interval,
label=QUERY_TASK_LABEL, user_id=..., mem_cube_id=..., mem_cube=...)

所以链路是:用户对话 → mem_update 任务入队 → 后台 handler 重排工作记忆 → 够钟就把它固化成 KV-Cache。默认 enable_activation_memory 是关的(base_scheduler.py:89)。


7. 监控与内存管理旁路(一句话职责)

这些子目录不在主消费链路上,但支撑"生产稳定"和"记忆加工"。各一句话:

目录/模块一句话职责入口符号
monitors/dispatcher_monitor.py盯线程池健康,卡死/失败超阈值就重启线程池SchedulerDispatcherMonitor (:23)
monitors/general_monitor.py通用监控:工作/激活记忆监控项、时间闸 timed_triggerSchedulerGeneralMonitor (:38)
monitors/task_schedule_monitor.py汇总/打印各任务运行状态TaskScheduleMonitor
memory_manage_modules/retriever.py调度器侧的检索 + 重排 + 去重(供 handler 调)SchedulerRetriever
memory_manage_modules/post_processor.py记忆增强/过滤的后处理管线MemoryPostProcessor
memory_manage_modules/activation_memory_manager.py明文 → KV-Cache 固化(第 6 节)ActivationMemoryManager
orm_modules/base_model.py带锁的数据库管理基类,给监控项做持久化BaseDBManager (:50)
orm_modules/redis_model.py把 list 等结构落到 Redis 的管理器SimpleListManager (:23)
analyzer/离线评测与调试用(非生产链路):eval / API 分析 / 测试用 MOSSchedulerForEvalEvalAnalyzerAPIAnalyzerForScheduler
webservice_modules/redis_service.pyRedis 连接与 Stream 基础能力的混入基类RedisSchedulerModule (:18)
webservice_modules/rabbitmq_service.pyRabbitMQ 接线(可选,按 auth 配置启用)RabbitMQSchedulerModule

注意 analyzer/ 是旁路。 它服务评测/调试(SchedulerForEval 继承 GeneralScheduler 供跑分用),不在在线消费链路里,阅读主流程时可跳过。


8. MOSCore 如何挂接(回指第 2 章)

本节讲第 2 章的内核怎么打开/关闭这层调度。

MOSCore 构造时读 enable_mem_scheduler 开关(mem_os/core.py:72)。开着就 _initialize_mem_scheduler(core.py:111)——用工厂造实例、initialize_modules 装配、start 启动:

# mem_os/core.py:124-138(节选)
self._mem_scheduler = SchedulerFactory.from_config(scheduler_config)
self._mem_scheduler.initialize_modules(
chat_llm=self.chat_llm,
process_llm=self.mem_reader.general_llm,
db_engine=self.user_manager.engine)
self._mem_scheduler.start()

运行期还提供两个开关方法:

方法作用位置
mem_scheduler_onscheduler.start() 重新拉起后台服务core.py:141
mem_scheduler_offscheduler.stop() 优雅停机core.py:153

挂接后,内核把 mem_cubesmem_reader 注入调度器(core.py:75-76),后者就能对这些记忆立方体做后台调度。边界:enable_mem_scheduler 为假,_mem_schedulerNone,所有后台能力关闭,写入/检索退回同步路径。


9. 边界与局限(诚实)

  • 默认单机、默认关激活记忆。 DEFAULT_USE_REDIS_QUEUE 由环境变量决定,默认 False(general_schemas.py:26);enable_activation_memory 默认 False。不配 Redis 就没有跨进程队列、没有 auto-claim、OptimizedScheduler 的混合检索也降级(optimized_scheduler.py:129)。
  • 优先级只有"立即 vs. 入队"两态生效。 虽有三档 TaskPriorityLevel,但实际路由只区分 LEVEL_1(同步直跑)和其余(入队);get_stream_quotas 的按优先级配额目前是 TODO,未实现(orchestrator.py:88-98)。
  • 激活记忆固化只保留"最后一条"缓存。 固化前 delete_all 清空旧缓存再写新(activation_memory_manager.py:88),即当前实现是"整体替换"而非增量累积。
  • pending 认领偏保守。 默认要空闲满 1 小时才抢回 pending 消息(task_schemas.py:63),崩溃后的消息恢复不是秒级。

10. 代码地图(导航索引)

主题文件路径符号名
调度器骨架src/memos/mem_scheduler/base_scheduler.pyBaseScheduler
装配子模块src/memos/mem_scheduler/base_scheduler.py:202initialize_modules
绑定单个立方体src/memos/mem_scheduler/base_scheduler.py:175init_mem_cube
失败回滚src/memos/mem_scheduler/base_scheduler.py:276_cleanup_on_init_failure
多立方体管理src/memos/mem_scheduler/base_scheduler.py:356mem_cubes (setter)
激活记忆薄封装src/memos/mem_scheduler/base_scheduler.py:394 :417update_activation_memory / _periodically
通用调度器src/memos/mem_scheduler/general_scheduler.py:16GeneralScheduler
增强调度器src/memos/mem_scheduler/optimized_scheduler.py:37OptimizedScheduler
混合检索src/memos/mem_scheduler/optimized_scheduler.py:117mix_search_memories
调度器工厂src/memos/mem_scheduler/scheduler_factory.py:9SchedulerFactory
生命周期 / 消费循环src/memos/mem_scheduler/base_mixins/queue_ops.py:266 :162start / _message_consumer
优先级分流src/memos/mem_scheduler/base_mixins/queue_ops.py:81submit_messages
队列门面src/memos/mem_scheduler/task_schedule_modules/task_queue.py:23ScheduleTaskQueue
分发器src/memos/mem_scheduler/task_schedule_modules/dispatcher.py:38SchedulerDispatcher
任务执行 + acksrc/memos/mem_scheduler/task_schedule_modules/dispatcher.py:603 :272execute_task / _create_task_wrapper
Redis Streams 队列src/memos/mem_scheduler/task_schedule_modules/redis_queue.py:34SchedulerRedisQueue
自动认领 pendingsrc/memos/mem_scheduler/task_schedule_modules/redis_queue.py:314task_broker
优先级/空闲登记src/memos/mem_scheduler/task_schedule_modules/orchestrator.py:30SchedulerOrchestrator
消息模型src/memos/mem_scheduler/schemas/message_schemas.py:38ScheduleMessageItem
任务标签/优先级src/memos/mem_scheduler/schemas/task_schemas.py:23 :31TaskPriorityLevel / 各 *_TASK_LABEL
激活记忆固化src/memos/mem_scheduler/memory_manage_modules/activation_memory_manager.py:31update_activation_memory
KV-Cache 激活记忆src/memos/memories/activation/kv.py:16KVCacheMemory
线程池健康监控src/memos/mem_scheduler/monitors/dispatcher_monitor.py:23SchedulerDispatcherMonitor
MOSCore 挂接src/memos/mem_os/core.py:111 :141 :153_initialize_mem_scheduler / mem_scheduler_on / _off