Skip to content

fix(prompt): scope B_NOTIFY injection to miloco background sessions - #479

Open
idootop wants to merge 3 commits into
mainfrom
fix/notify-scope-miloco-sessions
Open

fix(prompt): scope B_NOTIFY injection to miloco background sessions#479
idootop wants to merge 3 commits into
mainfrom
fix/notify-scope-miloco-sessions

Conversation

@idootop

@idootop idootop commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

问题

B_NOTIFY 原先无条件注入所有会话。两个后果:

  1. 语义歧义 —— agent 在与用户正常对话、只是回答提问时,也把 miloco-notify skill 当成硬前置绕一圈。原文里「而不是当面回答用户此刻的提问」这个例外句夹在句中容易被忽略,紧跟其后的「典型场景」又列了「设备反馈」,把刚排除的场景拉了回来。
  2. 常规 cron 被误导 —— 非 miloco 的 cron 任务(例如日常汇报)也拿到「必须先读 notify skill 才算送达」的硬约束,一次汇报曾被拖到 120s 超时。

方案

B_NOTIFY 收敛到只注入 miloco 后台会话——由感知引擎 / miloco 定时任务 / 规则与任务事件拉起、turn 跑在后台(deliver=false)、回复对用户不可见,只能按 skill 主动推送才算送达的那些会话。其余会话(用户 IM、常规 cron、CLI 主会话)自身的回复就能到人,不再注入。

新增 isMilocoBackgroundSession / is_miloco_background_session(TS 与 Python 端 1:1),两条线索任一命中即算:

  • sessionKey 有 miloco 段 —— 按 : 切段后判「等于 miloco 或以 miloco- 开头」。覆盖 dispatcher _ROUTEagent:main:miloco{,-rule,-suggest}、schedule runner 的 miloco-schedule:<cron_id>,以及 hermes 侧的 miloco:cron:… / miloco-rule-<id>
  • cron 消息头里的 job 名带 miloco —— isolated cron 的 sessionKey 是 agent:<id>:cron:<jobId>:run:<runId>,jobId 由宿主随机生成、看不出归属,只能从 [cron:<jobId> <jobName>] 里的 job 名认领;miloco 自管的 4 个 job 都叫 miloco-*(见 home-profile/scheduler.ts kCronTasks)。
会话 注入 B_NOTIFY
agent:main:miloco(感知 / 语音 / bind 交互 lane)
agent:main:miloco-rule / -suggest
miloco-schedule:<cron_id>(backend 定时任务)
miloco-* cron job(perception-digest / home-patrol / …)
用户自建 cron([cron:<id> PTM 汇报]
用户 IM 会话
CLI 主会话

顺带一提:onboarding 事件经 resolveTarget: "owner-channel" 改写成车主 IM 会话跑,这条闸门下自然不再注入——那个 turn 的回复本来就在用户眼前,符合预期。

B_NOTIFY 正文重写

作用域已由注入侧收敛,正文就不再靠「而不是当面回答用户此刻的提问」这种例外句消歧。那句在语音 lane 反而是错的:语音提问的答复同样得经 TTS 推回去。新正文只讲这类会话里该怎么做——本轮有信息要传达就先读 skill,不需要告知任何人就做完即止。同时把最含糊的「设备反馈」改成「设备异常」。

skill 侧同步

闸门生效后,常规对话里能拉起 miloco-notify 的就只剩 skill description,而它原本也含「设备反馈」这类歧义词。description 与「何时激活」一并改成按触发源判定,并显式排除「汇报用户刚让你做的设备操作结果」。只改源目录 plugins/skills/plugins/openclaw/skills/ 是 gitignore 的同步产物)。

已知取舍

  • 用户在 IM 里要求主动触达第三方(「帮我告诉爸爸该吃饭了」)时,miloco-notify 不再是常驻硬前置,改为靠 skill description 自行发现加载。B_CAPABILITIES 里「通过语音 / IM / 米家推送送达」仍在,配置通知渠道那句也保留在 description 中。
  • 用户若把自己的 agent 命名为 milocoagent:miloco:telegram:…)会误判成后台会话而多注入一段——等同本次改动前的行为,无功能损失,未额外加区分。

验证

plugins/openclaw   vitest  15 files / 165 tests passed;  tsc --noEmit clean
plugins/hermes     pytest  224 passed, 2 skipped

新增覆盖:闸门的 12 种 sessionKey 形态、cron 头认领 / 不认领、正文提到 miloco 不算数;组装层新增「用户 IM 不注入」「rule / suggest 仍注入」「miloco 定时任务(minimal)仍注入」「miloco cron vs 用户 cron」。原「minimal(cron) 含 miloco-notify」的断言按新语义翻转。

B_NOTIFY 原先无条件注入所有会话,导致 agent 在与用户正常对话、只是回答提问时
也把 miloco-notify skill 当成硬前置绕一圈;常规 cron 里还曾把一次汇报拖到 120s
超时。

改为只注入 miloco 后台会话——由感知引擎 / miloco 定时任务 / 规则与任务事件拉起、
turn 跑在后台、回复对用户不可见,只能按 skill 主动推送才算送达的那些会话。

新增 isMilocoBackgroundSession / is_miloco_background_session(TS 与 Python 1:1),
两条线索任一命中即算:

- sessionKey 有 miloco 段(按 `:` 切段,等于 miloco 或以 miloco- 开头)——覆盖
  dispatcher `_ROUTE` 的 agent:main:miloco{,-rule,-suggest}、schedule runner 的
  miloco-schedule:<cron_id>,以及 hermes 侧的 miloco:cron:… / miloco-rule-<id>
- cron 消息头里的 job 名带 miloco——isolated cron 的 sessionKey 是
  agent:<id>:cron:<jobId>:run:<runId>,jobId 随机、看不出归属,只能从
  `[cron:<jobId> <jobName>]` 认领;miloco 自管的 4 个 job 都叫 miloco-*

注意没有按 profile != "minimal" 来判:schedule runner 用
`[cron:<name>]` + `miloco-schedule:<id>`,miloco 自己的定时任务恰好落在 minimal,
按 profile 切会把「到点主动播报」的通知能力一起砍掉。

同时重写 B_NOTIFY 正文:作用域已由注入侧收敛,正文不再写「当面回答用户提问除外」
这类例外句——那句在语音 lane 反而是错的,语音提问的答复同样得经 TTS 推回去。

miloco-notify skill 的 description 与「何时激活」一并按触发源改写:闸门生效后,
常规对话里能拉起它的就只剩 description,而其中「设备反馈」等措辞同样有歧义。
@github-actions

Copy link
Copy Markdown

👋 感谢提交 PR @idootop!维护者会尽快 review。

提交前请确认:

  • CI 全绿(test / lint / build)
  • 改动聚焦单一主题,便于审阅
  • 若改动了依赖(lockfile / pyproject.toml / package.json),需维护者评论 /allow-dependencies-change <当前 head SHA> 放行(之后再 push 需重新放行)

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

[PR #479]: fix(prompt): scope B_NOTIFY injection to miloco background sessions

作者: idootop
范围: fix/notify-scope-miloco-sessions → main

修改方案

要解决的问题B_NOTIFY(「通知用户」指令块,要求 agent 动手前必须先读 miloco-notify skill)过去无条件注入所有会话。用户在 IM 里随口问设备状态也绕一圈 skill;非 miloco 的常规 cron 任务(用户自建汇报)曾被拖到 120s 超时。

整体方案(三条主线 + 一条 skill 侧同步):

  1. 新增「本轮是不是 miloco 后台会话」的判定闸门isMilocoBackgroundSession / is_miloco_background_session)——两条线索任一命中即算:

    • 会话标识段匹配:把 session key 按冒号切段,有一段正好等于 miloco 或以 miloco- 开头就算命中。覆盖后端 dispatcher _ROUTE 写死的三个后台 lane(agent:main:miloco / agent:main:miloco-rule / agent:main:miloco-suggest)和定时任务 runner session_keymiloco-schedule:<cron_id>),hermes 侧 miloco:cron:… / miloco-rule-<id> 也能覆盖
    • cron 消息头匹配:宿主把定时任务消息改写成 [cron:<jobId> <jobName>] …,jobId 是随机生成的看不出归属,只能从方括号里的 job 名认领。miloco 自管的 4 个 cron 任务都以 miloco- 开头(kCronTasks),用 (?:^|[\s:])miloco- 做词首匹配,避免用户自建「巡检 miloco 日志」这类 job 名被误认领
    • 两条线索是「或」关系;cron 头的方括号必须锚在消息最开头(正则 ^\[cron:),正文提到 miloco 不算
  2. 组装侧从"无条件拼上"改成"过闸门才拼"prompt.ts:357 / context_injection.py:334):通知块被移进 if 分支,语言块仍无条件拼在最后,其余块的取舍一行未动。hermes 侧额外在 send_turn 里算出 notify_session_key:常规后台 lane 传原始 session_key,owner-channel 投递(onboarding)传 None(因为回复会经 hermes send 推到车主 IM,用户看得见)

  3. 通知块正文重写B_NOTIFY):作用域由闸门收敛后,正文不再写「当面回答提问除外」的夹缝例外(那句在语音 lane 反而是错的),改成两条互斥分支——有话要传达 → 先读 skill;只是归档巡检 → 做完即止

  4. skill 侧同步miloco-notify/SKILL.md):description 和「何时激活」改成按触发源判定,渠道配置标为唯一例外,「设备反馈」统一换成「设备异常」

关键设计原则

  1. 不按 profile 切,按"是否 miloco 相关"切 —— 后端定时任务恰好被判成 minimal,按 profile 切会把"到点主动播报"的通知能力一起砍掉
  2. 双端 1:1 移植 —— TS 与 Python 实现同形(trimStartlstripincludesin),注释逐条对齐
  3. 误判方向选"多注入" —— 用户若把 agent 命名为 miloco 会被误判成后台会话,等同改动前行为,无功能损失
  4. hermes 侧用 miloco 侧 session_key 而非 hermes session_id 作判据 —— _map_session 给每个 id 都加了 miloco: 前缀,按段切必然命中,gate 恒为真等于没判。新增 test_build_system_rejects_hermes_session_id 把这点钉死

测试覆盖

主线 测试文件 用例摘要
1 prompt.test.ts::isMilocoBackgroundSession 12 种会话 key 参数化、cron 头带/不带 miloco、正文提到不算、词首匹配 5 例
2 prompt.test.ts::before_prompt_build 组装 常规 cron / 用户 IM 不注入、rule / suggest 仍注入、miloco 定时任务(minimal)仍注入、miloco cron vs 用户 cron 对照
1+2 test_context_injection.py 同形闸门参数化 + cron 头词首匹配;inject_context 层注入/不注入断言
2(生效路径) test_hermes_adapter.py build_system 直接断言 6 种 session_key;send_turn 端到端断言 POST body 含/不含通知块(interaction / rule / owner-channel 三种);_map_session 恒真反例测试

问题

上轮 ci-bot 四条问题修复验证

# 严重度 上轮问题 修复验证
🔴 严重 hermes 闸门接在死路径(inject_context),生效路径 build_system 没传 session_id,通知块被全量摘掉 send_turn 现在把 notify_session_key + text 透传给 build_systemadapter.py:319-321),owner-channel 传 None(:311)。新增 test_send_turn_passes_notify_scope_to_build_system 在生效路径上端到端断言 POST body。作者正确指出建议的 session_id 参数名会引入 _map_session 恒真陷阱,改用 session_key 并加 test_build_system_rejects_hermes_session_id 钉死
🟡 重要 SKILL.md 渠道配置入口被绝对禁令堵住 ✅ description 加了「唯一例外」标记(SKILL.md:3),正文「何时激活」第 3 条独立列出渠道配置不看触发源(:23),第 19 行「默认」限定词消除了自相矛盾
🔵① 建议 cron 头裸 substring 匹配太松 ✅ 双端正则改为 `(?:^
🔵② 建议 B_NOTIFY 正文「用户要配置通知渠道」在新作用域下不可能发生 ✅ 双端正文已删除该项,注释写明理由(prompt.ts:155-160 / context_injection.py:226-231

本轮独立扫描:对三条主线(判定闸门逻辑、组装侧门控接线、SKILL.md 文档一致性)+ 跨层一致性(openclaw 与 hermes 双端对齐、dispatcher _ROUTE / _DELIVERY / _PROFILE 与门控语义匹配)逐条验证,未发现新问题。

结论

LGTM — 上轮四条问题全部修复,hermes 侧生效路径已接通并有端到端测试兜底,判定闸门双端对齐,SKILL.md 渠道配置例外消除。


由 review-pr skill v1.6 生成

hermes 侧 pre_llm_call 是 noop(miloco-plugin/__init__.py:70),inject_context
根本不会被调用。唯一生效路径是 dispatcher → send_turn → build_system →
_build_prepend,而它没接门控参数——上一版合入会把「## 通知用户」从所有 backend
lane(interaction / bind / rule / suggestion)里整片摘掉,感知告警会被写进一条
没人收得到的回复里。

门控判据用 miloco 侧 session_key,不是 hermes 的 session_id:_map_session 给每个
id 都加了 `miloco:` 前缀,按段切必然命中,拿它当判据门控恒为真、等于没判。
owner-channel 投递(onboarding)回复会推到车主 IM,用户看得见,故按 delivery.deliver
传 None——语义对齐 openclaw webhooks/agent.ts 的 effectiveSessionKey。

其余 review 意见:
- SKILL.md 把「渠道配置」标为激活规则的唯一例外(description + 正文),消解
  「不在对话里加载」与「配渠道要加载」的表面矛盾
- cron 头改词首匹配 MILOCO_JOB_RE = /(?:^|[\s:])miloco-/,避免用户自建的
  「巡检 miloco 日志」这类 job 名被认领成后台会话(TS / Python 双端同步)
- B_NOTIFY 去掉「用户要配置通知渠道」从句:配渠道必然发生在用户说话的会话里,
  而那种会话已不注入本块(双端同步)

测试补在生效路径上(回退修复后 2 failed)。为此 conftest 复刻 install-hermes.sh
的摊平部署布局,否则 adapter.py 的 `from .context_injection import` 在仓库布局下
解析不到,build_system 永远测不着——这正是这个洞能溜过去的原因。
@idootop

idootop commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

四条都已修,commit b6c1592。其中 🔴 的修法与建议不同,说明如下。

🔴 门控接在死路径上 —— 确认,但建议的修法无效

问题属实且比描述的更严重:hermes 侧 pre_llm_call 是 noop(miloco-plugin/__init__.py:70),inject_context 全仓只在自己的定义和 test_context_injection.py 里出现过。唯一生效路径是 dispatcher.py:314send_turnbuild_system_build_prepend。原样合入会把「## 通知用户」从所有 backend lane(interaction / bind / rule / suggestion)里整片摘掉——「客厅有人跌倒」这类告警会被写进一条没人收得到的回复里,正好把 PR 的目的反过来。

不能传 session_id

_map_session("agent:main:telegram:dm:123", "miloco-interactive")
  → "miloco:agent:main:telegram:dm:123:miloco-interactive"
     ^^^^^^ 按段切必然命中

_map_session 给每个 id 都加了 miloco: 前缀,用它当判据门控恒为真、等于没判。今天行为恰好正确,只因 _ROUTE 里五种事件全是 miloco 来源;将来加一个用户可见的 lane 就会静默错注。

改用 miloco 侧 session_key + delivery.deliver 处理 owner-channel:

notify_session_key = None if delivery.get("deliver") else session_key

判据是「本轮回复用户看不看得见」,语义对齐 openclaw webhooks/agent.tseffectiveSessionKey——onboarding 的 turn 虽跑在新会话,但整轮回复经 hermes send 推到车主 IM,用户看得见,故传 None。已加 test_build_system_rejects_hermes_session_id 把「session_id 不可作判据」这点钉死。

🟡 / 🔵① / 🔵② 按建议修

  • 🟡 SKILL.md 把渠道配置标为激活规则的唯一例外(description 末句 + 正文第 3 条 + 第 19 行「默认看本轮由谁触发(渠道配置类请求除外,见第 3 条)」)。version 已在本 PR 的 65803a0 里 bump 到 3.2 / 2026-07-31,未重复再 bump。
  • 🔵① cron 头改词首匹配 MILOCO_JOB_RE = /(?:^|[\s:])miloco-/,严格度与 sessionKey 段判定(=== "miloco" / startsWith("miloco-"))对齐。TS / Python 双端同步,各加 5 例参数化测试(milocoish-report巡检 miloco 日志同步 miloco → false)。
  • 🔵② B_NOTIFY 去掉「以及用户要配置通知渠道」从句,双端同步,并在注释里写明理由。

测试:补在生效路径上

新增 test_send_turn_passes_notify_scope_to_build_system 断言 POST body 的 messages[0],覆盖 interaction / rule / owner-channel 三种。把修复回退后确实 2 failed。

为此 conftest.py 复刻了 install-hermes.sh:626-635 的摊平部署布局(sys.modules 别名)——生产里 adapter.pycontext_injection.py 同级,所以写的是 from .context_injection import ...;仓库布局下 hermes_adapter/ 只有 adapter.py,相对导入解析不到,build_system 根本没法在单测里跑。这正是这个洞能溜过去的原因:文件头 docstring 第 8 行声称覆盖了 build_system,实际没有对应测试函数。

全绿:hermes 239 passed / 2 skipped,openclaw 170 passed,tsc --noEmit clean。

两点补充:PR 描述里的注入表是 openclaw 侧的

review 提到的 hermes minimalmiloco-schedule:<cron_id> 两行,在 hermes 上都不是真实路径,故未扩大改动范围:

  1. minimal 在 hermes 不可达——dispatcher.py_PROFILE.get(event_type, "full") 只产出 rule / suggestion / fulladapter.py:302profile != "minimal" 恒真。minimal 只在 openclaw 侧由 resolveProfile 对 cron 平台产出。
  2. miloco-schedule:<cron_id> 不经 hermes adapter——backend schedule runner(schedule/runner.py:268-271)走 run_agent_turn → openclaw agent webhook,不调 send_turn。这个 session key 形态在 hermes 上不存在。

所以那张表描述的是 openclaw 行为,hermes 侧只有 dispatcher 那五种事件。

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant