Skip to content

Commit e431ff7

Browse files
wehosHongzhi Wenclaude
authored
新增用户活动追踪器(main_logic/activity/),重构主动搭话 Phase 2 决策 (#1015)
* 新增 main_logic/activity/ 用户活动追踪器,把窗口/进程/CPU/idle/GPU/语音/对话信号聚合成结构化 ActivitySnapshot 注入 proactive_chat Phase 2 prompt,解决 pending reflection 几乎不被采用、AI 两极化打断/沉默、对话连贯性差三个老问题 主要内容: - config/activity_keywords.py:943 titles + 692 processes + 518 domains 关键词库,EN/简/繁/JP/KR 多语言别名 + 词边界匹配防短缩写撞普通词 - 9 状态规则机(gaming / focused_work / casual_browsing / chatting / voice_engaged / idle / transitioning / stale_returning / away)+ unfinished_thread 5min 跟进窗口(最多 2 次)+ GPU 兜底未识别游戏 - emotion-tier LLM 增强:soft scores + 活动叙述 + 语义 open_threads 检测,三者均为 advisory,规则路径仍为 propensity 决策权威 - reminiscence 注册为 source channel + 配套 weight 衰减;focused_work / gaming 状态自动 bypass Phase 1 unified LLM 调用 - Phase 2 prompt 5 lang 重构:A/B/C/D 决策框架替代 9 条规则墙,结构性 token 开销 ~700 → ~370(47% off) - 远程部署降级:自动检测平台 + NEKO_ACTIVITY_TRACKER_REMOTE 环境变量强制降级 + 前端推信号 API(push_external_system_signal)预留 - 设计文档 docs/design/user-activity-tracker.md(650 行) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 github-code-quality bot 反馈修 system_signals.py 的 5 处代码质量问题: - start() 里 psutil prime 失败的 except 加 logger.debug 和注释,不再裸 pass - stop() 里 task await 异常拆分为 CancelledError(预期)和 Exception(异常路径打 log) - _read_idle_seconds / _read_active_process_name 里的 ctypes 导入统一成 'from ctypes import ...' 单一风格,避免同作用域里同时 import + from import 的 lint warning 无功能变化,仅风格 / 可观测性。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 codex bot 反馈修两处 race / 副作用问题 - P1 (main_logic/core.py): handle_input_transcript 仅在 transcript.strip() 非空时 才调 on_user_message。空 transcript(VAD 误触发或转录失败)不应当成"用户消息" 处理——之前会清掉 unfinished_thread、bump _user_msg_seq(让 open_threads 缓存 失效)、往 buffer 加空条目。on_voice_rms 仍无条件调用,维持 voice_engaged 状态。 - P2 (main_routers/system_router.py): Phase 2 渲染 state_section 前重拉 tracker 当前缓存的 enrichment 字段(activity_scores / activity_guess / open_threads)。 kickoff_open_threads_compute 是 Phase 1 起点 fire-and-forget 跑的,结果会在 Phase 1 中途陆续落到缓存——早期捕获的 activity_snapshot 看不到这些更新, 早先版本要等下一轮 proactive 才能用上。决策性字段(state / propensity / unfinished_thread)仍取自早期 snapshot,避免 Phase 1 中途状态变化导致 gating 不一致。 P1 已加 smoke 验证:空 / 纯空格 transcript 不动 unfinished_thread / seq; 真实 transcript 正常清除并递增。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 CI lint + coderabbit 反馈修若干 bug 与代码质量问题 CI 阻塞修复: - 新建 config/prompts_activity.py 收纳 ACTIVITY_GUESS_PROMPTS / OPEN_THREADS_PROMPTS / OS_DEGRADED_MARKER 三个多语言 dict(按 prompt-hygiene 约定多语言 str→str 表必须放 config/prompts_*.py),llm_enrichment.py 与 snapshot.py 改为 import 使用 bug 修复: - finish_proactive_delivery 没 flush _current_ai_turn_text 给 tracker。/api/proactive_chat 成功路径不走 _emit_turn_end / handle_proactive_complete,buffer 会污染下一轮用户消息。在 lock 里 send_lanlan_response 之后立即 on_ai_message + 清 buffer - _dispatch_openclaw_handoff 通过 handle_input_transcript 复用文本——会让 on_voice_rms 假触发 voice_engaged,且 on_user_message 在文本入口已调过一次再调一次会双计 _conv_seq + 重复进 buffer。给 handle_input_transcript 加 is_voice_source 参数,handoff 传 False 时跳过 tracker hooks(保留 queue/cache 主流程) - open_threads 缓存只盯 _user_msg_seq,AI 新开的线程(promise / 半截话)触发不到重算。改 _user_msg_seq → _conv_seq,on_user_message 与 on_ai_message 都 bump - get_snapshot_sync 直接读 _collector.snapshot(),绕过 _select_system_snapshot 选外部信号的逻辑——远端部署同步路径会拿到服务器侧降级信号。改走 _select_system_snapshot 与 async 路径一致 - reminiscence 重复写 _proactive_chat_history(用 'reminiscence' channel)会污染 _format_recent_proactive_chats / _is_similar_to_recent_proactive_chat 两条 dedup/相似度链路。新建独立 _reminiscence_usage_history buffer + _record_reminiscence_usage 工具函数;_compute_source_weights 单独读这个 buffer 把 reminiscence 当 channel 衰减,不再污染共享 history - Phase 2 prompt 5 lang 决策框架里写死 [CHAT] 标签与 get_proactive_format_sections 在无外部素材时输出格式段("直接输出正文,不需要来源标签")冲突。改成不指定具体 tag,由下方 {output_format_section} 决定输出形式 文档: - 顶部 Status 改为 "v1 (rules-primary, LLM advisory)",避免和后面 LLM enrichment 章节自相矛盾 - 接线点章节修正:on_user_message 来自 handle_input_transcript(语音)+ _process_stream_data_internal(文本),不是 handle_new_message;on_ai_message 来自 _emit_turn_end / handle_proactive_complete / finish_proactive_delivery 三处 E2E smoke 全过:empty transcript 不再误清 unfinished_thread / 不 bump _conv_seq;handoff 路径不再误触发 voice_engaged;AI 消息现在能 bump _conv_seq 让 open_threads 重算;prompt-hygiene + ruff + 其他 4 个 lint 全 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 coderabbit 反馈再修两处 - main_logic/activity/tracker.py _do_open_threads_compute:加 in-flight 守卫。 LLM 调用期间(1-3s)用户/AI 可能发新消息推进 _conv_seq;老 task 完成时 原本无脑覆盖缓存——本轮 prompt 看到的是 stale 结果。改成 LLM 返回后比对 seen_seq vs 当前 _conv_seq,不一致就丢弃结果(不更新 _open_threads_computed_at_seq), 下次 kickoff 看到 seq mismatch 自动重算。 - main_logic/core.py 抽 _flush_ai_turn_text_to_tracker helper:三个出口 (_emit_turn_end / handle_proactive_complete / finish_proactive_delivery) 共用, 消除 DRY。语义不变。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 coderabbit 反馈再修一处真 bug + 三处文档错配 - main_logic/activity/tracker.py _activity_guess 后台 loop:和 _do_open_threads_compute 一样,给 LLM 调用加 in-flight guard。先捕获 seen_conv_seq + buffer 快照,await 后比对 _conv_seq;不一致就丢弃结果,不更新 last_conv_seq。否则在 LLM 调用期间用户/AI 发新消息会让旧上下文的 result 把 last_conv_seq 推到新值,下次 tick 看到 seq 没动会被错误跳过。 - docs/design/user-activity-tracker.md: - 架构图里 propensity == screen_only 改为 restricted_screen_only(之前是错的枚举值) - open_threads 缓存失效条件从 "_user_msg_seq" 更新到 "_conv_seq + AI 侧也 bump + in-flight guard",反映当前实际语义 - Phase 2 快照流程改为"双层快照"准确描述:早期 snapshot 用于 gating,渲染前 fresh-fetch 用 dataclasses.replace 拼 enrichment 字段。原来的"only get_snapshot once"已经过时 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 coderabbit 反馈给 doc 5 处 fenced code block 加 text 语言标识 markdownlint MD040:line 102/128/288/311/325 都是无 language tag 的 ``` 代码块,markdownlint-cli2 会重复报 warning。改成 ```text(5 处都是 file tree / dataflow 图 / snapshot 例子,不是真实可执行代码,统一 text 即可)。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 coderabbit 反馈再修两处文档 - "Future work" 段时态错配:open_threads / activity_guess / 整段 emotion-tier 接入都已经在当前 PR 上线,原文写成"v2 will fold"/"will enter"会让读者误判 当前能力边界。改成两条 v2 quality upgrade(recall/排序、稳定性/cost), 以及把"emotion-tier finally enters"改成"already integrated"。 - _dispatch_openclaw_handoff 接线点说明:原文只说"without re-firing tracker hooks"被 coderabbit 误读为只 skip on_voice_rms。补 WHY:on_voice_rms 是 voice-only 在文本路径下会误判 voice_engaged;on_user_message 也跳过是因为 上一步 _process_stream_data_internal 已经 on_user_message(text=data) 调过 同一份 payload,再调会 double-bump _conv_seq + 重复进 buffer。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:把 activity 模块剩余 4 张嵌套 i18n 表搬进 config/prompts_activity.py snapshot.py 里原本以 _STATE_LABELS / _PROPENSITY_DIRECTIVES / _REASON_TEMPLATES / _STATE_SECTION_LABELS 形式存放的 5 语言嵌套 dict,规模虽然小但仍是 i18n 内容, 应按项目约定全部落在 config/prompts_*.py,方便后续扩展语言时一次 grep config/ 就能补全。lint (check_prompt_hygiene.py) 只扫平铺多语言 dict,嵌套结构需要按 约定自觉搬。 同时更新 .agent/rules/neko-guide.md 把这条规则显式写出来,避免下次再遗漏。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:清理 activity 进程名列表的跨池重复,加 import-time 去重断言 背景:state_machine 设计是"启动器(subcategory='launcher')不触发 gaming 状态" (逛 Steam 商店 ≠ 在玩游戏,见 state_machine.py:506-510),但若同一个 .exe 既 出现在 GAME_PROCESS_NAMES 又出现在 GAME_LAUNCHER_PROCESS_NAMES,_build_process _table 先迭代前者,需求被静默绕过。审查发现 6 处这种 launcher 误入 game pool 的情况,再用断言扫了一遍发现还有更多跨池冲突和池内 case-fold 冗余。 修改: A. game ↔ launcher 跨池冲突(6 项)—— 这些都是启动器,从 GAME_PROCESS_NAMES 删除,仅保留在 GAME_LAUNCHER_PROCESS_NAMES: - wegame.exe / WeGameLauncher.exe (Tencent WeGame) - TenioDL.exe (Tencent 下载器) - Battle.net.exe (Blizzard 启动器) - ffxivlauncher.exe (FFXIV 启动器;ffxivboot.exe 留下,是游戏内 boot loader) - NGM.exe (Nexon Game Manager) B. 其他跨池冲突 —— 一个 exe 名属于一个 category: - Origin.exe: EA Origin(launcher)/ OriginLab(work)共用同名是历史问题。 EA Origin 用户面更广,从 WORK_PROCESS_NAMES 删除;OriginLab 用户的现代 64 位版 Origin64.exe 仍归 work。 - OUTLOOK.EXE: 邮件 = communication,不是 office,从 WORK 删除(COMM 已有)。 - sumatrapdf.exe: PDF 阅读器主用途是工作文档,从 ENT 删除(WORK 已有)。 - discord/telegram/whatsapp/line/kakaotalk: IM 应用归 communication,从 ENT 删除(COMM 已有)。 - steam.exe / steamwebhelper.exe: 启动器归 launcher pool,从 ENT 删除。 C. 池内 case-fold 冗余 —— _make_needle 已 lowercase,重复列只会让查找表变胖: - GAME: re4.exe/RE4.exe → re4.exe;re8.exe/RE8.exe → re8.exe - LAUNCHER: Launcher.exe + launcher.exe → 单条 - WORK: OneNote/ONENOTE、Lightroom/lightroom、PaintDotNet/paintdotnet、 CINEMA 4D/Cinema 4D、MATLAB/matlab → 各保留单条 D. 加 _assert_no_process_dups(),模块 import 时调用,覆盖跨池 + 池内两种情况。 下次有 PR 误把 launcher 塞进 game pool(或诸如 outlook.exe 误入 work)时, 立即在 CI / 启动期抛 AssertionError,不再静默错分类。 实测:清理后 game=218 / launcher=41 / work=265 / comm=96 / ent=48,断言通过; 合成回归测试(往 GAME_PROCESS_NAMES 注入 'steam.exe')能正确抛出。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:同步 activity tracker 设计文档与最新代码 去重清理(commit 9ebeedf)后,关键词库的池规模变化没有同步到文档: - 总 title 行数 943 → 965(先前数错) - 进程名(不含 launcher 池)692 → 627(去重 + 重新统计) - launcher 进程 64 → 41(先前数错;和实际不符) 同时给"Extending the keyword library"章节补一段 dedup 不变量说明: - 一个 exe 只能在五个进程池里出现一次(含 case-fold) - 解释为什么这一约束是真的需求(_build_process_table 的优先级 + state_machine.py:506 的 launcher 豁免会被绕过) - 指向 _assert_no_process_dups() 作为 import-time 守门 - 给 Origin.exe 这种"同名不同产品"的特殊处理案例 源码侧把"=== GAMES (... titles / ... processes / ... launchers) ===" 区段头也校正到当前值。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:Phase 2 prompt 决策段加水印 + 三条 instruction 格式统一 + external 瘦身 A. 决策段加水印(5 langs) 将 "向{master_name}搭话决策:" 改成 "======以下为向{master_name}进行搭话的 决策方式======",{meme_instruction} 之后加 "======以上为向{master_name}进行 搭话的决策方式======" 配对水印,与该文件其他 system prompt 段落统一 (======以下为对话历史====== 等模板)。en/ja/ko/ru 同步对应翻译。 B. 三条 instruction 格式统一为 bullet(5 langs) 原状态:source_instruction = "- 你可以结合..."(bullet), music_instruction = "10. 关于音乐:..."(编号 10,但前面只有 1-5), meme_instruction = "11. 关于表情包:..."(编号 11,同样断层)。 风格断裂感很强。改成: 补充: - 重复判定:... - 风格:... - source bullet - 关于音乐:... (有素材时) - 关于表情包:... (有素材时) prompt 模板里去掉 {source}{music}{meme} 之间的 "\n",改为 {source_instruction}{music_instruction}{meme_instruction};music/meme 值 自带前导 "\n" 当存在、空字符串当不存在——所以三种组合(4/3/0 个素材) 都不出多余空行。en 走 "Additional rules:",ja 走 "補足:",ko 走 "보조 규칙:",ru 走 "Дополнительно:"。 C. external section 瘦身 1) EXTERNAL_TOPIC_HEADER 去掉 "你注意到一个有趣的话题:" 前导行(5 langs), 与 SCREEN_SECTION_HEADER / MUSIC_SECTION_HEADER / MEME_SECTION_HEADER 对齐—— 原前导行是 external 还是主信号源时的叙事框架,现在 vision/music/meme/ reminiscence 平行存在,不对称的口吻浪费 token 也显得别扭。 2) PROACTIVE_PHASE1_TOTAL_TOPICS: 20 → 12。早期 external 是主通道,候选池 开得很大;现在 Phase 2 多通道并行后 external 相对权重下降,多看 8 条 边际候选无助于 Phase 1 挑出更好的 top-1,反而把单次 LLM call 的 prompt 推向 2k 上限。 3) PROACTIVE_EXTERNAL_TOTAL_MAX_TOKENS: 2000 → 1500(同步收紧 Phase 1 拼合 兜底;12 × 200 = 2400 max 仍兜底截断到 1500,留 ~250 token 富余)。 4) docs/design/llm-prompt-budget.md 的对应表格同步到新值。 Phase 2 generate prompt 实测:4 种素材组合渲染均干净(无多余空行/编号断层), 水印对所有 5 langs 闭合。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:水印 ====== 数量统一为 6 + 修正前导空格 + 音乐/表情包补充加搭话条件前缀 A. 水印等号数量统一为 6(5 langs 全覆盖) - config/prompts_activity.py 的活动状态段从 ===活动状态=== / ===状态结束=== 升级到 ======活动状态====== / ======状态结束======(zh/en/ja/ko/ru 均改),与 config/prompts_*.py 其他段落保持视觉一致。 - config/prompts_memory.py 和 config/prompts_proactive.py 的 MEMORY_RECALL_HEADER / MEMORY_RESULTS_HEADER 把右侧 5 个等号补成 6 个;前者原本写成 "======...=====" 的非对称水印,是历史遗漏。 B. 移除多余前导空格 - 英文版 Phase 2 prompt 里 "★ When the activity state lists..." 的 第二行带 2 空格缩进,把整句合并成单行(其他 4 语言本来就是单行)。 C. 音乐 / 表情包补充指令加 "当你决定结合 xxx 进行搭话时" 前缀(5 langs) - 原本 _P2_MUSIC_INSTRUCTION 以 "如果提供了音乐素材..." 起头是段 条件描述,但 instruction 本身就是仅在 has_music=True 时注入, "如果提供了" 是冗余条件。改成 "当你决定结合音乐推荐进行搭话时" 直接告诉模型这条规则在它选 [MUSIC] 输出时才适用,语义更清晰。 - _P2_MEME_INSTRUCTION 同步改为 "当你决定结合表情包进行搭话时,系统 会自动发送...",主语回到 AI 决策。 - en: "When you decide to combine ... with your message, ..." - ja: "音楽のおすすめを取り入れて話しかけると決めたとき" / "ミームを取り入れて話しかけると決めたとき" - ko: "음악 추천을 결합하여 말을 걸기로 결정했을 때" / "밈을 결합하여 말을 걸기로 결정했을 때" - ru: "когда вы решаете включить музыкальную рекомендацию..." / "когда вы решаете включить мем..." prompt-hygiene lint 通过;4 种素材组合(max / mid / min)下决策段渲染干净, 无空行断层。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈对齐 Phase 2 规则措辞与回忆线索 header(仅 zh 一处错字) CodeRabbit 指出 Phase 2 的 "记忆线索 / Memory cues" 与实际 prompt 渲染对不上; 我去 trace 了一下,CodeRabbit 给的修复方向(改 MEMORY_RECALL_HEADER / MEMORY_RESULTS_HEADER)实际上方向错了——那两个常量只在 /search_for_memory/ 里用,而该端点 docstring 直接写着 "语义记忆已下线,返回空结果占位",proactive Phase 2 根本不走那条路径。 实际链路:proactive Phase 2 拿 memory_context 走 /new_dialog/ 端点,里头是 PERSONA_HEADER + INNER_THOUGHTS_HEADER + 原始 recent dialog 行;老话题 followup 由 PROACTIVE_FOLLOWUP_HEADER(标签是 [回忆线索] / [Memory cues] / [記憶の手がかり] / [기억 단서] / [Подсказки памяти])独立拼上去。 en/ja/ko 的规则措辞和 header label 已经精确一致;ru 的 "Тема из «Подсказок памяти»" 是规则一致的属格变形,俄语模型可识别。唯一真正的错配是 zh:规则 写 "记忆线索"("记"),实际 header 是 "[回忆线索]"("回")——一个字之差, 看起来是早期手抖。改规则那侧对齐到 "回忆线索"。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈:决策段水印 5 语言统一为中文 + 外部话题→网络话题 A. 决策段水印 5 语言统一中文(per .agent/rules/neko-guide.md 约定) "翻译 system prompt 时即使出于其他原因也应当保留 ====== 这是水印"——前 一版我把 en/ja/ko/ru 各自翻译了水印文字,违反了水印不变性。改回所有 语言都用中文 "======以下为向{master_name}进行搭话的决策方式======" / "======以上为向{master_name}进行搭话的决策方式======",水印只是结构 标记,不参与本地化。决策内部内容仍按各语种翻译。 B. EXTERNAL_TOPIC_HEADER / FOOTER 5 langs 改名 外部话题 → 网络话题 - zh: 外部话题 → 网络话题 - en: External Topic → Web Topic - ja: 外部の話題 → ウェブ話題 - ko: 외부 주제 → 웹 화제 - ru: Внешняя тема → Веб-тема 理由:该 channel 实际只装 web sources(news / video / social),prompt 别处又把 vision / music / meme 也归类为 "external material",光说 "外部话题" 容易和它们混。renamed 为 web 直接对应通道实际抓取范围。 C. _material_labels (5 langs) + system_router.py 两处 dev 注释同步改名。 _material_labels 决定 source_instruction 里 "你可以结合 X、Y 来搭话" 的素材列表表述,跟着 header 改才对得上。 注意 1-5 优先级里的 "外部素材" / "external material" 文案保持不变—— 那是包含 web + music + meme 的总称,不是单指网络话题这一个 channel。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈修日文 priority #1 用词错误:未収 → 未完 CodeRabbit 抓到 ja 优先级列表第 1 条 "前回未収のスレッド → 継続",未収 是 "未收/未回收"(如"料金未収"),不是 unfinished thread 的语义。该条要表达的 是"上轮挂着没收尾的话题",正确用词是 未完,正好和上面 ★ 行 "未完話題" 对齐。 改成 "前回の未完スレッド → 継続"。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 删除 prompts_proactive.py 里的死复制 MEMORY_RECALL_HEADER / MEMORY_RESULTS_HEADER 这两个常量在 prompts_proactive.py:2484-2498 与 prompts_memory.py:719-733 完全 重复,且 prompts_proactive.py 这份零 importer——memory_server.py 只 from config.prompts_memory import 那份。属于"配置/复制粘贴未清理"的死代码。 不动 prompts_memory.py 那份和 memory_server.py 的 /search_for_memory/ 端点; 那条链路是 plugin SDK 的 query_memory capability 唯一后端,给未来插件开发者 保留(哪怕当前 returns "语义记忆已下线" 占位也不是这次清理的范围)。 发现起源:proactive Phase 2 prompt 对偶性盘点(======X======/======X 结束====== 配对)。该文件其他对偶性都对得齐:5 对 section header/footer、CONTEXT_SUMMARY_TASK header/footer 都齐;CONTEXT_SUMMARY_READY / SYSTEM_NOTIFICATION_TASKS_DONE 是 自封闭 banner,AGENT_PLUGINS / AGENT_TASKS 走【...】内联标签风格——这三类故意 不配 footer。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈修 snapshot.py docstring 里 ===活动状态=== 仍是三等号的过时 example format_activity_state_section 的 docstring layout example 还在用 ===活动状态=== / ===状态结束===,是 cbd59e0 把实际标签从三等号升级到六等号时漏改的。仅文档字符 串,不影响任何运行时行为;修齐使读者对得上实际渲染。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈给 _REMINISCENCE_USAGE_MAX = 50 加 calibration 注释 CodeRabbit 提示这个魔术数字应该解释或合并。trace 后选择"加注释"路径—— 不能合 PROACTIVE_CHAT_HISTORY_MAX(=10),两个 buffer 服务相反的 sizing 约束: - chat_history.maxlen=10 限的是去重内存,10 条足够 - reminiscence.maxlen=50 限的是衰减信号完整性,必须 > _SOURCE_WEIGHT_WINDOW (1h) 内最坏使用次数,否则 _compute_source_weights 的指数和会 under-count 也不放进 config/__init__.py—— 跟同 module 已有的 _SOURCE_WEIGHT_* 私有常量 (_DECAY_LAMBDA / _K / _FLOOR / _WINDOW)保持风格一致:这一组都是 weight 模型 calibration,不是用户面向的 config knob,跟模型一起调才有意义。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈修 unfinished-thread 配额错误消耗 (CodeRabbit L4197) 原逻辑:本轮 snapshot 里有 _has_unfinished_thread 就 mark_used,无视 AI 实际 是否选了 [CHAT] 路径。原 comment 写"防 AI 反复忽略 override 烧光配额",但 trace 后发现这个理由站不住——UNFINISHED_THREAD_WINDOW_SECONDS=300 的自动过期 已经兜底了 thread 总暴露时间,旧逻辑实际上让两次选了外部素材的 proactive 轮 就能把"最多跟进 2 次"的配额提前烧光,真正想续接时 thread 已被 tracker 停止 暴露。 新逻辑:仅当 source_tag == 'CHAT'(或 build_proactive_response 落到 chat 通道) 才 mark_used。[PASS] 已在 4079 早 return,到这里 source_tag 只可能是 CHAT/WEB/MUSIC/MEME,分支干净。CHAT 也不严格等于"用了 unfinished thread override"——AI 可能用 CHAT 走 priority 2-3(回忆线索 / 屏幕评论)——但这是当前 最接近的可靠信号,也比"无脑曝光即计数"接近真实意图。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 按 review 反馈补 unfinished-thread 配额漏算路径 (CodeRabbit L4201) CodeRabbit 指出 8cde3c8 的修复在 source_tag 为空串时仍然漏掉 mark_used。 trace 确认:source_tag 只在 L4004-4008 regex 命中 [CHAT|WEB|PASS|MUSIC|MEME] 时才赋值,而 _of_none output_format_section 明确告诉 AI"不带 source tag" (无外部素材时的合法输出路径)。这种情况下 AI 真在跟进 unfinished thread 但 source_tag="" / primary_channel='unknown',新条件 (source_tag == 'CHAT' or primary_channel == 'chat') 不命中,配额被静默绕过,只剩 5 min 自动过期兜底。 修复:在 abort early return 之后、build_proactive_response 之前补一个兜底—— 非 abort 且 full_text.strip() 非空意味着 Phase 2 实际产出了文本,无标签时按 CHAT 处理。这样下游 primary_channel 也变 'chat',mark_used 条件正常命中。 对照 music-cooldown 分支 (L4109) 已有的 source_tag = 'CHAT' 降级先例,语义一致。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b14079f commit e431ff7

16 files changed

Lines changed: 7124 additions & 215 deletions

.agent/rules/neko-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ trigger: always_on
77
## 基本规则
88

99
- 使用 i18n 支持国际化,目前支持 en.json、ja.json、ko.json、zh-CN.json、zh-TW.json、ru.json 六种。每次改 i18n 字符串时必须同步更新全部 6 个 locale 文件,只改部分会被打回。
10+
- **后端 Python 多语言字符串一律落在 `config/prompts_*.py`**:无论是平铺 `dict[str, str]` 还是嵌套 `dict[str, dict[K, str]]`,凡键里出现 `'zh' / 'en' / 'ja' / 'ko' / 'ru'` 的语言映射,都必须放在 `config/prompts_*` 下。`scripts/check_prompt_hygiene.py` 只抓平铺结构,但规范是"加新语言时一次扫 `config/` 即可补全"——嵌套 dict 即使 lint 没抓也算技术债,需自觉搬迁。新增后端模块若有翻译需求,直接在 `config/prompts_<topic>.py` 加新模块或复用已有模块(如 `prompts_activity.py``prompts_proactive.py``prompts_memory.py`)。
1011
- 使用 `uv run` 来运行本项目的任何 Python 程序(pytest、脚本等),不要直接用系统 Python。原因:pyproject.toml 限制了 Python 版本(<3.13),uv 会自动选择合适版本并管理虚拟环境。
1112
- 任何涉及用户隐私(原始对话)的 log 只能用 `print` 输出,不得使用 `logger`
1213
- 翻译 system prompt 时,即使出于其他原因也应当保留 `======以上为`,这是一个水印。

config/__init__.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,10 +1139,15 @@ def translate_value(val):
11391139
- 用途:fetch_news_content / fetch_video_content 等的 limit 参数统一值。
11401140
- 上游:外部 web/news/video 抓取结果。"""
11411141

1142-
PROACTIVE_PHASE1_TOTAL_TOPICS = 20
1142+
PROACTIVE_PHASE1_TOTAL_TOPICS = 12
11431143
"""Phase 1 输入给筛选 LLM 的候选话题总数。
11441144
- 用途:从所有 source 合并后去重,截到此数后送 LLM 筛选。
1145-
- 上游:cap 后的 fetch 结果汇总。"""
1145+
- 上游:cap 后的 fetch 结果汇总。
1146+
- 设计依据:原值 20。早期 external 是主要信号源,候选池开得很大。
1147+
Phase 2 引入 vision / music / meme / reminiscence 等并行通道后,
1148+
external 的相对权重下降——筛选 LLM 多看 8 条边际候选无助于挑出更
1149+
好的 top-1,反而让 Phase 1 prompt 一次跑过 2k tokens 上限。下调到
1150+
12 仍给筛选 LLM 充分多样性,且单次调用 token 减半左右。"""
11461151

11471152
PROACTIVE_EXTERNAL_PER_ITEM_MAX_TOKENS = 200
11481153
"""Phase 2 外部内容(news/video/social/meme 等)单条 token 上限。
@@ -1152,11 +1157,15 @@ def translate_value(val):
11521157
- 设计依据:单条 200 token 已足够 LLM 知道"这是什么",详细信息靠
11531158
Phase 2 LLM 自行总结。"""
11541159

1155-
PROACTIVE_EXTERNAL_TOTAL_MAX_TOKENS = 2000
1156-
"""Phase 2 外部内容拼合后的总 token 上限。
1160+
PROACTIVE_EXTERNAL_TOTAL_MAX_TOKENS = 1500
1161+
"""Phase 1 外部候选拼合后的总 token 上限(Phase 2 实际只看 top-1)
11571162
- 用途:所有 selected web items 序列化后,再做一次总和截断。
11581163
- 上游:cap 后的 external_section 文本。
1159-
- 设计依据:留出主对话流的 5k 总预算给 character_prompt + memory + 历史。"""
1164+
- 设计依据:跟 PROACTIVE_PHASE1_TOTAL_TOPICS 同步下调。原值 2000 是
1165+
20 候选 × 200 token 留的硬顶;候选数收到 12 之后,1500 已留出
1166+
~250 token 富余,超出仍兜底截断。Phase 2 generate prompt 实际只
1167+
把 Phase 1 选中的单条 web_topic(~50-100 token)放进
1168+
external_section,本字段约束的是 Phase 1 的 prompt 大小。"""
11601169

11611170
PROACTIVE_PHASE2_OUTPUT_MAX_TOKENS = 300
11621171
"""Phase 2 流式输出的 abort fence。

config/activity_keywords.py

Lines changed: 2843 additions & 0 deletions
Large diffs are not rendered by default.

config/prompts_activity.py

Lines changed: 584 additions & 0 deletions
Large diffs are not rendered by default.

config/prompts_memory.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -717,19 +717,19 @@ def get_emotion_analysis_prompt(lang: str = 'zh') -> str:
717717
# =====================================================================
718718

719719
MEMORY_RECALL_HEADER = {
720-
'zh': '======{name}尝试回忆=====\n',
721-
'en': '======{name} tries to recall=====\n',
722-
'ja': '======{name}の回想=====\n',
723-
'ko': '======{name}의 회상=====\n',
724-
'ru': '======{name} пытается вспомнить=====\n',
720+
'zh': '======{name}尝试回忆======\n',
721+
'en': '======{name} tries to recall======\n',
722+
'ja': '======{name}の回想======\n',
723+
'ko': '======{name}의 회상======\n',
724+
'ru': '======{name} пытается вспомнить======\n',
725725
}
726726

727727
MEMORY_RESULTS_HEADER = {
728-
'zh': '====={name}的相关记忆=====\n',
729-
'en': '====={name}\'s Related Memories=====\n',
730-
'ja': '====={name}の関連する記憶=====\n',
731-
'ko': '====={name}의 관련 기억=====\n',
732-
'ru': '====={name} — связанные воспоминания=====\n',
728+
'zh': '======{name}的相关记忆======\n',
729+
'en': '======{name}\'s Related Memories======\n',
730+
'ja': '======{name}の関連する記憶======\n',
731+
'ko': '======{name}의 관련 기억======\n',
732+
'ru': '======{name} — связанные воспоминания======\n',
733733
}
734734

735735
# ---------- Persona header (static prefix) ----------
@@ -742,12 +742,15 @@ def get_emotion_analysis_prompt(lang: str = 'zh') -> str:
742742
}
743743

744744
# ---------- Proactive chat followup header ----------
745+
# 文案故意"鼓励性"而非"可选性"——之前的"可以选择性地回顾"语气太弱,配合
746+
# Phase 2 prompt 的反复读警告,会让模型把回忆当成"高重复风险"绕开。新表述
747+
# 强调这些是"久远的旧话题",与"最近 1h 内复读"明确区分。
745748
PROACTIVE_FOLLOWUP_HEADER = {
746-
'zh': '\n[回忆线索] 以下是之前对话中的话题,可以选择性地回顾或跟进\n',
747-
'en': '\n[Memory cues] Topics from previous conversations that could be revisited:\n',
748-
'ja': '\n[記憶の手がかり] 以前の会話のトピックで、再訪できるもの\n',
749-
'ko': '\n[기억 단서] 이전 대화에서 다시 다룰 수 있는 주제:\n',
750-
'ru': '\n[Подсказки памяти] Темы из предыдущих разговоров, к которым можно вернуться:\n',
749+
'zh': '\n[回忆线索] 以下旧话题距今较久,适合自然回忆与跟进\n',
750+
'en': '\n[Memory cues] Older topics from prior conversations — well-suited for natural reminiscence:\n',
751+
'ja': '\n[記憶の手がかり] 以前の会話で出た古い話題——自然に回想して持ち出すのに向いている\n',
752+
'ko': '\n[기억 단서] 이전 대화에서 나온 오래된 화제——자연스럽게 회상하여 꺼내기 좋음:\n',
753+
'ru': '\n[Подсказки памяти] Старые темы из прошлых разговоров — удачные для естественного возврата:\n',
751754
}
752755

753756
# =====================================================================

0 commit comments

Comments
 (0)